Skip to content
Merged
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
79 changes: 73 additions & 6 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2211,7 +2211,13 @@ describe("ClaudeAdapterV2 background wake turns", () => {
});
const makeAssistantErrorFrame = (input: {
readonly uuid: string;
readonly error: "authentication_failed" | "rate_limit" | "server_error" | undefined;
readonly error:
| "authentication_failed"
| "invalid_request"
| "rate_limit"
| "server_error"
| undefined;
readonly text?: string;
readonly parentToolUseId?: string | null;
}) =>
claudeSdkFrame({
Expand All @@ -2221,7 +2227,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
id: `msg_${input.uuid}`,
type: "message",
role: "assistant",
content: [{ type: "text", text: "Claude could not complete this request." }],
content: [{ type: "text", text: input.text ?? "Claude could not complete this request." }],
stop_reason: null,
stop_sequence: null,
usage: {
Expand Down Expand Up @@ -4639,10 +4645,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
assert.equal(terminal.status, "failed");
if (terminal.status !== "failed") return;
assert.isNotEmpty(terminal.failure.message);
assert.equal(
terminal.failure.class,
terminalReason === "blocking_limit" ? "usage_limit" : "provider_error",
);
assert.equal(terminal.failure.class, "provider_error");
assert.isFalse(
harness.events.some(
(event) =>
Expand All @@ -4662,6 +4665,70 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

// Frames follow #19832: a full context window ends the turn with
// blocking_limit while the plan's rate-limit window is still allowed.
it.effect.each([
{
windowStatus: "allowed",
expectedClass: "provider_error",
expectedMessage: "Claude stopped: the prompt exceeds the model's context window.",
},
{
windowStatus: "rejected",
expectedClass: "usage_limit",
expectedMessage: "Claude usage limit reached. Send the message again once the limit resets.",
},
] as const)(
"classifies blocking_limit by its rate-limit evidence ($windowStatus window)",
({ windowStatus, expectedClass, expectedMessage }) =>
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-blocking-limit-${windowStatus}`),
text: "Continue.",
attachments: [],
}),
);
yield* Queue.offerAll(harness.sdkMessages, [
claudeSdkFrame({
type: "rate_limit_event",
rate_limit_info: { status: windowStatus, rateLimitType: "five_hour" },
uuid: "00000000-0000-4000-8000-000000000660",
session_id: WAKE_NATIVE_SESSION,
}),
makeAssistantErrorFrame({
uuid: "00000000-0000-4000-8000-000000000661",
error: "invalid_request",
text: "Prompt is too long",
}),
makeResultFrame({
uuid: "00000000-0000-4000-8000-000000000662",
result: "Prompt is too long",
isError: true,
numTurns: 189,
terminalReason: "blocking_limit",
}),
]);
const terminal = yield* Queue.take(harness.terminalReceipts);
assert.equal(terminal.status, "failed");
if (terminal.status !== "failed") return;
assert.equal(terminal.failure.class, expectedClass);
assert.include(terminal.failure.message, expectedMessage);
assert.equal(terminal.failure.code, "blocking_limit");
if (expectedClass === "provider_error") {
assert.include(terminal.failure.message, "/compact");
}
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
);

const providerThreadRosterEvents = (
events: ReadonlyArray<ProviderAdapter.ProviderAdapterV2Event>,
) =>
Expand Down
13 changes: 8 additions & 5 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2618,6 +2618,9 @@ function resultUserFacingError(result: SDKResultMessage): string | undefined {
);
}

const CONTEXT_WINDOW_FULL_ERROR =
"Claude stopped: the prompt exceeds the model's context window. Run /compact or start a new thread.";

function terminalResultError(
reason: SDKResultMessage["terminal_reason"],
failureHint?: string,
Expand All @@ -2636,11 +2639,13 @@ function terminalResultError(
case "turn_setup_failed":
return "Claude could not start the turn.";
case "blocking_limit":
return "Claude stopped: a usage limit blocked the request.";
// The CLI also ends a turn with blocking_limit once the context window
// is full ("Prompt is too long"). Only rate-limit evidence sets the hint.
return failureHint ?? CONTEXT_WINDOW_FULL_ERROR;
case "rapid_refill_breaker":
return "Claude stopped: the context refilled too quickly after compaction.";
case "prompt_too_long":
return "Claude stopped: the prompt exceeds the model's context window.";
return CONTEXT_WINDOW_FULL_ERROR;
case "image_error":
return "Claude stopped: an image in the conversation could not be processed.";
case "model_error":
Expand Down Expand Up @@ -2817,9 +2822,7 @@ function providerFailureFromResult(
}
}
const failureClass =
message.terminal_reason === "blocking_limit" ||
(message.subtype === "success" && message.api_error_status === 429) ||
usageLimited
(message.subtype === "success" && message.api_error_status === 429) || usageLimited
? "usage_limit"
: "provider_error";
const listedError = resultUserFacingError(message);
Expand Down
Loading