Skip to content

Codex subagent notifications render as user prompts instead of structured agent state #291

Closed

Description

@Juliusolsson05

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.ts derives <providerSessionId>/subagents/ from a Claude root transcript path.
  • src/preload/api/types.ts documents Claude Code Agent tool sidecars and toolUseId joins.
  • SubAgentWatcherManager.observeParentEntry() only watches sibling subagents/ dirs from parent JSONL file paths.
  • runtime.subAgents is populated only via session:sub-agents IPC.

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:

  1. Immediate correctness: hide or specially parse Codex <subagent_notification> user-role messages so they stop rendering as user prompts.
  2. Proper feature: build Codex subagent state from child rollout metadata and parent notification/tool events, then render it through the same runtime.subAgents / TaskSubagentRow style 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 user Entrys.
  • Parse the JSON payload into a typed Codex subagent notification event:
    • agent_path
    • status.completed / error / running, if present
    • timestamp
    • parent provider session id from the active rollout
  • Add a Codex subagent indexer/watcher over ~/.codex/sessions/**/rollout-*.jsonl that finds child rollouts whose session_meta.payload.source.subagent.thread_spawn.parent_thread_id matches the parent provider session id.
  • Build provider-neutral SubAgentState records from child rollout summaries:
    • agentId = child rollout id
    • agentType / 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-agents IPC or a provider-neutral replacement.
  • Render Codex subagents with the existing SubAgentsContext row components when possible, but avoid assuming Claude toolUseId joins 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_id are 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

  1. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    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:

    /Users/juliusolsson/.config/agent-code/debug-bundles/manual/2026-06-18T11-12-28-535-21c515e9/note.json
    note: "Not handling codex subagents at all."
    

    Confirmed bug

    Codex persists subagent completion notifications as response_item messages with role: "user" and an input_text block starting with <subagent_notification>. Agent Code maps any Codex message with role user|assistant into a normal feed Entry, so these model-visible session-state fragments render as human user prompts.

    Relevant mapper path:

    • src/renderer/src/workspace/codex/rollout.ts
      • codexConversationEntryFromMessageItem() maps payload.type === "message" and role user|assistant into conversation rows.
      • Existing synthetic filters only drop AGENTS.md and <environment_context> bootstrap blocks.
      • There is no <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:

    /Users/juliusolsson/.codex/sessions/2026/06/17/rollout-2026-06-17T14-31-08-019ed590-5e7c-7a23-8e26-5fa45bca76c1.jsonl
    

    The latest bundle snapshot says:

    kind: codex
    runtime subAgents: 0
    totalEntries: 835
    runtime window entries: 160
    streamPhase: requesting
    semanticLog: 200
    

    But the parent rollout contains subagent activity. It has spawn_agent calls, wait_agent outputs, and persisted <subagent_notification> 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 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.rs defines the subagent notification tags.
    • vendor/codex-src/codex-rs/core/src/session_prefix.rs formats { 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.ts derives <providerSessionId>/subagents/ from a Claude parent .jsonl path.
    • SubAgentWatcher tails agent-*.jsonl and agent-*.meta.json.
    • The join key is meta.toolUseId === parent Agent tool_use.id.
    • SubAgentState is keyed by that parent toolUseId.
    • Renderer lookup is subAgents[block.id] inside TaskSubagentRow.
    • ConversationRow groups adjacent assistant blocks only when tool_use.name === "Agent".
    • Block.tsx currently routes Codex tool_use blocks to Codex rows before the Claude Agent special case, so even a synthetic Codex Agent tool_use would not automatically use TaskSubagentRow yet.

    Reusable parts:

    • runtime.subAgents
    • session:sub-agents full-map IPC shape, if generalized
    • SubAgentsContext
    • SubagentGroupHeader
    • TaskSubagentRow / SubagentMiniFeed as presentation primitives
    • capped tool-call timeline and droppedToolCalls

    Must be generalized:

    • toolUseId cannot remain the only join key.
    • Spawn detection cannot be hard-coded to Claude Agent tool_use blocks.
    • Data producers must split by provider: Claude watches 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 in session_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:

    • parent spawn_agent output: output.agent_id
    • child session_meta.payload.id
    • child rollout filename suffix
    • parent <subagent_notification>.agent_path
    • parent wait_agent output status key

    Observed children for this parent:

    019eda6c-0573-7993-a01e-a1d839486a35 Cicero     explorer
    019eda6c-160d-79d0-a5e5-d2164e88dfc2 Planck     explorer
    019eda6c-280d-7c81-b10f-b40adc6d64ca Helmholtz  explorer
    019eda6c-38ef-7322-973b-ee92507f6d55 Curie      explorer
    019eda6c-496c-7723-b8c4-150940e4a5ee Confucius  explorer
    019eda6c-580c-71f3-917e-f82c060aa455 Ramanujan  explorer
    

    Child lifecycle/tool shapes:

    • event_msg.task_started gives turn_id, started_at, model/context metadata.
    • event_msg.agent_message carries commentary/final text and phase.
    • event_msg.task_complete carries last_agent_message, completed_at, duration_ms.
    • response_item.function_call plus function_call_output gives 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

    1. Immediate correctness patch:

    Add isCodexSubagentNotificationText() beside existing bootstrap predicates in src/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 entering runtime.entries as a user prompt.

    1. Type/session-meta cleanup:

    packages/codex-headless/src/transcript/TranscriptTypes.ts currently types CodexSessionMeta.source as string, but real child rollouts use an object. Add a tolerant type/guard for:

    source: 'cli' | 'app_server' | {
      subagent?: { thread_spawn?: { parent_thread_id?: string; depth?: number; agent_path?: string | null; agent_nickname?: string; agent_role?: string } }
      subAgent?: { thread_spawn?: ... }
    }

    Handle both subagent and subAgent casing defensively.

    1. Structured committed event path:

    Teach codex-headless CommittedChannel to detect subagent notifications and emit a subagent_notification committed event instead of promoting them to turn_committed.

    The event should carry:

    agentId / agentPath
    status: completed | error | running | unknown
    completedText?: string
    raw item
    file
    ts
    1. Codex subagent indexer:

    Add a Codex-specific indexer over ~/.codex/sessions/**/rollout-*.jsonl that finds child rollouts where:

    child.session_meta.payload.source.subagent.thread_spawn.parent_thread_id === parent providerSessionId
    

    Build a SubAgentState-compatible record using:

    • agentId: child thread id
    • toolUseId: synthetic key for current renderer, probably child thread id until model is widened
    • agentType: agent_role
    • description: parent spawn_agent.arguments.message if available, otherwise first child user message / nickname
    • nickname: Codex-only extra if model is widened
    • status: parent notification or task_complete, else running
    • startedAt: child session_meta.timestamp or task_started.started_at
    • lastActivityAt: latest child event timestamp
    • turnCount: child assistant/message count
    • toolCalls: capped child function/custom/shell tool timeline
    • currentActivity: latest running tool or agent phase
    1. Renderer generalization:

    Do not force Codex through the Claude Agent block join. Either:

    • render a parent-level Codex subagent group keyed by child ids, or
    • generalize TaskSubagentRow into SubagentSpawnRow that accepts a provider-neutral subAgentKey and optional spawn block.

    Also move the Agent special-case before provider-specific Codex tool routing if we choose to synthesize spawn entries.

    1. Debug bundle improvements:

    Add subagent counts to manifest/state snapshot:

    subAgents.total
    subAgents.running
    subAgents.done
    subAgents.error
    provider-specific source: claude-subagents-dir | codex-child-rollouts
    

    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.ts
    • mapCodexRolloutToFeedEntries() returns [] for a role-user message whose only text starts with <subagent_notification>.
    • Normal user prompts still map.
    • A user prompt quoting <subagent_notification> mid-text still maps.
    • AGENTS/env bootstrap filtering remains covered.

    Codex metadata tests:

    • src/renderer/src/workspace/codex/metadata.test.ts or package-level equivalent.
    • Parse session_meta.payload.source.subagent.thread_spawn and subAgent.thread_spawn.
    • Return parent provider id, child id, nickname, role, depth, path.
    • Do not throw on malformed or string source.

    Subagent state tests:

    • Keep/expand src/main/subagents/subagentState.test.ts for capped timeline behavior.
    • Add Codex child rollout builder tests for function/custom/shell calls and matching outputs.

    Renderer tests:

    • src/renderer/src/features/feed/ui/Feed.subagents.renderer.test.tsx
    • Verify multiple subagents render grouped, counts/statuses show, mini-feed expands, and dropped-call count is visible.
    • Include a Codex provider case so Block.tsx provider routing cannot bypass subagent rows.

    Debug bundle test:

    • src/renderer/src/features/debug/saveDebugBundle.renderer.test.ts
    • Assert manifest/state snapshot include subagent counts and provider-specific source metadata.

    Ordering 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 toolUseId semantics.

  2. Juliusolsson05 commented on Jun 18, 2026

    @Juliusolsson05
    OwnerAuthor

    Implementation PR opened: #292

    What it covers:

    • maps Codex spawn_agent calls into the existing Claude-style subagent row/group UI
    • builds Codex SubAgentState from parent spawn outputs plus child rollout files
    • filters <subagent_notification> synthetic user messages at the Codex rollout mapper so they stop rendering as prompts
    • hides raw spawn_agent output JSON because it is join metadata, not user-visible agent work

    Verification so far:

    • targeted new tests pass
    • npx tsc --noEmit passes
    • full npm test / npm run build:app are blocked in the dedicated worktree by missing packages/claude-code-headless/src/... files, including src/proxy/mitmAddon.py, which is outside this patch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingprovider:codexAffects Codex integration

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions