Skip to content

Feature: Parent is never notified when a delegated child finishes a follow-up turn (V2) #13490

Description

@AmishHillBilly

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Parent calls delegate_task (async). The child finishes and the parent is woken as expected.
  2. Parent sends the same child more work with t3_thread_send.
  3. The child finishes that follow-up turn.

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:

const existingResultTransfer = parentProjection.contextTransfers.find(
(transfer) =>
transfer.type === "subagent_result" &&
transfer.sourceThreadId === childThreadId &&
transfer.targetThreadId === parentThreadId,
);
if (existingResultTransfer !== undefined) {
return;

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.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Sep 24, 2026
  2. juliusmarminge commented on Sep 24, 2026

    @juliusmarminge
    Member

    Triage

    Verdict: Not a bug. On t3code/codex-turn-mapping (PR #2829, HEAD f6da5af0f0) an async child wakes its parent once, when the delegated task’s result is first published. A later t3_thread_send turn is recorded on task_status.latestTerminal* and does not wake the parent. This code is not on main.

    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

    handleTerminalRun does call finalizeAppOwnedSubagent for every terminal child run, including a follow-up (Orchestrator.ts around 9035–9038). That function returns immediately when the parent already has a subagent_result transfer 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_status still exposes that turn: summary stays the original published result, and latestTerminalRunId / latestTerminalStatus / latestTerminalSummary move 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.ts around 1402) is the wake for the delegated task’s published completion. The async delegate_task text tells the parent to end the turn instead of polling for that completion. It does not promise a wake for later sends.

    task_status and the V2 MCP doc say the published summary and result transfer stay stable after publication, and that later non-monitor turns are latestTerminal* only:

    • apps/server/src/mcp/toolkits/orchestrator/tools.ts (task_status description)
    • docs/orchestration-v2/orchestrator-mcp-server.md lines 267–270 (03ad271bad, “distinguish delegated task results from completed turns”)

    The integration test encodes the same outcome. After a queued follow-up completes, summary and resultContextTransferId are still the original delegation, and latestTerminalResultContextTransferId is null (OrchestratorMcpToolkit.integration.test.ts around 3344–3354, from 92006e779c).

    Startup recovery uses the same latch. getRecoveryThreadIds("subagent-results") only revisits a child that is terminal and has no subagent_result transfer at all (ProjectionStore.ts around 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. planDelegatedCompletionDelivery refuses to claim or offer again once completionDelivery is acknowledged, delivered, or disposed (Orchestrator.ts 7897–7911). The early return runs before that planner, so it is the gate that actually fires. It also keeps the published summary from being overwritten by the follow-up text.

    Relation to #12285 and #13338

    #12285 is the per-cohort lifetime cap: settledDeliveryCount >= 2 stops wakes for other children delegated in the same parent run. #13338 (open) removes that cap in planDelegatedCompletionDelivery, finalizeDelegatedCompletionDelivery, and dispatchNotificationAccepted. Its diff does not change the existingResultTransfer return, 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. summary could stay stable while the wake carried latestTerminal*.

    Workarounds

    These already match the implementation:

    • t3_thread_wait on the runId returned by t3_thread_send blocks the current parent turn until that follow-up finishes. Waiting does not acknowledge a delegated result.
    • Poll task_status and read latestTerminal*. hasPendingChildRuns covers a follow-up that is still queued or running.
    • delegate_task again 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. Drop bug unless 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.

  3. added
    via-triageFiled through npx t3 triage
    and removed
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Sep 24, 2026
  4. 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
  5. Dropje97 commented on Sep 30, 2026

    @Dropje97

    Thanks for the detailed explanation. We encountered this same behavior today on 0.0.43-preview.20260928.2413 on 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 through task_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_send tool 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.

  6. matsvarn commented on Oct 3, 2026

    @matsvarn

    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_send and ends its turn, expecting a wake. The follow-up finishes, and the parent stays idle until the user intervenes. I reproduced this on 0.0.46-nightly.20261003 with a Claude parent and a Codex child. The follow-up result was visible only in task_status.latestTerminalSummary.

    #15115 avoids the hang by telling agents to call delegate_task again 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 continueTaskId can 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_busy error without dispatching. The call is not queued or steered.
    • Provider and model come from the child. target overrides are rejected.
    • Retries with the same clientRequestId are 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 returns invalid_request instead of replaying the old result.
    • t3_thread_send keeps its current behavior. Its description and the injected instructions point to continueTaskId for 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_send follow-ups.
    • Stop semantics. A separate issue covers a pre-existing gap I found while testing.

    Server shape

    • Each round is bound with a subagent_spawn transfer. The spawn and result transfer payloads gain an optional taskId. No SQL migration is needed. History without taskId falls 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 finalizeAppOwnedSubagent and the pair-wide recovery predicate.
    • Delegated task progress is scoped to the round's runs. task_cancel on 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=wait returns;
      • crash recovery parity between SQL and memory;
      • cancellation isolation;
      • unchanged plain-send assertions.
    • Live check with real providers: I adapted verify-background-live.ts to use a fresh T3 home, a Claude Opus 5.5 parent, and a Codex gpt-6.1-sol child.
      • The parent called delegate_task({ continueTaskId }) and ended its turn.
      • When round 2 finished, it was woken by a delegated_task notification 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.

    Is this direction and scope acceptable? If so, I'll open the PR with full evidence.

  7. royalaid commented on Oct 4, 2026

    @royalaid

    Still present on 0.0.46-nightly.20261003.2632 (macOS, Codex child), and it has a visible symptom in the UI as well:

    1. Parent calls delegate_task (async); the child's first run ends (here, by asking the parent a question).
    2. Parent answers with t3_thread_send to the child thread; the child starts a follow-up run and works.
    3. 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_status at that moment returns status: "completed", workState: "result_available", hasPendingChildRuns: true, and latestTerminal* 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_status polling.

    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.

  8. YL404 commented on Oct 8, 2026

    @YL404

    Reproduced on 0.0.46-nightly.20261008.2819 (macOS), provider acpRegistry_devin (Devin ACP), model swe-2-high — so this isn't Claude/Codex-specific.

    Concrete trace from a single delegated task:

    1. delegate_task (mode wait) → child completed, summary delivered to parent, resultContextTransferId set. Normal path works.
    2. t3_thread_send to the same childThreadId → 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.
    3. task_status afterwards: latestTerminalRunId/latestTerminalSummary hold the follow-up result, but latestTerminalResultContextTransferId is null and the parent was never woken — matching this issue exactly.

    One additional angle the current tool descriptions create, in case it helps prioritize: delegate_task says the childThreadId is "not the target for starting another delegated review round", and t3_thread_send repeats 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's childThreadId continues that subagent's session; results appear under task_status latestTerminal*" — would make the capability explicit while keeping the review-round guidance intact.

    Longer term, a first-class followUpOf/continueTaskId parameter on delegate_task would give follow-ups task tracking, clientRequestId idempotency, and completion wake-ups — but documenting the current path is a much smaller change that unblocks correct usage today.

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

    via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions