Skip to content

feat(codex): track subagents (full parity with Claude Code / Droid)#1570

Open
suhaanthayyil wants to merge 16 commits into
mainfrom
codex-subagent-tracking
Open

feat(codex): track subagents (full parity with Claude Code / Droid)#1570
suhaanthayyil wants to merge 16 commits into
mainfrom
codex-subagent-tracking

Conversation

@suhaanthayyil

@suhaanthayyil suhaanthayyil commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/702

Ready for review. Green across fmt + lint (0 issues) + unit + integration + e2e canary, validated end-to-end against real Codex 0.139, and hardened through a multi-round adversarial bug-bot audit (converged clean).

Problem

In Entire's per-agent subagent-tracking matrix, Codex was "None" — a Codex session that spawned subagents had that work folded into the parent session with no per-subagent attribution, no per-subagent token accounting, and no per-task checkpoints. Claude Code and Factory AI Droid have full support.

Codex 0.139 ships native subagents (custom agents in ~/.codex/agents/*.toml, spawn_agent/wait_agent/close_agent, and SubagentStart/SubagentStop hooks, GA May 2026). The repo's codex/AGENT.md said "No subagent hooks" — that reflected 0.116 and is now stale.

Approach

The framework's subagent path (DispatchLifecycleEventSubagentStart/EndSaveTaskStep, the SubagentAwareExtractor-preferring token path) is agent-agnostic, so Codex needed the agent-side pieces plus one small framework hook.

  1. Lifecycle hooks (types.go, lifecycle.go) — SubagentStartSubagentStart, SubagentStopSubagentEnd. agent_idSubagentID/ToolUseID (Codex has no per-task tool-use id), agent_typeSubagentType. SubagentStop carries agent_transcript_path (the child rollout).
  2. Hook install (hooks.go) — registers subagent-start/subagent-stop in .codex/hooks.json; AreHooksInstalled/trust/drift detection updated to include them.
  3. SubagentAwareExtractor (transcript.go) — at turn-end, discover subagents from spawn_agent function-call outputs (a JSON string carrying {"agent_id":…}) scoped to the checkpoint range, read each child rollout, and merge apply_patch files + sum token_count usage into TokenUsage.SubagentTokens.
  4. Framework (agent.go, capabilities.go, cli/lifecycle.go) — new optional SubagentTranscriptResolver; Codex child rollouts live under CODEX_HOME/sessions/… (not as sibling agent-<id>.jsonl), so the subagent-end handler prefers it.

Status / prompt fixes

Real-session testing surfaced two entire status issues, both fixed here:

  • Codex injects system content as user-role messages (<environment_context>, <subagent_notification>, # AGENTS.md, …). ExtractPrompts treated these as prompts, so status/commit messages showed noise. Now filtered via a shared textutil.IsCodexSyntheticContent (single source of truth, also used by the compact transcript; AGENTS.md anchored to its injected form to avoid dropping a real prompt).
  • A subagent's own UserPromptSubmit/Stop fires tagged with the parent's session_id but the child's transcript, which overwrote the parent's prompt with the worker's task (the "shows the latest worker" symptom). parseTurnStart/parseTurnEnd now skip when the hook's transcript is a subagent rollout (thread_source == "subagent").

Tests & verification

Test-driven throughout: hook parsing, the resolver, the extractor (files + token aggregation, fromOffset scoping, cross-offset spawn discovery, resume-double-count guard, string/object output parse), trust/drift, prompt-synthetic filtering, and subagent-turn skipping. Several regression tests are mutation-verified (they fail when the guarded code is reverted). Full suite green: fmt, lint (0 issues), unit, integration, e2e canary.

Validated against real Codex 0.139 (4-worker and single-worker spawn_agent runs): per-subagent task checkpoints created, real SubagentTokens attributed, and (post-fix) entire status shows the user's actual prompt.

Audit

Hardened via a multi-round adversarial bug-bot (each finding independently verified before acting): rounds found and fixed cross-turn token over-attribution, an AreHooksInstalled gap, parent-rollout over-attribution, a stale-hook-path fallback, the resume double-count, a real-data blocker (spawn output is a JSON string, not object), an offset asymmetry, two test-quality gaps, the prompt-noise filtering (+ its AGENTS.md false-positive), and the subagent-turn parent-prompt pollution — converging to two consecutive clean rounds.

Notes for review

  • Subagent display is not surfaced in the web product for any agent today (session-level only); that's net-new, all-agents work, decoupled from this PR.
  • Already-recorded sessions keep their stored status prompt; the fixes apply to new sessions (their checkpoint list prompts are already correct).

Codex 0.139 ships native subagents (spawn_agent / wait_agent / close_agent,
with SubagentStart / SubagentStop hooks). This moves Codex from "no subagent
tracking" to parity with Claude Code and Factory Droid: per-task checkpoints
plus per-subagent file and token attribution.

Agent side (cmd/entire/cli/agent/codex):
- lifecycle.go / types.go: parse SubagentStart/SubagentStop hooks into the
  framework's SubagentStart/SubagentEnd events (agent_id -> SubagentID and
  ToolUseID, agent_type -> SubagentType, agent_transcript_path stashed for
  child-transcript resolution).
- hooks.go: install/uninstall the two subagent hooks in .codex/hooks.json.
- transcript.go: implement SubagentAwareExtractor — enumerate spawned child
  thread-ids from the parent rollout's agent-management tool calls
  (wait_agent/close_agent/resume_agent), read each child rollout, and merge
  files + sum SubagentTokens.
- trust.go: include the subagent hooks in trust-gap and drift detection so
  doctor + the SessionStart banner surface them.

Framework:
- agent.go / capabilities.go: new optional SubagentTranscriptResolver interface
  (+ As-helper) for agents whose child transcripts aren't sibling files — Codex
  stores them under CODEX_HOME/sessions/... The subagent-end handler in
  lifecycle.go prefers it over the sibling-file convention.

Docs: rewrote codex/AGENT.md subagent section + version (0.139.0); added a Codex
row note to agent-guide.md's event-mapping table.

Validated against real Codex 0.139.0: the extractor produces the subagent's
files + real SubagentTokens from an actual spawn_agent session, and an
end-to-end run through the entire binary creates a task checkpoint
("Completed 'explorer' agent ...") on subagent-stop. mise run check
(fmt + lint + test:ci) is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 29, 2026 21:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds full subagent tracking support for the Codex agent integration, bringing it to parity with existing Claude Code / Factory AI Droid behavior in Entire’s lifecycle + checkpoint framework.

Changes:

  • Extend Codex hook installation, trust-gap detection, and event parsing to include SubagentStart / SubagentStop.
  • Add Codex-side subagent-aware transcript extraction for modified files + token aggregation across child rollouts.
  • Add a framework-level SubagentTranscriptResolver capability so agents can customize subagent transcript locations (needed for Codex’s CODEX_HOME/sessions/... layout).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/architecture/agent-guide.md Documents Codex hook→event mappings and its transcript resolver behavior.
cmd/entire/cli/lifecycle.go Uses an agent-provided resolver to locate subagent transcripts during SubagentEnd.
cmd/entire/cli/doctor_test.go Updates Codex canonical hooks + trust fixture to include subagent hooks.
cmd/entire/cli/agent/codex/types.go Adds hook payload structs for Codex SubagentStart / SubagentStop.
cmd/entire/cli/agent/codex/trust.go Includes subagent hooks in declared-events and missing-hook detection.
cmd/entire/cli/agent/codex/trust_test.go Updates expected missing hooks and adds trust-gap coverage for subagent hooks.
cmd/entire/cli/agent/codex/transcript.go Implements subagent-aware file extraction + token aggregation by reading child rollouts.
cmd/entire/cli/agent/codex/subagent_test.go Adds unit tests for subagent event parsing, resolver behavior, and aggregation.
cmd/entire/cli/agent/codex/lifecycle.go Parses subagent hooks, maps IDs/types, and implements transcript resolution.
cmd/entire/cli/agent/codex/hooks.go Installs/uninstalls SubagentStart / SubagentStop entries in .codex/hooks.json.
cmd/entire/cli/agent/codex/hooks_test.go Updates hook-install test expectations to 6 total hooks.
cmd/entire/cli/agent/codex/AGENT.md Updates Codex agent docs for subagent support and rollout-path differences.
cmd/entire/cli/agent/capabilities.go Adds AsSubagentTranscriptResolver helper.
cmd/entire/cli/agent/agent.go Introduces SubagentTranscriptResolver interface for agents with non-sibling transcripts.

Comment on lines +1035 to 1037
if subagentTranscriptPath != "" && !fileExists(subagentTranscriptPath) {
subagentTranscriptPath = ""
}
suhaanthayyil and others added 9 commits June 29, 2026 17:31
…re subagent hooks

Bug-audit round 1 fixes:
- transcript.go: thread fromOffset into subagent discovery
  (readSubagentRollouts / extractSpawnedAgentIDs). A Codex rollout grows across
  turns in one file, so scanning the whole parent re-attributed every prior
  subagent's files + tokens to every later checkpoint — unbounded over-counting
  of SubagentTokens. Now only subagents spawned at/after fromOffset count,
  matching Claude Code. Also preserve CalculateTokenUsage's nil "no data" return
  when no subagents are present, and memoize the child-rollout read so the
  turn-end path doesn't glob/read child rollouts twice.
- hooks.go: AreHooksInstalled now requires SubagentStart/SubagentStop, so a stale
  4-hook install reports incomplete and re-install adds the subagent hooks (the
  missing check gated re-install off in setup.go).
- tests: fromOffset over-attribution regression, stale-install AreHooksInstalled
  case, stronger doctor stale-hooks assertions (subagent events + count).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sume double-count)

- lifecycle.go: never scan the PARENT rollout as the subagent transcript when the
  child path can't be resolved — that mis-attributed the parent's whole apply_patch
  history to one subagent checkpoint. Only scan an actual resolved subagent
  transcript; the pre-task git-status delta still scopes real changes.
- codex/lifecycle.go: ResolveSubagentTranscript returns the SubagentStop
  agent_transcript_path only if it exists; a stale/moved/archived path now falls
  through to globbing the sessions tree by agent_id instead of returning a dead path.
- codex/transcript.go: discover subagents from spawn_agent function-call OUTPUTS
  ({"agent_id":...}) scoped to the checkpoint range, not wait/close/resume
  references. Each child is attributed exactly once (its spawn turn), so a child
  resumed/re-waited in a later turn is no longer double-counted into SubagentTokens.
- tests rewritten to the real spawn_agent + function_call_output structure; added
  stale-hook-path glob fallback + spawned-but-no-rollout cases. AGENT.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e format)

Round-3 blocker: agentIDFromSpawnOutput parsed function_call_output.output as a
JSON object, but Codex emits it as a JSON *string* containing JSON
("output":"{\"agent_id\":...}") — confirmed against a real rollout (12/12 string
form) and the codebase's own `Output string` typing for function_call_output.
The object-only parse returned "", so spawn-output subagent discovery silently
found ZERO subagents on real data; the unit fixture used an object, masking it.

Now handle both the string and object forms. Fixture updated to the real string
form, plus a direct agentIDFromSpawnOutput test covering string/object/non-JSON.
Validated end-to-end against a real 4-worker Codex session: discovers all 4
children + their files + SubagentTokens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TestSubagentDiscovery_RespectsFromOffset asserted subagent tokens aren't
re-attributed before the offset, but the parent had no token_count line so
CalculateTotalTokenUsage returned nil at offset 3 — the `if usage != nil` guard
was always false and require.Nil never ran (dead assertion: zero live coverage
for the token cross-turn scoping). Add a parent token_count so usage is non-nil,
then assert directly. Verified the assertion now fails if offset-scoping regresses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extractSpawnedAgentIDs collects spawn_agent call_ids across the whole transcript
(first pass) so a spawn CALL before fromOffset still matches its OUTPUT landing
in range — the output line, not the call line, decides the checkpoint range. No
test exercised that, so the first pass could be deleted undetected. Add
TestSubagentDiscovery_SpawnOutputLineDecidesRange (call before offset + output in
range => discovered; output at/before offset => not). Mutation-verified: the test
fails if the first pass starts honoring fromOffset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-6 fix: handleLifecycleTurnEnd scoped subagent FILES by the resolved offset
(resolveTranscriptOffset, with the CheckpointTranscriptStart fallback) but scoped
subagent TOKENS by the raw preState offset (no fallback → 0 when pre-prompt state
is absent, e.g. exec/non-interactive runs). That made subagent token discovery
run from offset 0 and re-attribute every historically-spawned subagent's
cumulative tokens each turn — defeating the cross-turn scoping — and broke the
readSubagentRollouts memo (the two call sites passed different offsets, double-
reading child rollouts). Pass the resolved offset to the token calc too; for the
common path (preState carries a positive offset) the value is unchanged.

Also harden the resume_agent double-count guard test: TestExtractSpawnedAgentIDs
previously "excluded" childID3 via a wait_agent function_call, but discovery only
reads function_call_OUTPUTS, so the case was vacuous. Now model a resume_agent
call + an output carrying an agent_id and assert it's excluded by the
spawn-call_id gate. Mutation-verified: the test fails if the gate is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpts

Codex injects system content (environment_context, AGENTS.md, permissions,
collaboration/skills preamble, turn_aborted, and — with subagents —
subagent_notification) as user-role messages in the rollout. ExtractPrompts
treated these as user prompts, so `entire status` and commit messages showed a
subagent notification or env block instead of the user's actual prompt
(verified on a real rollout: 6 "prompts", 5 synthetic).

Add textutil.IsCodexSyntheticContent as the single source of truth for these
markers, filter them in codex ExtractPrompts, and route the compact parser's
isCodexSystemContent through it too (which also fixes the compact transcript
silently including subagent_notification). Real-data check now yields exactly the
one user prompt. Tests: textutil predicate, codex ExtractPrompts synthetic
filtering; full test:ci green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
IsCodexSyntheticContent now gates prompt extraction (commit messages / status),
so a bare "# AGENTS.md" HasPrefix match could drop a real user prompt that merely
opens with that heading (e.g. "# AGENTS.md needs a testing section") — emptying
the commit message/status. Codex always injects AGENTS.md as a markdown H1 block
followed by a line break, so anchor the marker to the exact heading or
heading+newline (incl. CRLF). The XML-tag markers stay prefix-matched (a prompt
never starts with those). Test now asserts "# AGENTS.md needs a testing section"
is preserved; mutation-verified it fails on the bare-prefix form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…urnEnd

Codex fires a spawned subagent's UserPromptSubmit/Stop hooks tagged with the
PARENT session_id but the CHILD transcript. entire processed those as parent
turns, which overwrote the parent session's LastPrompt with the worker's task
instruction (so `entire status` showed "You are Worker 4 ..." instead of the
user's prompt) and churned the parent's phase. The subagent's lifecycle is
already tracked via SubagentStart/SubagentEnd, so skip these.

parseTurnStart/parseTurnEnd now return nil when the hook's transcript is a
subagent rollout, detected via session_meta.thread_source == "subagent"
(isCodexSubagentRollout, reads only the first line; best-effort, defaults to
not-subagent for user sessions and older rollouts). Verified end-to-end on a
fresh real Codex 0.139 session: status now shows the user's actual prompt.
Test: TestParseHookEvent_SkipsSubagentOwnTurns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@suhaanthayyil suhaanthayyil changed the title feat(codex): track subagents (full parity with Claude Code / Droid) (WIP) feat(codex): track subagents (full parity with Claude Code / Droid) Jun 30, 2026
@suhaanthayyil suhaanthayyil marked this pull request as ready for review June 30, 2026 03:13
@suhaanthayyil suhaanthayyil requested a review from a team as a code owner June 30, 2026 03:13
suhaanthayyil and others added 6 commits June 30, 2026 09:46
Resolves the review packet on the Codex subagent change:

- #1 Codex task-checkpoint UUID: documented that CheckpointUUID is
  Claude-format-specific (tool_result/UUID) and resolves to "" for Codex —
  rewind to a Codex task checkpoint restores files via the shadow tree but does
  not truncate the transcript (graceful fall-through; restore truncation is also
  Claude-only). Codex-aware line-offset truncation is a deferred follow-up.
  Comment at the call site + AGENT.md.
- #2 Resumed-subagent token under-count: documented deliberate trade-off (chosen
  over the worse cross-turn double-count) + observability — logResumedOutOfRangeChildren
  debug-logs when a resume_agent targets a child spawned in a prior range.
- #3 Regression tests: CalculateTotalTokenUsage nil-main-with-subagent-tokens
  unit test (guards the nil-deref); new integration test
  TestCodexSubagentEnd_SourcesFilesFromChildNotParent (+ SimulateCodexSubagentStart/Stop
  harness helpers) guarding handleLifecycleSubagentEnd's child-not-parent resolver.
- #4 Debug logging across the discover→resolve→read→parse chain
  (readSubagentRolloutsUncached, CalculateTotalTokenUsage, spawn-output drop) so
  Codex wire-format drift is visible instead of silently zeroing attribution.
- #5 isCodexSubagentRollout: nested source.subagent fallback for rollouts without
  thread_source + backward-compat test (missing marker → treated as parent turn).
- #6 + nits: t.Parallel() on the pure-logic codex tests; AGENT.md PostToolUse line
  corrected (it IS consumed); StepTranscriptStart aligned to the resolved
  transcriptOffset (removes the exec/no-preState divergence; field is currently
  unconsumed by the strategy).

go build (incl. integration tag), vet, unit + codex integration tests, lint (0) all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'Parallel file attribution' bullet claimed the git-status diff only
supplements new/deleted detection, but handleLifecycleSubagentEnd also merges
changes.Modified into the subagent's modified set (lifecycle.go:1117) — which is
exactly why the diff can over-attribute concurrently-changed files. Reworded to
reflect the merge (new/deleted + modified-tracked safety net).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an explicit parent->child subagent link + roster on the parent session so
the web UI can render the parent->children tree directly (addresses dipree's
PR #1570 review). Shared across Claude Code / Codex / Droid — none emitted this.

- api/checkpoint: new SubagentLink{ToolUseID, AgentID, SubagentType, Description,
  FileCount}; Subagents []SubagentLink on WriteOptions, UpdateOptions, Metadata.
- session.State.Subagents: session-level cumulative roster (deduped by
  ToolUseID/AgentID), not reset on carry-forward.
- Recorded in handleLifecycleSubagentEnd (strategy.RecordSubagentLink) before the
  no-file-change early-return, so EVERY completed subagent is listed — including
  read-only ones. An ID-less SubagentEnd (e.g. copilot) is skipped; the first
  SubagentEnd (with the pre-task baseline) is authoritative on re-fire.
- Published to the pushed v1 metadata.json via condensation and the turn-end
  finalize (UpdateOptions.Subagents -> replaceSubagents), so trailing subagents
  recorded after the last condensation still reach the UI.

Tests cover recording, upsert/merge, both sync paths, the finalize wiring, and a
full-roster integration test (incl. a no-file-change child). Hardened across 4
adversarial bug-bot audit rounds (10 -> 5 -> 2 -> 0 findings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPB9vREpnb3uC814nEcn4F
Resolve cmd/entire/cli/agent/codex/hooks.go conflict: adopt main's OS-aware
hook wrapping (WrapProductionSilentHookCommandForOS) for the subagent-start/-stop
hooks. Also fix a migration bug the merge surfaced — the subagent hooks used
addHook (append-only) instead of syncHookCommand, so a non-Windows->Windows
re-wrap left both the old `sh -c` and new cmd.exe commands. Switch them to
syncHookCommand like the base hooks so migration replaces in place; extend the
Windows hook tests to cover all six hooks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPB9vREpnb3uC814nEcn4F
Resolve cmd/entire/cli/checkpoint/persistent.go: main extracted an
updateSessionMetadata helper and rewrote replaceSkillEvents onto it (plus added
setCompactTranscriptStart). Refactor replaceSubagents onto the same helper and
keep all three methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants