Skip to content

fix: stop a run on an auth failure instead of retrying it (#86) - #87

Merged
Lexus2016 merged 1 commit into
mainfrom
fix/86-auth-error-stop
Sep 2, 2026
Merged

fix: stop a run on an auth failure instead of retrying it (#86)#87
Lexus2016 merged 1 commit into
mainfrom
fix/86-auth-error-stop

Conversation

@Lexus2016

Copy link
Copy Markdown
Owner

Closes #86.

What was reported

Failed to authenticate: OAuth session expired and could not be refreshed

The work stops and nothing says why. The ask was: display the root cause, and refresh
the session automatically.

What was actually broken

The visible symptom hid a worse defect. An auth failure is not subtype:'success', so
all three agent loops auto-continued it. Every continue fails instantly with the
identical error, the whole MAX_AUTO_CONTINUES budget empties in seconds, and the run
ends as a generic agent_incomplete that names nothing. A chain task then retried twice
more on top of that. The burnt budget is the part nobody could see.

What this PR does — and does not do

auth-errors.js is the third class of "the turn stopped and it was not the agent's
fault", alongside the two in rate-limit-utils.js — and the only one that never
passes on its own
. An overload clears after a backoff; a quota clears at resetsAt;
an expired OAuth session clears when a human re-authenticates the CLI, and not before.

It refreshes nothing, and cannot. The OAuth tokens belong to the claude CLI's own
credentials store; the refresh exchange needs a refresh token and client credentials
this server has never held and must not hold. "Refresh automatically" is the CLI's job —
when the CLI reports that the refresh failed, the only correct move here is to stop at
once and name the action. Detecting it precisely and refusing to retry is the fix.

  • Detection is anchored to CLI/API-internal wording, disjoint from the rate-limit
    anchors in both directions (reading a quota banner as an auth stop would strand work
    that would have resumed by itself).
  • A clean success is never an auth stop. This repository's own issue, module and
    test all contain the phrase "OAuth session expired", so an agent asked to read any of
    them reproduces it in a turn that succeeded. Same guard shouldRetryOverload() has.
  • A bare /login or unauthorized is deliberately not matched — this project serves
    its own /login route and answers {error:'unauthorized'} from its own middleware.
  • The detector runs before the success break and before the auto-continue in all
    three loops (CLI chat, SSH chat, taskWorker), and the chain auto-retry excludes it.
    That ordering is the fix, so the test pins it by index — verified by mutation.
  • The chat gets a real status line (leading --- fence, or the SPA stamps its own
    "✅ Done" badge over the warning), the log gets a root-cause kind, and the Kanban card
    gets a parseable auth_error:<kind> <line> reason plus a 🔐 badge in all 5 languages.

Note that claudeCliStatus().authenticated cannot cover this by construction: it tests
that ~/.claude/.credentials.json exists, and an expired token's file exists too.
Telling a live token from a dead one needs a network round trip, so the stop is caught
reactively, mid-turn.

Verification

npm test — 77 files, 1617 passed, 0 failed, exit 0. The new
test/auth-errors.test.js contributes 66 assertions across 8 sections: detection,
false-positive guards, disjointness from rate-limit-utils, the clean-success guard,
root-cause buckets, notice format, structural pins on the ordering in server.js, and
kbAuthStop lifted out of kanban.html and executed.

Two mutations were run to prove the pins are real: removing !taskAuthStop from the
chain-retry condition, and moving the detector below taskContinueCount++. Both were
caught.

"Failed to authenticate: OAuth session expired and could not be refreshed"
stopped the work and said nothing about why. The visible symptom hid a
worse defect: an auth failure is not subtype:'success', so all three agent
loops AUTO-CONTINUED it. Each continue fails instantly with the identical
error, the whole MAX_AUTO_CONTINUES budget empties in seconds, and the run
ends as a generic 'agent_incomplete' that names nothing. A chain task then
retried twice more on top of that.

auth-errors.js is the third class of "the turn stopped and it was not the
agent's fault", alongside the two in rate-limit-utils.js — and the only one
that NEVER passes on its own. It refreshes nothing and cannot: the OAuth
tokens belong to the `claude` CLI's own credentials store, and the refresh
exchange needs client credentials this server has never held and must not
hold. Detecting it precisely and refusing to retry IS the fix.

- Detection is anchored to CLI/API-internal wording, disjoint from the
  rate-limit anchors, and a CLEAN SUCCESS is never classified as an auth
  failure — this repo's own issue, module and test contain the phrase, so
  an agent asked to read them reproduces it in a turn that succeeded.
- A bare "/login" or "unauthorized" is deliberately not matched: the studio
  serves its own /login route and answers {error:'unauthorized'}.
- The detector runs BEFORE the success break and before the auto-continue in
  all three loops (chat, SSH, taskWorker); the chain retry excludes it.
- The chat gets a real status line (--- fence, or the SPA stamps "Done" over
  the warning), the log gets a root-cause kind, the Kanban card gets a
  parseable auth_error:<kind> reason and a lock badge in 5 languages.

test/auth-errors.test.js: 66 assertions, including structural pins on the
ordering in server.js — a reorder there silently restores the bug.
@Lexus2016
Lexus2016 merged commit 1334ff4 into main Sep 2, 2026
1 check passed
@Lexus2016
Lexus2016 deleted the fix/86-auth-error-stop branch September 2, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: OAuth Session Expired Cannot Be Refreshed

1 participant