From 2b7420e0293cd96bef2049fd995254f6fa32cebe Mon Sep 17 00:00:00 2001 From: Lexus2016 Date: Wed, 2 Sep 2026 17:26:41 +0200 Subject: [PATCH] fix: stop a run on an auth failure instead of retrying it (#86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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: 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. --- CLAUDE.md | 47 +++++++++- auth-errors.js | 159 ++++++++++++++++++++++++++++++++++ package.json | 4 +- public/kanban.html | 24 ++++-- server.js | 62 +++++++++++++- test/auth-errors.test.js | 179 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 463 insertions(+), 12 deletions(-) create mode 100644 auth-errors.js create mode 100644 test/auth-errors.test.js diff --git a/CLAUDE.md b/CLAUDE.md index 5a83901..ffee2e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ docker compose up -d docker compose logs -f claude-chat ``` -No linting and no build step configured. `npm test` chains 76 test files under `test/`: 19 DOM-less render/UI-logic tests (`test/render/*.test.mjs`, run through `node --test`) plus 57 plain-`node` suites in `test/` covering the overload detector, env load order, multi-agent results, terminals, bots, telegram, updates, kanban scheduling, the Kanban card's run-settings badges (`kanban-run-badges.test.js` pins the card's model/effort/engine chain against the one `startTask` actually resolves — the two live in different files and the card silently lies when they drift), the board-only `create_task` status (`task-backlog.test.js`), i18n completeness, the config precedence resolver plus its secret masking, usage-limit detection, the filesystem path guard (including the SVG sandbox header and the symlink rule on the `@`-mention search endpoints) plus the tunnel-blocks-terminal rule, WS session re-subscription, the SSH remote CLI-session import, the live engine pane / interactive-prompt watchdog, the cross-project global workspace aggregation, the rule that an SSH credential never leaves the server process, the Windows command-quoting oracle, the auth token lifecycle, the multi-agent dependency scheduler (waves, plan sanitising, and the rule that a failure warning must survive dep-context truncation), the SSH stream parser's three guards, the SSH run's termination guarantee (`ssh-termination.test.js` drives `ClaudeSSH.send()` through a fake ssh2 `Client` in `require.cache` and asserts `onDone` fires EXACTLY once on every ending — a missed one hangs the chat forever, a doubled one re-emits stderr) and the recovery contract of "Restart Session" (`session-restart.test.js` boots a real server against a fake `claude` that never exits, then pins that a restart ABORTS that turn and releases the session instead of refusing), the remote non-interactive shell environment (`remote-env.test.js`, which runs the generated prelude through real `bash -lc`: it must parse, print nothing on stdout, and never end on a false test — the caller chains `&& claude …` behind it), the remote CLI-list framing parser, the bot inbox's SQL seam (`bot-inbox.test.js` pins that `from_bot AS "from"` keeps the exact key `planInboxDelivery` reads — rename one without the other and every letter is silently retired as malformed), the one-time config/.env migration onto CCS_CONFIG_PATH and the mid-task clarification delivery contract on the subscription engine (`interrupt-delivery.test.js` — pins that the tmux injection block sits BEFORE the poll loop's completion `break`, that draining does not imply delivery, that a failed paste is re-queued and warns non-terminally, and that the task runner passes the same callbacks the chat path does), the CLAUDE.md / AGENTS.md discovery rules (`agents-md.test.js`, which also pins that AGENTS.md reaches the subprocess as `--append-system-prompt` and never as `--system-prompt`), and the remote file browser's three guard layers (`remote-files.test.js` runs the generated POSIX script through a real `/bin/sh` against a temp tree that contains symlinks OUT of the project; `remote-files-api.test.js` boots a server against a fake remote via `CCS_REMOTE_EXEC_HOOK` and drives `/api/files` the way the SPA does), the editor deep links (`editor-links.test.js` pins the two URI shapes literally — the browser link puts `vscode-remote` in the AUTHORITY and the CLI argument puts it in the SCHEME, and collapsing the two silently breaks one path; `editor-open-api.test.js` boots a real server with `PATH` pointed at an EMPTY directory, which both makes the `opened:'client'` fallback deterministic and guarantees the suite never launches an editor window on a developer's desktop), and the new-chat defaults chain (`chat-defaults.test.js` pins the pure resolver — the built-ins are asserted to be exactly what the SPA hardcoded before #58, and the choice lists to be exactly the toolbar's `data-v` sets and `MODEL_MAP`'s aliases; `chat-defaults-api.test.js` boots a real server in a throwaway `APP_DIR` and pins that a project writes back a SPARSE override object — a five-key snapshot passes every other assertion in that file and still breaks the feature). On the render side, `tables.test.mjs` also pins the ReDoS bound in renderMd step 3.4, `xss.test.mjs` runs 24 adversarial payloads end-to-end, and `forged-tokens.test.mjs` covers the case where user text contains the renderer's own placeholder control bytes, and `pane-font.test.mjs` pins the clamp DIRECTION of `_fitEnginePaneFont` (a wide engine pane may only shrink; a narrow split pane must be allowed to grow). `script-scope.test.mjs` pins which `