Skip to content

Commit 58b3079

Browse files
fix(claude-local): classify an unrefreshable OAuth session as auth required
The Claude CLI reports a dead OAuth session as Failed to authenticate: OAuth session expired and could not be refreshed which matches none of the branches in `CLAUDE_AUTH_REQUIRED_RE` — not `unauthorized`, not `authentication required` (the CLI says "Failed to authenticate"), and it is an OAuth session rather than an API key. With `detectClaudeLoginRequired` returning false and neither the transient-upstream nor the provider-quota pattern matching either, the run ends up unclassified and fails hard instead of surfacing `claude_auth_required` and its login path. On one host's run-log corpus this was the single largest error bucket: 2811 of 8179 failed runs, spread over every one of the 11 agents in a continuous ~79h window — a shared host credential expiring, not a per-agent problem. Because nothing classified it, all 11 agents kept being woken and kept burning runs for the entire window. Match the CLI's whole failure sentence rather than `OAuth session expired` alone. `detectClaudeLoginRequired` reads the run's own `result` text, and agents that *report on* an outage quote that phrase back; counter-checking both forms over the same corpus, the short form newly matched 5 successful runs while the anchored form matched none.
1 parent d1b9448 commit 58b3079

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

packages/adapters/claude-local/src/server/parse.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,36 @@ describe("detectClaudeLoginRequired", () => {
3333
}).requiresLogin,
3434
).toBe(false);
3535
});
36+
37+
it("classifies an unrefreshable OAuth session as auth required", () => {
38+
expect(
39+
detectClaudeLoginRequired({
40+
parsed: {
41+
result:
42+
"Failed to authenticate: OAuth session expired and could not be refreshed",
43+
is_error: true,
44+
},
45+
stdout: "",
46+
stderr: "",
47+
}).requiresLogin,
48+
).toBe(true);
49+
});
50+
51+
it("does not classify a run that merely reports on an OAuth outage", () => {
52+
expect(
53+
detectClaudeLoginRequired({
54+
parsed: {
55+
result: [
56+
"## Finding: the host was down for ~3.3 days",
57+
"997 of 997 runs in the window died on an expired OAuth session,",
58+
"so the credential — not the agents — was at fault.",
59+
].join("\n"),
60+
},
61+
stdout: "",
62+
stderr: "",
63+
}).requiresLogin,
64+
).toBe(false);
65+
});
3666
});
3767

3868
describe("isClaudeModelNotFoundError", () => {

packages/adapters/claude-local/src/server/parse.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import {
66
parseJson,
77
} from "@paperclipai/adapter-utils/server-utils";
88

9-
const CLAUDE_AUTH_REQUIRED_RE = /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+(?:`?claude\s+login`?|\/login)|login\s+required|requires\s+login|unauthorized|authentication\s+required|invalid\s+api\s+key[\s\S]{0,120}(?:\/login|claude\s+login|log\s+in))/i;
9+
// The OAuth branch is deliberately matched as the CLI's whole failure sentence
10+
// rather than on `OAuth session expired` alone: agents routinely quote that
11+
// phrase back when they *report on* an outage, and `detectClaudeLoginRequired`
12+
// reads the run's own result text, so the short form misreads those successful
13+
// reports as an auth failure.
14+
const CLAUDE_AUTH_REQUIRED_RE = /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+(?:`?claude\s+login`?|\/login)|login\s+required|requires\s+login|unauthorized|authentication\s+required|failed\s+to\s+authenticate[\s\S]{0,40}oauth\s+session\s+expired\s+and\s+could\s+not\s+be\s+refreshed|invalid\s+api\s+key[\s\S]{0,120}(?:\/login|claude\s+login|log\s+in))/i;
1015
const URL_RE = /(https?:\/\/[^\s'"`<>()[\]{};,!?]+[^\s'"`<>()[\]{};,!.?:]+)/gi;
1116

1217
const CLAUDE_TRANSIENT_UPSTREAM_RE =

0 commit comments

Comments
 (0)