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

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

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

Visibility: public

[fix\(server\): show self-resumed Claude subagents as running](https://git.cafe/pingdotgg/t3code/pull/23831.md)

Version: 10

Head commit: 74c7c7722be756840635c1062cb005ace3cedb69

Base commit: f41617580439f33ab55fa1ba100437a5b4c7d516

Comparison base: f41617580439f33ab55fa1ba100437a5b4c7d516

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; +197; −0

```diff
@@ -8424,6 +8424,203 @@
     ),
   );
 
+  // Claude reports a background subagent `completed` when its turn ends while
+  // its own background shell still runs. When that shell finishes, Claude
+  // resumes the subagent with a new task_started for the same task and launch
+  // tool_use_id while the root is idle. Frame order mirrors a live capture.
+  it.effect("reopens a completed background subagent that resumes while the root is idle", () =>
+    Effect.scoped(
+      Effect.gen(function* () {
+        const TASK_ID = "ae3f32459af762a5f";
+        const LAUNCH = "toolu_01LcidmwEK1Y6GC6LpX3kvxX";
+        const OWN_SHELL = "toolu_01QAahgijM2KRBBSkWxnui4g";
+        const RESUMED_BASH = "toolu_01CkA1juicPNCtQrqKL8h6Rz";
+        const harness = yield* makeWakeHarness;
+        const now = yield* DateTime.now;
+        let sequence = 0;
+        const system = (frame: object) =>
+          claudeSdkFrame({
+            type: "system",
+            ...frame,
+            uuid: `00000000-0000-4000-8000-0000000162${String(++sequence).padStart(2, "0")}`,
+            session_id: WAKE_NATIVE_SESSION,
+          });
+        const agentEntry = (runId: string) => ({
+          task_id: TASK_ID,
+          run_id: runId,
+          task_type: "local_agent",
+          subagent_type: "general-purpose",
+          description: "Audit recent commits",
+        });
+        const shellEntry = (taskId: string) => ({
+          task_id: taskId,
+          run_id: `run-${taskId}`,
+          task_type: "local_bash",
+          description: "Sleep",
+          parent_task_id: TASK_ID,
+        });
+        const roster = (tasks: ReadonlyArray<object>) =>
+          system({ subtype: "background_tasks_changed", tasks });
+        const shellStarted = (taskId: string, toolUseId: string) =>
+          system({
+            subtype: "task_started",
+            task_id: taskId,
+            tool_use_id: toolUseId,
+            task_type: "local_bash",
+            description: "Sleep",
+          });
+        const startTurn = (ordinal: number, continuation: boolean) =>
+          harness.runtime.startTurn(
+            makeClaudeTestTurnInput({
+              threadId: harness.threadId,
+              providerThread: harness.providerThread,
+              now,
+              attemptId: RunAttemptId.make(`attempt-claude-self-resume-${ordinal}`),
+              text: continuation ? "Background task completed." : "Launch a background agent.",
+              attachments: [],
+              providerTurnOrdinal: ordinal,
+              ...(continuation
+                ? {
+                    messageCreatedBy: "agent" as const,
+                    messageCreationSource: "provider" as const,
+                  }
+                : {}),
+            }),
+          );
+        const subagentUpdates = () =>
+          harness.events.flatMap((event) =>
+            event.type === "subagent.updated" ? [event.subagent] : [],
+          );
+
+        yield* startTurn(1, false);
+        for (const frame of [
+          roster([agentEntry("run-1")]),
+          makeSubagentTaskStartedFrame({
+            taskId: TASK_ID,
+            toolUseId: LAUNCH,
+            uuid: "00000000-0000-4000-8000-000000016101",
+          }),
+          makeResultFrame({ uuid: "00000000-0000-4000-8000-000000016102", result: "LAUNCHED" }),
+        ]) {
+          yield* harness.offerAndWait(frame);
+        }
+        yield* Queue.take(harness.terminalReceipts);
+
+        // The subagent starts its own background shell, ends its turn, and
+        // Claude reports it completed; the parent wakes, answers, goes idle.
+        for (const frame of [
+          ...makeSubagentAssistantFrames({
+            parentToolUseId: LAUNCH,
+            uuid: "00000000-0000-4000-8000-000000016103",
+            bashToolUseId: OWN_SHELL,
+          }),
+          roster([agentEntry("run-1"), shellEntry("b455x9pq5")]),
+          shellStarted("b455x9pq5", OWN_SHELL),
+          makeSubagentToolResultFrame({
+            parentToolUseId: LAUNCH,
+            uuid: "00000000-0000-4000-8000-000000016104",
+            toolUseId: OWN_SHELL,
+          }),
+          makeSubagentNotificationFrame({
+            taskId: TASK_ID,
+            toolUseId: LAUNCH,
+            summary: "WAITING",
+            uuid: "00000000-0000-4000-8000-000000016105",
+          }),
+          roster([shellEntry("b455x9pq5")]),
+          wakeTurnInit,
+        ]) {
+          yield* harness.offerAndWait(frame);
+        }
+        yield* Queue.take(harness.continuationReceipts);
+        yield* startTurn(2, true);
+        yield* harness.offerAndWait(
+          makeResultFrame({ uuid: "00000000-0000-4000-8000-000000016106", result: "Noted." }),
+        );
+        yield* Queue.take(harness.terminalReceipts);
+
+        // Its shell finishes and Claude resumes it under a new native run.
+        for (const frame of [
+          system({
+            subtype: "task_notification",
+            task_id: "b455x9pq5",
+            tool_use_id: OWN_SHELL,
+            status: "completed",
+            summary: "Background command completed (exit code 0)",
+            output_file: "/tmp/b455x9pq5.output",
+          }),
+          roster([]),
+          roster([agentEntry("run-2")]),
+          makeSubagentTaskStartedFrame({
+            taskId: TASK_ID,
+            toolUseId: LAUNCH,
+            uuid: "00000000-0000-4000-8000-000000016107",
+          }),
+        ]) {
+          yield* harness.offerAndWait(frame);
+        }
+        // Events reach the app only through a run, and the launching run ended
+        // with the interim completion.
+        assert.equal(harness.continuationRequests.length, 2);
+
+        yield* Queue.take(harness.continuationReceipts);
+        yield* startTurn(3, true);
+        // Claude does not wake the root for this, so the root must not stay busy.
+        assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed");
+        yield* startTurn(4, false);
+        yield* harness.offerAndWait(
+          makeResultFrame({ uuid: "00000000-0000-4000-8000-000000016110", result: "PONG" }),
+        );
+        assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed");
+        for (const frame of [
+          ...makeSubagentAssistantFrames({
+            parentToolUseId: LAUNCH,
+            uuid: "00000000-0000-4000-8000-000000016108",
+            bashToolUseId: RESUMED_BASH,
+          }),
+          shellStarted("beyhxkdpc", RESUMED_BASH),
+          roster([agentEntry("run-2"), shellEntry("beyhxkdpc")]),
+        ]) {
+          yield* harness.offerAndWait(frame);
+        }
+        // Events are ordered: once this roster is recorded, so is every earlier event.
+        yield* harness.until(() =>
+          harness.events.some(
+            (event) =>
+              event.type === "provider_thread.updated" &&
+              event.providerThread.pendingBackgroundTasks?.some(
+                (task) => task.taskId === "beyhxkdpc",
+              ) === true,
+          ),
+        );
+        const resumed = subagentUpdates().at(-1);
+        assert.equal(resumed?.status, "running");
+        assert.isNull(resumed?.completedAt);
+        assert.equal(resumed?.runId, "run-attempt-claude-self-resume-3");
+        const routing = subagentRouting(harness.events, [RESUMED_BASH]);
+        assert.deepEqual(
+          [...(routing.toolThreadIds.get(RESUMED_BASH) ?? [])],
+          [routing.childThreadId],
+        );
+
+        yield* harness.offerAndWait(
+          makeSubagentNotificationFrame({
+            taskId: TASK_ID,
+            toolUseId: LAUNCH,
+            summary: "DONE",
+            uuid: "00000000-0000-4000-8000-000000016109",
+          }),
+        );
+        yield* harness.until(() => subagentUpdates().at(-1)?.result === "DONE");
+        assert.equal(subagentUpdates().at(-1)?.status, "completed");
+      }).pipe(
+        Effect.provide(
+          Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
+        ),
+      ),
+    ),
+  );
+
   it.effect("keeps a nested subagent's launch in order while the root turn is idle", () =>
     Effect.scoped(
       Effect.gen(function* () {
```

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

Status: modified; +30; −19

```diff
@@ -7133,31 +7133,41 @@
             message.type === "system" &&
             message.subtype === "task_started" &&
             (yield* Ref.get(sessionSubagentsByTaskId)).has(message.task_id);
+          // A completed subagent that Claude resumes on its own (under its launch
+          // call, unlike SendMessage) has no live run left to show it while the
+          // root is idle; only a continuation does.
+          let isResumedSubagentTaskStarted = false;
           if (
             isKnownSubagentTaskStarted &&
             message.type === "system" &&
             message.subtype === "task_started"
           ) {
             const now = yield* DateTime.now;
+            isResumedSubagentTaskStarted = yield* Ref.modify(
+              sessionSubagentsByTaskId,
+              (current) => {
+                const registered = current.get(message.task_id);
+                if (registered === undefined || registered.task.status === "running") {
+                  return [false, current] as const;
+                }
+                const { progress: _staleProgress, ...priorTask } = registered.task;
+                return [
+                  registered.runToolUseId === message.tool_use_id,
+                  new Map(current).set(message.task_id, {
+                    ...registered,
+                    confirmedOutcome: undefined,
+                    task: {
+                      ...priorTask,
+                      status: "running",
+                      result: null,
+                      startedAt: now,
+                      completedAt: null,
+                      updatedAt: now,
+                    },
-            yield* Ref.update(sessionSubagentsByTaskId, (current) => {
-              const registered = current.get(message.task_id);
-              if (registered === undefined || registered.task.status === "running") {
-                return current;
-              }
-              const { progress: _staleProgress, ...priorTask } = registered.task;
-              return new Map(current).set(message.task_id, {
-                ...registered,
-                confirmedOutcome: undefined,
-                task: {
-                  ...priorTask,
-                  status: "running",
-                  result: null,
-                  startedAt: now,
-                  completedAt: null,
-                  updatedAt: now,
-                },
-              });
+                  }),
+                ] as const;
-            });
+              },
+            );
           }
           const isWakeTurnStart = isClaudeTurnStartMessage(message);
           const isWakeEvidence =
@@ -7233,6 +7243,7 @@
             parentToolUseIdFromSdkMessage(message) === null;
           if (
             !isPendingSubagentNotification &&
+            !isResumedSubagentTaskStarted &&
             !isNativeOpaqueWakeFrame &&
             !isWakeTurnStart &&
             message.type !== "result"
```

## scripts/fixtures/issue-16610/scenario.json

Status: added; +6; −0

```diff
@@ -1,0 +1,6 @@
+{
+  "provider": "claudeAgent",
+  "model": "claude-opus-5-5",
+  "effort": "medium",
+  "prompt": "Call the Agent tool once: subagent_type \"general-purpose\", run_in_background true, description \"Repro self-resume\", prompt: \"Run Bash with run_in_background true: python3 -c 'import time; time.sleep(30)'. Then end your turn with exactly: WAITING. When the background command's completion notification arrives, make 4 Bash calls one after another, each running python3 -c 'import time; time.sleep(15)', then reply with exactly: DONE_SELF\". After the Agent call returns, reply with exactly LAUNCHED and stop."
+}
```

## scripts/fixtures/issue-16610/seed.js

Status: added; +22; −0

```diff
@@ -1,0 +1,22 @@
+// Run with Node and evaluate its output in an isolated app's new-thread draft,
+// with the scenario's Claude model selected. Submit through the normal composer;
+// this preserves the real SDK lifecycle instead of seeding projection rows.
+import * as NodeFS from "node:fs";
+
+const scenario = JSON.parse(
+  NodeFS.readFileSync(new URL("./scenario.json", import.meta.url), "utf8"),
+);
+
+console.log(`(() => {
+  if (!location.pathname.startsWith('/draft/')) {
+    throw new Error('Open a new thread draft in an isolated development app first.');
+  }
+  const composer = document.querySelector('[role="textbox"][aria-label="Message"]');
+  if (!(composer instanceof HTMLElement) || !composer.isContentEditable) {
+    throw new Error('The message composer is unavailable.');
+  }
+  composer.focus();
+  document.execCommand('selectAll', false);
+  document.execCommand('insertText', false, ${JSON.stringify(scenario.prompt)});
+  return { seeded: composer.innerText === ${JSON.stringify(scenario.prompt)} };
+})()`);
```
