Skip to content

feat(OMN-19513): node_projection_claude_hook_events -- every Claude Code hook, with subagent lineage - #2956

Merged
jonahgabriel merged 14 commits into
devfrom
jonah/omn-19513-claude-hook-events-projection
Sep 28, 2026
Merged

jonahgabriel merged 14 commits into
devfrom
jonah/omn-19513-claude-hook-events-projection

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Ticket OMN-19513. Operator ruling on the rolling ledger (RULING by lane all-hooks-capture-83-ruling, 2026-09-26T11:15:41Z): "we need all hooks captured". This PR is build task 3 of the all-hooks task split: the consumer (writer) half.

It builds on omniclaude#2370 (merged 2026-09-26T12:38Z), the capture contract: one topic onex.evt.omniclaude.hook-event.v1 for all 33 Claude Code hook types, lineage on every event, content only as references.

New node node_projection_claude_hook_events. Subscribes to onex.evt.omniclaude.hook-event.v1. Writes omninode_internal.claude_hook_events (one row per event, session_id is a real column, agent_id NULL means main thread, tool_use_id, parent_tool_use_id, parent_agent_id, workflow_run_id, spawn_depth, turn_id, correlation and causation ids, payload JSONB metadata only, content_ref_ids) and omninode_internal.claude_agent_spans (one row per subagent: parent_agent_id and parent_resolution main_thread / resolved / workflow / unknown, spawn depth, start and stop, tool_call_count).

Rule 7a pair: HandlerProjectionClaudeHookEvents is the pure fold; ClaudeHookEventsProjectionWriter is the in-process writer (onex_runtime_inprocess_dispatch = True). Only the writer is a routed entry, because the event has a top-level payload mapping and event_id and the runtime typed arm would unwrap through it and strip the lineage.

Idempotent: event rows insert ON CONFLICT (event_id) DO NOTHING; span tool_call_count is recounted from the event table, never incremented; a known parent replaces unknown and never the reverse; a child written before its spawning call is repaired when the call arrives.

The wire model refuses a payload key the capture contract does not declare for the metadata topic (content cannot be stored by a producer regression; it is dead-lettered to onex.dlq.omnimarket.projection-claude-hook-events-malformed.v1). Top-level transport stamps are dropped by name, never by ignoring extras.

Seam defect found and fixed before push (second commit): the real omniclaude hook_emit_append.py stamps lane, lane_source, lane_ticket, workspace_path and turn_id at top level. The first commit's model rejected those, so every real hook event would have been dead-lettered. Reproduced by journaling a fixture through the real appender; the fix adds them to the named stamp set, and lineage.turn_id stays authoritative.

Two migrations: 0000 creates both tables, 0001 grants to omninode_runtime.

Proof

Local proof: 63 focused tests passed, 2 skipped (the real-Postgres tests skip without a local DB). ruff format, ruff check, mypy --strict clean; commit hooks passed.

Lab proof on the .201 dev lane (compose project omnibase-infra), no service restart, no container recreate, no site-packages edit: the branch node was loaded inside the omninode-runtime container from a temp directory (removed afterwards) and its writer runtime entry was driven per consumed message. The two migrations were applied by hand to omnidash_analytics in the earlier pass (both are IF NOT EXISTS, no DROP). 20 contract scenario fixture events (nested subagent tree, workflow agent, orphan agent) plus 1 PostToolUse record journaled through the real omniclaude appender were published to onex.evt.omniclaude.hook-event.v1 on the dev broker, keyed by session_id. A fresh consumer group read 41 messages (this run's 21 plus 20 from the earlier pass): 0 validated as ModelEventEnvelope, so all 41 take the payload=data branch of the runtime's auto-wiring consume path (handler_wiring.py), which the driver mirrors; 1 new event row inserted, the 40 fixture replays wrote no new rows. Readback as role omninode_runtime: 20 of 20 fixture event rows and 4 of 4 span rows equal the contract's expected_projection rows exactly. Spans: a1 main_thread with 2 tool calls, a2 resolved to parent a1 at depth 2 with 1 tool call, workflow-a1 workflow, orphan-a1 unknown. The appender-shaped row landed with no stamp keys in its payload and turn_id taken from lineage.

What the lab pass did not exercise: the runtime's own auto-wiring of this node (the node is not installed in the runtime image until merge), and the DLQ route.

Rule 24(d): no tenant, RLS or reader-authorization path touched; the lab pass did not exercise onex-api.

Merge order

The forced three-PR order from scripts/application-relation-ownership.yaml: omnimarket#2958 (ownership declaration only), then omnibase_infra#4169 (vendors this node's two migrations; its Application Database Domain Enforcement gate reads ownership from omnimarket dev), then this PR, whose node-migration-vendor-parity-gate stays red until #4169 is on omnibase_infra dev. This branch has merged #2958's branch so its CI evaluates the combined state.

Later commits on this branch: the landing lane added hook_fired_at (stamped by the omniclaude drainer) to the dropped-stamp set; and the repo rosters this node moves are pinned (the in-process-dispatch writer set gains ClaudeHookEventsProjectionWriter as the eighth; the relation inventory is regenerated, source_created_tables 72 to 74; dogfood node_dirs 425 to 426 and entry_points 416 to 417). 85 focused tests pass at 3467e4f.

Not in this PR

  • Producer hooks (omniclaude, lane all-hooks-producers-83)
  • Topic registration and redaction (omnimarket#2955)
  • Full-content capture (omnimarket#2905, omniclaude#2343)
  • Any change to node_projection_hook_ledger or node_projection_work_events
  • Cloud legs

Until the producer and registration land, the topic carries only the lab fixtures. The node goes live on the lab after merge through the deploy agent.

Evidence-Ticket: OMN-19513
Evidence-Source: OCC#11488

…ode hook, with subagent lineage

The consumer half of the all-hooks capture contract (omniclaude#2370). One
subscribe topic, onex.evt.omniclaude.hook-event.v1, carries all 33 Claude
Code hook types with lineage on every event. This node projects it into
omninode_internal.claude_hook_events (one row per event, session_id as a real
column, agent_id NULL for the main thread) and folds subagent lineage into
omninode_internal.claude_agent_spans (parent_agent_id plus parent_resolution:
main_thread, resolved, workflow or unknown).

Rule 7a pair: HandlerProjectionClaudeHookEvents is the pure definition-B
fold; ClaudeHookEventsProjectionWriter is the in-process writer that reads
the spawning PreToolUse, calls the fold and persists. Idempotent by
construction: event rows insert ON CONFLICT (event_id) DO NOTHING, the span
tool_call_count is recounted from the event table rather than incremented,
and a known parent replaces unknown but never the reverse. A child written
before its spawning call is repaired when the call arrives.

Only the writer is a routed entry. The contract's event has a top-level
payload mapping and an event_id, which the runtime's typed arm reads as a
transport envelope and unwraps through (OMN-16050); a routed fold would
receive the per-hook payload without its lineage and dead-letter every event.

The wire model refuses a payload key the capture contract does not declare
for the metadata topic, so content that reaches this topic by a producer
regression is dead-lettered, not stored. It drops, by name, the five
top-level stamps the emit seam adds (measured on a live lab record).

Tests reproduce the contract's expected_projection rows for the nested
subagent, workflow-agent and orphan-agent scenarios through the writer's
runtime entry, pin the hook list, payload keys and projection columns to the
contract, and prove the SQL against real Postgres (skips without a DB).
…ps by name

The wire model for onex.evt.omniclaude.hook-event.v1 forbids extra top-level
keys and drops transport stamps by name. It previously dropped only five
stamps: correlation_id, causation_id, entity_id, session_id, and
redaction_state. The real appender (omniclaude hook_emit_append.py) also
merges lane, lane_source, lane_ticket, workspace_path, and turn_id into
every record. Because these five keys were not in the drop set, a fixture
event journaled through the real appender was rejected with extra_forbidden,
so every real hook event would have been dead-lettered.

The fix adds the five appender keys to the named stamp set so they are
dropped instead of rejected. The event's lineage.turn_id remains
authoritative over the appender's top-level turn_id. The seam test now
carries the appender stamps with a different top-level turn_id and asserts
the stored turn_id is the lineage one. A new test proves an unnamed
top-level key such as prompt is still refused.
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

⚠️ Architectural Review — WARNINGS

Errors: 0
Warnings: 1

What this checks

Rule Description
ARCH-TOPIC-001 No hardcoded Kafka topic strings in handler code
ARCH-DI-001 No event_bus=None bypass
ARCH-DI-002 No direct Handler instantiation outside workflow_runner/adapters
ARCH-DI-003 No reinvented DI containers
ARCH-TOPIC-002 contract.yaml topics follow `onex.{cmd

ARCH-DI-002: Direct Handler instantiation (warning)\n\n\nsrc/omnimarket/nodes/node_projection_claude_hook_events/handlers/handler_claude_hook_events_writer.py:210: self._derive = HandlerProjectionClaudeHookEvents()\n\n\nHandler classes should be wired only in workflow_runner.py or adapter modules. Direct instantiation elsewhere bypasses the contract system.\n\n

Static architectural lint — no model inference (OMN-14176).

@jonahgabriel
jonahgabriel enabled auto-merge (squash) September 26, 2026 14:02
onexbot-occ-writer Bot added a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 26, 2026
…nimarket#2956

The declared checks were executed for real in the OmniNode-ai/omnimarket checkout at the PR head by the product-repo OCC receipt runner, and the results written append-only into this companion. Producer-minted PENDING receipts are superseded by a net-new record; no existing receipt was edited.

Ticket: OMN-16859 (AC3b)
Onex-Workflow: OCC Receipt Runner
Onex-Run: https://github.com/OmniNode-ai/omnimarket/actions/runs/36247082497
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

✅ Hostile Reviewer — PASSED

Blocking findings (critical/error): 0
Total findings: 17
Models succeeded: qwen3-review,gpt-oss-review


Gate semantics

Verdict Meaning Blocks merge?
passed >=2 models succeeded, no critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded Fewer than 2 models succeeded (infra unavailable/timeout) Yes (OMN-15110)

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

_SCENARIOS = ("subagent_tree", "workflow_agent", "orphan_agent")


async def _connect_or_skip() -> asyncpg.Connection:
…nership ahead of their vendoring

Step 1 of the three-PR order scripts/application-relation-ownership.yaml
documents, as omnimarket#2937 did for session_content. omnibase_infra#4169
vendors the node_projection_claude_hook_events migrations and its
Application Database Domain Enforcement gate resolves ownership from this
file at omnimarket dev tip, so the declaration has to land first;
omnimarket#2956 then lands the node and its migration. No SQL, no exemption.
The relation inventory is regenerated: source_declared_tables 81 to 83,
source_created_tables unchanged at 72.
… stamp by name

The omniclaude emit drainer stamps hook_fired_at on every published record, so every real drained hook event failed validation and was dead-lettered. This adds hook_fired_at to TRANSPORT_STAMP_KEYS, so the wire model drops it by name while still refusing unknown top-level keys. Tests carry the new key and assert it is accepted while an unknown key is still rejected.

Onex-Lane: all-hooks-contract-83
Onex-Session: 9a8f8ac2b564447cbcbd3142788e28a1
…ook-events-ownership' into jonah/omn-19513-claude-hook-events-projection
onexbot-occ-writer Bot added a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 26, 2026
…nimarket#2956

The declared checks were executed for real in the OmniNode-ai/omnimarket checkout at the PR head by the product-repo OCC receipt runner, and the results written append-only into this companion. Producer-minted PENDING receipts are superseded by a net-new record; no existing receipt was edited.

Ticket: OMN-16859 (AC3b)
Onex-Workflow: OCC Receipt Runner
Onex-Run: https://github.com/OmniNode-ai/omnimarket/actions/runs/36251210117
onexbot-occ-writer Bot pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 26, 2026
…2956

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head d5084f6ac5e446dd080c19c91217d2346d657a58.
onexbot-occ-writer Bot added a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 26, 2026
…nimarket#2956

The declared checks were executed for real in the OmniNode-ai/omnimarket checkout at the PR head by the product-repo OCC receipt runner, and the results written append-only into this companion. Producer-minted PENDING receipts are superseded by a net-new record; no existing receipt was edited.

Ticket: OMN-16859 (AC3b)
Onex-Workflow: OCC Receipt Runner
Onex-Run: https://github.com/OmniNode-ai/omnimarket/actions/runs/36251278975
…osters

CI on the first push failed three roster tests this node moves.
ClaudeHookEventsProjectionWriter joins the pinned exact set of
in-process-dispatch writers as the eighth, on the same reviewed terms (its
pure fold does not declare the capability). The relation inventory is
regenerated after merging the omnimarket#2958 ownership declaration:
source_created_tables 72 to 74 and the arithmetic unreconciled bound 14 to
12, declared unchanged here. The dogfood inventory counts node_dirs 425 to
426 and entry_points 416 to 417.
…ts-projection' into jonah/omn-19513-claude-hook-events-projection
onexbot-occ-writer Bot added a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 26, 2026
…nimarket#2956

The declared checks were executed for real in the OmniNode-ai/omnimarket checkout at the PR head by the product-repo OCC receipt runner, and the results written append-only into this companion. Producer-minted PENDING receipts are superseded by a net-new record; no existing receipt was edited.

Ticket: OMN-16859 (AC3b)
Onex-Workflow: OCC Receipt Runner
Onex-Run: https://github.com/OmniNode-ai/omnimarket/actions/runs/36251674264
jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 26, 2026
…imarket#2956 (#11473)

* evidence(OMN-19513): author OCC companion for OmniNode-ai/omnimarket#2956

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

* evidence(OMN-19513): self-bind OCC#11473 + rebind contract_sha256

* evidence(OMN-16859): executed test_passes receipts for OmniNode-ai/omnimarket#2956

The declared checks were executed for real in the OmniNode-ai/omnimarket checkout at the PR head by the product-repo OCC receipt runner, and the results written append-only into this companion. Producer-minted PENDING receipts are superseded by a net-new record; no existing receipt was edited.

Ticket: OMN-16859 (AC3b)
Onex-Workflow: OCC Receipt Runner
Onex-Run: https://github.com/OmniNode-ai/omnimarket/actions/runs/36251278975

* evidence(OMN-16859): executed test_passes receipts for OmniNode-ai/omnimarket#2956

The declared checks were executed for real in the OmniNode-ai/omnimarket checkout at the PR head by the product-repo OCC receipt runner, and the results written append-only into this companion. Producer-minted PENDING receipts are superseded by a net-new record; no existing receipt was edited.

Ticket: OMN-16859 (AC3b)
Onex-Workflow: OCC Receipt Runner
Onex-Run: https://github.com/OmniNode-ai/omnimarket/actions/runs/36251674264

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
Co-authored-by: onexbot-occ-writer[bot] <307849072+onexbot-occ-writer[bot]@users.noreply.github.com>
… sequences

Two changes carried here so this node package stays byte-equal to what
omnibase_infra#4169 vendors and to what omnimarket#2958 declares:

- 0001 now asserts the omninode_runtime USAGE grant on schema omninode_internal
  with has_schema_privilege, answering the Hostile Reviewer MAJOR finding on
  omnibase_infra#4169 that it was the one issued privilege left unasserted. The
  grant set itself is unchanged: it is already exactly the writer scope. The
  file is byte-identical to the vendored copy on omnibase_infra#4169 (cmp).

- scripts/application-relation-ownership.yaml gains the two
  database_objects sequence entries, identical to omnimarket#2958, because the
  OMN-15361 SQL gate refused both sequences by name with only the tables
  declared. The ownership test from #2958 is carried too, so the hunks are
  identical and merge cleanly in either order.

Lab (.201 dev lane, omnibase-infra-postgres, database omnidash_analytics):
both files applied inside BEGIN ... ROLLBACK with ON_ERROR_STOP=1, clean; with
the schema USAGE revoked inside a rolled-back transaction the new assertion
raised division by zero. Post-state unchanged (USAGE true, 21 event rows,
4 span rows).
Resolved three census conflicts by adding this PR's increments on top of dev's figures after OMN-19716 landed. Dogfood node_dirs moves from 427 to 428 and entry_points from 418 to 419. The relation inventory's source_created_tables moves from 73 to 75 for claude_hook_events and claude_agent_spans. source_declared_tables stays at dev's 84 because the declarations landed with omnimarket#2958, and minimum_unreconciled_live_base_tables drops from 13 to 11. The inventory JSON was regenerated with the generator's --write mode, and 120 focused tests pass with 2 skipped (real Postgres).

Onex-Lane: all-hooks-contract-83
Onex-Session: 9a8f8ac2b564447cbcbd3142788e28a1
@onexbot-occ-writer

Copy link
Copy Markdown
Contributor

OCC autobind rebound this PR's evidence-source stamp line to OCC#11491.

The receipt gate's own eligibility validator, run against the change-control tree the gate pins for OCC#11491, returned eligible for head 73b260da0756782fbdac0ec136c887fe24da908e. The body now carries exactly one stamp line. Displaced: OCC#11473.

Reported by occ_companion_emitter (OMN-18853).

…evmerge

# Conflicts:
#	tests/test_market_node_runtime_dogfood.py
@onexbot-occ-writer

Copy link
Copy Markdown
Contributor

OCC autobind rebound this PR's evidence-source stamp line to OCC#11473.

The receipt gate's own eligibility validator, run against the change-control tree the gate pins for OCC#11473, returned eligible for head 63121eebbbc56c105a23959cb784c4dc1804dc8a. The body now carries exactly one stamp line. Displaced: OCC#11491.

Reported by occ_companion_emitter (OMN-18853).

Three test files conflicted on count assertions that both sides moved.
Resolved by adding this PR's node on top of dev's counts: node_dirs 434
(dev 433 + node_projection_claude_hook_events), entry_points 423 (dev 422
+ 1), source_created_tables 77 (dev 75 + claude_hook_events and
claude_agent_spans), minimum_unreconciled_live_base_tables 9
(max(0, 86 - 77)). The writer-dispatch test keeps both the
ClaudeHookEventsProjectionWriter and PrLandingProjectionWriter notes.
docs/evidence/OMN-15423-relation-inventory.json regenerated with
scripts/generate_application_relation_inventory.py --write.
@onexbot-occ-writer

Copy link
Copy Markdown
Contributor

OCC autobind rebound this PR's evidence-source stamp line to OCC#11488.

The receipt gate's own eligibility validator, run against the change-control tree the gate pins for OCC#11488, returned eligible for head 617ccc7b1fb7735eb0c8bc7bbdedf4e080205b88. The body now carries exactly one stamp line. Displaced: OCC#11473.

Reported by occ_companion_emitter (OMN-18853).

…evmerge-84e

# Conflicts:
#	docs/evidence/OMN-15423-relation-inventory.json
#	tests/test_market_node_runtime_dogfood.py
#	tests/unit/scripts/test_application_relation_inventory.py
…evmerge-84f

# Conflicts:
#	tests/test_market_node_runtime_dogfood.py
@jonahgabriel

Copy link
Copy Markdown
Contributor Author

Lab pool readback (OMN-18893, rule 24) for head 39e7cb0.

Host lab-202 (192.168.86.202), isolated project omnibase-infra-local, lane prove-omnimarket-2956-84f, driver prepr_runtime_pool (profile omnimarket.runtime_image), run 2026-09-28T20:37:58Z to 20:54:29Z. Result PASS, receipt lab_pass_receipt.v1 for exactly this sha (base 341957d).

Checks, all ok: head identity (fetched head equals expected head); stack built (build rc=0, up rc=0); runtime image identity (omnimarket package tree 4787 files against 4788 in both omninode-runtime and runtime-effects images, missing=0 differ=0); health 8085 and 8086 healthy with no failed handlers; migration gate healthy with 0 restarts; no wiring failures per the judge (runtime: autowire-fail=1, dup-dispatcher=0, ERROR lines 69, Traceback 2; effects: autowire-fail=1, dup-dispatcher=0, ERROR lines 26, Traceback 0, all within the judge's bar; I did not attribute those counts to this PR); focused tests rc=0 (projection, real-Postgres, golden chain, consumer-flow writer dispatch, dogfood roster, relation inventory).

Teardown: no omnibase-infra-local containers, no pool lease directory and no prove-pool work directory on lab-202 at 2026-09-28T21:41Z.

What this pass did not exercise: the tenant, RLS and reader-authorization paths through onex-api (rule 24(d); this PR touches none), and the live runtime auto-wiring of the node on the .201 dev lane, which is read back after merge.

The receipt was written by the prove lane; this comment was posted by lane hook-events-chain-83 after re-reading the receipt file and the host.

…evmerge-84f

# Conflicts:
#	tests/test_market_node_runtime_dogfood.py
@jonahgabriel

Copy link
Copy Markdown
Contributor Author

Lab pool readback (OMN-18893, rule 24) for head 31e80c5, the head after the second dev merge (only tests/test_market_node_runtime_dogfood.py conflicted: dev added node_pr_landing_decision_compute, so the roster asserts are node_dirs 438 and entry_points 428).

Host lab-105 (192.168.86.105), isolated project omnibase-infra-local, lane hook-events-chain-83, driver prepr_runtime_pool with --base-control, profile omnimarket.runtime_image, run 2026-09-28T21:56:35Z to 22:15:25Z. Result PASS for exactly this sha.

Checks, all ok: head identity; stack built (build rc=0, up rc=0); runtime image identity (omnimarket package tree 4797 files against 4798 in the image, missing=0 differ=0, both runtimes); health 8085 and 8086 healthy with no failed handlers; migration gate healthy with 0 restarts; no new wiring failures (the isolated bundle fails to wire 29 contracts in the runtime and 7 in runtime-effects for missing DSNs, and the base control built at dev on the same host shows the same failures; none is this PR's node); focused tests rc=0 (projection, real-Postgres, golden chain, consumer-flow writer dispatch, dogfood roster, relation inventory), and the migration probe read both omninode_internal.claude_hook_events and omninode_internal.claude_agent_spans back as existing.

A first run of this head without the base control failed only no_wiring_failures on those same inherited environment failures; the earlier head 39e7cb0 passed the same way on lab-202 (lane prove-omnimarket-2956-84f).

Teardown: restored=yes (containers 0, volumes 0, networks 0, images 0, listeners 0, workdir gone, snapshot diff 0). The receipt verifier line reads MISSING_MANDATORY_CHECK for changed_path_live because no live-grep list was passed, the same note the omnimarket#3045 proof carried.

Not exercised: onex-api tenant, RLS and reader-authorization paths (this PR touches none), and the runtime's auto-wiring of the node on the .201 dev lane, which is read back after merge.

@jonahgabriel
jonahgabriel merged commit 2ddaf9b into dev Sep 28, 2026
271 of 305 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-19513-claude-hook-events-projection branch September 28, 2026 22:19
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.

2 participants