App-owned follow-up completes while nested work runs, then loses the final result#21837

opened14d agobyJjmastrangelo

What happened

An app-owned follow-up published INTERIM as a completed result while its nested checker was still running. The checker later resumed the writer, which produced FINAL, but FINAL was not automatically delivered to the parent. The parent could mistake the interim response for completion.

Expected: keep the follow-up pending until its nested work and continuation finish, preserve continuation ownership, and deliver FINAL to the parent exactly once while preserving the original READY task result.

This was one controlled lifecycle with a deliberately interim response, not evidence of the model spontaneously stopping. Frequency is unknown.

Diagnosis

Saved task responses and scoped projections establish three observations: premature terminal publication, an automatic continuation without the follow-up task association, and no parent result transfer for that continuation through the final snapshot.

Source inspection at upstream commit 10f7db7e38ea353c5b2b6c324c155cb6a6bedf72 is consistent with this sequence: finalization gates nested-work readiness for the original task; follow-up intake excludes agent/server dispatches such as the delegated-completion wake. This is a source-level explanation, not a verified mapping to the tested executable.

Correction and original-obligation retention passed after recovery. All child work completed. Native compaction was neither exercised nor witnessed; behavior on newer builds and other providers has not been established.

Steps to reproduce

  1. Use async delegate_task to start a disposable writer. Ask it to retain a synthetic value M1 and an obligation for later work, then return READY.
  2. After READY, send one t3_thread_send(mode="queue") to that child. It starts immediately because the child is idle. Replace M1 with M2 and request one async nested checker via delegate_task.
  3. Have the checker wait about 40 seconds and return a synthetic marker. Instruct the writer to end its first follow-up turn immediately with INTERIM, then consume the checker result on automatic continuation and return FINAL.
  4. While the checker runs, inspect the returned follow-up task ID with task_status. After the automatic continuation ends, capture its task association and exact source-run parent transfers before reading the final result.
  5. Read that follow-up's task_status once to recover the later terminal summary.

This is a same-conversation continuation, not an independent review round. Independent delegated review rounds still require fresh delegate_task calls.

Version

v0.1.1-nightly.20261018.3335 — observed on 2026-10-19 UTC. This is the affected running build recorded during the experiment, not a triage CLI version or a claim about the current installation.

Environment

Linux/WSL, installed t3 serve runtime; T3 app-owned delegation. Writer and nested checker: Codex gpt-6.1-sol, reasoningEffort=high, serviceTier=default. Client, Node, and provider CLI versions were not recorded.

Evidence

Normalized excerpts from retained projections and tool responses; UTC on 2026-10-19:

text
102:36:27: checker running; follow-up completed/result_available
2 hasPendingChildRuns=false; published summary=INTERIM
302:37:02: checker completes; automatic writer run 3 starts
4 delegatedCompletion.parentRunId points to follow-up run 2
5 run 3 delegatedTaskId absent (SQL extraction: null)
602:37:42: run 3 completes with substantive FINAL
702:38:04: no parent subagent_result transfer for run 3
802:38:10: task_status exposes FINAL in latestTerminalSummary
9 latestTerminalResultContextTransferId=null
10 stable summary remains INTERIM
1102:38:20: final snapshot still has no parent transfer for run 3

Parent transfers exist for READY and INTERIM. The pre-recovery snapshot predates the recovery read. Four child runs and three task records completed.

Recorded executable SHA256: d01ba2e9bbdcaa4da0cb9a60a17a73a0861c2841080555b71401102a0b6cd30d. Installed executable-to-source mapping remains unknown.

Related issues

  • #13490, closed by merged #17696: direct follow-up delivery. This case adds nested work and an automatic continuation after that follow-up.
  • #16603: early completion with live work; includes an unconfirmed nested-agent observation. This report supplies a specific app-owned follow-up lifecycle with lost FINAL evidence; a common cause has not been established.
  • #21772 / #21813: provider-triggered wakes. The PR explicitly excludes delegated-completion wakes from grandchildren.
  • #18649, closed without merge: related result-delivery proposal, not a shipped fix.

Fix applied or workaround

No runtime fix applied. One later task_status read recovered FINAL from latestTerminalSummary; check the later run identity rather than treating the stable INTERIM summary as final. An explicit final parent handoff remains an operational workaround, but was deliberately not exercised here.

Filed by

Prepared by GPT-6 Astra through the Codex harness in T3 Code, using retained experiment evidence from Codex GPT-6.1 Sol. No t3 triage CLI run was performed.

Note

Grok responding on behalf of Julius.

Thanks for the controlled lifecycle and the careful limits. Current main matches both parts of your reading.

  1. Only the original delegated task waits for nested work before finishing (apps/server/src/orchestration-v2/Orchestrator.ts:10603-10614). A follow-up task finishes as soon as its latest run reaches a terminal state (Orchestrator.ts:10252-10262), so a follow-up can publish while its own nested delegate_task is still running.
  2. The wake that delivers a nested result is dispatched by the server as an agent message (apps/server/src/orchestration-v2/ProviderContinuationService.ts:119-135). Follow-up intake ignores that kind of message (Orchestrator.ts:6940-6949), so the continuation run has no task, and its result never reaches the parent.

This overlaps with #16603 and #21772, but neither covers it. #16603 is about the original task and running commands. The open fix for #21772 still leaves this wake out on purpose. Keeping this open as its own bug.

For now, reading the follow-up's task status after the nested work finishes shows the later result.

Reproduced on current main 70e5e384da using the web dev server on Linux with isolated, pruned real data. Writer and nested checker used Codex gpt-6.1-sol, high reasoning, standard service tier.

Ran READY → one queued follow-up → async checker (sleep 40) → INTERIM → automatic FINAL. While the checker ran, the follow-up already reported completed/result_available, summary INTERIM, hasPendingChildRuns=false. Writer run 3 later completed with FINAL BETA21837 CHECK21837, without delegatedTaskId or a parent result transfer. Parent received READY and INTERIM only. A final status read recovered FINAL via latestTerminalSummary; published summary stayed INTERIM and original READY remained intact.

Runtime transcript and scoped projection excerpts. The duplicate scan found no matching open report for this nested follow-up lifecycle.

Existing fix #21877 addresses this lifecycle; no replacement PR opened. The retained Linux web/server reproduction on 70e5e384da shows READY and INTERIM delivered, with FINAL recovered only through task status. Independent verification of the existing PR head could not finish: the VM became saturated and tool calls stalled. Issue remains open pending verification and landing.

Synced from GitHub

Still happening on 0.1.2 (stable) with Claude children: twice in two days, a t3_thread_send follow-up task finalized at the end of its own turn while the child's delegate_task reviewers and a run_in_background Bash command were still running. The real result came in a later turn and never reached the parent. Same shape as the report above. The race on the original-task path is filed separately as #24297.

Synced from GitHub

Additional production impact observed by our project orchestrator on 2026-10-25. The installed macOS desktop reports 0.1.3-nightly.20261024.3630. This is a current user/operator report, not a new controlled reproduction or an executable-to-source mapping.

Six delegated lanes today followed the same pattern: an interim result such as waiting on a background suite reached the parent; a background/watch completion automatically started a later child run; that run produced final PR/head/verdict information, but no completion notification reached the parent. The parent recovered the later result through task_status.latestTerminalSummary or a child timeline read. Original-task and follow-up paths should both be checked; #24297 may cover part of the former.

Could the existing fix ensure every terminal run of an owned child has durable parent-result delivery, including background/watch/self-wake runs after the original task has already finalized? Please preserve original task lineage and distinguish interim from final state; either associate those wakes with the owning task or deliver a child-run result keyed by child thread and terminal run. Exactly-once processing needs a durable delivery/consumption identity so crash/retry/manual recovery cannot lose a result or trigger duplicate integration.

Useful regression cases: original task and queued follow-up; nested child and background command completion; multiple later child runs; completed/idle versus running parent; interrupted/provider-failed terminal child; app restart between terminal event and delivery; late/manual status recovery; settled predecessor with explicit custody transfer. Preserve active background custody rather than settling a child merely because its interim task says completed.

We are adding explicit child-to-parent completion receipts plus per-terminal-run parent reconciliation as an operational workaround. This is not a runtime fix or independent proof that the pending PR resolves all reported cases. No credentials, private transcript or customer data are attached.

Synced from GitHub

Written by Claude Opus 5.5 on behalf of @ScottN-PV.

#21877 (head 3dda05ac50) covers less than this. It fixes one case: a queued follow-up whose own run starts a nested delegate_task. That follow-up stays pending until the nested task ends. The nested-result wake then runs under the follow-up's task, so the final reply reaches the parent once.

It does not cover wakes from background commands or watches. The follow-up waits while the provider lists the command as pending. The wake run that the command's exit starts has no task, so its result is not delivered. It also leaves the original task unchanged, and it adds no per-run or restart delivery handling. #21772 and #24297 track those paths.

This comes from reading the source and from our Linux test of the nested case. We have not run your macOS build.

Synced from GitHub

Focused tests confirm ScottN-PV's reading above. With #21877, a follow-up that starts a background command (CookerTea's run_in_background case) is held while the command runs, but it still completes with its interim reply.

When the command exits, Claude wakes the child in a new run. The original task already has a result, so recordSubagentFollowup gives that run a new task (Orchestrator.ts:7458 on main). The real reply does reach the parent, but as a second task with the same title, while the follow-up itself completes with its interim text. #21877's continuedTaskId avoids this for nested results, which name their source run in delegatedCompletion.parentRunId. A background-command wake names no run.

Two focused tests in DelegatedCompletionDelivery.test.ts, on main at 570d4b03ee:

Testmain+ #21877
holds a follow-up while a background command from its run still runsfailpass
completes a follow-up with the result of its background command's wakefailfail

The tests write orchestrator events directly. They model the adapter with #24391, which keeps the command published until its wake run ends. Without #24391, the published roster empties before the wake run starts (#24297), which gives the follow-up an earlier chance to finalize on its interim reply.

Regression tests (DelegatedCompletionDelivery.test.ts)

Put seedBackgroundFollowup above the subagent follow-ups block and the two tests inside it; they use the file's existing helpers and imports.

ts
1// A parent sends a delivered child a follow-up. The follow-up's run starts a background
2// command and ends its turn with an interim reply while the command still runs.
3const seedBackgroundFollowup = (name: string) =>
4 Effect.gen(function* () {
5 const orchestrator = yield* Orchestrator.OrchestratorV2;
6 const eventSink = yield* EventSink.EventSinkV2;
7 const requests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
8 const now = yield* DateTime.now;
9 const parentThreadId = ThreadId.make(`thread:${name}-parent`);
10 const parentRunId = RunId.make(`run:${name}-parent`);
11 yield* seedParentWithTerminalTask({
12 threadId: parentThreadId,
13 projectId: ProjectId.make(`project:${name}`),
14 runId: parentRunId,
15 rootNodeId: NodeId.make(`node:${name}-parent`),
16 taskId: NodeId.make(`node:${name}-seed`),
17 deliveryState: "acknowledged",
18 now,
19 });
20 const child = yield* seedRestartCancelledChild({
21 parentThreadId,
22 projectId: ProjectId.make(`project:${name}`),
23 parentRunId,
24 rootNodeId: NodeId.make(`node:${name}-parent`),
25 name: `${name}-child`,
26 completionWake: "always",
27 continuationPending: false,
28 runStatus: "completed",
29 now,
30 });
31 yield* orchestrator.recoverDelegatedTasks;
32 let firstWake = yield* requests.take;
33 while (firstWake.threadId !== parentThreadId) firstWake = yield* requests.take;
34 yield* orchestrator.dispatch({
35 type: "delegated_task.completion-delivery.acknowledge",
36 commandId: CommandId.make(`command:${name}-ack`),
37 parentThreadId,
38 taskId: child.taskId,
39 observedByRunId: parentRunId,
40 });
41 const parentRun = (yield* orchestrator.getThreadProjection(parentThreadId)).runs.find(
42 (run) => run.id === parentRunId,
43 )!;
44 yield* eventSink.write({
45 commandId: reconcileCommandId(`${name}-parent-finished`),
46 events: [
47 {
48 id: EventId.make(`event:${name}-parent-finished`),
49 type: "run.updated",
50 threadId: parentThreadId,
51 runId: parentRunId,
52 occurredAt: now,
53 payload: { ...parentRun, status: "completed", completedAt: now },
54 },
55 ],
56 });
57
58 const sendId = MessageId.make(`message:${name}-send`);
59 yield* orchestrator.dispatch({
60 type: "message.dispatch",
61 createdBy: "agent",
62 creationSource: "mcp",
63 commandId: CommandId.make(`command:${name}-send`),
64 threadId: child.childThreadId,
65 senderThreadId: parentThreadId,
66 messageId: sendId,
67 text: "Run the build in the background and report when it ends.",
68 attachments: [],
69 dispatchMode: { type: "start_immediately" },
70 });
71 const followupRun = (yield* orchestrator.getThreadProjection(child.childThreadId)).runs.find(
72 (run) => run.userMessageId === sendId,
73 )!;
74
75 const providerThread = {
76 id: ProviderThreadId.make(`provider-thread:${name}-child`),
77 driver,
78 providerInstanceId: modelSelection.instanceId,
79 providerSessionId: null,
80 appThreadId: child.childThreadId,
81 ownerNodeId: null,
82 nativeThreadRef: null,
83 nativeConversationHeadRef: null,
84 status: "idle" as const,
85 firstRunOrdinal: 1,
86 lastRunOrdinal: 2,
87 handoffIds: [],
88 forkedFrom: null,
89 createdAt: now,
90 updatedAt: now,
91 };
92 let rosters = 0;
93 const roster = (taskIds: ReadonlyArray<string>) => ({
94 id: EventId.make(`event:${name}-roster-${(rosters += 1)}`),
95 type: "provider-thread.updated" as const,
96 threadId: child.childThreadId,
97 driver,
98 providerInstanceId: modelSelection.instanceId,
99 occurredAt: now,
100 payload: {
101 ...providerThread,
102 pendingBackgroundTasks: taskIds.map((taskId) => ({ taskId, kind: "command" as const })),
103 },
104 });
105 const finishRun = (run: OrchestrationV2Run, text: string) =>
106 eventSink.write({
107 commandId: reconcileCommandId(`${name}-${run.id}-finished`),
108 events: [
109 {
110 id: EventId.make(`event:${name}-${run.id}-result`),
111 type: "message.updated",
112 threadId: child.childThreadId,
113 runId: run.id,
114 occurredAt: now,
115 payload: {
116 id: MessageId.make(`message:${name}-${run.id}-result`),
117 threadId: child.childThreadId,
118 runId: run.id,
119 nodeId: null,
120 role: "assistant",
121 text,
122 attachments: [],
123 streaming: false,
124 createdBy: "agent",
125 creationSource: "server",
126 createdAt: now,
127 updatedAt: now,
128 },
129 },
130 {
131 id: EventId.make(`event:${name}-${run.id}-finished`),
132 type: "run.updated",
133 threadId: child.childThreadId,
134 runId: run.id,
135 occurredAt: now,
136 payload: { ...run, status: "completed", startedAt: now, completedAt: now },
137 },
138 ],
139 });
140
141 yield* eventSink.write({ events: [roster(["sleep-75"])] });
142 yield* finishRun(followupRun, "The build is running; I'll report when it ends.");
143 yield* orchestrator.recoverDelegatedTasks;
144 const followupTask = orchestrator
145 .getThreadProjection(parentThreadId)
146 .pipe(
147 Effect.map((parent) =>
148 parent.subagents.find((task) => task.id === followupRun.delegatedTaskId)!,
149 ),
150 );
151 return { parentThreadId, child, followupRun, followupTask, roster, finishRun };
152 });
153
154 it.effect("holds a follow-up while a background command from its run still runs", () =>
155 Effect.gen(function* () {
156 const orchestrator = yield* Orchestrator.OrchestratorV2;
157 const { child, followupRun, followupTask } = yield* seedBackgroundFollowup(
158 "followup-background-hold",
159 );
160 const run = (yield* orchestrator.getThreadProjection(child.childThreadId)).runs.find(
161 (run) => run.id === followupRun.id,
162 )!;
163 assert.equal(run.status, "completed");
164 const followup = yield* followupTask;
165 assert.isNull(followup.result);
166 assert.equal(followup.status, "running");
167 }),
168 );
169
170 it.effect("completes a follow-up with the result of its background command's wake", () =>
171 Effect.gen(function* () {
172 const orchestrator = yield* Orchestrator.OrchestratorV2;
173 const eventSink = yield* EventSink.EventSinkV2;
174 const { parentThreadId, child, followupTask, roster, finishRun } =
175 yield* seedBackgroundFollowup("followup-background-wake");
176 // The command exits. Claude wakes the child in a new run, and the adapter keeps
177 // the command listed until that run ends (#24391).
178 const wakeId = MessageId.make("message:followup-background-wake");
179 yield* orchestrator.dispatch({
180 type: "message.dispatch",
181 createdBy: "agent",
182 creationSource: "provider",
183 commandId: CommandId.make("command:followup-background-wake"),
184 threadId: child.childThreadId,
185 messageId: wakeId,
186 text: "Background activity updated",
187 notification: {
188 source: { kind: "command" },
189 outcome: "completed",
190 summary: 'Command "sleep 75" finished',
191 },
192 attachments: [],
193 dispatchMode: { type: "queue_after_active" },
194 });
195 const wakeRun = (yield* orchestrator.getThreadProjection(child.childThreadId)).runs.find(
196 (run) => run.userMessageId === wakeId,
197 )!;
198 yield* finishRun(wakeRun, "The build passed.");
199 yield* eventSink.write({ events: [roster([])] });
200 yield* orchestrator.recoverDelegatedTasks;
201
202 const followup = yield* followupTask;
203 assert.equal(followup.result, "The build passed.");
204 assert.equal(followup.status, "completed");
205 const tasks = (yield* orchestrator.getThreadProjection(parentThreadId)).subagents.filter(
206 (task) => task.childThreadId === child.childThreadId,
207 );
208 assert.sameMembers(
209 tasks.map((task) => task.id),
210 [child.taskId, followup.id],
211 );
212 }),
213 );

Results at 570d4b03ee:

text
1main
2 × holds a follow-up while a background command from its run still runs
3 → expected 'The build is running; I\'ll report wh…' to equal null
4 × completes a follow-up with the result of its background command's wake
5 → expected 'The build is running; I\'ll report wh…' to equal 'The build passed.'
6
7main + #21877
8 ✓ holds a follow-up while a background command from its run still runs
9 × completes a follow-up with the result of its background command's wake
10 Expected: "The build passed."
11 Received: "The build is running; I'll report when it ends."

#21877 is waiting on verification, so I'd follow up after it lands rather than stack on it.

Reported by DanielJomphe. Investigation and tests by Claude Opus 5.5 in Claude Code, through T3 Code.

Synced from GitHub

Still observable on 0.1.3-nightly.20261027.3754 on Linux x64 (Debian 13), with a Claude Fable 5.1 parent and Claude Opus 5.5 children. Provider initialization records confirm Claude Code 2.1.311 and bypassPermissions. Codex CLI 0.165.0 is installed at inspection; the cases below were traced in Claude children, not independently reproduced with Codex.

This is a local build of upstream d2c440312e5f39d59827ed466838a623516454d6 with SQLite read-performance patches. The task intake/finalization and completion-dispatch logic are unchanged; the MCP service and continuation service match that upstream revision byte for byte. No clean-release reproduction was run.

Read-only database inspection found these completed later runs without a delegatedTaskId or a parent subagent_result transfer. All times are 2026-10-27 UTC; A–C are separate children:

Completed atChild/runUndelivered report
22:41:29.218ZA / 5Completed design report after a nested review
23:06:44.693ZB / 11Repairs completed; next nested review running
23:18:22.096ZB / 14Decision request after that nested review
23:45:32.037ZC / 17READY report after a nested review

Delivery is selective: child C's runs ending at 23:36:57.337Z and 23:39:28.194Z had task associations and parent transfers. Child B's intervening runs at 23:07:16.761Z and 23:08:45.841Z did too.

An additional original-task case produced READY at 21:55:09.277Z; its first parent result transfer instead referenced a later rebase/status reply at 22:02:15.234Z. I have not established that this shares the nested-follow-up cause.

Our workaround is to check PR status and read child task/thread results explicitly. The needed behavior is to retain the follow-up's ownership through nested-result wakes and deliver its eventual report once, including decision requests. #21877 remains open and unmerged; I have not tested its fix.

Synced from GitHub

Note

Claude Opus 5.5 (Claude Code) posting on behalf of @Hassan-Zbib.

Background-command case on 0.1.4-nightly.20261028.3814 (Windows 11, Claude Code 2.1.312, Opus 5.5 parent and child, async delegate_task): a t3_background_wait mark (#24610) doesn't hold a wake run's follow-up task.

After the original task closed early (#24297), each wake run got its own provider-continuation task. Each of those completed when its run ended, even though the child had marked a still-running command and got it back in matched. From statev2.sqlite, UTC:

RunMarked, still runningRun ended → task completedParent received
2bpoldxej6, br8ttxfbr19:25:45.205 → .713"The test binaries and pnpm gate are both running in the background…"
3bpoldxej619:27:48.473 → .996"Waiting on incidents + platform (task bpoldxej6)."
4bo50xuxz019:30:58.482 → 19:30:59.015"Waiting on the sqlx check and the cross-cutting run…"
5none19:37:35.988 → 19:37:36.747the final report

The final result is no longer lost; it arrives as another task with the same title. But the parent got four interim "completed" notices, each costing a turn. Holding a follow-up open while its own marked waits run, as the original task is held, would fix that.

Aside: the system prompt says to call t3_background_wait "with its task ID", but the tool takes a taskIds array, so the child's first call ({"taskId":"b3gyl80d9"}) failed validation.

Synced from GitHub

Still present on stable 0.1.5 (macOS, Claude Opus 5.5 parent and child), and on main at 32ecd71726. A provider-continuation task completed when its run ended while its command was still marked awaited: true. A visible effect not mentioned above: the parent's Lineage moves the child to "Previous agents" as completed for the whole wait (about 24 minutes here).

Synced from GitHub

Note

Written by Claude Opus 5.5 (high reasoning effort, Claude Code harness inside T3 Code) on behalf of @grisetio, who reviewed it before posting.

Another case on macOS arm64, Claude Opus 5.5 parent and child, both on high reasoning effort, async delegate_task. It covers the background-wake shape above and also shows a server restart variant: the child's final run is a restart continuation with no task at all, so its result never reaches the parent.

The server runs a fork of stable with one patch (a Codex account-usage view) that doesn't touch orchestration. Runs 1–2 were on a v0.1.3 base with Claude Code 2.1.312. The child's own work restarted the server mid-sequence, so run 3 was on v0.1.5 with Claude Code 2.1.314.

Times in UTC:

  1. 13:12:25: the parent delegates a long task. The child starts a background command and ends its turn with an interim reply ("waiting for the background deploy; I'll pick up when it notifies").
  2. 13:54:57, run 1 ends: the original task completes with that interim reply as its summary, and the parent is woken with it. That's #24297.
  3. 13:55:27, run 2: the "commands finished" notification wakes the child under a new provider-continuation task. The child starts another background command, which restarts the T3 server once it activates, marks it with t3_background_wait, and ends its turn with another interim reply. The parent gets that as a second completed task with the same title.
  4. About 13:56: the server restarts.
  5. 13:56:45, run 3: a restart continuation starts in the child ("T3 Code restarted and stopped background work … resume or restart what you still need").
  6. 14:03:14, run 3 ends: run 3 finishes the work and writes the final report. No completion is delivered to the parent. The parent's task_status still shows the original task's run-1 interim summary. The final report could only be found by reading the child thread.

As we read #27222 ("once a task has settled, it no longer owns later work in the child thread"), dropping run 3 is intended behavior once the original task has settled. So with an early settlement (#24297) and a restart, the final result is lost entirely, rather than arriving as a duplicate task as in the cases above. #21877 wouldn't change this either, because nothing here involves nested delegate_task in a follow-up.

Expected: the parent receives the run-3 result exactly once, either because the original task stays open across the background wait and the restart, or because restart continuations of a child thread report under the last task that owned the thread.

Synced from GitHub

Note

Written by Claude Opus 5.5 (Claude Code, through T3 Code) on behalf of @DavidPerezA12.

Re-ran the follow-up tests posted above on current main, with and without #21877. These tests write orchestrator events directly, so they aren't a live provider run.

Testmain (e6e2ab05cc)+ #21877 (9df1a246ed)
holds a follow-up while its awaited background command runsfailpass
holds a provider-continuation task while its wake run's awaited command runsfailpass
completes a follow-up with the result of its background command's wakefailfail
  • The tests need one change since #27468. Unmarked commands no longer hold a task, so the roster rows need awaited: true.
  • The second row is the case I reported above. It is the first test with the follow-up run started by a wake instead of a t3_thread_send: a message.dispatch with a provider-continuation:<messageId> command ID and a command notification. #21877's hold covers it.
  • The third row still fails with #21877. The task publishes the interim reply (The build is running; I'll report when it ends.) instead of the wake's result. #21877 says Closes #21837, so merging it would close this issue with that part still open.

I'm not opening a PR: #21877 covers the hold, and the comment from Krarilotus on #24391 describes a local patch for the wake ownership.

Synced from GitHub

Additional confirmation on stable v0.1.6, macOS arm64, using T3 app-owned delegation. This appears to be the same nested-follow-up defect, so I am adding evidence here rather than filing a duplicate.

In the observed unmodified installation:

  • The parent sent more work to a completed child with t3_thread_send, which returned a new follow-up task ID.
  • That child resumed its workers and ended the follow-up turn with a waiting message. The parent received an early “finished” notification.
  • Worker completions subsequently woke the child through three more runs. The last run produced the final report.
  • In the inspected parent timeline, no corresponding final notification or automatic parent turn appeared between that final child result and the user's later progress question (about 63 minutes later).
  • The parent then read the task and recovered the latest result. Worker-to-child delivery was functioning; the missing part was the final child-to-parent handoff.

We also reproduced premature publication at the service-test level against the v0.1.6 source tag, commit 2359643cffa6ba9ee84d6f2694b935de54a8013e, by extending the existing DelegatedCompletionDelivery.test.ts follow-up case: READY → explicit follow-up → nested task → interim terminal turn → nested-result continuation. The unmodified code failed the assertion that the follow-up result should remain null while the nested result was still owed:

text
1expected 'Child task completed without an assistant result.' to equal null

Inspection of this version suggests two related paths worth checking:

  1. The nested-work gate in finalizeAppOwnedSubagent applies to the original fork task; the follow-up can reach terminal publication too early.
  2. The server's delegatedCompletion wake does not inherit that follow-up's delegatedTaskId. Preventing early publication alone therefore does not make the finalizer select the eventual continuation's result.

This is not a claim that every missing notification has the same cause, or that current main has been retested. The service test uses temporary persistence and controlled events, not a fresh real-provider reproduction on an untouched current release. A temporary local experimental fix also passed a real three-level synthetic run without explicit handoff messages, but I am withdrawing that custom runtime and prefer an upstream fix; I am not asking maintainers to adopt the local implementation.

The acceptance case should preserve the original READY result, keep the follow-up pending while its own nested work is outstanding, and deliver the final continuation result exactly once without attributing a later unrelated user/send turn to the old task. An explicit final handoff to the known parent is our temporary operational workaround.

No private task contents, machine paths, credentials, or production database excerpts are included. Prepared with GPT-6 Astra in Codex/T3 Code on behalf of @ytwytw.

Synced from GitHub

Note

Written by Claude Opus 5.5 (Claude Code 2.1.316, running inside T3 Code), following .github/triage/PLAYBOOK.md, on behalf of the poster, who reviewed it before posting.

Fresh evidence on newer nightlies, read-only from the state DB, plus one surface that #21877 doesn't cover.

What happened

A supervisor thread runs long delegated lanes with Codex (gpt-6.1-sol) and Claude Opus 5.5 children. Every background-exit wake of a child that already has a result starts a new provider-continuation task. That task completes and notifies the parent as soon as its run ends. Counted from the parent's task projection (2026-10-29 to 11-02):

Child providerOriginal tasksthread-send tasksprovider-continuation tasks
Codex3671135
Claude241319

Each of those tasks woke the parent once. The supervisor sampled 14 of the Codex-era wake notices: 8 were interim status, 5 restated known results, and 1 carried a new result.

On Claude children the extra tasks also complete while the child still waits on work it marked with t3_background_wait (evidence below). t3_background_wait is Claude-only, so I can't show the same for Codex.

In the desktop app's Lineage panel at about 06:16 UTC, three Claude lanes waiting on awaited gates were listed under "Previous agents". The panel read "2 running": one Claude child mid-turn and one Codex child in a long turn.

Diagnosis

Server line references are to tag v0.1.6-nightly.20261031.3973 (85bd3f2a2a). Client line references are to v0.1.7-nightly.20261101.4035 (80120b1aa34a). The same logic is on main c65f8d552b, except for #27468.

  • mayStartSubagentFollowup (Orchestrator.ts:639, main :678) admits every notification except monitor. recordSubagentFollowup (:7742, main :7843) gives each wake run its own task with completionWake: "always".
  • Only the original task is held on delegatedTaskProgress (:12509, main :12612). Other tasks finalize once their own run is terminal (finalizeAppOwnedSubagentTask, :12185, main :12288). Awaited roster entries don't hold them.
  • task_status agrees with the early finalization: for a task that owns runs, workState comes from those runs alone (OrchestratorMcpService.ts:1485-1490, main :1506-1511).
  • Lineage takes a subagent edge's status from activityRunStatus (packages/client-runtime/src/state/threadRelationships.ts:112). ThreadRelationshipsControl.tsx:476 files completed/idle/… edges under "Previous agents". Neither reads pendingBackgroundTasks or awaited.

Steps to reproduce (derived from the source and the rows below; not rerun as a clean reproduction)

  1. delegate_task to a Claude child: "Reply READY." Wait for its result.
  2. t3_thread_send to the child: "Run sleep 60 in the background and mark it with t3_background_wait, then end your turn. When it exits, do the same with sleep 300. Report DONE when that one ends."
  3. The thread-send task completes when the first turn ends, while sleep 60 is still awaited.
  4. When sleep 60 exits, the wake run gets a new provider-continuation task. That task completes a few seconds after the run ends, while sleep 300 is still awaited. Lineage shows the child under "Previous agents".
  5. DONE arrives later as a third task with the same title.

Version

  • Server: 0.1.6-nightly.20261031.3973 (85bd3f2a2a), the version the server reports.
  • Client: macOS desktop app 0.1.7-nightly.20261101.4035 (80120b1aa34a).

Environment

  • Server: Ubuntu 26.04.1 LTS, Linux 7.0, Node 26.13.0, Claude Code 2.1.316.
  • Client: the macOS desktop app, connected to that server remotely.

Evidence (from statev2.sqlite, opened read-only, 2026-11-02 UTC)

When I read the DB, the three Claude children below each had one pending roster entry, kind: command, awaited: true. Every parent task for them was completed / delivered.

LaneCommand started (run)t3_background_wait → matchedRun endedParent task completed
M06:22:50.088 (run 93)06:23:11.85206:25:55.40606:25:57.325
W06:11:26.678 (run 6)06:11:27.71706:11:29.12306:11:32.692
T06:10:45.654 (run 19)06:11:12.70506:11:16.76606:11:18.915
T (same command)— (run 20)06:12:41.31206:12:43.80706:12:45.600
T (same command)— (run 21)06:16:57.22306:17:04.50606:17:06.402

Every task in the table is a provider-continuation task. Lane T's single awaited command produced three completed notices while it ran.

Lane W's original task completed at 05:11:55.784. Its last run had ended at 04:45:02, and the background-exit wake run 4 started 0.5 s later, at 05:11:56.323. That is the #24297 gap.

Related issues

  • This is the background-command case reported above on 10-29, now on newer nightlies.
  • #21877 holds the follow-up (per the 11-02 test table) but leaves wake ownership open. It doesn't touch Lineage.
  • #24297 / #24391 cover the original-task race seen on lane W.
  • #16603 covers the original task completing while the child still has work.

A fix covering this report would need three parts:

  1. A follow-up stays pending while awaited work started by its own runs is pending (#21877).
  2. A background-exit wake continues the task that owns that work instead of opening a new one. The roster has to record that owner and keep it across a restart. This part also applies to Codex children.
  3. Lineage and task_status use the same backgroundWorkHoldsCompletion check, so a waiting child shows as active or waiting, not previous.

Fix applied or workaround

None applied. T3 was not modified.

Filed by

Claude Opus 5.5 via Claude Code 2.1.316 inside T3 Code, using the T3 triage playbook.