Bind Codex hook delivery to the consuming thread
Summary
A Codex subagent hook can carry its parent session ID and inherit its parent's environment while a shared app-server owns both threads' writer locks. That allowed a message addressed to the parent to be consumed by a child hook. A real GitHub comment demonstrated this: the registration targeted the root, but the native developer message appeared only in its child rollout.
Bind PostToolUse and Stop consumption to the hook's turn_id matching the requested target rollout's verified active turn. The rollout is resolved locally using the existing reader; the nearest trusted Codex ancestor must hold its writer lock and the rollout with matching captured birth timestamp and canonical path before and after the read. UID and process-start/ancestry checks remain intact. The optional ordinary caller resolver constrains contradictions: a child caller cannot claim its parent even with the parent’s correct turn. An inherited ancestor ID is permitted only when the existing held-rollout parent-chain reader proves the requested thread is its descendant, with captured birth/path validation around the read. The ancestry check takes one kernel file snapshot before header reads and one afterward, avoiding repeated full scans per rollout. This does not authorize a hook by itself: requested-target active-turn and trusted-writer evidence are still mandatory. Inherited THREAD/SESSION environment values cannot authorize consumption, and fresh hooks do not require them. Ordinary caller identity APIs are unchanged.
Codex documents parent session IDs for subagent hooks and an active turn ID for consuming hooks. SessionStart has no turn ID: it receives context only when the trusted writer holds the requested thread alone. Shared writers defer SessionStart context; a subsequent verified PostToolUse can deliver registration guidance. Mismatches leave pending offers and guidance untouched; valid parent and child hooks each receive their own queue exactly once.
The outer hook CLI already converts socket refusals and non-2xx responses to {} with a successful exit, so a rejected hook silently defers delivery. This PR changes no installed hook, runtime binary, configuration, or previously claimed message.
Test Plan
- Exact-base/head RelayFlow proof: all three actual Outbound/LocalSessions regressions fail on
cf0b8fd27c4c182920a5f7ad238f30ab2ed9b98eand pass on96e804cbbb490d1cc3b78d8f5931cf3ebcb9d010. Also demonstrated all three stronger regressions fail on the intermediate environment-conjunction guard, and that the child-caller/parent-turn negative fails on the turn-only implementation. Restoring valid inherited-parent-environment child offer/guidance positives then reproduced 1 pass / 2 failures on8201b40a1; this head passes all three. - A caller identified as CHILD cannot claim PARENT even with the correct parent turn. Inherited parent THREAD + child turn cannot consume a parent offer; parent and child valid turns each take their own offer once, for both PostToolUse and Stop. Valid child offers and registration guidance work without resetting inherited parent environment, only with proven held ancestry. Missing, invalid, unrelated, reverse or stale ancestor evidence fails closed. Shared SessionStart defers without consuming guidance. A fresh hook without THREAD or resume arguments succeeds with verified turn/writer evidence; fresh sole-lock SessionStart without THREAD or resume succeeds through the actual Outbound route; legacy sole-lock resume and UUID case handling remain supported.
- Bounded scan-count regression: 2 versus 12 held rollouts caused 5 versus 25
opened_filescalls on the previous implementation; the current implementation makes exactly 2 for both. The old source fails this assertion; a fresh current-source rebuild passes. - Existing hook tests: 4 pass; process/caller tests: 15 pass; fresh-kernel LocalSessions test: 1 pass; existing Codex tests: 301 pass, 2 ignored.
- Rustfmt, pinned Prettier, and final Veto code/security/secrets review pass. Quality comparison has no new findings (105 inherited on head versus 106 on base); the touched process-file ceiling is tightened.
- Current-head CI and independent review.
- Fresh same-parent live comment test after an approved runtime update.
RelayFlow Proof
- Change type:
bugfix - RelayFlow case:
codex-hook-thread-affinity
The case overlays the same regression fixture onto exact clean base/head production implementations, using a trusted shared app-server holding both thread locks and both active rollouts. It verifies actual test results and restores the fixture overlays. Dirty production and harness inputs are rejected before Cargo runs.