diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
index f0c4d00c7bdf3..3bb0aaf7fe549 100644
--- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
+++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
@@ -6555,6 +6555,52 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);
+ it.effect("does not count a live model change's stdout echo as background work", () =>
+ Effect.scoped(
+ Effect.gen(function* () {
+ const harness = yield* makeWakeHarness;
+ yield* harness.runtime.startTurn(
+ makeClaudeTestTurnInput({
+ threadId: harness.threadId,
+ providerThread: harness.providerThread,
+ now: yield* DateTime.now,
+ attemptId: RunAttemptId.make("attempt-model-change-echo"),
+ text: "Hello",
+ attachments: [],
+ }),
+ );
+ yield* harness.offerAndWait(turnOneResult);
+ yield* Queue.take(harness.terminalReceipts);
+
+ // Claude Code answers set_model with this replayed user frame before the next turn attaches.
+ yield* harness.offerAndWait(
+ claudeSdkFrame({
+ type: "user",
+ message: {
+ role: "user",
+ content:
+ "Set model to `claude-fable-5-2`",
+ },
+ parent_tool_use_id: null,
+ isReplay: true,
+ uuid: "00000000-0000-4000-8000-000000000e01",
+ session_id: WAKE_NATIVE_SESSION,
+ }),
+ );
+ assert.isFalse(yield* harness.hasPendingBackgroundWork);
+ }).pipe(
+ Effect.provide(
+ Layer.mergeAll(
+ IdAllocator.layer,
+ McpProviderSessions.layer,
+ NodeServices.layer,
+ NodeCrypto.layer,
+ ),
+ ),
+ ),
+ ),
+ );
+
it.effect.each([
{
content: { type: "advisor_tool_result_error", error_code: "overloaded" },
diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
index 74cdde264e7e1..4342149a8ace2 100644
--- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
@@ -3618,6 +3618,17 @@ function isClaudeTurnStartMessage(message: SDKMessage): boolean {
return message.type === "system" && message.subtype === "init";
}
+// Claude Code replays a local command's stdout, such as set_model's "Set model to", as a user frame.
+function isClaudeLocalCommandEcho(message: SDKMessage): boolean {
+ return (
+ message.type === "user" &&
+ "isReplay" in message &&
+ message.isReplay === true &&
+ typeof message.message.content === "string" &&
+ message.message.content.startsWith("")
+ );
+}
+
function isClaudeRootTurnFrame(message: SDKMessage): boolean {
return (
isClaudeTurnStartMessage(message) ||
@@ -9374,6 +9385,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
readonly message: SDKMessage;
}) {
const message = wakeInput.message;
+ // An idle echo is no wake: buffered, it reads as background work and blocks switches.
+ if (isClaudeLocalCommandEcho(message)) return;
const isNotification =
message.type === "system" && message.subtype === "task_notification";
const isPendingTaskNotification =