Skip to content

feat(lineage): resolve Codex subagent parent and depth from the thread's own rollout (#423) - #480

Merged
ScriptedAlchemy merged 5 commits into
mainfrom
feat/codex-host-gaps-in-framework
Sep 4, 2026
Merged

feat(lineage): resolve Codex subagent parent and depth from the thread's own rollout (#423)#480
ScriptedAlchemy merged 5 commits into
mainfrom
feat/codex-host-gaps-in-framework

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 3, 2026

Copy link
Copy Markdown
Owner

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_path on SubagentStop), and that file's first line — the session_meta Codex 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 read CODEX_HOME/sessions (or a rollout not yet flushed) still falls back to inference — that residue is recorded on the issue.

Gap-by-gap

#423 gap Envelope evidence Verdict What landed
1. SubagentStart carries agent_id, agent_type, turn_id, root session_id, no parent_thread_id fixtures/host-lineage/codex-0.147.0.ndjson rows 17, 28 Supported (derived) SubagentStart.transcript_path is the new thread's own rollout; its head names the parent and depth. Registry places the node with resolution: 'transcript', provenance derived. Verified on all 8,049 thread-spawn rollouts on the capturing machine (cli 0.130.0 → 0.152.0); matrix §10.
2. Hooks inside a subagent carry root + self only; SubagentStop hints at the parent via transcript_path rows 19–27, 30–36 (own rollout), 37 and 39 (transcript_path = parent rollout) Supported (derived) A thread first seen mid-flight (cold registry) is placed from the rollout its hook names; a stop for an unplaced thread is placed from agent_transcript_path; an inferred parent is corrected at SubagentStop from the parent rollout basename (rollout-<ts>-<thread>.jsonl), descendants shift with it. Standalone hooks (no warm runtime) resolve the same way via the new resolveStandaloneLineage.
3. spawn_agent tool_input.message encrypted; PostToolUse {"task_name": …} fires before SubagentStart, no child id rows 15–17, 26–28 Supported (derived) The PostToolUse tool_response.task_name is a path (/root/host_probe, /root/host_probe/nested_probe) equal to the child rollout's agent_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 certain toolCallId. 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 whose payload.id ≠ the payload's agent_id), codexThreadFromRolloutPath.
  • registry.ts: readTranscript option (default fs, tests inject captured heads); claimSpawn accepts a parent/agent-path target; placeCodexFromRollout, placeUnknownCodexThread; Codex branches in observeStart/observeStop; resolve() reports transcript.
  • state.ts: LineageNode.placement?: 'transcript', OpenToolCall.agentPath?, toolCallClosed.agentPath?, new nodeReparented event (shifts descendants).
  • AgentLineageResolution gains 'transcript' (runtime type, workbench zod enum, contracts wire type).
  • lineage-native.ts: resolveStandaloneLineage (async; resolveNativeLineage unchanged); generated standalone wrapper awaits it (hook-contract.ts).
  • Capability table codex-0.147.0.json: lineage.parent and lineage.depthsupported with dated evidence (hosts page regenerates).
  • Docs: docs/entry-conventions.md, matrix §5/§6/§7 + new §10, website/docs/{en,zh}/guide/authoring/mcp.mdx.
  • Fixtures: fixtures/host-lineage/codex-0.147.0-rollouts/ — one session_meta head 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 + exact toolCallIds; 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 _meta case; stop-only placement; SubagentStop correction with depth shift; replay/idempotency; standalone resolution incl. agent_transcript_path on stop.
  • adapter-capability-states.test.ts: Codex parent/depth now supported; target-hook-contract.test.ts: wrapper awaits resolveStandaloneLineage.
  • Local: build, unit (3044), route-unit (44), projection (145), integration (975), lint, typecheck, pnpm docs:site:build all green.

Non-goals (decisions respected)

Review status

  • Reviewed by chatgpt-codex-connector at 189bd4c (two findings). Both addressed by code (originally 82c199b, carried through the rebases):
    • Depth after a stop-time parent correction to an unseen parent: observeStop reads 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".
    • A recorded agent path must not claim a differently-pathed spawn: claimSpawn restricts candidates to calls whose agentPath matches 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".
  • Rebased onto main through chore(ci): remove the release audit gate #487. Merge notes:
    • feat(lineage): confirm Claude subagent parent edges from the parent's Agent PostToolUse (#422) #486 (Claude confirmed resolution, unplaced starts): AgentLineageResolution is now native | registry | confirmed | transcript | inferred everywhere (runtime type, contracts, workbench zod enum, examples/worktree-proximity provenance enum, docs en+zh). resolve() reports transcript for a rollout-placed node, else registryResolution.
    • A Codex start that main now 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); nodeStarted drops the unplaced entry. Tests: "places a start held unplaced … once a later hook finds the rollout readable", "materializes an unplaced start as already stopped …".
    • chore(examples): use built-in framework surface over hand-rolled setup #473 (examples/worktree-proximity mirrors the runtime enum): transcript added there, README updated.
  • Current head: 6614fdb. No fresh review requested; merging on green CI per maintainer instruction. Threads opened after merge are answered in a follow-up PR.

@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6614fdb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@agent-bundle/runtime Patch
agent-bundle Patch

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T23:44:54.720881Z 6bf206f PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ScriptedAlchemy added a commit that referenced this pull request Sep 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +553 to +554
const parentDepth = parent === node.root ? 0 : nodeFor(parent)?.depth;
await dispatch('nodeReparented', { ...(parentDepth === undefined ? {} : { depth: parentDepth + 1 }), id: stopped, parent }, keys);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Comment on lines +325 to +327
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

ScriptedAlchemy added a commit that referenced this pull request Sep 3, 2026
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/codex-host-gaps-in-framework branch from 00c80e9 to 189bd4c Compare September 3, 2026 23:47
@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@480
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@480
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@480

commit: 6614fdb

ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
… 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.
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
… 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.
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/codex-host-gaps-in-framework branch from 82c199b to feacef4 Compare September 4, 2026 00:16
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
… 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.
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/codex-host-gaps-in-framework branch from feacef4 to df070ac Compare September 4, 2026 01:02
…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.
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/codex-host-gaps-in-framework branch from df070ac to 6614fdb Compare September 4, 2026 01:02
@ScriptedAlchemy
ScriptedAlchemy merged commit 48c89c1 into main Sep 4, 2026
13 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the feat/codex-host-gaps-in-framework branch September 4, 2026 01:25
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
#502)

#490 removed `headers.detail` as unused; #480 landed concurrently and its
new lineage tables read it, so the main docs build failed to typecheck.
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