| author | |
| committer | |
| log | d765d03cb7f980794c409c38e6ea5e25d89fde33 |
| tree | 3b9ff4f2b091b97c9a907c5620796678f91bda64 |
| parent | cb65643b076c79cfe39a5197553b143889da64ab |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: PR Batch Tester <agent@local.test>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>2 files changed, 53 insertions(+), 5 deletions(-)
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts+43| ... | @@ -3197,6 +3197,49 @@ describe("ClaudeAdapterV2 background wake turns", () => { | ... | @@ -3197,6 +3197,49 @@ describe("ClaudeAdapterV2 background wake turns", () => { |
| 3197 | ), | 3197 | ), |
| 3198 | ); | 3198 | ); |
| 3199 | 3199 | ||
| 3200 | it.effect("shows a 403 permission error instead of the login hint", () => | ||
| 3201 | Effect.gen(function* () { | ||
| 3202 | const harness = yield* makeWakeHarness; | ||
| 3203 | const now = yield* DateTime.now; | ||
| 3204 | yield* harness.runtime.startTurn( | ||
| 3205 | makeClaudeTestTurnInput({ | ||
| 3206 | threadId: harness.threadId, | ||
| 3207 | providerThread: harness.providerThread, | ||
| 3208 | now, | ||
| 3209 | attemptId: RunAttemptId.make("attempt-claude-permission-denied"), | ||
| 3210 | text: "Continue.", | ||
| 3211 | attachments: [], | ||
| 3212 | }), | ||
| 3213 | ); | ||
| 3214 | // Claude CLI 2.1.288 labels a 403 authentication_failed too. | ||
| 3215 | const cliText = | ||
| 3216 | "Failed to authenticate. API Error: 403 Your organization does not have access to this model."; | ||
| 3217 | yield* Queue.offerAll(harness.sdkMessages, [ | ||
| 3218 | makeAssistantErrorFrame({ | ||
| 3219 | uuid: "00000000-0000-4000-8000-000000000608", | ||
| 3220 | error: "authentication_failed", | ||
| 3221 | }), | ||
| 3222 | makeResultFrame({ | ||
| 3223 | uuid: "00000000-0000-4000-8000-000000000609", | ||
| 3224 | result: cliText, | ||
| 3225 | isError: true, | ||
| 3226 | apiErrorStatus: 403, | ||
| 3227 | terminalReason: "api_error", | ||
| 3228 | }), | ||
| 3229 | ]); | ||
| 3230 | |||
| 3231 | const terminal = yield* Queue.take(harness.terminalReceipts); | ||
| 3232 | assert.equal(terminal.status, "failed"); | ||
| 3233 | if (terminal.status !== "failed") return; | ||
| 3234 | assert.equal(terminal.failure.message, cliText); | ||
| 3235 | assert.equal(terminal.failure.code, "api_error_403"); | ||
| 3236 | }).pipe( | ||
| 3237 | Effect.provide( | ||
| 3238 | Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), | ||
| 3239 | ), | ||
| 3240 | ), | ||
| 3241 | ); | ||
| 3242 | |||
| 3200 | it.effect.each([ | 3243 | it.effect.each([ |
| 3201 | { recovered: false, expected: "Claude usage limit reached" }, | 3244 | { recovered: false, expected: "Claude usage limit reached" }, |
| 3202 | { recovered: true, expected: "Claude gave up after repeated API errors" }, | 3245 | { recovered: true, expected: "Claude gave up after repeated API errors" }, |
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+10-5| ... | @@ -7676,11 +7676,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | ... | @@ -7676,11 +7676,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( |
| 7676 | (message.terminal_reason == null || | 7676 | (message.terminal_reason == null || |
| 7677 | message.terminal_reason === "api_error" || | 7677 | message.terminal_reason === "api_error" || |
| 7678 | message.terminal_reason === "blocking_limit"); | 7678 | message.terminal_reason === "blocking_limit"); |
| 7679 | const failureHint = | 7679 | // The CLI also labels a 403 authentication_failed. Logging in again |
| 7680 | context.authenticationFailureMessage ?? | 7680 | // can't grant a permission, so show the CLI's own text instead. |
| 7681 | (usageLimited | 7681 | const permissionDenied = |
| 7682 | ? "Claude usage limit reached. Send the message again once the limit resets." | 7682 | message.subtype === "success" && message.api_error_status === 403; |
| 7683 | : undefined); | 7683 | const failureHint = permissionDenied |
| 7684 | ? message.result | ||
| 7685 | : (context.authenticationFailureMessage ?? | ||
| 7686 | (usageLimited | ||
| 7687 | ? "Claude usage limit reached. Send the message again once the limit resets." | ||
| 7688 | : undefined)); | ||
| 7684 | const resetTimes = Array.from(context.rateLimitResetTimes.values()); | 7689 | const resetTimes = Array.from(context.rateLimitResetTimes.values()); |
| 7685 | const resetAt = | 7690 | const resetAt = |
| 7686 | resetTimes.length > 0 && resetTimes.every((time) => time !== null) | 7691 | resetTimes.length > 0 && resetTimes.every((time) => time !== null) |