Skip to content

feat(OMN-20905): the Stop capture marks the prompts the operator typed, so decisions are taken from the bus - #2670

Merged
jonahgabriel merged 2 commits into
devfrom
jonah/omn-20905-prompt-origin
Oct 11, 2026
Merged

jonahgabriel merged 2 commits into
devfrom
jonah/omn-20905-prompt-origin

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Ticket: OMN-20905. Companion: omnimarket#3745 (the bus consumer).

Operator RULING 2026-10-10T22:40:43Z: the operator's decisions and asks are extracted asynchronously from the bus, not by capture hooks. The bus must therefore say which prompts the operator typed.

Change

The existing Stop content capture (hook_content_capture.py --kind stop) also journals, from the transcript, each prompt whose entry has origin.kind == "human" (not meta, not sidechain) as content_kind=operator_prompt with its prompt_id and said_at, once per session (a marker of entry ids beside the turn state; the first Stop looks back at most an hour). A claude -p lane brief (turnOrigin sdk, no origin), a scheduled prompt, a task notification, a compaction summary and a subagent Stop never produce one. Why Stop and not UserPromptSubmit: the UserPromptSubmit payload carries identical fields for an operator prompt and a lane brief, and the transcript entry that distinguishes them is written only after that hook returns (measured on Claude Code 2.1.295). A message typed mid-turn, which never fires UserPromptSubmit, is recorded too. The redaction contract adds prompt_id and said_at as verbatim fields (identifiers and a timestamp, no content).

No new hook, no settings change.

Tests

uv run pytest tests/unit/hooks/test_hook_operator_prompt_omn20905.py tests/unit/hooks/test_hook_content_capture_omn19551.py tests/hooks/test_capture_redaction_sync_omn18357.py tests/hooks/test_capture_redaction_omn17959.py -q: 76 passed. ruff clean.

Lab: .201 dev lane bus, 2026-10-11T00:05:28Z. This branch's Stop capture was run on the operator's orchestrator session transcript and on a claude -p lane session; the hook-emit drainer published both to onex.cmd.omniintelligence.content-captured.v1; the omnimarket#3745 capture host on lane dev took exactly one operator prompt ("Yes, to your recommendations", the operator's session) and appended its ledger row 22 s later; the lane session produced no operator_prompt record and no row (host handled=1, skipped=169).

Evidence-Ticket: OMN-20905
Evidence-Source: OCC#13625

…d, so decisions are taken from the bus

RULING 2026-10-10T22:40:43Z: operator decisions and asks are extracted
asynchronously from the bus, not by capture hooks. The content-capture
producer now journals each prompt whose transcript origin is human as
content_kind operator_prompt (with its prompt id and said_at), once per
session; lane briefs (claude -p, turnOrigin sdk), scheduled prompts, task
notifications, compaction summaries and subagent stops never are.
@jonahgabriel
jonahgabriel enabled auto-merge (squash) October 11, 2026 00:36
jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Oct 11, 2026
)

* evidence(OMN-20905): author OCC companion for OmniNode-ai/omniclaude#2670

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head b2b4d3fb666aef115727e44e9fdf59584b5db41a.

* evidence(OMN-20905): self-bind OCC#13625 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Hostile Reviewer — PASSED

Blocking findings (critical/error): 0
Total findings: 15
Models succeeded: qwen3-review,local-studio-planner

Findings

Severity Location Finding
warning plugins/onex/hooks/lib/hook_content_capture.py, operator_prompt_items:1 ai-reviewer:qwen3-review:correctness Operator prompt deduplication is broken by marker read failure | In operator_prompt_items, the deduplication logic uses known = set(seen or ()). When _read_marker returns None (marker file does not exist or is unreadable), seen is None, so known becomes an empty set. This means that on the first Stop of a session, if the marker file is missing, all operator prompts in the transcript tail are considered 'new' and will be recorded. However, the critical issue is that if the marker file exists bu
warning plugins/onex/hooks/lib/hook_content_capture.py, _operator_marker:1 ai-reviewer:qwen3-review:security Session ID is hashed for marker filename, but the hash is not salted or protected | The operator marker filename is derived from hashlib.sha256(session_id.encode('utf-8')).hexdigest(). This is a deterministic hash of the session ID. An attacker who can enumerate session IDs (which may be predictable or guessable) could pre-compute the marker filenames and potentially interfere with the marker files. However, the marker files are written to a turn_dir which is presumably protected by filesystem permissio
warning plugins/onex/hooks/lib/hook_content_capture.py, capture:1 ai-reviewer:qwen3-review:performance Transcript tail is read and parsed on every Stop, even when no operator prompts are expected | The operator_prompt_items function is called on every Stop when agent_id is not set and a transcript path is available. It reads the tail of the transcript (default TRANSCRIPT_TAIL_BYTES) and parses each line as JSON. This is done even when there are no operator prompts in the tail. For large transcripts, this could be a performance issue. The tail size is not specified in the diff, but it is likely a few KB
warning plugins/onex/hooks/lib/hook_content_capture.py, operator_prompt_items:1 ai-reviewer:qwen3-review:correctness The lookback window is only applied on the first Stop, not on subsequent Stops | In operator_prompt_items, the lookback window (OPERATOR_LOOKBACK_SECONDS) is only applied when seen is None (i.e., the marker file does not exist, which is the first Stop of a session). On subsequent Stops, seen is not None, so floor is None, and the lookback window is not applied. This means that on subsequent Stops, all operator prompts in the transcript tail are considered, regardless of their age. This is by d
warning tests/unit/hooks/test_hook_operator_prompt_omn20905.py:1 ai-reviewer:qwen3-review:testing No test for the case where the marker file is missing on a subsequent Stop | The test test_a_failed_append_records_the_prompt_again_next_stop tests the case where the journalling fails, so the marker is not written, and the next Stop re-records the operator prompts. But there is no test for the case where the marker file is missing on a subsequent Stop (e.g., due to a filesystem error or manual deletion). In this case, the lookback window is not applied (because seen is None), and all operator prompts
info plugins/onex/hooks/lib/hook_content_capture.py, _said_at:1 ai-reviewer:qwen3-review:correctness The _said_at function returns None for timestamps without timezone info | In _said_at, if the timestamp string does not have timezone info, the function returns None. This means that operator prompts with timestamps that lack timezone info are not recorded, because said_at is None, and the code checks if floor is not None and (said_at is None or said_at < floor). If floor is not None (i.e., it is not the first Stop), and said_at is None, the prompt is skipped. This could lead to operat
info plugins/onex/hooks/lib/hook_content_capture.py, is_operator_prompt_entry:1 ai-reviewer:qwen3-review:architecture The operator prompt extraction is tightly coupled to the transcript format | The is_operator_prompt_entry function checks for origin.kind == 'human' to identify operator prompts. This is tightly coupled to the transcript format used by Claude Code 2.1.295. If the transcript format changes, this function will break. The code does not have a fallback or a configurable way to identify operator prompts. This is a maintenance concern. | Evidence: In is_operator_prompt_entry, origin = entry.get('origin')
info plugins/onex/hooks/lib/hook_content_capture.py, operator_prompt_items:1 ai-reviewer:qwen3-review:completeness The operator prompt items do not include the session ID or the turn ID | The operator prompt items created in operator_prompt_items do not include the session ID or the turn ID. This makes it difficult to correlate operator prompts with specific sessions or turns. The items only include content_kind, content, prompt_id (if present), and said_at (if present). The session ID and turn ID are available in the capture function, but they are not included in the operator prompt items. This is a complet
hint plugins/onex/hooks/lib/hook_content_capture.py, _tail_lines:1 ai-reviewer:qwen3-review:style The _tail_lines function is not used consistently | The _tail_lines function is introduced and used in last_turn_reply, but it is also used in operator_prompt_items. The function is not used in other places where the transcript tail is read. This is not a bug, but it is a style issue. The function is well-named and well-documented, so it is a good addition. However, the code does not use it in all places where the transcript tail is read, which is inconsistent. | Evidence: The _tail_lines function
error plugins/onex/hooks/lib/hook_content_capture.py:1 ai-reviewer:local-studio-planner:correctness Operator prompt capture is skipped when the assistant reply is already captured | The logic for capturing operator prompts is nested inside the if items: block that checks for an existing assistant reply. If the assistant reply has already been captured (deduplication), items is set to an empty list. The subsequent if not agent_id ... block is still executed, but the items list is empty. However, the critical issue is that the operator_prompt_items function is called after the deduplication chec
warning plugins/onex/hooks/lib/hook_content_capture.py:1 ai-reviewer:local-studio-planner:security Session ID is hashed for marker filename, but the marker file is world-readable/writable by default | The _operator_marker function creates a file in turn_dir with a name derived from the SHA256 of the session ID. The turn_dir is created with mkdir(parents=True, exist_ok=True). The default permissions for mkdir and write_text are subject to the umask. If the umask is permissive (e.g., 002), the marker file may be readable/writable by other users on a multi-user system. The marker file contains t
warning plugins/onex/hooks/lib/hook_content_capture.py:1 ai-reviewer:local-studio-planner:correctness Race condition in marker file read/write | The operator_prompt_items function reads the marker file to get the known set. The record_operator_marker function writes the marker file. If two Stop hooks run concurrently for the same session (e.g., due to a race in the harness or a retry), they may both read the marker, find the same set of known IDs, and both attempt to write the marker. The second write may overwrite the first, or the kept list may be computed from a stale seen set, leading to loss
info plugins/onex/hooks/lib/hook_content_capture.py:1 ai-reviewer:local-studio-planner:performance Inefficient marker file update | The record_operator_marker function reads the entire marker file, appends new IDs, and writes the entire file back. If the marker file grows large (up to OPERATOR_MARKER_KEEP = 512 lines), this is inefficient. Each Stop hook call reads and writes the entire file. For high-frequency Stop hooks, this could be a performance bottleneck. A more efficient approach would be to append to the file and periodically truncate it, or to use a database or a more efficient data structu
info tests/unit/hooks/test_hook_operator_prompt_omn20905.py:1 ai-reviewer:local-studio-planner:testing Missing test for concurrent Stop hooks | The test suite does not include a test for concurrent Stop hooks for the same session. Given the race condition identified in the marker file read/write, a test that simulates concurrent access would be valuable to ensure that the marker file is handled correctly. | Evidence: tests/unit/hooks/test_hook_operator_prompt_omn20905.py | Fix: Add a test that simulates two concurrent Stop hooks for the same session and verifies that the marker file is updated correctly and
info plugins/onex/hooks/lib/hook_content_capture.py:1 ai-reviewer:local-studio-planner:completeness Missing handling for malformed transcript entries | The operator_prompt_items function uses json.loads to parse each line of the transcript. If a line is malformed JSON, it is skipped. However, if a line is valid JSON but not a dict, it is skipped. If a line is a dict but missing required fields, it is skipped. This is robust, but the error handling is silent. A log message might be helpful for debugging. | Evidence: for raw in _tail_lines(transcript, tail_bytes): try: entry = json.loads(raw) except jso

Gate semantics (pilot phase)

Verdict Meaning Blocks merge?
passed No critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded Reviewer ran but every model failed mid-run No (pilot)
infra_error cli_review failed to run at all (install/crash) after 2 attempts Yes
unavailable_fork_isolation Fork PR — the LAN-only model is unreachable and the fleet is off limits Yes

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524)

PR omniclaude#2670 head b2b4d3f hand-edited the mirror src/omniclaude/hooks/contracts/capture_redaction.yaml to declare prompt_id and said_at as capture_verbatim on onex.cmd.omniintelligence.content-captured.v1.
omnimarket owns that contract (OMN-18357); the mirror must equal the copy at the omnimarket rev uv.lock pins (1b1041574e98), which does not declare them.
So "Registry Consistency" (scripts/validation/generate_event_registry.py --check) and Tests Gate (tests/hooks/test_capture_redaction_omn17959.py, 2 tests) failed with drift on ['topics'].
This commit restores the mirror to its dev bytes.
The producer still journals prompt_id and said_at; until omnimarket's owning contract declares them, the fan-out hashes them as undeclared fields, which is the contract's fail-safe default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant