Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/codex-lineage-from-rollout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@agent-bundle/runtime': patch
'agent-bundle': patch
---

Resolve a Codex subagent's `request.lineage` parent and depth from the thread's own rollout instead of spawn ordering: the registry reads the `session_meta` head of the rollout every hook payload names in `transcript_path` (`agent_transcript_path` on `SubagentStop`), places the thread with the new `resolution: 'transcript'` (provenance `derived`), matches the `spawn_agent` call to the child by `agent_path`, and corrects an inferred parent at `SubagentStop` from the parent rollout it names. Standalone hooks gain `resolveStandaloneLineage` for the same read. The Codex capability table's `lineage.parent` and `lineage.depth` rows move to `supported`. Refs #423 (#480)
53 changes: 50 additions & 3 deletions docs/audits/2026-09-03-host-lineage-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,15 @@ gives shell commands the agent runs `CURSOR_CONVERSATION_ID`/`CURSOR_REQUEST_ID`
| Host | Answer | How |
| --- | --- | --- |
| Claude | Yes, with the parent inferred | `SubagentStart` names the child (`agent_id`). The parent is not in the payload; it is the agent whose `Agent`/`Task` `PreToolUse` is open when `SubagentStart` fires (root when none is open) — re-verified live at depth 1 and 2 in all three runs, including two spawns issued in one message (the host serialises them: each `SubagentStart` fires before the next `Agent` `PreToolUse` opens). The host then confirms the link on the parent's `Agent` `PostToolUse` (`tool_response.agentId` = the child, `status` `async_launched` or `completed`). `Stop`/`SubagentStop` list the children still running **in the background** in `background_tasks[]` (empty when every child ran in the foreground). |
| Codex | Yes | `SubagentStart` names the child thread; the parent is inferred from the open `collaborationspawn_agent` call and confirmed at `SubagentStop` by the parent rollout in `transcript_path`. MCP calls carry `parent_thread_id` directly. |
| Codex | Yes | `SubagentStart` names the child thread and its rollout (`transcript_path`); the rollout's `session_meta` head records `source.subagent.thread_spawn.{parent_thread_id, depth, agent_path}` (§10), so the parent is read, not inferred. The `collaborationspawn_agent` call is matched to the child by `agent_path` (= the call's `PostToolUse` `tool_response.task_name`, rows 16/27). With the rollout unreadable, the parent is inferred from the newest unclaimed spawn call and corrected at `SubagentStop` — from the child's own rollout (`agent_transcript_path`) when readable by then, else from the parent rollout in `transcript_path`. MCP calls carry `parent_thread_id` directly. |
| Cursor | Yes for the spawn, weakly for the child's traffic | `subagentStart` carries `parent_conversation_id`, `subagent_id`/`tool_call_id`, `is_parallel_worker`. The child's own `conversation_id` is not in that payload, so the first event with an unseen conversation id after a `subagentStart` is bound to it (unambiguous when children start sequentially; ambiguous for parallel workers). |

**Can a plugin running under a subagent know its parent/root?**

| Host | Root | Parent |
| --- | --- | --- |
| Claude | Yes — `session_id` on every event is the root session (re-verified live: all 42 + 46 + 127 hook payloads across the three runs carry the root id — through resumed turns and a compaction too — and `CLAUDE_CODE_SESSION_ID` in every plugin process is the root id) | Only through the runtime's registry (placed at `SubagentStart`, confirmed by the parent's `Agent` PostToolUse); nothing in the child's payload |
| Codex | Yes — `session_id` is the root thread on every event, and `_meta.x-codex-turn-metadata.session_id` on MCP calls | Yes on MCP calls (`parent_thread_id`); on hooks only through the registry (or the parent rollout at `SubagentStop`) |
| Codex | Yes — `session_id` is the root thread on every event, and `_meta.x-codex-turn-metadata.session_id` on MCP calls | Yes on MCP calls (`parent_thread_id`); on hooks from the thread's own rollout head, which every hook inside the thread names in `transcript_path` (§10) — even in a standalone hook process with no registry |
| Cursor | Only through the registry — a child's payload carries neither root nor parent | Only through the registry (ordering-bound) |

**Operator identity is out of scope.** The maintainer decided on 2026-09-03
Expand All @@ -342,12 +342,18 @@ root, and the parent-of-subagent chain — is the only identity-adjacent surface
- Hook→MCP correlation: Codex from `_meta`, Claude from
`claudecode/toolUseId`, Cursor from the open `MCP:<tool>` pre-tool hook.
- Claude `PostToolUse` string `tool_response` (MCP tools) accepted.
- Codex hook-side parent and depth read from the thread's own rollout head
(§10; `resolution: 'transcript'`), with the spawn call matched by
`agent_path`; `SubagentStop` corrects an inferred parent from the parent
rollout it names; standalone Codex hooks resolve the same way
(`resolveStandaloneLineage`). Landed after #444 for #423.

## 7. Gaps and host-blocked items

| Gap | Host | Evidence | Status |
| --- | --- | --- | --- |
| No parent id on `SubagentStart`; child events carry no parent | Claude, Codex | §1, §2 | Inferred from the newest unclaimed spawn call under the same root; refused when two parents have unclaimed spawns; filed as #422 / #423 |
| No parent id on `SubagentStart`; child events carry no parent | Claude | §1, §2 | Inferred from the newest unclaimed spawn call under the same root; refused when two parents have unclaimed spawns; filed as #422 |
| No parent id on `SubagentStart`; child events carry no parent | Codex | §1, §2, §10 | Ours: read from the thread's own rollout head (`thread_spawn.parent_thread_id`, `depth`), which every hook names in `transcript_path`; spawn call matched by `agent_path`; inferred parent corrected at `SubagentStop`. Remaining host-side: the payload itself carries no `parent_thread_id`, so a hook on a machine that cannot read `CODEX_HOME/sessions` (or a rollout not yet flushed) falls back to inference (#423) |
| Child conversation id absent from `subagentStart`; child events carry no parent/root | Cursor | §1, §2 | Bound by elimination in the registry (single pending start per workspace); refused while ambiguous for parallel workers; filed as #424 |
| `_meta` carries no conversation/tool-call id | Cursor | §3 | Hook-correlated only; filed |
| `sessionStart` never dispatched on the desktop (`workspaceOpen`/`sessionEnd` are) | Cursor | §1, §9 | Host-side (#424 gap 4); lineage never depends on it to establish a root |
Expand Down Expand Up @@ -437,3 +443,44 @@ Consequences for lineage:
`subagentStart`/`subagentStop` delivery is unobserved here; the §1 CLI
capture remains the evidence for the subagent families.


## 10. Codex rollout heads: the parent the hook payloads omit (added 2026-09-03)

Every Codex hook payload names a rollout file. Inside a subagent thread it is
the thread's own (`transcript_path` on `SubagentStart` and on every tool hook,
fixture rows 17, 19–27, 28, 30–36; `agent_transcript_path` on `SubagentStop`,
rows 37 and 39, where `transcript_path` is the **parent's** rollout). The
rollout basename is `rollout-<YYYY-MM-DDTHH-MM-SS>-<thread id>.jsonl`, and its
first line is a `session_meta` item. For a spawned thread that line carries the
edge the hook payload does not:

```json
{"type":"session_meta","payload":{"id":"<thread>","session_id":"<root>","parent_thread_id":"<parent>","forked_from_id":"<parent>",
"source":{"subagent":{"thread_spawn":{"parent_thread_id":"<parent>","depth":2,"agent_path":"/root/host_probe/nested_probe","agent_nickname":"…","agent_role":null}}},
"thread_source":"subagent","agent_path":"/root/host_probe/nested_probe","cli_version":"0.147.0", …}}
```

Checked on the capturing machine's own `~/.codex/sessions` (8,567 rollouts,
cli 0.130.0 → 0.152.0): all 8,049 `thread_spawn` rollouts carry
`source.subagent.thread_spawn.parent_thread_id` and `depth`; `agent_path`
is present from 0.141.0 on (absent on 839 older rollouts, `null` inside
`thread_spawn` on ≤0.136.0); the top-level `parent_thread_id` copy appears
later than the nested one. Roots carry a string `source` (`exec`, `cli`,
`vscode`) and `thread_source: "user"`; host-internal helpers carry
`{"subagent":{"other":"guardian"}}` with no spawn lineage. First-line size:
13–43 KiB (`base_instructions` is inlined). The `agent_path` equals the
`spawn_agent` `PostToolUse` `tool_response.task_name` (`/root/host_probe`,
`/root/host_probe/nested_probe`, rows 16 and 27), which is how the spawning
call is matched to the child without relying on order.

`fixtures/host-lineage/codex-0.147.0-rollouts/` holds one head per thread of
the capture in this shape (ids, paths, agent paths, and commit from the
capture; long strings redacted — see its README). The registry reads the head
the payload names, verifies `payload.id` equals the payload's `agent_id`, and
places the thread with `resolution: 'transcript'`; the MCP `_meta` lineage
(§3) and the hook-side lineage therefore agree by construction. What stays
host-side: the payload itself still carries no `parent_thread_id`, so a hook
that cannot read `CODEX_HOME/sessions` (or a rollout not yet flushed) falls
back to spawn-ordering inference and is corrected at `SubagentStop` (from the
child's own rollout when readable by then, else from the parent rollout's
basename).
36 changes: 27 additions & 9 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ interface AgentLineage {
depth: number; // 0 at the root, +1 per subagent level
generation?: string; // Cursor generation_id, Codex turn_id, Claude prompt_id
subagent?: { id: string; type?: string; toolCallId?: string; isParallelWorker?: boolean };
resolution: 'native' | 'registry' | 'confirmed' | 'inferred';
resolution: 'native' | 'registry' | 'confirmed' | 'transcript' | 'inferred';
}
```

Expand Down Expand Up @@ -473,15 +473,31 @@ for every event by the id the payload carries. The observed host vocabulary
| Host | `conversation` | `root` | Parent of a new subagent | MCP call correlation |
| --- | --- | --- | --- | --- |
| Claude | `agent_id`, else `session_id` | `session_id` | the agent whose `Agent`/`Task` `PreToolUse` is the newest unclaimed spawn; confirmed by that agent's `Agent` `PostToolUse` (`tool_response.agentId` = the child) | `_meta["claudecode/toolUseId"]` = the open `PreToolUse` `tool_use_id` |
| Codex | `agent_id`, else `session_id` | `session_id` | the thread whose `spawn_agent` call is the newest unclaimed spawn | `_meta["x-codex-turn-metadata"]` carries `thread_id`, `parent_thread_id`, `session_id`, `turn_id` natively |
| Codex | `agent_id`, else `session_id` | `session_id` | `source.subagent.thread_spawn.parent_thread_id` (and `depth`) in the head of the thread's own rollout, which every hook names in `transcript_path` (`agent_transcript_path` on `SubagentStop`); with that file unreadable, the thread whose `spawn_agent` call is the newest unclaimed spawn | `_meta["x-codex-turn-metadata"]` carries `thread_id`, `parent_thread_id`, `session_id`, `turn_id` natively |
| Cursor | `conversation_id` | the bound root | `parent_conversation_id` on `subagentStart`; the child's fresh `conversation_id` is bound to the single pending start in the same workspace when it first speaks | the newest open `preToolUse` whose `tool_name` is `MCP:<tool>`; when several conversations have that tool open, the one whose hook `tool_input` equals the call's `arguments` (identical arguments stay `id-not-resolvable`) |

A Claude or Codex subagent is placed only when its spawning pre-tool hook
(`Agent`/`Task`, `collaborationspawn_agent`) was observed, so projects that
want `parent`/`depth` for subagents route `tool/before` alongside
`agent/start`; a start with no claimable spawn — none open, or several
parents with one — stays `id-not-resolvable`, and the registry keeps what the
start said (id, type, time, and a stop that follows) as an unplaced start.
A Claude subagent is placed only when its spawning pre-tool hook
(`Agent`/`Task`) was observed, so projects that want `parent`/`depth` for
subagents route `tool/before` alongside `agent/start`; a start with no
claimable spawn — none open, or several parents with one — stays
`id-not-resolvable`, and the registry keeps what the start said (id, type,
time, and a stop that follows) as an unplaced start.

A Codex thread carries its own evidence: the rollout its payload names opens
with a `session_meta` line recording the spawning thread, the depth, and the
`agent_path`, so the registry places it from that file (`resolution:
'transcript'`, provenance `derived`) at `SubagentStart`, on the first hook of
a thread it never saw start (or held unplaced), and at `SubagentStop` — no
spawn ordering involved, and two parents with unclaimed spawns are no longer
ambiguous. The `spawn_agent` call is still claimed, by parent and by the
`agent_path` its `PostToolUse` `tool_response.task_name` carried, so
`subagent.toolCallId` is exact even for same-parent siblings. Only when the
rollout is unreadable does Codex fall back to the Claude rule (`resolution:
'registry'`), and a parent inferred that way is corrected at `SubagentStop`,
— from the child's own rollout (`agent_transcript_path`, exact parent and
depth) when it is readable by then, else from the parent rollout in
`transcript_path` (`rollout-<timestamp>-<thread id>.jsonl`); descendants move
with it.

Claude Code names no parent on any hook a subagent emits (#422; hooks
reference "Common input fields": `agent_id` and `agent_type` are the only
Expand Down Expand Up @@ -559,7 +575,9 @@ target defines no subagent families — portable), `id-not-resolvable` (the
payload names an agent the registry never saw start, e.g. a cold runtime),
`cloud-agent-no-user-hooks` (Cursor cloud agents run no user hooks),
`no-shared-runtime` (a standalone hook process holds no registry; Claude and
Codex root payloads still resolve to depth 0 from the payload alone),
Codex root payloads still resolve to depth 0 from the payload alone, and a
Codex subagent payload resolves fully from the rollout it names —
`resolveStandaloneLineage`),
`unsupported-surface` (routed CLI and rendered scripts run outside any host
conversation), or `not-provided` (no registry was mounted). Per-host
capability rows live under `lineage` in each pinned capability table.
Expand Down
7 changes: 4 additions & 3 deletions examples/worktree-proximity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ the runtime's `outside-invocation` error outside a request.

Every identity claim records where it came from. `native` is read from the
host envelope (or a `request.lineage` the runtime resolved natively),
`registry`, `inferred` and `confirmed` are the runtime lineage registry's own
resolutions (`confirmed` once the host has named every edge up to the root),
and `derived` is this application's fallback:
`registry`, `inferred`, `confirmed` and `transcript` are the runtime lineage
registry's own resolutions (`confirmed` once the host has named every edge up
to the root; `transcript` is read from the host's own rollout file), and
`derived` is this application's fallback:

- `session/start` observes `session:<root>` as the root actor, where the root
is `(await agent()).lineage.root` when the runtime resolved a lineage and the
Expand Down
9 changes: 5 additions & 4 deletions examples/worktree-proximity/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ const nonEmpty = z.string().trim().min(1);
/**
* Where an identity claim came from: `native` is read straight from the host
* envelope (or a `request.lineage` the runtime resolved natively), `registry`,
* `inferred` and `confirmed` are the runtime lineage registry's own
* resolutions (`confirmed` once the host has named every edge to the root),
* and `derived` is this application's fallback (`worktree:<root>`).
* `inferred`, `confirmed` and `transcript` are the runtime lineage registry's
* own resolutions (`confirmed` once the host has named every edge to the root;
* `transcript` read from the host's own rollout file), and `derived` is this
* application's fallback (`worktree:<root>`).
*/
const provenance = z.enum(['native', 'registry', 'inferred', 'confirmed', 'derived']);
const provenance = z.enum(['native', 'registry', 'inferred', 'confirmed', 'transcript', 'derived']);

export type IdentityProvenance = z.output<typeof provenance>;

Expand Down
21 changes: 21 additions & 0 deletions fixtures/host-lineage/codex-0.147.0-rollouts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Codex 0.147.0 rollout heads for the lineage capture

One file per thread of the `codex-0.147.0.ndjson` capture, named exactly as the
hook payloads name them in `transcript_path` / `agent_transcript_path`
(`rollout-<timestamp>-<thread id>.jsonl`). Each holds the `session_meta` line
Codex writes first, followed by one redacted `response_item` line so readers
prove they stop at the first newline.

Provenance: the ids, paths, timestamps, `agent_path` values (the
`spawn_agent` `PostToolUse` `tool_response.task_name` of the capture, rows 16
and 27), and git commit are the capture's own. The `session_meta` field set and
nesting (`source.subagent.thread_spawn.{parent_thread_id, depth, agent_path,
agent_nickname, agent_role}`, `thread_source`, `forked_from_id`,
`parent_thread_id`, `subagent_history_start_ordinal`, `context_window`) are
copied from real cli 0.147.0 rollouts on the capturing machine
(`~/.codex/sessions`, 1,191 thread-spawn rollouts at that version; every one of
the 8,049 thread-spawn rollouts across 0.130.0 → 0.152.0 carries
`parent_thread_id` and `depth`). The capture's own rollout files under the
isolated `CODEX_HOME` were not retained, so `base_instructions.text`,
`agent_nickname`, the repository URL, and the `context_window.window_id`
values are redacted placeholders, not observations.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"timestamp":"2026-09-03T08:26:06.402Z","type":"session_meta","payload":{"session_id":"01a06660-110e-7290-8d1c-8ef1b2b68fc2","id":"01a06660-110e-7290-8d1c-8ef1b2b68fc2","timestamp":"2026-09-03T08:26:06.089Z","cwd":"/tmp/host-test/codex-workspace","originator":"codex_exec","cli_version":"0.147.0","model_provider":"openai","base_instructions":{"text":"<redacted: 17,730-character default instructions>"},"history_mode":"legacy","multi_agent_version":"v2","git":{"commit_hash":"840e5bd01b2601e82ed34c286c17597b29959a50","branch":"main","repository_url":"<redacted>"},"source":"exec","thread_source":"user","context_window":{"window_id":"01a06660-1110-7a4e-9c7b-2f4c1a6d8e01"}}}
{"timestamp":"2026-09-03T08:26:06.402Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"<redacted>"}]}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"timestamp":"2026-09-03T08:26:39.611Z","type":"session_meta","payload":{"session_id":"01a06660-110e-7290-8d1c-8ef1b2b68fc2","id":"01a06660-8faf-7122-80af-24ba2da81ad7","forked_from_id":"01a06660-110e-7290-8d1c-8ef1b2b68fc2","parent_thread_id":"01a06660-110e-7290-8d1c-8ef1b2b68fc2","timestamp":"2026-09-03T08:26:39.318Z","cwd":"/tmp/host-test/codex-workspace","originator":"codex_exec","cli_version":"0.147.0","model_provider":"openai","base_instructions":{"text":"<redacted: 17,730-character default instructions>"},"history_mode":"legacy","multi_agent_version":"v2","git":{"commit_hash":"840e5bd01b2601e82ed34c286c17597b29959a50","branch":"main","repository_url":"<redacted>"},"source":{"subagent":{"thread_spawn":{"parent_thread_id":"01a06660-110e-7290-8d1c-8ef1b2b68fc2","depth":1,"agent_path":"/root/host_probe","agent_nickname":"<redacted>","agent_role":null}}},"thread_source":"subagent","agent_nickname":"<redacted>","agent_path":"/root/host_probe","subagent_history_start_ordinal":9,"context_window":{"window_id":"01a06660-8fb0-7c2d-8e11-6a0b5d3c2f02"}}}
{"timestamp":"2026-09-03T08:26:39.611Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"<redacted>"}]}}
Loading
Loading