feat(lineage): resolve Codex subagent parent and depth from the thread's own rollout (#423) - #480
Conversation
🦋 Changeset detectedLatest commit: 6614fdb The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6bf206f427
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const parentDepth = parent === node.root ? 0 : nodeFor(parent)?.depth; | ||
| await dispatch('nodeReparented', { ...(parentDepth === undefined ? {} : { depth: parentDepth + 1 }), id: stopped, parent }, keys); |
There was a problem hiding this comment.
Recompute depth when correcting to an unseen parent
In the Codex SubagentStop path, when the basename identifies a non-root parent that the registry has never placed, parentDepth is undefined and nodeReparented preserves the depth inferred from the old, incorrect parent. For example, a child initially misfiled directly under the root remains at depth 1 after being reparented beneath an unseen subagent, leaving its lineage and all descendants too shallow. The same stop event's agent_transcript_path points to the child's rollout and can supply the exact depth, so use that evidence when correcting the parent.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 82c199b. At SubagentStop the registry now reads the child's own rollout (agent_transcript_path) first: when readable it re-parents with the exact parent_thread_id and depth from thread_spawn and marks the node transcript; only when that head is unreadable does it fall back to the parent basename in transcript_path (depth follows the parent when known, otherwise stays — recorded as the residual case). Tests: "takes exact parent and depth from the child rollout at SubagentStop when the parent thread was never placed" (misfiled at depth 1 under the root → depth 2 under an unseen parent, grandchild shifts to 3) and "keeps the inferred depth when only the parent basename is available and that parent is unknown".
| const fromParent = target === undefined ? underRoot : underRoot.filter((call) => call.conversation === target.parent); | ||
| const byPath = target?.agentPath === undefined ? [] : fromParent.filter((call) => call.agentPath === target.agentPath); | ||
| const candidates = byPath.length > 0 ? byPath : fromParent; |
There was a problem hiding this comment.
Refuse pending spawns with a different known agent path
When the rollout provides an exact agentPath but none of the parent's pending spawns matches it, this falls back to every spawn from that parent. If child B's spawn hook was missed while child A's pending call has a different recorded path, B consumes A's call and reports A's toolCallId as certain, preventing A from claiming it later. A supplied path should restrict candidates to matching or pathless calls rather than explicitly nonmatching ones.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 82c199b. With an exact agentPath from the rollout, claimSpawn now restricts the parent's pending spawns to those recorded with the same path or with no path at all (response never observed); a call recorded with a different path is never consumed. Tests: "never consumes a sibling's spawn call whose recorded path differs from the starting child's" (B starts with /root/b while only A's /root/a call is pending → B gets no toolCallId, A later claims its own call) and "still claims a spawn call whose response (and path) was missed, when it is the parent's only pending one".
00c80e9 to
189bd4c
Compare
commit: |
… depth; a recorded agent path never claims a differently-pathed spawn Review threads on #480: (1) reparenting to a parent the registry never placed kept the inferred depth; the same SubagentStop names the child's own rollout in agent_transcript_path, so exact parent and depth are read from it first, with the parent-rollout basename as the fallback. (2) with an exact agent path from the rollout, pending spawns recorded with a different path are excluded; only matching or pathless (response missed) calls remain eligible.
… depth; a recorded agent path never claims a differently-pathed spawn Review threads on #480: (1) reparenting to a parent the registry never placed kept the inferred depth; the same SubagentStop names the child's own rollout in agent_transcript_path, so exact parent and depth are read from it first, with the parent-rollout basename as the fallback. (2) with an exact agent path from the rollout, pending spawns recorded with a different path are excluded; only matching or pathless (response missed) calls remain eligible.
82c199b to
feacef4
Compare
… depth; a recorded agent path never claims a differently-pathed spawn Review threads on #480: (1) reparenting to a parent the registry never placed kept the inferred depth; the same SubagentStop names the child's own rollout in agent_transcript_path, so exact parent and depth are read from it first, with the parent-rollout basename as the fallback. (2) with an exact agent path from the rollout, pending spawns recorded with a different path are excluded; only matching or pathless (response missed) calls remain eligible.
feacef4 to
df070ac
Compare
…d's own rollout (#423) SubagentStart names no parent thread, but every Codex hook payload names the thread's rollout, whose session_meta head records source.subagent.thread_spawn.{parent_thread_id, depth, agent_path}. The registry reads it at start, on the first hook of a thread seen mid-flight, and at SubagentStop (resolution: 'transcript', provenance derived); the spawn_agent call is matched to the child by agent_path (= the call's PostToolUse tool_response.task_name), so two parents with unclaimed spawns are no longer ambiguous and toolCallId is exact. An inferred parent (rollout unreadable) is corrected at SubagentStop from the parent rollout named in transcript_path. Standalone hooks resolve the same way via resolveStandaloneLineage. Codex lineage.parent/depth rows move to supported.
… depth; a recorded agent path never claims a differently-pathed spawn Review threads on #480: (1) reparenting to a parent the registry never placed kept the inferred depth; the same SubagentStop names the child's own rollout in agent_transcript_path, so exact parent and depth are read from it first, with the parent-rollout basename as the fallback. (2) with an exact agent path from the rollout, pending spawns recorded with a different path are excluded; only matching or pathless (response missed) calls remain eligible.
…ranscript' lineage resolution
…by a later hook, as it started
df070ac to
6614fdb
Compare
Summary
Framework-side support for the Codex host-side lineage gaps tracked in #423, without contacting the vendor. Every Codex hook payload names the thread's rollout (
transcript_path;agent_transcript_pathonSubagentStop), and that file's first line — thesession_metaCodex writes when it creates the thread — records what the payload omits:source.subagent.thread_spawn.{parent_thread_id, depth, agent_path}. The registry now reads it instead of guessing from spawn ordering.Refs #423. Not "Fixes": the hook payload itself still carries no
parent_thread_id, so a hook that cannot readCODEX_HOME/sessions(or a rollout not yet flushed) still falls back to inference — that residue is recorded on the issue.Gap-by-gap
SubagentStartcarriesagent_id,agent_type,turn_id, rootsession_id, noparent_thread_idfixtures/host-lineage/codex-0.147.0.ndjsonrows 17, 28SubagentStart.transcript_pathis the new thread's own rollout; its head names the parent and depth. Registry places the node withresolution: 'transcript', provenancederived. Verified on all 8,049 thread-spawn rollouts on the capturing machine (cli 0.130.0 → 0.152.0); matrix §10.SubagentStophints at the parent viatranscript_pathtranscript_path= parent rollout)agent_transcript_path; an inferred parent is corrected atSubagentStopfrom the parent rollout basename (rollout-<ts>-<thread>.jsonl), descendants shift with it. Standalone hooks (no warm runtime) resolve the same way via the newresolveStandaloneLineage.spawn_agenttool_input.messageencrypted;PostToolUse{"task_name": …}fires beforeSubagentStart, no child idPostToolUsetool_response.task_nameis a path (/root/host_probe,/root/host_probe/nested_probe) equal to the child rollout'sagent_path. Kept on the pending spawn (toolCallClosed.agentPath), so the start claims its exact call by parent + path: two parents with unclaimed spawns are no longer refused, and same-parent siblings get a certaintoolCallId. The encrypted message is irrelevant to lineage.Changes
packages/rsc-runtime/src/lineage/codex-rollout.ts(new): bounded first-line reader (readCodexRolloutHead, 1 MiB cap; observed 13–43 KiB),parseCodexRolloutMeta,readCodexSpawnLineage(refuses a rollout whosepayload.id≠ the payload'sagent_id),codexThreadFromRolloutPath.registry.ts:readTranscriptoption (default fs, tests inject captured heads);claimSpawnaccepts a parent/agent-path target;placeCodexFromRollout,placeUnknownCodexThread; Codex branches inobserveStart/observeStop;resolve()reportstranscript.state.ts:LineageNode.placement?: 'transcript',OpenToolCall.agentPath?,toolCallClosed.agentPath?, newnodeReparentedevent (shifts descendants).AgentLineageResolutiongains'transcript'(runtime type, workbench zod enum, contracts wire type).lineage-native.ts:resolveStandaloneLineage(async;resolveNativeLineageunchanged); generated standalone wrapper awaits it (hook-contract.ts).codex-0.147.0.json:lineage.parentandlineage.depth→supportedwith dated evidence (hosts page regenerates).docs/entry-conventions.md, matrix §5/§6/§7 + new §10,website/docs/{en,zh}/guide/authoring/mcp.mdx.fixtures/host-lineage/codex-0.147.0-rollouts/— onesession_metahead per capture thread (ids/paths/agent paths from the capture; shape from real 0.147.0 rollouts; README states what is redacted).Tests
packages/rsc-runtime/tests/lineage-codex-rollout.test.ts(new, 17 tests): parser on captured heads, root/helper/older-build shapes, foreign-thread refusal; full capture replay with rollouts →transcript+ exacttoolCallIds; replay with rollouts unreadable → unchanged pre-PR behaviour (registry); two-parent ambiguity resolved; cold-registry placement from a tool hook and the previously refused grandchild_metacase; stop-only placement;SubagentStopcorrection with depth shift; replay/idempotency; standalone resolution incl.agent_transcript_pathon stop.adapter-capability-states.test.ts: Codexparent/depthnowsupported;target-hook-contract.test.ts: wrapper awaitsresolveStandaloneLineage.pnpm docs:site:buildall green.Non-goals (decisions respected)
request.lineageis populated (feat(lineage): request.lineage is the only identity-adjacent surface; scoped, self-correcting Cursor child binding #444).agent_nickname/agent_rolein the rollout are not read.codex --version).Review status
chatgpt-codex-connectorat 189bd4c (two findings). Both addressed by code (originally 82c199b, carried through the rebases):observeStopreads the child's own rollout (agent_transcript_path) first and takes parent and depth from it; the parent-basename fallback recomputes depth only when the parent is known. Tests: "takes exact parent and depth from the child rollout at SubagentStop when the parent thread was never placed", "keeps the inferred depth when only the parent basename is available and that parent is unknown".claimSpawnrestricts candidates to calls whoseagentPathmatches or was never recorded. Tests: "never consumes a sibling's spawn call whose recorded path differs", "still claims a spawn call whose response (and path) was missed".mainthrough chore(ci): remove the release audit gate #487. Merge notes:confirmedresolution, unplaced starts):AgentLineageResolutionis nownative | registry | confirmed | transcript | inferredeverywhere (runtime type, contracts, workbench zod enum,examples/worktree-proximityprovenance enum, docs en+zh).resolve()reportstranscriptfor a rollout-placed node, elseregistryResolution.mainnow holds unplaced (rollout unreadable at start, no claimable spawn) is placed from its rollout by any later hook of that thread, as it started (time, type, and already stopped when the stop came first);nodeStarteddrops the unplaced entry. Tests: "places a start held unplaced … once a later hook finds the rollout readable", "materializes an unplaced start as already stopped …".examples/worktree-proximitymirrors the runtime enum):transcriptadded there, README updated.