Skip to content

fix: announce ad-hoc spawned subagents so their output is not dropped - #482

Open
gurungabit wants to merge 1 commit into
agentclientprotocol:mainfrom
gurungabit:fix-announce-adhoc-spawned-subagents
Open

fix: announce ad-hoc spawned subagents so their output is not dropped#482
gurungabit wants to merge 1 commit into
agentclientprotocol:mainfrom
gurungabit:fix-announce-adhoc-spawned-subagents

Conversation

@gurungabit

Copy link
Copy Markdown

Problem

A spawn is registered as pending in CodexSubagentEventRouter.handle() and is only announced when a subAgentActivity item later supplies its agent path:

if (item.type === "subAgentActivity") {
    
    await this.materialize(item.agentThreadId, item.agentPath);

Codex sends that item for subagents it names, but not for an ad-hoc spawn_agent. Every notification from the child then matches the pending-spawn branch and is buffered against a materialization that never arrives — until MAX_PENDING_NOTIFICATIONS, after which its oldest updates are dropped. The client is never told the subagent exists, and the transcript it produced is lost.

Because the spawn's item/completed is reported as represented, it is also swallowed, so the client is left with the item/started copy — which leaks through only because its receiverThreadIds is still empty — stuck at in_progress for the rest of the session.

Evidence

Codex 0.153.4, adapter 1.7.0, native subagent sessions negotiated, prompt "use subagents to analyze this repo" (APP_SERVER_LOGS):

item/started   collabAgentToolCall  tool: spawnAgent  receiverThreadIds: []
item/completed collabAgentToolCall  tool: spawnAgent  receiverThreadIds: ['01a07280-e0fa-…']
                                    agentsStates: {'01a07280-e0fa-…': {status: 'pendingInit'}}
item/started   collabAgentToolCall  tool: wait        receiverThreadIds: ['01a07280-e18f-…']
  • subAgentActivity items in the whole log: 0
  • child-thread notifications: 422
  • Pending subagent 01a07280-e18f-… exceeded the notification buffer; dropping oldest updates

Found while integrating an ACP client; the same shape is described in manaflow-ai/cmux#5698, and #238 mapped spawn_agent onto tool calls with nested sessions left optional.

Fix

Announce from the spawn's own item/completed, which carries receiverThreadIds and lands before any child output — the point where subagent_spawned is supposed to go. materialize() takes the agent path as optional, since an ad-hoc spawn has none, and falls back to the identity helper that already existed for exactly that case.

Named subagents are unaffected: an activity item that arrives first still wins, because materializing a known child is a no-op, so they keep the name their path gives them. That ordering is what the existing tests assert, and they pass unmodified — including emits native lifecycle and routes child output after capability negotiation, which is the case where child output precedes the announcement.

Terminal states are skipped, so a spawn that completes already-terminal still finishes as pending rather than being announced, per waits for a pending spawn without publishing fallback identity and suppresses late activity.

Testing

  • New: announces an unannounced spawn so its output is not lost — asserts subagent_spawned against the parent under the fallback identity with the spawn prompt as its task, the child's own tool call routed to the child session, and no bare spawnAgent tool call in the thread.
  • npm run typecheck and npm test: 542 passed, 26 skipped, 0 failed, no existing test modified.
  • Exercised end to end against real Codex through an ACP client: subagents announced, their work attributed to them, nothing left buffered.

🤖 Generated with Claude Code

A spawn is registered as pending and only announced when a subAgentActivity
item supplies its agent path. Codex sends that item for subagents it names,
but not for an ad-hoc `spawn_agent`, so the child's notifications buffer
against a materialization that never comes: the buffer reaches
MAX_PENDING_NOTIFICATIONS, the oldest updates are dropped, and the client is
never told the subagent exists.

Announce from the spawn's own `item/completed` instead, which carries
receiverThreadIds and lands before any child output. An activity item that
arrives first still wins, since materializing a known child is a no-op, so
named subagents keep the identity their path gives them.
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