# Claude wake latch: a wake queued behind a held run swallows later wakes, so idle-launched subagents stay Running · Issue \#22033 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/issues/22033)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: backlog

Priority: 0

## Description

[Discussion and activity](https://git.cafe/pingdotgg/t3code/issues/22033)

---

A Claude native subagent launched while the root is idle can be discovered from task_progress during a later user turn, yet stay Running after its final task_notification arrives.

Observed in the macOS desktop nightly `0.1.1-nightly.20261018.3331`. The deterministic replay runs on Windows, which has the same nightly installed, and fails against both that nightly's adapter source and `main` at `cb0553ea39`.

Minimal SDK replay, using the production Claude adapter:

1. Start and finish an initial root user turn.
2. Deliver `system/task_started` for one background `local_agent` while the root is idle. Leave its buffered continuation undrained.
3. Start an ordinary user turn. Deliver `system/task_progress` for that task. The adapter creates the subagent from this progress event, with a fallback title and a late start time.
4. Finish that user turn.
5. Deliver the task's final `system/task_notification {status: completed, summary: "Audit finished."}` while the root is idle.
6. Read the latest `subagent.updated`: it remains `running`, with no completion timestamp/result.

Expected: the authoritative final notification settles the registered subagent, even when its older launch frame remains in the wake buffer.

No SendMessage resume, nested agent, provider switch, process replacement, or background shell is needed. The external SDK stream is stubbed in this deterministic replay; this is not a live authenticated Claude CLI reproduction. The matching production incident has both raw `task_updated {status: completed}` and `task_notification completed` in the provider log while the persisted row stays running.

The failing gate is in [ClaudeAdapterV2.ts at cb0553ea39](https://github.com/pingdotgg/t3code/blob/cb0553ea39/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts#L7064): `hasBufferedStart` prevents the idle outcome from being applied, even though progress already registered that same launch during the later user turn. The normal user-turn path does not drain the wake buffer. The guard came from `265fb7000f` (#18778), which already exists in the affected nightly. A raw completion signal reaches T3; the SDK is not dropping it in this case.

Related: #17809, #18910 and #20430. The stuck continuation gate in #20430 can leave such a start undrained, but this replay needs no held queued run. #16610 concerns interim completed notifications followed by self-resume; that is a separate observation. #21724/#21725 reconcile unregistered tasks after process replacement and do not remove this guard for registered tasks.

Reproduction added to the existing `ClaudeAdapterV2.test.ts` suite:

```sh
vp test run apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts -t 'settles an idle-launched subagent discovered'
# expected 'running' to equal 'completed'
# 1 failed, 299 skipped
```

The same assertion failure occurs with only the adapter source replaced by the exact installed nightly tag, and with current-main adapter source. All task names, IDs, prompts and output in the test are synthetic. No user transcripts, database, screenshots or private project data are attached.

Implemented/investigated with GPT-6.1 Sol through Codex in T3 Code.

Repro correction: the first short test waited for SDK processing but did not wait for the separate event collector. The test-only patch below adds an ordered provider-thread roster receipt after the final notification. That receipt arrives on both broken and fixed adapters, so the status assertion is independent of whether a completion event was emitted. This strengthened test still fails on `cb0553ea39` with actual `running`, expected `completed`; the candidate fix passes it, the SQLite persistence case, and the existing genuine-resume case. Broader focused checks and review are still running.

<details><summary>Test-only reproduction patch against cb0553ea39</summary>

Apply this patch, then run the command above. It changes only the existing test file.

```diff
diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
index 3b09094aa1..98c6018f5d 100644
--- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
+++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
@@ -2538,6 +2538,10 @@ describe("ClaudeAdapterV2 background wake turns", () => {
         yield* Queue.unbounded<
           Extract<ProviderAdapter.ProviderAdapterV2Event, { type: "subagent.updated" }>
         >();
+      const providerThreadReceipts =
+        yield* Queue.unbounded<
+          Extract<ProviderAdapter.ProviderAdapterV2Event, { type: "provider_thread.updated" }>
+        >();
       const systemNoticeReceipts =
         yield* Queue.unbounded<
           Extract<ProviderAdapter.ProviderAdapterV2Event, { type: "turn_item.updated" }>
@@ -2646,6 +2650,9 @@ describe("ClaudeAdapterV2 background wake turns", () => {
             if (event.type === "subagent.updated") {
               yield* Queue.offer(subagentReceipts, event);
             }
+            if (event.type === "provider_thread.updated") {
+              yield* Queue.offer(providerThreadReceipts, event);
+            }
             if (event.type === "turn.terminal") {
               yield* Queue.offer(terminalReceipts, event);
             }
@@ -2687,6 +2694,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
         events,
         terminalReceipts,
         subagentReceipts,
+        providerThreadReceipts,
         systemNoticeReceipts,
         getOpenedOptions: () => openedOptions,
         terminalEvents,
@@ -10092,6 +10100,109 @@ describe("ClaudeAdapterV2 background wake turns", () => {
     ),
   );
 
+  it.effect("settles an idle-launched subagent discovered during a later user turn", () =>
+    Effect.scoped(
+      Effect.gen(function* () {
+        const harness = yield* makeWakeHarness;
+        const taskId = "task-idle-launch-late-discovery";
+        const toolUseId = "toolu-idle-launch-late-discovery";
+        const startTurn = (ordinal: number) =>
+          harness.runtime.startTurn(
+            makeClaudeTestTurnInput({
+              threadId: harness.threadId,
+              providerThread: harness.providerThread,
+              now: DateTime.makeUnsafe(0),
+              attemptId: RunAttemptId.make(`attempt-idle-discovery-${ordinal}`),
+              providerTurnOrdinal: ordinal,
+              text: "Continue work.",
+              attachments: [],
+            }),
+          );
+        yield* startTurn(1);
+        yield* harness.offerAndWait(
+          makeResultFrame({
+            uuid: "00000000-0000-4000-8000-000000004101",
+            result: "Initial turn finished.",
+          }),
+        );
+        yield* Queue.take(harness.terminalReceipts);
+        yield* harness.offerAndWait(
+          makeSubagentTaskStartedFrame({
+            taskId,
+            toolUseId,
+            uuid: "00000000-0000-4000-8000-000000004102",
+          }),
+        );
+        yield* startTurn(2);
+        yield* harness.offerAndWait(
+          claudeSdkFrame({
+            type: "system",
+            subtype: "task_progress",
+            task_id: taskId,
+            tool_use_id: toolUseId,
+            description: "Checking the result.",
+            uuid: "00000000-0000-4000-8000-000000004103",
+            session_id: WAKE_NATIVE_SESSION,
+          }),
+        );
+        yield* harness.offerAndWait(
+          makeResultFrame({
+            uuid: "00000000-0000-4000-8000-000000004104",
+            result: "User turn finished.",
+          }),
+        );
+        yield* Queue.take(harness.terminalReceipts);
+        yield* harness.offerAndWait(
+          makeSubagentNotificationFrame({
+            taskId,
+            toolUseId,
+            summary: "Audit finished.",
+            uuid: "00000000-0000-4000-8000-000000004105",
+          }),
+        );
+        const markerId = "task-idle-discovery-collector-barrier";
+        yield* harness.offerAndWait(
+          claudeSdkFrame({
+            type: "system",
+            subtype: "task_started",
+            task_id: markerId,
+            tool_use_id: `toolu-${markerId}`,
+            description: "Outcome collector barrier",
+            is_backgrounded: true,
+            task_type: "local_bash",
+            uuid: "00000000-0000-4000-8000-000000004106",
+            session_id: WAKE_NATIVE_SESSION,
+          }),
+        );
+        // A later roster receipt also proves the collector has seen the outcome.
+        yield* Stream.fromQueue(harness.providerThreadReceipts).pipe(
+          Stream.filter(
+            (event) =>
+              event.providerThread.pendingBackgroundTasks?.some(
+                (task) => task.taskId === markerId,
+              ) === true,
+          ),
+          Stream.take(1),
+          Stream.runDrain,
+        );
+        const latest = harness.events.findLast(
+          (event) =>
+            event.type === "subagent.updated" && event.subagent.nativeTaskRef?.nativeId === taskId,
+        );
+        assert.equal(latest?.type, "subagent.updated");
+        if (latest?.type !== "subagent.updated") return;
+        assert.equal(latest.subagent.status, "completed");
+        assert.equal(latest.subagent.result, "Audit finished.");
+        assert.equal(latest.subagent.title, "Audit recent commits");
+        assert.equal(latest.subagent.prompt, "Audit the last five commits.");
+      }).pipe(
+        Effect.provide(
+          Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
+        ),
+      ),
+    ),
+  );
+
   it.effect("holds subagent frames that precede task_started and shows its result once", () =>
     Effect.scoped(
       Effect.gen(function* () {
```

</details>

