Repository navigation
Description
What happened
A delegate_task call came back as failed right away, so the parent agent retried it. The first child was still running, though. It finished later and reported completed, so both children did the same work at the same time and edited the same files.
Diagnosis
delegate_task(mode: "wait", Claude provider,claude-fable-5-2, effort high) returned immediately withstatus: "failed",workState: "result_available",latestTerminalStatus: "failed". The summary was a transient upstream error:Failed to authenticate. API Error: 403 Attention Required! | Cloudflare. Native Claude subagents in the same thread hit the same 403 at about the same time, so this was an edge or auth blip, not a bad credential. It lines up with a Claude incident, Elevated errors on claude.ai, Claude Code, and platform.claude.com (opened 09:58 UTC for authentication failures, fix deployed 11:05 UTC). The upstream error is only the trigger. This report is about how T3 handles a provider failure, which any provider outage can cause.- The parent retried with an identical
delegate_task. That child completed and said files had been changed under it by "another writer". - Later the parent received a completed notification with the full result of the first task. It arrived under a different node id than the one
delegate_taskhad returned:- returned:
node:delegated-task:command%3Amcp%3A<uuid>%3Adelegate-task%3A<task-id> - notified:
node:delegated-task:<other-id>
- returned:
Likely code path:
apps/server/src/mcp/OrchestratorMcpService.ts(around line 1813):waitreportsresult_availableplus the run's terminal status as soon as one child run ends, so a single failed run becomes the task's final result.- The same file (around line 1540,
heldForRestart/delegatedTaskResultPending) holds the status back only when the child will be continued after a T3 restart. Nothing similar covers a run that fails on a transient provider error and then continues. - T3 did not restart during this. The desktop app last installed an update and relaunched at 07:58 UTC, nearly two hours before the failure, and the server process ran without a restart from then on. So restart continuation is not what kept the child going.
- Because the completion arrived under a different node id, the parent had no way to tie it back to the "failed" task and stop its retry.
This looks related to #29988 (a delegated task fails on a passing provider error, so a retry repeats finished steps). That issue is closed as completed, but no fix is linked to it, and it doesn't cover the "failed" child carrying on and completing on its own.
Steps to reproduce
Observed once, not yet a deterministic repro:
- From a Claude thread, call
delegate_taskwithmode: "wait"for a long task on a Claude model. - Have the provider return a transient error (here, a Cloudflare 403) early in the child's run.
delegate_taskreturnsfailed. Retry it.- Both children run at the same time, and the first one later notifies completion under a different node id.
Expected: a transient provider failure keeps the task working until the child really stops (as across restarts), or a task reported failed has its child stopped. A delegated task's node id should also stay the same from the delegate_task result through to its completion notification.
Version
0.1.10-nightly.20261113.4616 (desktop app, local server)., running since 07:58 UTC with no restart during the incident
Environment
macOS 26.7.1 arm64, Node v26.13.0, Claude Code 2.1.311
Evidence
Timeline (UTC, 2026-11-13), from the parent thread's activity:
| Time | Event |
|---|---|
| 07:58 | Desktop update to 0.1.10-nightly.20261113.4616 installed and the app relaunched; no restart after this |
| ~09:43 | delegate_task called |
| 09:47:17 | It returns failed with the 403 |
| 09:53 | Parent retries with a second delegate_task |
| 09:58 | Claude incident opened |
| 10:00:43 | The first task finishes as completed under node:delegated-task:<other-id> |
| 10:07:45 | The retry completes and reports "another writer" rewrote the files at 10:00 |
| 10:51:08 | The parent receives the late "Delegated task … finished" notification for the first task. It was queued at 10:00:43 but only delivered when the parent's current turn ended |
delegate_task -> status: failed, workState: result_available, latestTerminalStatus: failed
summary: "Failed to authenticate. API Error: 403 Attention Required! | Cloudflare"
retry delegate_task -> completed ("another writer finished it while I was reading")
later notification: node:delegated-task:<other-id> "…" status: completed (full result of the first task)Related issues
- [Bug]: Delegated task fails on a passing provider error instead of continuing the child, so a retry repeats finished steps #29988, the same retry-duplicates-work problem; closed with no linked fix
- Transient provider errors show as raw, repeated text: an assistant message, an error item, and again inside failed task cards #36149, how these provider errors are displayed
🤖 Claude Opus 5.5
Activity
juliusmarminge commentedon Nov 13, 2026
@joshka In the first delegated child thread (the one that returned the 403), what started the run that finished at 10:00:43: a message you sent or queued there, a 'background task finished' notification, a delegated sub-task's completion, or a message from the parent thread?
joshka commentedon Nov 13, 2026
A message I sent in the child thread myself. The child's run 2 starts from a
continueI typed there in the macOS desktop app at 09:53:22 (recorded ascreationSource: web), about 14 seconds after I typedcontinuein the parent. The parent'scontinueis what made it retry with a seconddelegate_task, so onecontinuein each thread gave two children doing the same work.So the child didn't continue on its own; I resumed it by hand. I wonder if this was PEBKAC, but oddly so: the UI showed every piece as failed (the parent's turn, the delegated-task card, and the child's own turn, see #36149), so continuing both looked like the obvious move. What still looks wrong:
failedafter its child is continued, and the run that completes is reported to the parent as "Delegated task … finished" undernode:delegated-task:36dd8cef-…, not the node iddelegate_taskreturned. The parent can't connect the two, so it doesn't know its retry is redundant.🤖 Claude Opus 5.5
joshka commentedon Nov 13, 2026
I suspect this is something that a coding agent could reasonably do "find the bits that are confusing about this when failure conditions happen and make the next steps clear regardless of how each failure happens" or something like that. hill climb against unhappy path responses and inject failures into these so they do the right thing?
jhillyerd commentedon Nov 14, 2026
This happens to me 100% of the time with 0.1.7 running a Pi agent, triggered by my
smart-loopskill which is based on tstack/sol-loopjoshka commentedon Nov 14, 2026
Ah good - likely worth getting a better repo then. For me this was a onetime thing, and felt worth fixing as it was duplicated work done on two threads to be preventative, but if you've got a dependable repro then I'll leave that to your clankers to eek out some way to write up the state better.
jhillyerd commentedon Nov 14, 2026
It seems a bit more reliable since upgrading to 0.1.9, but I managed to catch a failure and run the triage skill on it...
jhillyerd commentedon Nov 14, 2026
Note
🤖 GLM 5.3 responding on behalf of jhillyerd
Reproduced the same hazard class from a second, deterministic trigger: the caller's own MCP request timeout on a held
mode:"wait"response — no provider outage required.On T3 Code desktop/server 0.1.9 (Linux x64, Node v26.12.1, pi provider, parent and child both glm-5.3), a
delegate_taskwithmode:"wait",timeoutMs: 900000held its HTTP response while the child ran. The pi harness's MCP client (Nodefetch/undici, default 300 s headers/body timeout) aborted at almost exactly 300 s. Server trace:McpServer tools/call → delegate_taskspan 20:12:47.126Z → 20:17:48.044Z, 300,918 ms, exitInterrupted; the child thread was created at 20:12:47.143Z (17 ms after the call began) and completed normally at 20:18:47Z.What the parent saw: the tool call failed with the raw transport error
fetch failed— notaskIdorchildThreadId, so it could not tie the error to a task and had to find the child viat3_thread_list. A parent that treated the failure as real would have started a second reviewer on the same diff — exactly the duplicate-work outcome here. Recovery worked only because a project skill explicitly says afetch failedreturn is not a dead delegate.Differences from the original report: the server never reported the task
failed— nothing restarted the child; only the response connection died (server-side wait constants allow 10–60 min, far past the client's 300 s). And the "completion under a different node id" part did not reproduce on this path: the child's completion was delivered ~1 min after the transport failure under the samenode:delegated-task:command:mcp:<session>:delegate-task:<task-id>as the dispatch, mid-turn.This is the same client-timeout-vs-server-wait mismatch as #35714 (Cursor ~60 s) and #30006, confirmed here with server-side span timing. It argues for the expected behavior in both issues: return the
taskId/childThreadIdbefore the client's request timeout (or cap/downgradewaitper client), so an error return always leaves the parent a handle to the child.jhillyerd commentedon Nov 14, 2026
Note
🤖 GLM 5.3 responding on behalf of jhillyerd
Picking up the suggestion upthread — "find the bits that are confusing about this when failure conditions happen and make the next steps clear regardless of how each failure happens" — here's a walk of the delegation failure surface, verified against
main@f56f27e6(2026-11-14) and v0.1.9 (24e77473). Grouped by whether the next step is already clear.Already clear on
main:timeoutMsexpires server-sidewaitTimedOut: true+next: "Completion wakes this thread. Do not wait on it or poll it…"hasPendingQuestion/pendingApprovalend the wait with flags setheldForRestartkeepsworkState: "working"until the result is deliveredtask_statusdoesn't lie about livenessStill confusing — the rows where an error leaves the caller without a handle or a next step:
mode:"wait"outlasts the client's request timeout (pi/undici ~300 s here; Cursor ~60 s in [Bug]: delegate_task mode "wait" outlasts Cursor's ~60s MCP timeout; the child starts but the parent never gets its taskId #35714; Codex 30 s in [Bug]: delegate_task with mode: "wait" triggers continuous 30s polling loop in cell-based agent harnesses #30006). Caller gets a raw transport error — notaskId, nochildThreadId— while the child runs on. Since fix(server): a steer during a delegate_task wait no longer drops the child's result #27387 (Nov 11, in the Nov 13+ nightlies, not in v0.1.9) an interrupted wait upgrades the wake uninterruptibly, so the result still reaches the parent; what's missing is the handle at the moment of failure. The caller's only recovery is hunting viat3_thread_list, and a blind retry duplicates the work.main: one failed run ends the wait withstatus: "failed",workState: "result_available", the provider error assummary. Nothing distinguishes it from a real failure, nothing says the child thread is idle-but-continuable (t3_thread_sendtochildThreadId) or how to stop it (task_cancel) — that's exactly what invited the reporter's retry.continue):mainnow surfaceslatestTerminalRunId/Status/Summaryontask_status, which is the right window into it — but the "Delegated task … finished" notification can still arrive under a different delegated-task node than the onedelegate_taskreturned, so a parent matching on node id still can't tie them together.A retry mechanism already exists, but nothing advertises it:
delegate_taskis idempotent perclientRequestId—requestKeyuses it verbatim (OrchestratorMcpService.ts), and dispatch replays an accepted receipt's stored events instead of re-executing (replayCommandReceipt,Orchestrator.ts) — sametaskId, no second child. But omittedclientRequestId⇒ random key ⇒ a retry duplicates. In the v0.1.9 incident the agent passed none, so the receipt path was unavailable and thet3_thread_listhunt was the only safe move. If the tool copy said "pass a stableclientRequestIdwhen you may need to retry awaitcall; after a transport failure, re-send the identical call", rows 1 (and the restart case) would have a one-line correct next step. Cheap win, no protocol change.For the "inject failures and hill-climb" idea, the harness already exists —
OrchestratorMcpToolkit.integration.test.ts,TaskCancelNativeSubagent.integration.test.ts,OrchestratorMcpService.activity.test.ts. The four injections worth writing first, one per confusing row: (a) kill the HTTP response mid-wait; (b) fail the child's first provider call transiently, then let it pass; (c) continue a failed task's child to completion; (d) restart the server mid-wait. Assert each returns either a result, a handle (taskId/childThreadId), or an explicitnext— that's the invariant that makes every row clear: no delegated-task error or terminal return without either the handle or the instruction.Related: #35714 (client-timeout row), #30006, #36149 (the UI half — everything showing failed is what made continuing both threads look like the obvious move).