Repository navigation
Description
Summary
The latest saved Codex debug bundle shows that Agent Code is not handling Codex subagents at all. Codex subagent completion notifications are persisted in the parent rollout as response_item messages with role: "user" and literal <subagent_notification>... payloads. Our Codex rollout mapper treats those as ordinary user prompts, so completed subagent reports get rendered as giant user-message bubbles instead of structured subagent state.
This is the concrete bug behind the broader feature request in #178.
Debug bundle evidence
Bundle:
/Users/juliusolsson/.config/agent-code/debug-bundles/manual/2026-06-18T11-12-28-535-21c515e9
Debug note:
Not handling codex subagents at all.
Manifest:
kind: codex
Agent Code sessionId: 21c515e9-8ce9-4ee2-ad8b-47d914ec635c
providerSessionId / resumed rollout: 019ed590-5e7c-7a23-8e26-5fa45bca76c1
capturedAtIso: 2026-06-18T11:12:28.449Z
proxyRunDir: /Users/juliusolsson/.config/agent-code/proxy/Users-juliusolsson-Desktop-Development-agent-code/resume-019ed590-5e7c-7a23-8e26-5fa45bca76c1/2026-06-18T10-58-16-369Z
Runtime snapshot:
totalEntries: 835
entries in runtime window: 160
streamPhase: requesting
semanticLog: 200
subAgents: 0
The key contradiction is subAgents: 0 while the parent rollout has multiple Codex subagents and subagent notifications.
Parent rollout evidence
Parent rollout:
/Users/juliusolsson/.codex/sessions/2026/06/17/rollout-2026-06-17T14-31-08-019ed590-5e7c-7a23-8e26-5fa45bca76c1.jsonl
The parent rollout contains spawn_agent calls and wait_agent outputs, then Codex writes notification messages back into the rollout as user-role messages:
line 949: response_item:message role=user, starts with <subagent_notification>, len 9572
line 956: response_item:message role=user, starts with <subagent_notification>, len 5132
line 963: response_item:message role=user, starts with <subagent_notification>, len 8435
line 969: response_item:message role=user, starts with <subagent_notification>, len 8006
line 994: response_item:message role=user, starts with <subagent_notification>, len 7092
Example head:
<subagent_notification>
{"agent_path":"019eda6c-0573-7993-a01e-a1d839486a35","status":{"completed":"I could not fetch GitHub issue #290 itself: ..."}}
These are not user prompts. They are system/tool notifications about subagent completion. Rendering them as user messages is wrong ownership.
Child rollout evidence
Codex already records structured child-agent metadata in child rollouts via session_meta.payload.source.subagent.thread_spawn.
Examples from /Users/juliusolsson/.codex/sessions/2026/06/18/:
rollout-2026-06-18T13-09-32-019eda6c-0573-7993-a01e-a1d839486a35.jsonl
parent_thread_id: 019ed590-5e7c-7a23-8e26-5fa45bca76c1
agent_nickname: Cicero
agent_role: explorer
lines: 168
last: 2026-06-18T11:12:16.579Z
rollout-2026-06-18T13-09-37-019eda6c-160d-79d0-a5e5-d2164e88dfc2.jsonl
parent_thread_id: 019ed590-5e7c-7a23-8e26-5fa45bca76c1
agent_nickname: Planck
agent_role: explorer
rollout-2026-06-18T13-09-41-019eda6c-280d-7c81-b10f-b40adc6d64ca.jsonl
parent_thread_id: 019ed590-5e7c-7a23-8e26-5fa45bca76c1
agent_nickname: Helmholtz
agent_role: explorer
rollout-2026-06-18T13-09-46-019eda6c-38ef-7322-973b-ee92507f6d55.jsonl
parent_thread_id: 019ed590-5e7c-7a23-8e26-5fa45bca76c1
agent_nickname: Curie
agent_role: explorer
rollout-2026-06-18T13-09-50-019eda6c-496c-7723-b8c4-150940e4a5ee.jsonl
parent_thread_id: 019ed590-5e7c-7a23-8e26-5fa45bca76c1
agent_nickname: Confucius
agent_role: explorer
rollout-2026-06-18T13-09-54-019eda6c-580c-71f3-917e-f82c060aa455.jsonl
parent_thread_id: 019ed590-5e7c-7a23-8e26-5fa45bca76c1
agent_nickname: Ramanujan
agent_role: explorer
So the provider gives us enough structure to build a Codex-side subagent model. We currently ignore it.
Relevant code paths
Current broad subagent model is Claude-shaped only:
src/main/subagents/index.tsderives<providerSessionId>/subagents/from a Claude root transcript path.src/preload/api/types.tsdocuments Claude CodeAgenttool sidecars andtoolUseIdjoins.SubAgentWatcherManager.observeParentEntry()only watches siblingsubagents/dirs from parent JSONL file paths.runtime.subAgentsis populated only viasession:sub-agentsIPC.
Codex mapper currently treats any Codex response_item.message role=user as a normal user feed entry unless it matches AGENTS.md or <environment_context> bootstrap filters:
src/renderer/src/workspace/codex/rollout.ts
codexConversationEntryFromMessageItem()
That function needs at minimum to filter/parse <subagent_notification> messages so they do not render as user prompts.
Working diagnosis
There are two layers to fix:
- Immediate correctness: hide or specially parse Codex
<subagent_notification>user-role messages so they stop rendering as user prompts. - Proper feature: build Codex subagent state from child rollout metadata and parent notification/tool events, then render it through the same
runtime.subAgents/TaskSubagentRowstyle model used for Claude where possible.
This is not just a missing renderer. It is currently a feed ownership bug: tool/system notifications are entering runtime.entries as if the human typed them.
Suggested implementation direction
- Add a Codex-specific detector for
<subagent_notification>message text. - In
mapCodexRolloutToFeedEntries, do not map those messages into normal userEntrys. - Parse the JSON payload into a typed Codex subagent notification event:
agent_pathstatus.completed/ error / running, if present- timestamp
- parent provider session id from the active rollout
- Add a Codex subagent indexer/watcher over
~/.codex/sessions/**/rollout-*.jsonlthat finds child rollouts whosesession_meta.payload.source.subagent.thread_spawn.parent_thread_idmatches the parent provider session id. - Build provider-neutral
SubAgentStaterecords from child rollout summaries:agentId= child rollout idagentType/ role =agent_role- description/title = prompt or first user message, with nickname as label
- status = notification completion or child
task_complete - toolCalls = child function/custom/local shell calls, capped
- Thread that into existing
session:sub-agentsIPC or a provider-neutral replacement. - Render Codex subagents with the existing
SubAgentsContextrow components when possible, but avoid assuming ClaudetoolUseIdjoins exist.
Acceptance criteria
<subagent_notification>payloads no longer render as user prompts in Codex feeds.- The latest debug bundle above shows structured subagent state instead of
subAgents: 0. - Parent Codex sessions can show spawned child agents with nickname/role/status.
- Child Codex rollouts linked by
parent_thread_idare discoverable on resume/history load. - Completed child results are grouped under the subagent UI, not flattened into giant user bubbles.
- Multiple concurrent Codex subagents render independently and do not collide with Claude subagent assumptions.
- Debug bundles include enough Codex subagent metadata to diagnose parent/child linkage.
Related: #178.
Activity
Juliusolsson05 commentedon Jun 18, 2026
Research update: Codex subagent rendering and immediate ownership fix
This is a read-only investigation from the latest debug bundle and current code. The bundle note is the right anchor:
Confirmed bug
Codex persists subagent completion notifications as
response_itemmessages withrole: "user"and aninput_textblock starting with<subagent_notification>. Agent Code maps any Codexmessagewith roleuser|assistantinto a normal feedEntry, so these model-visible session-state fragments render as human user prompts.Relevant mapper path:
src/renderer/src/workspace/codex/rollout.tscodexConversationEntryFromMessageItem()mapspayload.type === "message"and roleuser|assistantinto conversation rows.<environment_context>bootstrap blocks.<subagent_notification>filter.This must be fixed at the mapper/state layer, not hidden in React/CSS. Otherwise runtime entries, history replay, session preview, copy/debug surfaces, and counts remain polluted.
Bundle/rollout evidence
Parent rollout:
The latest bundle snapshot says:
But the parent rollout contains subagent activity. It has
spawn_agentcalls,wait_agentoutputs, and persisted<subagent_notification>messages:Example shape:
{ "type": "response_item", "payload": { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "<subagent_notification>\n{\"agent_path\":\"019eda6c-...\",\"status\":{\"completed\":\"...\"}}\n</subagent_notification>" } ] } }Upstream Codex source confirms these are contextual fragments, not user intent:
vendor/codex-src/codex-rs/core/src/contextual_user_message.rsdefines the subagent notification tags.vendor/codex-src/codex-rs/core/src/session_prefix.rsformats{ agent_path, status }and comments that these are model-visible session-state fragments, not human prompts.Existing Claude subagent system is not provider-neutral
Claude flow today:
src/main/subagents/index.tsderives<providerSessionId>/subagents/from a Claude parent.jsonlpath.SubAgentWatchertailsagent-*.jsonlandagent-*.meta.json.meta.toolUseId === parent Agent tool_use.id.SubAgentStateis keyed by that parenttoolUseId.subAgents[block.id]insideTaskSubagentRow.ConversationRowgroups adjacent assistant blocks only whentool_use.name === "Agent".Block.tsxcurrently routes Codex tool_use blocks to Codex rows before the ClaudeAgentspecial case, so even a synthetic CodexAgenttool_use would not automatically useTaskSubagentRowyet.Reusable parts:
runtime.subAgentssession:sub-agentsfull-map IPC shape, if generalizedSubAgentsContextSubagentGroupHeaderTaskSubagentRow/SubagentMiniFeedas presentation primitivesdroppedToolCallsMust be generalized:
toolUseIdcannot remain the only join key.Agenttool_use blocks.subagents/; Codex indexes rollout files.Codex gives enough structure to build this properly
Child rollouts under
/Users/juliusolsson/.codex/sessions/2026/06/18/contain structured subagent metadata insession_meta.payload.source.subagent.thread_spawn:{ "subagent": { "thread_spawn": { "parent_thread_id": "019ed590-5e7c-7a23-8e26-5fa45bca76c1", "depth": 1, "agent_path": null, "agent_nickname": "Cicero", "agent_role": "explorer" } } }The same child id appears in multiple places:
spawn_agentoutput:output.agent_idsession_meta.payload.id<subagent_notification>.agent_pathwait_agentoutput status keyObserved children for this parent:
Child lifecycle/tool shapes:
event_msg.task_startedgivesturn_id,started_at, model/context metadata.event_msg.agent_messagecarries commentary/final text and phase.event_msg.task_completecarrieslast_agent_message,completed_at,duration_ms.response_item.function_callplusfunction_call_outputgives tool timeline.custom_tool_call,custom_tool_call_output,web_search_call, and shell/event entries need the same capped timeline treatment as the main Codex feed.Recommended implementation stages
Add
isCodexSubagentNotificationText()beside existing bootstrap predicates insrc/renderer/src/workspace/codex/rollout.ts.Drop role-user messages whose entire content consists of subagent notification blocks, exactly like the AGENTS/env bootstrap “every block is synthetic” rule.
This stops
<subagent_notification>from enteringruntime.entriesas a user prompt.packages/codex-headless/src/transcript/TranscriptTypes.tscurrently typesCodexSessionMeta.sourceasstring, but real child rollouts use an object. Add a tolerant type/guard for:Handle both
subagentandsubAgentcasing defensively.Teach
codex-headlessCommittedChannelto detect subagent notifications and emit asubagent_notificationcommitted event instead of promoting them toturn_committed.The event should carry:
Add a Codex-specific indexer over
~/.codex/sessions/**/rollout-*.jsonlthat finds child rollouts where:Build a
SubAgentState-compatible record using:agentId: child thread idtoolUseId: synthetic key for current renderer, probably child thread id until model is widenedagentType:agent_roledescription: parentspawn_agent.arguments.messageif available, otherwise first child user message / nicknamenickname: Codex-only extra if model is widenedstatus: parent notification ortask_complete, else runningstartedAt: childsession_meta.timestamportask_started.started_atlastActivityAt: latest child event timestampturnCount: child assistant/message counttoolCalls: capped child function/custom/shell tool timelinecurrentActivity: latest running tool or agent phaseDo not force Codex through the Claude
Agentblock join. Either:TaskSubagentRowintoSubagentSpawnRowthat accepts a provider-neutralsubAgentKeyand optional spawn block.Also move the
Agentspecial-case before provider-specific Codex tool routing if we choose to synthesize spawn entries.Add subagent counts to manifest/state snapshot:
The current bundle only showed
subAgents: 0, which was useful but not explanatory.Tests to add
Immediate mapper test:
src/renderer/src/workspace/codex/rollout.test.tsmapCodexRolloutToFeedEntries()returns[]for a role-user message whose only text starts with<subagent_notification>.<subagent_notification>mid-text still maps.Codex metadata tests:
src/renderer/src/workspace/codex/metadata.test.tsor package-level equivalent.session_meta.payload.source.subagent.thread_spawnandsubAgent.thread_spawn.source.Subagent state tests:
src/main/subagents/subagentState.test.tsfor capped timeline behavior.Renderer tests:
src/renderer/src/features/feed/ui/Feed.subagents.renderer.test.tsxBlock.tsxprovider routing cannot bypass subagent rows.Debug bundle test:
src/renderer/src/features/debug/saveDebugBundle.renderer.test.tsOrdering recommendation
Do the mapper filter first. It is small, clearly correct, and stops the worst user-visible corruption immediately.
Then build the Codex indexer/state path behind tests. The full renderer can reuse much of the Claude presentation, but the join model should be provider-neutral rather than pretending Codex has Claude
toolUseIdsemantics.Juliusolsson05 commentedon Jun 18, 2026
Implementation PR opened: #292
What it covers:
spawn_agentcalls into the existing Claude-style subagent row/group UISubAgentStatefrom parent spawn outputs plus child rollout files<subagent_notification>synthetic user messages at the Codex rollout mapper so they stop rendering as promptsspawn_agentoutput JSON because it is join metadata, not user-visible agent workVerification so far:
npx tsc --noEmitpassesnpm test/npm run build:appare blocked in the dedicated worktree by missingpackages/claude-code-headless/src/...files, includingsrc/proxy/mitmAddon.py, which is outside this patch