# Files changed · PR \#24391 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/24391/diffs)

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

Visibility: public

[fix\(server\): wait for delegated Claude background wake results](https://git.cafe/pingdotgg/t3code/pull/24391.md)

Version: 4

Head commit: 0a26c94c757e685e910fb3df4390efcebfadbc8a

Base commit: 27e0c684c585f955252d65f06206dd11a143bff5

Comparison base: 27e0c684c585f955252d65f06206dd11a143bff5

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts

Status: modified; +97; −9

```diff
@@ -6255,7 +6255,7 @@
     );
 
   it.effect(
-    "uses task_started as an incremental roster fallback and clears on empty snapshot",
+    "uses task_started as an incremental roster fallback and clears the internal roster on empty snapshot",
     () =>
       Effect.scoped(
         Effect.gen(function* () {
@@ -6317,15 +6317,16 @@
             ),
           );
 
+          yield* harness.offerAndWait(emptyRoster);
+          assert.isFalse(yield* harness.hasPendingBackgroundWork);
-          yield* Queue.offer(harness.sdkMessages, emptyRoster);
-          yield* harness.until(() =>
-            providerThreadRosterEvents(harness.events).some(
+          // The exited command still owes its wake run, so the published roster keeps it.
+          assert.include(
+            (
+              providerThreadRosterEvents(harness.events).at(-1)?.providerThread
+                .pendingBackgroundTasks ?? []
+            ).map((task) => task.taskId),
+            WAKE_TASK_ID,
-              (event) =>
-                event.providerThread.status === "idle" &&
-                (event.providerThread.pendingBackgroundTasks?.length ?? 0) === 0,
-            ),
           );
-          assert.isFalse(yield* harness.hasPendingBackgroundWork);
         }).pipe(
           Effect.provide(
             Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
@@ -7650,6 +7651,93 @@
     ),
   );
 
+  // #24297: an empty roster lets the orchestrator finalize a delegated child on its pre-wake
+  // result, and a finalized task never reopens for the wake run that follows.
+  // The SDK leaves the empty level's order against the notification unspecified.
+  it.effect.each(["none", "before", "after"] as const)(
+    "keeps an exited background command on the roster until its wake run starts (empty level %s notification)",
+    (emptyLevel) =>
+      Effect.scoped(
+        Effect.gen(function* () {
+          const harness = yield* makeWakeHarness;
+          const now = yield* DateTime.now;
+          const wakeAttemptId = RunAttemptId.make("attempt-claude-wake-roster-2");
+          const emptyRoster = claudeSdkFrame({
+            type: "system",
+            subtype: "background_tasks_changed",
+            tasks: [],
+            uuid: "00000000-0000-4000-8000-000000000204",
+            session_id: WAKE_NATIVE_SESSION,
+          });
+
+          yield* harness.runtime.startTurn(
+            makeClaudeTestTurnInput({
+              threadId: harness.threadId,
+              providerThread: harness.providerThread,
+              now,
+              attemptId: RunAttemptId.make("attempt-claude-wake-roster-1"),
+              text: "Run the build in the background.",
+              attachments: [],
+            }),
+          );
+          yield* Queue.offer(harness.sdkMessages, wakeTaskStarted);
+          yield* Queue.offer(harness.sdkMessages, turnOneResult);
+          yield* harness.awaitTerminal(1);
+          if (emptyLevel === "before") yield* harness.offerAndWait(emptyRoster);
+          yield* harness.offerAndWait(wakeNotification);
+          if (emptyLevel === "after") yield* harness.offerAndWait(emptyRoster);
+          // Query replacement reads the internal roster, which the exit already cleared.
+          assert.isFalse(yield* harness.hasPendingBackgroundWork);
+          yield* harness.offerAndWait(wakeTurnInit);
+          yield* harness.runtime.startTurn(
+            makeClaudeTestTurnInput({
+              threadId: harness.threadId,
+              providerThread: harness.providerThread,
+              now,
+              attemptId: wakeAttemptId,
+              text: "Background task completed.",
+              attachments: [],
+              providerTurnOrdinal: 2,
+              messageCreatedBy: "agent",
+              messageCreationSource: "provider",
+            }),
+          );
+          yield* harness.offerAndWait(wakeAssistant);
+          yield* harness.offerAndWait(wakeResult);
+          yield* harness.awaitTerminal(2);
+
+          const wakeStart = harness.events.findIndex(
+            (event) =>
+              event.type === "provider_turn.updated" &&
+              event.providerTurn.runAttemptId === wakeAttemptId,
+          );
+          assert.isAbove(wakeStart, -1);
+          const rosters = harness.events.flatMap((event, index) =>
+            event.type === "provider_thread.updated" &&
+            event.providerThread.id === harness.providerThread.id
+              ? [
+                  {
+                    beforeWake: index < wakeStart,
+                    taskIds: (event.providerThread.pendingBackgroundTasks ?? []).map(
+                      (task) => task.taskId,
+                    ),
+                  },
+                ]
+              : [],
+          );
+          assert.deepEqual(
+            rosters.filter((roster) => roster.beforeWake).map((roster) => roster.taskIds),
+            rosters.filter((roster) => roster.beforeWake).map(() => [WAKE_TASK_ID]),
+          );
+          assert.deepEqual(rosters.at(-1)?.taskIds, []);
+        }).pipe(
+          Effect.provide(
+            Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
+          ),
+        ),
+      ),
+  );
+
   it.effect.each([true, false])(
     "carries a rejected wake rate limit into the continuation failure (event=%s)",
     (standaloneEvent) =>
```

## apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts

Status: modified; +20; −1

```diff
@@ -4563,6 +4563,7 @@
           readonly nativeThreadId: string;
           readonly providerThread: OrchestrationV2ProviderThread;
           readonly status?: OrchestrationV2ProviderThread["status"];
+          readonly owedWakeTasks?: ReadonlyArray<OrchestrationV2PendingBackgroundTask>;
         }) {
           const roster = rosterForNativeThread(
             yield* Ref.get(pendingBackgroundTasksByNativeThread),
@@ -4573,7 +4574,10 @@
             ...input.providerThread,
             providerSessionId: session.id,
             ...(input.status === undefined ? {} : { status: input.status }),
+            pendingBackgroundTasks: [
-            pendingBackgroundTasks: claudePendingBackgroundTasksFromRoster(roster),
+              ...claudePendingBackgroundTasksFromRoster(roster),
+              ...(input.owedWakeTasks ?? []).filter((task) => !roster.has(task.taskId)),
+            ],
             ...(goalsByNativeThread.has(input.nativeThreadId)
               ? { goal: goalsByNativeThread.get(input.nativeThreadId) ?? null }
               : {}),
@@ -7357,6 +7361,20 @@
           if (baseThread === undefined) {
             return true;
           }
+          // While idle, a published task Claude still owes a wake run stays published after the
+          // edge or level drops it (their order is unspecified): an empty roster lets a delegated
+          // child finalize on its pre-wake result. Only the published roster keeps it, so it never
+          // blocks query replacement; the wake run's finalize or a process reset publishes it away.
+          const owedWakeTasks: OrchestrationV2PendingBackgroundTask[] = [];
+          if (input.activeContext === null) {
+            for (const task of baseThread.pendingBackgroundTasks ?? []) {
+              if (
+                yield* isKnownOpaqueBackgroundTaskOnNativeThread(input.nativeThreadId, task.taskId)
+              ) {
+                owedWakeTasks.push(task);
+              }
+            }
+          }
 
           const status =
             input.activeContext === null
@@ -7368,6 +7386,7 @@
             nativeThreadId: input.nativeThreadId,
             providerThread: baseThread,
             status,
+            owedWakeTasks,
           });
           return true;
         });
```
