delegate_task async completions are silently starved after two wake deliveries per cohort — parent never wakes for third-wave children (V2 branch)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Summary
On the Orchestrator V2 branch (t3code/codex-turn-mapping, PR #2829 — this code is not on main), delegate_task with mode: "async" promises the parent a wake on every child terminal (completionWake: "always", OrchestratorMcpService.ts:1282), and its tool description tells the agent to "end the turn instead of polling." But each delegation cohort (the set of children delegated in one parent run) carries a lifetime cap of two wake deliveries: settledDeliveryCount. Any child whose completion lands in a third wave stays completionDelivery: "pending" forever — no wake, no queued message, no re-arm on restart. The orchestrating agent, following the contract, sits idle waiting for a completion that already happened.
With 4–5+ parallel children whose completions straddle wake turns this is near-guaranteed; with 2–3 it almost never fires, which is why it reads as intermittent.
Originally reported as a comment on #2829 (2026-08-30, comment 5466160819); re-filing as an issue per the triage of #11168 (same class: V2 MCP contract violation). Re-verified today on the current V2 head b4cc55c24 (0.0.42) — all three cap sites are still present.
Where the mismatch lives
Three sites in apps/server/src/orchestration-v2/Orchestrator.ts at b4cc55c24:
planDelegatedCompletionDelivery— line 7098:if (settledDeliveryCount >= 2) { // A cohort permits one initial delivery and one successor. Keep the // result pending and inspectable instead of recursively re-arming the // parent for every child that finishes after that bounded handoff. ... completionDelivery: { state: "pending" ... }, offer: falsefinalizeDelegatedCompletionDelivery(the sweep that runs when a wake turn settles) — line 7468:canReserveFollowUp = ... && settledDeliveryCount < 2 && pendingTaskIds.length > 0dispatchNotificationAccepted(the durable-mailbox re-arm path, added since the original report) — line 7570:pendingTaskIdsis computed only when(cohort.settledDeliveryCount ?? 0) < 2, so an accepted delivery never reserves a successor either.
settledDeliveryCount never resets, so this is not a throttle — it is lifetime starvation per cohort. Nothing revisits a "pending" task after the cap engages: handleTerminalRun only sweeps delivery runs, the startup recovery path re-runs the same capped sweep, and finalizeAppOwnedSubagent early-returns on existingResultTransfer.
Evidence (projection forensics from a real thread)
Parent run 7 delegated five async children (t10, t11, t13, t15, t17), so all five share run 7's cohort. From orchestration_v2_projection_*:
| time (UTC) | event | cohort |
|---|---|---|
| 01:01:52 | run 7 delegates t10, t11, t13, t15, t17 | — |
| 01:05:49 | t10 completes → delivery 1 → wake run 8 | settled=0, gen 1 |
| 01:05:56–01:07:01 | t17, t13, t15 complete during run 8 → "pending" |
|
| 01:07:11 | run 8 settles → sweep reserves delivery 2 = {t15, t17, t13} → wake run 9 | settled=1, gen 2 |
| 01:07:20 | t11 completes 8 s into run 9 → "pending" |
|
| 01:09:12 | run 9 settles → settledDeliveryCount → 2, pendingTaskIds=[t11], canReserveFollowUp requires < 2 → refused |
settled=2, delivery=null |
| 01:09–01:29 | parent idle for 20 min; t11's result queryable, no wake exists or ever will | |
| 01:29:48 | human notices and nudges; that run reads task_status → t11 finally acknowledged |
Final cohort row: settledDeliveryCount=2, nextGeneration=3, disposition=open, delivery=null with a terminal, never-delivered task. Children delegated during later wake turns started fresh cohorts, so their wakes worked — which masks the bug at moderate parallelism.
(The queued-delivery merging — t13/t15/t17 amending one bundled wake — is the right behaviour. Only the lifetime cap is the problem.)
Steps to reproduce
- From a parent thread,
delegate_taskfive async children in a single turn, sized so their completions stagger (e.g. 1 min, 3 min, 3 min, 3 min, 5 min). - Let the parent end its turn after dispatch (as the tool text instructs).
- Observe: wake 1 for the first completion; wake 2 bundles the next three; the fifth child completes during wake 2 and is never delivered.
task_statusshows itcompleted; the parent never wakes.
The integration test OrchestratorMcpToolkit.integration.test.ts currently asserts the capped behaviour (a third terminal does not reserve a third delivery), so the cap is intentional — the report is that the intent contradicts the tool contract.
Suggested fixes (any one closes it)
- Make the cap a concurrency bound, not a lifetime bound. The recursion the comment fears is already bounded by "one delivery in flight + one reserved successor", and each child settles exactly once. Drop the
< 2gates (keep the counter as metadata). We run the V2 branch daily with exactly this patch (all three sites; integration test updated so a third terminal reserves a third delivery) — 800+ server tests green, no runaway re-arming observed across ~3 weeks of heavydelegate_taskuse. Happy to open it as a PR if wanted. - Sweep pendings on any parent-run settle: extend
handleTerminalRunto arm a delivery for terminal app-owned tasks left"pending"whenever the parent goes non-live. The existing generation +alreadyDispatcheddedupe inProviderContinuationServiceguards duplicates. - At minimum, don't be silent: surface refused deliveries (system line: "N delegated results pending — read with task_status") and soften the tool description's "end the turn instead of polling", which is currently a trap.
Environment
- V2 branch head
b4cc55c24(0.0.42), packaged macOS arm64 desktop build; also reproduced on the 2026-08-28 and 2026-09-13 heads. - Provider-independent (orchestrator code path); observed with Pi and Claude children.
- macOS 15, Apple Silicon.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/server/src/orchestration-v2/Orchestrator.ts at planDelegatedCompletionDelivery, finalizeDelegatedCompletionDelivery, and dispatchNotificationAccepted, then read the related cohort and wake-delivery flow. Run OrchestratorMcpToolkit.integration.test.ts to reproduce the intentional two-delivery behavior. Done means the chosen contract is implemented consistently and the integration test verifies terminal children beyond the second delivery are handled without starvation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100