delegate_task mode:"wait" dies at the client ~5 min ceiling and returns no taskId, causing duplicate child dispatch
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Summary
delegate_task with mode: "wait" is unsurvivable for any child that runs longer than ~5 minutes when the MCP client is Node-based (Pi provider here, but this is true of any fetch/undici client). The wait is a single synchronous MCP request held open for the child's entire run; undici's default headersTimeout is 300 s, so the HTTP request dies first and the caller gets a bare fetch failed — with no taskId and no childThreadId.
The consequence is worse than a lost result: the parent agent has no handle to reconcile with, cannot call task_status, and cannot tell "the child never launched" from "the child is running fine". The only apparent recourse is re-dispatch — which spawns a second child writing into the same cwd.
Evidence (two independent occurrences, same day, same fingerprint)
| parent A | parent B | |
|---|---|---|
| child dispatched | 21:20:12Z | 01:10:01Z |
child actually ran to completed |
21:55:50Z | 01:15:03Z |
| parent's wait call failed → duplicate re-dispatch | 21:25:52Z (+5m40s) | 01:15:52Z (+5m51s) |
| duplicate child | interrupted 21:27:02Z |
interrupted 01:18:17Z |
Both retries landed at ≈5m45s after dispatch — the undici 300 s ceiling plus one agent turn. Control: a trivial mode:"wait" child (seconds long) returns a clean, complete envelope (taskId, childThreadId, status: completed, summary), so wait-mode itself is fine; only its duration tolerance is broken.
In the second case the duplicate child was smart enough to notice the first child's commit and refuse to redo the work. That was luck, not a guarantee — two concurrent writers in one working tree is the real hazard.
Where the mismatch lives
apps/server/src/mcp/OrchestratorMcpService.ts clamps the wait budget to a default of 10 minutes and a maximum of 60 minutes. Both sit well above the ~5-minute ceiling a Node fetch client can hold a response open, and nothing is emitted on the wire during the wait to keep the connection alive. The tool description also advertises waitTimedOut + "keep that taskId and read status on later task_status" — but that graceful path is only reachable when the server's own timer fires first, which by construction it cannot for the default budget.
Related: when the parent's turn was handed off/interrupted while a wait was in flight, the delegated child was interrupted too, so a child's lifetime appears coupled to the parent's in-flight tool call.
Suggested fixes (any one helps; 1+3 would close it)
- Emit MCP progress notifications (or SSE keep-alives) during
mode: "wait". Traffic on the stream resets the client's header/body timers, which makes long waits viable and is the behavior most MCP clients expect for long-running tools. - Or clamp the default/max wait below the client ceiling (e.g. default 120 s, max 240 s) and return the documented
waitTimedOut: trueenvelope, which at least preserves thetaskId. - Make the
taskIdobservable before the wait resolves — e.g. send it in an early progress notification, so a parent whose wait died can reconcile viatask_statusinstead of re-dispatching. Documenting "on anydelegate_tasktransport error, callt3_thread_listbefore retrying" would also help agents, since a server-generatedclientRequestIdcannot dedupe a retry (the caller never saw it).
Environment
macOS 15, Pi provider (anthropic/claude-opus-5), full-access runtime. Build is a local fork tracking the Orchestrator V2 branch (PR #2829) at 8f44bec / 0.0.40, with two unrelated local patches (Pi in the Usage dashboard; delegated-wake cap removal) — neither touches the MCP layer. The code paths cited are upstream-unmodified.
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 with apps/server/src/mcp/OrchestratorMcpService.ts, focusing on delegate_task wait budgets, timeout handling, and response delivery. Reproduce a wait longer than five minutes with a Node fetch/undici client, then trace whether progress or timeout responses preserve taskId and childThreadId. Done means long waits do not fail without a reconciliation handle or cause duplicate child dispatch.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100