From 538393a5da398f371435f8d63569b3d776ebef2e Mon Sep 17 00:00:00 2001 From: Alan Yang <42523849+syanga@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:17:44 -0800 Subject: [PATCH] fix(server): keep Claude sessions alive for owed wake turns --- .../Adapters/ClaudeAdapterV2.test.ts | 22 +++++++++++++------ .../Adapters/ClaudeAdapterV2.ts | 7 ++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 18a16e2f63102..657d1e006d51e 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -11013,7 +11013,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); it.effect( - "uses task_started as an incremental roster fallback, keeps its tool call, and clears internal work on empty snapshot", + "uses task_started as an incremental roster fallback, keeps its tool call, and retains owed wakes on empty snapshot", () => Effect.gen(function* () { const harness = yield* makeWakeHarness; @@ -11161,7 +11161,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { ).map((task) => task.taskId), WAKE_TASK_ID, ); - assert.isFalse(yield* harness.hasPendingBackgroundWork); + assert.isTrue(yield* harness.hasPendingBackgroundWork); }).pipe( Effect.provide( Layer.mergeAll( @@ -12185,6 +12185,8 @@ describe("ClaudeAdapterV2 background wake turns", () => { assert.equal(closes, 0); assert.isTrue(yield* harness.hasPendingBackgroundWork); + yield* harness.offerAndWait(wakeNotification); + assert.isTrue(yield* harness.hasPendingBackgroundWork); yield* harness.runtime.interruptTurn({ providerThread: settledThread ?? harness.providerThread, providerTurnId: harness.terminalEvents()[0]!.providerTurnId, @@ -13889,7 +13891,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { yield* harness.offerAndWait(wakeNotification); assert.lengthOf(harness.continuationRequests, 0); - assert.isFalse(yield* harness.hasPendingBackgroundWork); + assert.isTrue(yield* harness.hasPendingBackgroundWork); yield* Queue.offer(harness.sdkMessages, wakeAssistant); yield* harness.awaitContinuation(1); @@ -13902,6 +13904,8 @@ describe("ClaudeAdapterV2 background wake turns", () => { assert.lengthOf(harness.continuationRequests, 1); assert.lengthOf(harness.terminalEvents(), 1); assert.isTrue(yield* harness.hasPendingBackgroundWork); + yield* harness.continuationRequests[0]!.onRunEndedWithoutStarting!(); + assert.isFalse(yield* harness.hasPendingBackgroundWork); }).pipe( Effect.provide( Layer.mergeAll( @@ -14778,9 +14782,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { 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); + assert.isTrue( + yield* harness.hasPendingBackgroundWork, + "the completed command still owns a pending native notice turn", + ); yield* harness.offerAndWait(wakeTurnInit); + assert.isTrue(yield* harness.hasPendingBackgroundWork); yield* harness.runtime.startTurn( makeClaudeTestTurnInput({ threadId: harness.threadId, @@ -14877,6 +14884,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { [], ); } + assert.isFalse(yield* harness.hasPendingBackgroundWork); }).pipe( Effect.provide( Layer.mergeAll( @@ -19095,7 +19103,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { ); yield* Queue.offer(firstProcess, turnOneResult); yield* changes.until(() => events.some((event) => event.type === "turn.terminal")); - assert.isFalse(yield* hasPendingBackgroundWork); + assert.isTrue(yield* hasPendingBackgroundWork); const alternateModel = CLAUDE_TEST_RESTART_MODEL_SELECTION; const activeProviderThread = { @@ -19293,7 +19301,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { yield* changes.until( () => events.filter((event) => event.type === "turn.terminal").length === 2, ); - assert.isFalse(yield* hasPendingBackgroundWork); + assert.isTrue(yield* hasPendingBackgroundWork); assert.lengthOf(continuationRequests, 0); yield* Queue.offer(secondProcess, wakeResult); diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 16a697a7e1353..3f6dea70c8a85 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -13103,6 +13103,13 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( return true; } } + for (const nativeThreadId of (yield* Ref.get( + lastProviderThreadByNativeThread, + )).keys()) { + if ((yield* owedWakeTasksForNativeThread(nativeThreadId)).length > 0) { + return true; + } + } for (const subagent of (yield* Ref.get(sessionSubagentsByTaskId)).values()) { if (subagent.task.status === "running") { return true;