Skip to content

feat(orchestration-v2): attribute reused subagents to the run driving them (3/5) - #4662

Draft
shivamhwp wants to merge 1 commit into
subagent-obs/02-adaptersfrom
subagent-obs/03-reuse
Draft

feat(orchestration-v2): attribute reused subagents to the run driving them (3/5)#4662
shivamhwp wants to merge 1 commit into
subagent-obs/02-adaptersfrom
subagent-obs/03-reuse

Conversation

@shivamhwp

@shivamhwp shivamhwp commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Third of five stacked PRs replacing #4551. Stacked on #4629 — review that first.

A subagent row keeps the run id it was spawned under. Routing admits a row by run or by parent thread, and the parent thread is excluded by construction — so a later run re-activating that subagent could only see its updates while the spawning run's ingestion fiber happened to still be alive. The Claude adapter already re-attributed on reopen for exactly this reason; this brings the same treatment to routing and to Codex.

The bug that made this invisible

The rebind was hooked to item/started. Codex's reuse frames — resumeAgent, sendInput — only ever arrive on item/completed, and registerSubagentThreads ignores them because they aren't spawnAgent. So the rebind never fired, and I could not reproduce the fix working.

Moving the hook is what makes it observable. subagent_continue now asserts the subagent is attributed to the run that drove it:

subagent.runId
before ordinal:1 (spawning run)
after ordinal:2 (run that drove the reuse)

Routing

Identity becomes a third ownership test, seeded from the projection at turn start. Two constraints shape it:

  • The child thread is adopted only while the agent is live. A settled row is also re-emitted by trailing traffic — a token-usage frame, a collab state sweep — and adopting on those would hand a later run an interrupted agent's thread, readmitting exactly the stale events that post-interrupt recovery exists to exclude.
  • A subagent's row and its timeline item are emitted together, so both route by identity. Matching only the row advanced the agent while its item stayed frozen on the spawning run.

That is also why the reactivation predicate is separate from canRouteRelatedSubagent rather than a widening of it: an interrupted agent must not have its child thread pre-owned, but its identity is still resumable — the user stopped it, they didn't lose it.

Rehydration

Seeds turn ordinals alongside the registry (restarting at 1 would re-derive childRootNodeId for a different native turn) and parks non-terminal leftovers at idle, since a new session can never terminalize an activation it didn't drive and would otherwise keep the session pinned open.

Not proven. The fixture that would exercise rehydration is recorded and contains a genuine reuse, but doesn't replay to completion — for reasons unrelated to these changes; the hang reproduces with them reverted, and persists now that the rebind demonstrably fires. Left unregistered with the open question written up in fixtures/index.ts.

Testing

Typecheck clean across server, contracts, client-runtime, web. Server 1,684 passing.

Reviewed by an independent agent pass, which found the item/started / item/completed bug above plus the child-thread adoption hole, the row/item routing asymmetry, and both rehydration gaps. All fixed here.

Note

Attribute reused subagents to the run reactivating them in orchestration-v2

  • Rehydrates previously spawned subagents at root-turn start in CodexAdapterV2 by rebuilding the process-local registry from existingSubagents and rebinding them to the active run on collab agent interactions.
  • Extends event routing in RunExecutionService to match subagent rows, activations, and turn items by identity (not just run/thread), so reused subagents are admitted and tracked correctly.
  • Adds canReactivateSubagent to allow reactivation of idle/interrupted subagents; failed/cancelled are excluded. Classifies idle as a terminal status for cascade/routing purposes.
  • Finalizing a run now also terminalizes any open subagent activations and clears currentActivationId via cascadeTerminalizeRunOwnedSubagents.
  • Adds test fixtures and assertions for the subagent_reuse_after_idle scenario; the fixture is scaffolded but not yet registered in the test suite due to a replay hang.
  • Behavioral Change: live status updates for a subagent are suppressed when currentActivationId is null, replacing the removed isSettledCodexSubagent check.
📊 Macroscope summarized cde2598. 10 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0876b0a0-c70c-46fc-8dc3-1fd66b396790

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch subagent-obs/03-reuse

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 27, 2026
… them

A subagent row keeps the run id it was spawned under. Routing admits a
row by run or by parent thread, and the parent thread is excluded by
construction, so a later run re-activating that subagent could only see
its updates while the spawning run's ingestion fiber happened to still be
alive. The Claude adapter already re-attributed on reopen for exactly
this reason; this brings the same treatment to routing and to Codex.

Reuse arrives on item/completed, not item/started: resumeAgent and
sendInput only ever complete, and registerSubagentThreads ignores them
because they are not spawnAgent. Hooking the rebind there is what makes
it fire — subagent_continue now asserts the subagent is attributed to the
run that drove it, where before it stayed pinned to the spawning run.

Routing gains a third test: subagent identity, seeded from the projection
at turn start. The child thread is adopted only while the agent is live,
because a settled row is also re-emitted by trailing traffic — a token
usage frame or a collab state sweep — and adopting on those would hand a
later run an interrupted agent's thread, readmitting the stale events
that post-interrupt recovery exists to exclude.

A subagent's row and its timeline item are emitted together, so both
route by identity; matching only the row advanced the agent while its
item stayed frozen on the spawning run.

The reactivation predicate is separate from canRouteRelatedSubagent
rather than a widening of it, for the same reason: an interrupted agent
must not have its child thread pre-owned, but its identity is still
resumable, because the user stopped it rather than losing it.

Rehydration seeds turn ordinals alongside the registry — restarting them
at 1 would re-derive childRootNodeId for a different native turn — and
parks non-terminal leftovers at idle, since a new session can never
terminalize an activation it did not drive and would otherwise keep the
session pinned open.

Not proven: rehydration itself. The fixture that would exercise it is
recorded but does not replay to completion, for reasons unrelated to
these changes, and is left unregistered with the open question written
up in fixtures/index.ts.
@shivamhwp
shivamhwp force-pushed the subagent-obs/02-adapters branch from 9296337 to 760a4c2 Compare July 27, 2026 23:03
@shivamhwp
shivamhwp force-pushed the subagent-obs/03-reuse branch from 5187d5b to cde2598 Compare July 27, 2026 23:03

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.

🟡 Medium

A reused subagent's turn_item.updated event passes the new ownsSubagent route check but trackChildLifecycle only considers belongsToRootRun = event.turnItem.runId === input.run.id for the subagent-item branch, so the item is never inserted into openRunOwnedSubagents.turnItems. When the reactivating run is interrupted or fails, the cascade terminalizes the subagent row and activation but leaves this timeline item non-terminal, so it stays visibly stuck in the projection. Extend the root-ownership test in trackChildLifecycle to include routing.ownedSubagentIds.has(event.turnItem.subagentId) (matching the routing logic).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/RunExecutionService.ts around line 775:

A reused subagent's `turn_item.updated` event passes the new `ownsSubagent` route check but `trackChildLifecycle` only considers `belongsToRootRun = event.turnItem.runId === input.run.id` for the subagent-item branch, so the item is never inserted into `openRunOwnedSubagents.turnItems`. When the reactivating run is interrupted or fails, the cascade terminalizes the subagent row and activation but leaves this timeline item non-terminal, so it stays visibly stuck in the projection. Extend the root-ownership test in `trackChildLifecycle` to include `routing.ownedSubagentIds.has(event.turnItem.subagentId)` (matching the routing logic).

sandboxPolicy: workspaceWriteSandbox(),
},
steps: [
{

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.

🟡 Medium scripts/record-codex-app-server-replay-fixture.ts:467

The subagent_reuse_after_interrupt scenario hangs indefinitely during recording. When interruptAfterCommandExecutionStarted is set, runTurnStep waits on a commandExecution notification keyed by the root turn's id. But the command that the fixture prompt asks the subagent to run (sleep 30) is reported with the child subagent's turn id, not the root turn id. So the deferred for the root turn is never completed, and the recorder blocks before sending turn/interrupt and never reaches the reuse step. Consider keying startCommandExecution (or the wait condition) on the spawned subagent's turn id, or using item/started/item/completed notifications from the child turn rather than the root turnId.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/scripts/record-codex-app-server-replay-fixture.ts around line 467:

The `subagent_reuse_after_interrupt` scenario hangs indefinitely during recording. When `interruptAfterCommandExecutionStarted` is set, `runTurnStep` waits on a `commandExecution` notification keyed by the root turn's id. But the command that the fixture prompt asks the subagent to run (`sleep 30`) is reported with the child subagent's turn id, not the root turn id. So the deferred for the root turn is never completed, and the recorder blocks before sending `turn/interrupt` and never reaches the reuse step. Consider keying `startCommandExecution` (or the wait condition) on the spawned subagent's turn id, or using `item/started`/`item/completed` notifications from the child turn rather than the root `turnId`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant