Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7343,6 +7343,188 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

it.effect("tells the user once when Claude Code runs a Fast turn at standard speed", () =>
Effect.gen(function* () {
const harness = yield* makeWakeHarness;
const now = yield* DateTime.now;
const notices = () =>
harness.events.flatMap((event) =>
event.type === "turn_item.updated" && event.turnItem.type === "system_notice"
? [event.turnItem.message]
: [],
);
const runTurn = (ordinal: number, fastMode: boolean) =>
Effect.gen(function* () {
yield* harness.runtime.startTurn(
makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now,
attemptId: RunAttemptId.make(`claude-fast-mode-off-attempt:${ordinal}`),
text: `Request ${ordinal}`,
attachments: [],
providerTurnOrdinal: ordinal,
modelSelection: {
instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
model: "claude-opus-5-5",
options: [{ id: "fastMode", value: fastMode }],
},
}),
);
yield* Queue.offer(
harness.sdkMessages,
claudeSdkFrame({
type: "system",
subtype: "init",
fast_mode_state: "off",
fast_mode_disabled_reason: "extra_usage_disabled",
session_id: WAKE_NATIVE_SESSION,
uuid: `00000000-0000-4000-8000-00000000041${ordinal}`,
}),
);
yield* Queue.offer(
harness.sdkMessages,
claudeSdkFrame({
type: "system",
subtype: "notification",
key: "fast-mode-overage-rejected",
text: "Fast mode disabled · Out of usage credits",
priority: "immediate",
session_id: WAKE_NATIVE_SESSION,
uuid: `00000000-0000-4000-8000-00000000042${ordinal}`,
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
);
yield* Queue.offer(
harness.sdkMessages,
makeResultFrame({
uuid: `00000000-0000-4000-8000-00000000043${ordinal}`,
result: `Done ${ordinal}`,
}),
);
yield* Queue.take(harness.terminalReceipts);
});

yield* runTurn(1, false);
assert.deepEqual(notices(), []);
yield* runTurn(2, true);
yield* runTurn(3, true);
assert.deepEqual(notices(), [
"Fast mode is off (extra usage disabled), using standard speed",
]);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
NodeCrypto.layer,
IdAllocator.layer,
McpProviderSessions.layer,
),
),
),
);

const runFastClaudeTurn = (frames: ReadonlyArray<unknown>) =>
Effect.gen(function* () {
const harness = yield* makeWakeHarness;
const now = yield* DateTime.now;
yield* harness.runtime.startTurn(
makeClaudeTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now,
attemptId: RunAttemptId.make("claude-fast-mode-frames-attempt"),
text: "Request",
attachments: [],
modelSelection: {
instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
model: "claude-opus-5-5",
options: [{ id: "fastMode", value: true }],
},
}),
);
for (const frame of frames) {
yield* Queue.offer(harness.sdkMessages, claudeSdkFrame(frame));
}
yield* Queue.take(harness.terminalReceipts);
return harness.events.flatMap((event) =>
event.type === "turn_item.updated" && event.turnItem.type === "system_notice"
? [event.turnItem.message]
: [],
);
});

it.effect(
"shows Claude Code's notice when the API rejects Fast while the CLI reports it on",
() =>
Effect.gen(function* () {
const notices = yield* runFastClaudeTurn([
{
type: "system",
subtype: "init",
fast_mode_state: "on",
session_id: WAKE_NATIVE_SESSION,
uuid: "00000000-0000-4000-8000-000000000451",
},
{
type: "system",
subtype: "notification",
key: "fast-mode-overage-rejected",
text: "Fast mode disabled · Out of usage credits",
priority: "immediate",
session_id: WAKE_NATIVE_SESSION,
uuid: "00000000-0000-4000-8000-000000000452",
},
{
...makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000453", result: "Done" }),
fast_mode_state: "on",
},
]);
assert.deepEqual(notices, ["Fast mode disabled · Out of usage credits"]);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
NodeCrypto.layer,
IdAllocator.layer,
McpProviderSessions.layer,
),
),
),
);

it.effect("waits past a pending Fast decision and reads the state from the result", () =>
Effect.gen(function* () {
const notices = yield* runFastClaudeTurn([
{
type: "system",
subtype: "init",
fast_mode_state: "off",
fast_mode_disabled_reason: "pending",
session_id: WAKE_NATIVE_SESSION,
uuid: "00000000-0000-4000-8000-000000000461",
},
{
...makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000462", result: "Done" }),
fast_mode_state: "off",
fast_mode_disabled_reason: "extra_usage_disabled",
},
]);
assert.deepEqual(notices, ["Fast mode is off (extra usage disabled), using standard speed"]);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
NodeCrypto.layer,
IdAllocator.layer,
McpProviderSessions.layer,
),
),
),
);

it.effect(
"runs native compaction and keeps its context watermark separate from billed usage",
() =>
Expand Down
41 changes: 35 additions & 6 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2996,6 +2996,24 @@ function isClaudeBackgroundTasksChangedMessage(message: SDKMessage): boolean {
return message.type === "system" && (message.subtype as string) === "background_tasks_changed";
}

// Claude Code drops a requested Fast mode to standard speed without failing the turn.
function claudeFastModeNotice(message: SDKMessage): string | null {
if (message.type === "system" && message.subtype === "notification") {
return message.key.startsWith("fast-mode") ? message.text : null;
}
// Result frames carry the state too, for Fast turned on in a process whose init predates it.
const stateFrame =
message.type === "result" || (message.type === "system" && message.subtype === "init")
? message
: null;
if (stateFrame?.fast_mode_state !== "off") return null;
const reason = stateFrame.fast_mode_disabled_reason;
// pending: Claude Code has not decided yet, so a later frame may still turn Fast on.
if (reason === "pending") return null;
const label = reason?.replaceAll("_", " ");
return `Fast mode is off${label === undefined ? "" : ` (${label})`}, using standard speed`;
}

function isClaudeTurnStartMessage(message: SDKMessage): boolean {
return message.type === "system" && message.subtype === "init";
}
Expand Down Expand Up @@ -4190,6 +4208,7 @@ interface ClaudeLiveQueryContext {
// The CLI may fall back from the requested mode on its first init frame.
openedPermissionMode: PermissionMode;
receivedInit: boolean;
announcedFastModeNotice: boolean;
permissionMode: PermissionMode;
stopping: boolean;
readonly subagentsFromEarlierProcesses: ReadonlySet<ActiveClaudeSubagent>;
Expand Down Expand Up @@ -8878,14 +8897,22 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
return;
}

if (
const cliNotice =
message.type === "system" &&
(message.subtype === "model_refusal_fallback" ||
(message.subtype === "informational" && message.level !== "info"))
) {
? message.content
: null;
const fastModeNotice =
liveQuery.selection.settings.fastMode === true && !liveQuery.announcedFastModeNotice
? claudeFastModeNotice(message)
: null;
if (fastModeNotice !== null) liveQuery.announcedFastModeNotice = true;
for (const notice of [cliNotice, fastModeNotice]) {
if (notice === null || !("uuid" in message)) continue;
const now = yield* DateTime.now;
const nativeItemId = message.uuid;
context.assistant.noticeTexts.add(message.content);
context.assistant.noticeTexts.add(notice);
yield* emitProviderEvent({
type: "turn_item.updated",
driver: CLAUDE_PROVIDER,
Expand All @@ -8908,15 +8935,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
ordinal: yield* resolveItemOrdinal(context, nativeItemId),
type: "system_notice",
status: "completed",
title: message.content,
message: message.content,
title: notice,
message: notice,
startedAt: now,
completedAt: now,
updatedAt: now,
},
});
return;
}
// Fast-mode frames (init, result) still need their normal handling below.
if (cliNotice !== null) return;

if (message.type === "system" && message.subtype === "api_retry") {
const updatedAt = yield* DateTime.now;
Expand Down Expand Up @@ -10590,6 +10618,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
promptEchoMode: "unknown",
openedPermissionMode: queryOptions.permissionMode,
receivedInit: false,
announcedFastModeNotice: false,
permissionMode: queryOptions.permissionMode,
stopping: false,
handedOffSubagents: new Set(),
Expand Down
Loading