From 5dc0bb4fce7cb1354f973098d3ba41f969fce2d2 Mon Sep 17 00:00:00 2001 From: F4llen <76191805+f4llenz@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:42:08 +0000 Subject: [PATCH 1/2] fix(server): changing Claude effort no longer re-sends the model The live selection switch sent setModel with the unchanged model on every effort, Fast mode or Ultracode change. That logged a model change each time and replaced the model a custom binary path chose by rewriting --model. Send setModel only for a new model, or after a failed live change. Closes #22265. --- .../Adapters/ClaudeAdapterV2.test.ts | 66 ++++++++++++++++++- .../Adapters/ClaudeAdapterV2.ts | 9 ++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 138528263fbac..2fb850ce6be37 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -2690,6 +2690,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; @@ -2829,7 +2830,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); @@ -3768,7 +3771,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 }, @@ -3793,7 +3795,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( @@ -3803,6 +3805,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 53c71b9eed71a..06bad59d8eaee 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -9066,7 +9066,14 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( existing.openedPermissionMode === queryPolicy.permissionMode) ) { const live = existing; - yield* live.query.setModel(compiledSelection.apiModelId).pipe( + // `setModel` bypasses a custom binary's `--model` rewrite, so send it only for a + // new model, or after a failed change (empty identity) left the live model unknown. + yield* ( + live.selection.queryIdentity !== "" && + live.selection.apiModelId === compiledSelection.apiModelId + ? Effect.void + : live.query.setModel(compiledSelection.apiModelId) + ).pipe( Effect.andThen( live.query.applyFlagSettings(claudeLiveSelectionFlagSettings(compiledSelection)), ), From 147a119341b74e1d3d3da093e99b9acab31d6730 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 21:11:15 +0000 Subject: [PATCH 2/2] refactor(server): clarify Claude live model guard --- .../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index be36633d06fb6..a5145d821b637 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -9659,13 +9659,12 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( existing.openedPermissionMode === queryPolicy.permissionMode) ) { const live = existing; - // `setModel` bypasses a custom binary's `--model` rewrite, so send it only for a - // new model, or after a failed change (empty identity) left the live model unknown. + // Preserve launcher-selected models unless the model changed or a failed update left it unknown. yield* ( - live.selection.queryIdentity !== "" && - live.selection.apiModelId === compiledSelection.apiModelId - ? Effect.void - : live.query.setModel(compiledSelection.apiModelId) + live.selection.queryIdentity === "" || + live.selection.apiModelId !== compiledSelection.apiModelId + ? live.query.setModel(compiledSelection.apiModelId) + : Effect.void ).pipe( Effect.andThen( live.query.applyFlagSettings(claudeLiveSelectionFlagSettings(compiledSelection)),