Repository navigation
Feature: Parent is never notified when a delegated child finishes a follow-up turn (V2) #13490
Description
Activity
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.Issue needs maintainer review and initial categorization.
on Sep 24, 2026 Triage
Verdict: Not a bug. On
t3code/codex-turn-mapping(PR #2829, HEADf6da5af0f0) an async child wakes its parent once, when the delegated task’s result is first published. A latert3_thread_sendturn is recorded ontask_status.latestTerminal*and does not wake the parent. This code is not onmain.The report’s mechanism is right, and it is still the behavior at this HEAD. It is a separate issue from #12285 / #13338.
What the report gets right
handleTerminalRundoes callfinalizeAppOwnedSubagentfor every terminal child run, including a follow-up (Orchestrator.tsaround 9035–9038). That function returns immediately when the parent already has asubagent_resulttransfer for that child thread:const existingResultTransfer = parentProjection.contextTransfers.find( (transfer) => transfer.type === "subagent_result" && transfer.sourceThreadId === childThreadId && transfer.targetThreadId === parentThreadId, ); if (existingResultTransfer !== undefined) { return; }
Those lines are unchanged since
6952f57c88c. The check is on the thread pair, not the child run, so the second terminal turn never creates another transfer and never offers another wake.task_statusstill exposes that turn:summarystays the original published result, andlatestTerminalRunId/latestTerminalStatus/latestTerminalSummarymove to the follow-up. That matches the reported symptom on 0.0.43-preview.20260923.2138 and on this newer HEAD.Why that is the contract
completionWake: "always"(OrchestratorMcpService.tsaround 1402) is the wake for the delegated task’s published completion. The asyncdelegate_tasktext tells the parent to end the turn instead of polling for that completion. It does not promise a wake for later sends.task_statusand the V2 MCP doc say the published summary and result transfer stay stable after publication, and that later non-monitor turns arelatestTerminal*only:apps/server/src/mcp/toolkits/orchestrator/tools.ts(task_statusdescription)docs/orchestration-v2/orchestrator-mcp-server.mdlines 267–270 (03ad271bad, “distinguish delegated task results from completed turns”)
The integration test encodes the same outcome. After a queued follow-up completes,
summaryandresultContextTransferIdare still the original delegation, andlatestTerminalResultContextTransferIdisnull(OrchestratorMcpToolkit.integration.test.tsaround 3344–3354, from92006e779c).Startup recovery uses the same latch.
getRecoveryThreadIds("subagent-results")only revisits a child that is terminal and has nosubagent_resulttransfer at all (ProjectionStore.tsaround 484–489 and the SQL around 3318–3323). A follow-up after the first transfer is not recovered into a wake either.Deleting only the early return would still not wake the parent after the first delivery settles.
planDelegatedCompletionDeliveryrefuses to claim or offer again oncecompletionDeliveryisacknowledged,delivered, ordisposed(Orchestrator.ts7897–7911). The early return runs before that planner, so it is the gate that actually fires. It also keeps the publishedsummaryfrom being overwritten by the follow-up text.Relation to #12285 and #13338
#12285 is the per-cohort lifetime cap:
settledDeliveryCount >= 2stops wakes for other children delegated in the same parent run. #13338 (open) removes that cap inplanDelegatedCompletionDelivery,finalizeDelegatedCompletionDelivery, anddispatchNotificationAccepted. Its diff does not change theexistingResultTransferreturn, the one-result recovery query, or the “published summary stays stable” test. One child plus a follow-up send still would not wake the parent with that PR applied.If a follow-up wake is wanted
That is a feature change to the published-result contract, not a fix for a skipped first wake. It would need a per-run result transfer, a new completion delivery after the previous one has settled, a recovery predicate that notices a newer terminal run, and updates to the doc and the integration test that currently require
latestTerminalResultContextTransferId: null.summarycould stay stable while the wake carriedlatestTerminal*.Workarounds
These already match the implementation:
t3_thread_waiton therunIdreturned byt3_thread_sendblocks the current parent turn until that follow-up finishes. Waiting does not acknowledge a delegated result.- Poll
task_statusand readlatestTerminal*.hasPendingChildRunscovers a follow-up that is still queued or running. delegate_taskagain if a new wake is required. That starts a new child and does not continue the previous child’s conversation.
Disposition
Take off
needs-triage. Dropbugunless this is retitled as a feature request for a wake on each later child turn. Do not close it as a duplicate of #12285 or #13338.- addedvia-triageFiled through npx t3 triageFiled through npx t3 triageand removedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.Issue needs maintainer review and initial categorization.
on Sep 24, 2026 - changed the title
[-][Bug]: Parent is never notified when a delegated child finishes a follow-up turn (V2)[/-][+]Feature: Parent is never notified when a delegated child finishes a follow-up turn (V2)[/+]on Sep 24, 2026 Thanks for the detailed explanation. We encountered this same behavior today on
0.0.43-preview.20260928.2413on Linux, with Claude Opus orchestrating a Codex child.After the initial review, the parent sent fixes back to the same child for verification using
t3_thread_send. The parent then ended its turn expecting to resume when the follow-up finished. The child completed with actionable findings, but the parent remained idle until the user intervened about 12 minutes later. The result was available throughtask_status.latestTerminalSummary.We understand that this follows the current contract. From the user's perspective, though, it feels like a broken continuation: reusing a reviewer with its context intact is a natural workflow, yet completion silently stops waking the parent after the first result.
Starting a fresh delegated task works around this, but loses the existing conversation context and adds repeated reading and handoff work.
Would you consider supporting a parent wake for follow-up results, while keeping the original published task summary stable? If that is outside the intended design, an explicit warning in the
t3_thread_sendtool description/result, together with the supported way to wait for that follow-up, would help agents avoid this trap.Sharing this as another real-world example of the impact. Thanks for considering it.
Scope proposal: continue a delegated child as a new task round
I'd like maintainer approval of a narrow direction before opening a PR.
Problem
An agent reuses a reviewer child to keep its context. It sends a follow-up with
t3_thread_sendand ends its turn, expecting a wake. The follow-up finishes, and the parent stays idle until the user intervenes. I reproduced this on0.0.46-nightly.20261003with a Claude parent and a Codex child. The follow-up result was visible only intask_status.latestTerminalSummary.#15115 avoids the hang by telling agents to call
delegate_taskagain for each round. That works, but each round starts a fresh child. The agent has to re-brief the reviewer, and the reviewer has to re-read what it already read.Proposal
Add one optional field to
delegate_task,continueTaskId:delegate_task({ continueTaskId, task, clientRequestId, mode })
This creates a new task whose child is the existing child thread of
continueTaskId. The current contract stays the same: one task, one published result, one completion delivery.- The original task's status, summary, and result transfer do not change.
- The new round has its own
taskId, result transfer, and wake. The parent is woken through the existing mailbox, as for any delegation. - Only the direct app-owned parent of
continueTaskIdcan continue it. - Only an idle child can be continued. The previous round must be published, with no active or queued run and no pending request. A busy child returns a retryable
task_busyerror without dispatching. The call is not queued or steered. - Provider and model come from the child.
targetoverrides are rejected. - Retries with the same
clientRequestIdare idempotent. Each round needs a new key, as fix(server): keep delegated review rounds on the task API #15115 already asks. Reusing the previous round's key returnsinvalid_requestinstead of replaying the old result. t3_thread_sendkeeps its current behavior. Its description and the injected instructions point tocontinueTaskIdfor continuing a delegated child.
Out of scope
- Changing the original task's status or result after a follow-up.
- New client UI. The new round renders as a normal delegated task in the parent's current turn. The only client change is that a child's lineage entry shows its latest round.
- Waking the parent for plain
t3_thread_sendfollow-ups. - Stop semantics. A separate issue covers a pre-existing gap I found while testing.
Server shape
- Each round is bound with a
subagent_spawntransfer. The spawn and result transfer payloads gain an optionaltaskId. No SQL migration is needed. History withouttaskIdfalls back to the founding task. - Finalization and startup recovery select by task instead of by the (child thread, parent thread) pair. This replaces the early return in
finalizeAppOwnedSubagentand the pair-wide recovery predicate. - Delegated task progress is scoped to the round's runs.
task_cancelon a round interrupts that round's run.
Evidence so far
I have a working branch on top of
main: 22 files, +1900/−362, most of it tests. It isn't pushed yet; I'll open the PR only if this direction is approved.- Focused tests pass: 11 files and 136 tests, covering contracts, the MCP service, the toolkit integration test, the projection store, subagent projection, delivery, and client lineage.
- Integration tests cover:
- two rounds on one child, with distinct IDs and results, where round 2 wakes the parent;
- busy and foreign-task rejection;
- retry idempotency;
- continuing right after
mode=waitreturns; - crash recovery parity between SQL and memory;
- cancellation isolation;
- unchanged plain-send assertions.
- Live check with real providers: I adapted
verify-background-live.tsto use a fresh T3 home, a Claude Opus 5.5 parent, and a Codexgpt-6.1-solchild.- The parent called
delegate_task({ continueTaskId })and ended its turn. - When round 2 finished, it was woken by a
delegated_tasknotification and acknowledged the result. - The child recalled a code word from round 1, so the conversation context was kept.
- Round 1's result was unchanged, and a server restart created no duplicate runs.
- The parent called
Is this direction and scope acceptable? If so, I'll open the PR with full evidence.
Still present on 0.0.46-nightly.20261003.2632 (macOS, Codex child), and it has a visible symptom in the UI as well:
- Parent calls
delegate_task(async); the child's first run ends (here, by asking the parent a question). - Parent answers with
t3_thread_sendto the child thread; the child starts a follow-up run and works. - While that follow-up run is active, the parent's Previous agents list shows the task as Done, with the first run's duration (1m 24s), although the child is working.
task_statusat that moment returnsstatus: "completed",workState: "result_available",hasPendingChildRuns: true, andlatestTerminal*still points at the first run. So both the wake-up described here and the task's status label stop at the first result.We also hit the wake-up half today: three delegated children each finished a follow-up turn and none of them woke the parent; the results only showed up through
task_statuspolling.Expected: while a follow-up run is active the list shows the child as working, and the task's status and the parent's wake-up follow each later run's result.
- Parent calls
Reproduced on
0.0.46-nightly.20261008.2819(macOS), provideracpRegistry_devin(Devin ACP), modelswe-2-high— so this isn't Claude/Codex-specific.Concrete trace from a single delegated task:
delegate_task(modewait) → child completed,summarydelivered to parent,resultContextTransferIdset. Normal path works.t3_thread_sendto the samechildThreadId→ a second run started in the backing thread (ordinal:2) and completed. The follow-up demonstrably ran in the same session — the child still remembered context from round 1 with no transcript supplied.task_statusafterwards:latestTerminalRunId/latestTerminalSummaryhold the follow-up result, butlatestTerminalResultContextTransferIdisnulland the parent was never woken — matching this issue exactly.
One additional angle the current tool descriptions create, in case it helps prioritize:
delegate_tasksays thechildThreadIdis "not the target for starting another delegated review round", andt3_thread_sendrepeats the same scoped prohibition. Agents reading these descriptions tend to generalize that into "the childThreadId is off-limits" entirely, so most never discover that session-continuing follow-ups are possible at all. Two consequences: the feature gets less real-world usage, and this gap gets hit less often. A short positive sentence in either description — e.g. "sending a message to a delegated task'schildThreadIdcontinues that subagent's session; results appear undertask_statuslatestTerminal*" — would make the capability explicit while keeping the review-round guidance intact.Longer term, a first-class
followUpOf/continueTaskIdparameter ondelegate_taskwould give follow-ups task tracking,clientRequestIdidempotency, and completion wake-ups — but documenting the current path is a much smaller change that unblocks correct usage today.
Before submitting
Area
apps/server
Steps to reproduce
Expected behavior
The parent is woken for the follow-up result, the same as for the first one.
Actual behavior
The parent is never woken. The result only shows up if the parent polls task_status (latestTerminal*).
It looks like finalizeAppOwnedSubagent returns early once the child has already delivered one result, so every later turn is skipped:
t3code/apps/server/src/orchestration-v2/Orchestrator.ts
Lines 8168 to 8175 in 5239fe0
This is on the V2 branch (#2829), not main. It's different from #12285 / #13338: it happens with just one child, and #13338 doesn't change this check.
Impact
Major degradation or frequent failure
Version or commit
0.0.43-preview.20260923.2138 (Linux). Mac:
Environment
Linux (self-hosted server) and macOS. Claude and Codex providers.
Logs or stack traces
Screenshots, recordings, or supporting files
No response
Workaround
Start a new delegate_task for each follow-up instead of using t3_thread_send, or have the parent poll task_status.