diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 7fece9817ce01..239f06d5365b5 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -2621,6 +2621,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { readonly close?: (sdkMessages: Queue.Queue) => Effect.Effect; readonly interrupt?: Effect.Effect; readonly setPermissionMode?: ClaudeAdapterV2.ClaudeAgentSdkQuerySession["setPermissionMode"]; + readonly applyFlagSettings?: ClaudeAdapterV2.ClaudeAgentSdkQuerySession["applyFlagSettings"]; readonly stopTask?: ( taskId: string, ) => Effect.Effect; @@ -2768,7 +2769,9 @@ describe("ClaudeAdapterV2 background wake turns", () => { applyFlagSettings: (settings) => Effect.sync(() => { liveSelectionChanges.push(settings); - }), + }).pipe( + Effect.andThen(() => options?.applyFlagSettings?.(settings) ?? Effect.void), + ), setPermissionMode: (mode) => Effect.sync(() => { permissionModeChanges.push(mode); @@ -4267,7 +4270,6 @@ describe("ClaudeAdapterV2 background wake turns", () => { yield* harness.runtime.startTurn(turn(4, { ...normal, model: "claude-sonnet-4-6" })); yield* settle; assert.deepEqual(harness.liveSelectionChanges, [ - { model: "claude-opus-5-5" }, { effortLevel: "xhigh", fastMode: true, ultracode: true }, { model: "claude-sonnet-4-6" }, { effortLevel: "high", fastMode: null, ultracode: null }, @@ -4292,7 +4294,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { } assert.strictEqual(harness.getOpenedOptions(), originalOptions); assert.lengthOf(harness.offeredMessages, 4); - assert.lengthOf(harness.liveSelectionChanges, 4); + assert.lengthOf(harness.liveSelectionChanges, 3); assert.isTrue(yield* harness.hasPendingBackgroundWork); }).pipe( Effect.provide( @@ -4302,6 +4304,64 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("re-sends the model on the turn after a live selection change fails", () => + Effect.scoped( + Effect.gen(function* () { + let failFlags = true; + const harness = yield* makeWakeHarnessWithOptions({ + applyFlagSettings: () => + Effect.suspend(() => { + if (!failFlags) return Effect.void; + failFlags = false; + return Effect.fail( + new ClaudeAdapterV2.ClaudeAgentSdkQueryRunnerError({ + method: "applyFlagSettings", + cause: new Error("control request failed"), + }), + ); + }), + }); + const now = yield* DateTime.now; + const opus = { + instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID, + model: "claude-opus-5-5", + } satisfies ModelSelection; + const turn = (ordinal: number, modelSelection: ModelSelection) => + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now, + attemptId: RunAttemptId.make(`attempt-failed-live-change:${ordinal}`), + text: `Request ${ordinal}`, + attachments: [], + providerTurnOrdinal: ordinal, + modelSelection, + }); + yield* harness.runtime.startTurn(turn(1, opus)); + yield* harness.offerAndWait(turnOneResult); + yield* Queue.take(harness.terminalReceipts); + + const failed = yield* harness.runtime + .startTurn(turn(2, { ...opus, model: "claude-sonnet-4-6" })) + .pipe(Effect.result); + assert.equal(failed._tag, "Failure"); + + // The process may now run Sonnet, so returning to Opus must send it. + yield* harness.runtime.startTurn(turn(3, opus)); + assert.deepEqual(harness.liveSelectionChanges, [ + { model: "claude-sonnet-4-6" }, + { effortLevel: "high", fastMode: null, ultracode: null }, + { model: "claude-opus-5-5" }, + { effortLevel: "medium", fastMode: false, ultracode: null }, + ]); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + it.effect.each(["completed", "interrupted"] as const)( "projects Claude thinking blocks when %s", (status) => diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index baf9a0b0a690b..a5145d821b637 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -9659,7 +9659,13 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( existing.openedPermissionMode === queryPolicy.permissionMode) ) { const live = existing; - yield* live.query.setModel(compiledSelection.apiModelId).pipe( + // Preserve launcher-selected models unless the model changed or a failed update left it unknown. + yield* ( + live.selection.queryIdentity === "" || + live.selection.apiModelId !== compiledSelection.apiModelId + ? live.query.setModel(compiledSelection.apiModelId) + : Effect.void + ).pipe( Effect.andThen( live.query.applyFlagSettings(claudeLiveSelectionFlagSettings(compiledSelection)), ),