Skip to content

fix(error-classifier): stop reading embedded digits as auth signals - #248

Merged
steipete merged 4 commits into
openclaw:mainfrom
Yigtwxx:fix/error-classifier-transport-vs-auth
Aug 2, 2026
Merged

fix(error-classifier): stop reading embedded digits as auth signals#248
steipete merged 4 commits into
openclaw:mainfrom
Yigtwxx:fix/error-classifier-transport-vs-auth

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

analyzeConnectionError classifies any message containing the substring 401 as an auth failure. The check is not word-bounded, so digits that appear inside a port number, a duration, a hostname, or a request id are read as an auth signal. A server that is simply unreachable is then reported as needing authorization, and its definition is promoted to auth: 'oauth' — which hides the real fault the user has to fix.

The same condition also lets the message heuristic override a status code that is already known exactly, so code: 404 with a message mentioning unauthorized classifies as auth rather than http.

Root cause

src/error-classifier.ts:53

if (AUTH_STATUSES.has(statusCode ?? -1) || containsAuthToken(normalized)) {
  return { kind: 'auth', rawMessage, statusCode };
}
if (statusCode && statusCode >= 400) {
  return { kind: 'http', rawMessage, statusCode };
}

containsAuthToken matched raw substrings ('401', 'unauthorized', 'invalid_token', 'forbidden'), and this auth branch ran before both the http branch and the OFFLINE_PATTERNS branch. Two consequences:

  1. Any larger token containing 401 satisfies the auth branch, even when the same message also matches OFFLINE_PATTERNS (econnrefused, timed out, getaddrinfo).
  2. A known statusCode cannot win, because the right side of the || is evaluated regardless of it.

The intended precedence is already established in this repository. tests/error-classifier.test.ts locks it with committed regressions — classifies code=404 as http (not auth), classifies code=500 as http, classifies HTTP 405 as transport/http instead of auth — and the history records two earlier fixes in the same direction (Fix 405 misclassified as auth error in error-classifier, fix: detect auth errors from error.code property). Those tests pass today only because their messages contain no auth-like text: 'Not Found', 'Internal Server Error'. The existing offline test passes for the same reason — its port is 9000.

Why this reaches the OAuth flow

shouldAbortSseFallback does not intercept these errors: isPostAuthConnectError and isOAuthFlowError both test for a symbol marker (src/runtime/oauth.ts:81-91) that a raw fetch error does not carry. So the flow continues into the promotion branch:

src/error-classifier.ts:53         analyzeConnectionError -> kind: 'auth'
src/runtime-oauth-support.ts:21    isUnauthorizedError -> true
src/runtime/transport.ts:398,480   maybePromoteHttpDefinition(...)
src/runtime-oauth-support.ts:14    logger.info('Detected OAuth requirement ...')
                                   return { ...definition, auth: 'oauth' }

Fix

  • src/error-classifier.ts
    • AUTH_TOKEN_PATTERNS replaces the substring checks with word-bounded patterns, so 401 matches only as a standalone token and forbidden no longer matches inside forbidden_tool.
    • Classification order now follows the contract the committed tests already describe: a known statusCode decides first, then OFFLINE_PATTERNS, then the keyword heuristic for messages that carry no status code.

No special cases were added and no new dependency is introduced. Auth detection for messages without a status code is unchanged, and is now covered by tests.

Behavior proof

Measured through the real production helpers (isUnauthorizedError -> maybeEnableOAuth), with no mocks, no injected transports, and no network. The identical script was run against src/error-classifier.ts at origin/main and at this branch. The first case is a control: it contains no auth-like digits and must stay offline on both sides.

=== BEFORE FIX (src/error-classifier.ts @ origin/main) ===
fetch failed: connect ECONNREFUSED 127.0.0.1:9000
  isUnauthorizedError : false
  definition.auth     : (not promoted)

fetch failed: connect ECONNREFUSED 127.0.0.1:14012
  isUnauthorizedError : true
  definition.auth     : oauth
  logger.info         : Detected OAuth requirement for 'example-http'. Launching browser flow...

Request timed out after 4010ms
  isUnauthorizedError : true
  definition.auth     : oauth
  logger.info         : Detected OAuth requirement for 'example-http'. Launching browser flow...

getaddrinfo ENOTFOUND mcp-4015.internal
  isUnauthorizedError : true
  definition.auth     : oauth
  logger.info         : Detected OAuth requirement for 'example-http'. Launching browser flow...

=== AFTER FIX (this branch) ===
fetch failed: connect ECONNREFUSED 127.0.0.1:9000
  isUnauthorizedError : false
  definition.auth     : (not promoted)

fetch failed: connect ECONNREFUSED 127.0.0.1:14012
  isUnauthorizedError : false
  definition.auth     : (not promoted)

Request timed out after 4010ms
  isUnauthorizedError : false
  definition.auth     : (not promoted)

getaddrinfo ENOTFOUND mcp-4015.internal
  isUnauthorizedError : false
  definition.auth     : (not promoted)

The only difference between the control case and the second case is the port number.

Tests

Added to tests/error-classifier.test.ts (10 cases, three groups):

  • transport failures whose text embeds auth-like digits — refused connection on port 14012, timeout of 4010ms, hostname mcp-4015.internal, and request id 8401f2.
  • known status codes take precedence over message keywordscode: 404 with 'Not Found: /unauthorized', code: 500 with 'Internal Server Error (request 401ab3)', and a parsed 429 with 'rate limited for forbidden_tool'.
  • auth detection without a status code is preserved — bare 'Unauthorized', standalone 'Server replied 401', and 'invalid_token' still classify as auth.

The new tests are load-bearing. Stashing only src/error-classifier.ts and re-running the file on the unmodified classifier:

=== BEFORE (fix reverted, tests kept) ===
 FAIL  ... > keeps a refused connection offline when the port embeds 401
 FAIL  ... > keeps a timeout offline when the duration embeds 401
 FAIL  ... > keeps a DNS failure offline when the hostname embeds 401
 FAIL  ... > does not treat a request id that embeds 401 as auth
 FAIL  ... > classifies code=404 as http when the message also mentions unauthorized
 FAIL  ... > classifies code=500 as http when the message embeds 401
 FAIL  ... > classifies a parsed 429 as http when the message also mentions forbidden
      Tests  7 failed | 19 passed (26)

The three auth detection ... is preserved cases pass on both sides by design — they guard the behavior this change must not alter.

Follow-up from review feedback

Codex flagged a real regression in the first revision (P2, src/error-classifier.ts:16): word-bounding the keyword patterns dropped OAuth error codes that embed those keywords with underscores. \bunauthorized\b does not match unauthorized_client — a standard RFC 6749 error code — because underscore is a word character, so a status-less OAuth failure would have fallen through to other and skipped the auth path.

Verified and fixed in 47adc37. Only the numeral needs a boundary, so 401 is bounded against adjacent digits while the keywords stay substring matches:

const AUTH_TOKEN_PATTERNS = [/(?<!\d)401(?!\d)/, /unauthorized/, /invalid_token/, /forbidden/];

This keeps the original finding fixed while leaving keyword behavior exactly as it is on main. Added it.each coverage for unauthorized_client, invalid_token_hint, and invalid_token as status-less OAuth error codes; those three pass on main too, so they lock the behavior this change must not alter.

Verification

Run on Windows 11 with Node 24.15.0 and pnpm 10.33.2, at head 47adc37.

  • pnpm exec vitest run tests/error-classifier.test.ts — 26 passed (26)
  • pnpm check — passed (oxfmt --check on 352 files, oxlint --type-aware --deny-warnings, tsc --noEmit)
  • pnpm test — 808 passed, 48 skipped (856); 128 test files passed, 3 skipped
  • CHANGELOG.md is untouched, since it is release-owned for this repository's PR flow.

One pre-existing unhandled error is reported by the full suite in tests/runtime-integration.test.ts (a DOMException: ABORT_ERR raised from StreamableHTTPClientTransport.close during teardown). It reproduces on unmodified main at 7259c8c on this machine — baseline there was 795 passed with the same single error — so it is unrelated to this change. It is the reason pnpm test exits non-zero locally in both runs.

Connection failures whose text happens to embed "401" inside a port,
duration, hostname, or request id were classified as auth issues. That
sends transport and DNS failures down the OAuth promotion path, so a
server that is simply unreachable is reported as needing authorization
and its definition is promoted to auth: 'oauth'.

Word-bound the auth keyword patterns, and let a known status code decide
the classification before the message heuristic runs. The repository
already locks that precedence for clean messages via the committed
code=404 and 405 regressions; this extends it to messages that also
contain auth-like text.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 077226d1f7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/error-classifier.ts Outdated
…auth

Word-bounding the keyword patterns dropped OAuth error codes that embed them
with underscores, such as unauthorized_client and invalid_token_hint, because
underscore is a word character. Only the 401 numeral needs a boundary, so bound
it against adjacent digits and leave the keywords as substring matches.
@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. labels Aug 1, 2026
@clawsweeper

clawsweeper Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed August 2, 2026, 12:40 PM ET / 16:40 UTC.

ClawSweeper review

What this changes

The branch makes HTTP connection-error classification ignore embedded 401 digits, prioritizes known status codes, and adds regressions that prevent accidental OAuth promotion for transport failures.

Merge readiness

⚠️ Ready for maintainer review - 3 items remain

This PR addresses a real current-main misclassification and is technically sound after the earlier OAuth keyword regression was fixed. It remains necessary because current main still treats any embedded 401 as an auth signal; the remaining merge blocker is maintainer intent on how a status-less error containing both an offline phrase and a standalone numeric 401 should be classified.

Priority: P2
Reviewed head: c3a43f403a92ec1784fd0742c192fb283c623165
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🦞 diamond lobster (5/6) A focused, well-covered repair with convincing before/after runtime-helper evidence; only the intentional ambiguous-error precedence needs maintainer confirmation.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The PR body supplies before/after terminal output through the real classifier-to-OAuth-promotion helpers, plus focused and full-suite results; the proof directly shows the unintended OAuth promotion disappearing for embedded digits.
Patch quality 🦞 diamond lobster (5/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body supplies before/after terminal output through the real classifier-to-OAuth-promotion helpers, plus focused and full-suite results; the proof directly shows the unintended OAuth promotion disappearing for embedded digits.
Evidence reviewed 6 items Current-main defect: Current main checks containsAuthToken before both the generic HTTP and offline branches, and that helper uses a raw includes('401') match. A port, duration, hostname, or identifier containing those digits can therefore become an auth result.
OAuth effect is reachable: isUnauthorizedError delegates directly to the classifier, while maybeEnableOAuth promotes eligible HTTP server definitions to OAuth. The reported false classification can therefore alter runtime recovery behavior rather than only diagnostics.
Patch and regression coverage: The PR handles known status codes first, preserves explicit keyword-based OAuth signals ahead of offline patterns, and defers only a standalone numeric 401 until after offline detection. Its tests cover embedded digits, conflicting known statuses, OAuth keyword codes, and the chosen ambiguous bare-401 precedence.
Findings None None.
Security None None.

How this fits together

HTTP MCP server connection failures are classified before runtime transport decides whether to surface an offline/HTTP error or promote a server definition into the OAuth flow. The classifier’s auth result feeds isUnauthorizedError, which can change a server definition to auth: 'oauth'.

flowchart LR
  Error[HTTP server error] --> Classifier[Connection error classifier]
  Classifier --> Status[Known status code]
  Classifier --> Signals[Status-less message signals]
  Status --> Outcome[Auth, HTTP, or offline result]
  Signals --> Outcome
  Outcome --> Runtime[Runtime transport handling]
  Runtime --> OAuth[OAuth promotion or surfaced failure]
Loading

Decision needed

Question Recommendation
Should a status-less error containing both an offline phrase and a standalone numeric 401 be treated as an offline connection failure, as this PR implements, rather than an OAuth requirement? Prefer offline for ambiguous numeric 401: Merge the current implementation so ports, durations, hostnames, identifiers, and offline failures do not promote a server to OAuth unless an explicit keyword or trusted status code supports auth.

Why: The source proves the embedded-digit false-positive bug and the patch preserves keyword-based OAuth errors, but the final precedence for an inherently ambiguous status-less numeric token determines whether runtime launches OAuth recovery or surfaces a transport failure.

Before merge

  • Resolve merge risk (P1) - For a status-less message containing both an offline pattern and a standalone numeric 401, this branch now keeps the result offline instead of triggering OAuth promotion. Existing users would encounter that compatibility change only on this ambiguous failure shape.
  • Complete next step (P2) - A maintainer must choose the runtime contract for ambiguous status-less numeric 401 errors before this otherwise correct patch can merge.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch surface 2 files affected; 129 added, 13 removed The implementation is confined to the classifier and its focused regression suite, with no dependency, workflow, or generated-file changes.

Merge-risk options

Maintainer options:

  1. Confirm the narrowed fallback rule (recommended)
    Approve the current precedence if avoiding accidental OAuth promotion is more important than treating an unstructured standalone 401 as authoritative auth evidence.
  2. Keep numeric auth precedence
    Pause for a small revision only if maintainers require any standalone numeric 401 to launch OAuth even when the same message reports an offline transport failure.

Technical review

Best possible solution:

Confirm that numeric 401 without a trusted status code is intentionally subordinate to transport-failure evidence; if so, merge the current narrow split between keyword auth signals and the ambiguous numeral.

Do we have a high-confidence way to reproduce the issue?

Yes—source-reproducible with high confidence: current main applies raw includes('401') before its offline branch, and the supplied before/after production-helper transcript specifically demonstrates the resulting OAuth promotion for a port containing 14012. The checkout was inspected read-only rather than executing the test suite.

Is this the best way to solve the issue?

Unclear pending maintainer intent: the implementation is the narrowest technical repair and retains status-less OAuth keyword handling, but only maintainers can choose the desired precedence for an otherwise ambiguous standalone numeric 401 plus transport wording.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against e1689c3dec7c.

Labels

Label justifications:

  • P2: This corrects a bounded runtime misclassification that can send an unreachable HTTP server into OAuth recovery, without evidence of broad outage or data loss.
  • merge-risk: 🚨 auth-provider: The changed status-less-error precedence determines when runtime promotes an HTTP server definition to OAuth.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body supplies before/after terminal output through the real classifier-to-OAuth-promotion helpers, plus focused and full-suite results; the proof directly shows the unintended OAuth promotion disappearing for embedded digits.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body supplies before/after terminal output through the real classifier-to-OAuth-promotion helpers, plus focused and full-suite results; the proof directly shows the unintended OAuth promotion disappearing for embedded digits.

Evidence

What I checked:

  • Current-main defect: Current main checks containsAuthToken before both the generic HTTP and offline branches, and that helper uses a raw includes('401') match. A port, duration, hostname, or identifier containing those digits can therefore become an auth result. (src/error-classifier.ts:53, e1689c3dec7c)
  • OAuth effect is reachable: isUnauthorizedError delegates directly to the classifier, while maybeEnableOAuth promotes eligible HTTP server definitions to OAuth. The reported false classification can therefore alter runtime recovery behavior rather than only diagnostics. (src/runtime-oauth-support.ts:21, e1689c3dec7c)
  • Patch and regression coverage: The PR handles known status codes first, preserves explicit keyword-based OAuth signals ahead of offline patterns, and defers only a standalone numeric 401 until after offline detection. Its tests cover embedded digits, conflicting known statuses, OAuth keyword codes, and the chosen ambiguous bare-401 precedence. (src/error-classifier.ts:62, c3a43f403a92)
  • Previous review finding resolved: The original word-boundary change would have stopped recognizing underscore-delimited OAuth error codes; the final head keeps unauthorized and invalid_token as substring signals and adds regression coverage for unauthorized_client and invalid_token_hint. (tests/error-classifier.test.ts:178, c3a43f403a92)
  • Feature history and ownership: Classifier history includes earlier fixes for non-auth status precedence and error.code handling, followed by a recent current-main refactor of the same area. This supports routing the remaining precedence decision to the established classifier/runtime contributors. (src/error-classifier.ts:53, 69c98e88127c)
  • Still absent from current main: The PR head is four commits beyond its merge base, while current main has independent commits after that base; current main still contains the raw token matcher. The requested behavior is not already implemented on the default branch. (src/error-classifier.ts:125, e1689c3dec7c)

Likely related people:

  • Peter Steinberger: Most recent current-main history for src/error-classifier.ts is the runtime/OAuth/CLI modularization commit, and current classifier lines are release-maintained under this area. (role: recent area contributor; confidence: high; commits: 69c98e88127c, f20febe322b6; files: src/error-classifier.ts, src/runtime-oauth-support.ts)
  • yukaibo.me: Added classifier support for HTTP error code properties, directly related to this PR’s requirement that known status codes decide before message heuristics. (role: introduced status-code handling; confidence: high; commits: cc06993494a3; files: src/error-classifier.ts, tests/error-classifier.test.ts)
  • Casey Gollan: Authored the earlier fix preventing HTTP 405 from being classified as auth, which is the closest historical precedence work. (role: prior classifier precedence contributor; confidence: medium; commits: 67785b95ebe3; files: src/error-classifier.ts, tests/error-classifier.test.ts)
  • Sebastian B Otaegui: Worked on preserving OAuth-disable behavior across headless runtime paths, adjacent to the downstream OAuth-promotion behavior affected here. (role: OAuth runtime compatibility contributor; confidence: medium; commits: 3e27b64021c9; files: src/runtime-oauth-support.ts, src/runtime/transport.ts)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (9 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-01T13:26:08.068Z sha 9a0a9f7 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T19:16:17.852Z sha 9a0a9f7 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T19:32:57.399Z sha c3a43f4 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-01T23:08:17.099Z sha c3a43f4 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-02T00:17:01.334Z sha c3a43f4 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-02T09:40:16.796Z sha c3a43f4 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-02T11:53:19.722Z sha c3a43f4 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-02T14:30:20.236Z sha c3a43f4 :: needs maintainer review before merge. :: none

The digit-only lookarounds still matched 401 inside identifiers built from
letters or underscores, so a status-less message such as "request abc401def
failed" or "request_401_id failed" was classified as auth and could promote an
HTTP server definition to OAuth. Widen the boundary to alphanumerics and
underscore; the OAuth keyword codes stay substring matches so unauthorized_client
and invalid_token_hint keep working.
@Yigtwxx

Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the remaining finding in 9a0a9f7.

The lookarounds only excluded neighbouring digits, so a 401 sitting inside a letter- or underscore-delimited identifier still reached the auth branch:

/(?<!\d)401(?!\d)/.test('request abc401def failed')  // true
/(?<!\d)401(?!\d)/.test('request_401_id failed')     // true

src/error-classifier.ts:18 now uses an alphanumeric/underscore boundary. The OAuth keyword codes stay substring matches, so unauthorized_client and invalid_token_hint are unaffected:

const AUTH_TOKEN_PATTERNS = [/(?<![0-9a-z_])401(?![0-9a-z_])/i, /unauthorized/, /invalid_token/, /forbidden/];

Proof the new cases are load-bearing

Two regression cases cover the reported forms. Restoring only the previous pattern and rerunning fails exactly those two and nothing else, which also shows the tighter boundary does not reclassify any existing case:

$ vitest run tests/error-classifier.test.ts     # old pattern restored
 x does not treat a letter-embedded 401 as auth
 x does not treat an underscore-delimited 401 as auth
   Tests  2 failed | 26 passed (28)

$ vitest run tests/error-classifier.test.ts     # 9a0a9f7
   Tests  28 passed (28)

Verification on 9a0a9f7

oxfmt --check .                                   ok
oxlint --type-aware --deny-warnings --max-warnings=0   ok
tsc --project tsconfig.json --noEmit              ok
tsc -p tsconfig.build.json                        ok
node scripts/test-runner.js                       Test Files 128 passed | 3 skipped (131)
                                                  Tests      810 passed | 48 skipped (858)

The branch is 2 ahead / 0 behind origin/main.

@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 1, 2026
A rejected token is often reported alongside transport wording, such as an
expired-token payload whose description reads "Access token expired;
connection timed out". Placing the offline patterns ahead of every auth
signal therefore narrowed OAuth promotion well beyond the embedded-digit
case this branch targets.

Keyword signals (unauthorized, invalid_token, forbidden) are unambiguous,
so they now run before the offline check. Only the bare 401 numeral, which
is genuinely ambiguous in ports, durations, and request ids, stays
subordinate to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnrQ4wMJsicJUpQsarAHkw
@Yigtwxx

Yigtwxx commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Closed the P1 merge risk in code rather than asking for it to be accepted, in c3a43f4.

The flagged tradeoff was that ordering OFFLINE_PATTERNS ahead of every auth signal narrows OAuth promotion for status-less messages that read as both transport and auth. That narrowing was wider than this branch needs, because a rejected token is routinely reported alongside transport wording:

analyzeConnectionError(new Error('{"error":"invalid_token","error_description":"Access token expired; connection timed out"}'))
// before c3a43f4 -> { kind: 'offline' }   // "timed out" wins, no OAuth promotion
analyzeConnectionError(new Error('SSE stream disconnected: Unauthorized, connection closed'))
// before c3a43f4 -> { kind: 'offline' }   // "connection closed" wins

Neither case involves an embedded digit, so neither is what this PR set out to fix.

The split

Keywords are unambiguous auth signals; the bare 401 numeral is not. Only the numeral belongs behind the transport check:

const KEYWORD_AUTH_PATTERNS = [/unauthorized/, /invalid_token/, /forbidden/];
const NUMERIC_AUTH_PATTERNS = [/(?<![0-9a-z_])401(?![0-9a-z_])/i];
status code (401/403 -> auth, other 4xx/5xx -> http)
  -> KEYWORD_AUTH_PATTERNS
  -> OFFLINE_PATTERNS
  -> NUMERIC_AUTH_PATTERNS

So the compatibility change is now confined to messages whose only auth signal is a bare 401 that also carry a transport signal — which is exactly the reported defect and nothing else.

Proof the new cases are load-bearing

Three regression cases were added. Run against the previous ordering, exactly two fail and nothing else moves:

$ vitest run tests/error-classifier.test.ts     # ordering from 9a0a9f7
 x classifies an expired-token payload as auth even when it mentions a timeout   -> 'offline'
 x classifies an unauthorized stream disconnect as auth rather than offline      -> 'offline'
   Tests  2 failed | 29 passed (31)

$ vitest run tests/error-classifier.test.ts     # c3a43f4
   Tests  31 passed (31)

The third case (fetch failed: connect ECONNREFUSED 127.0.0.1:401 stays offline) is green before and after; it pins the bare numeral below the transport check so this reordering cannot drift back into the original bug.

Verification on c3a43f4

oxfmt --check .                                        ok
oxlint --type-aware --deny-warnings --max-warnings=0   ok
tsc --project tsconfig.json --noEmit                   ok
tsc -p tsconfig.build.json                             ok
node scripts/test-runner.js       Test Files 128 passed | 3 skipped (131)
                                  Tests      813 passed | 48 skipped (861)

810 -> 813 is exactly the three added cases. The branch is 4 ahead / 0 behind origin/main.

One environment caveat: this was verified on Node v22.20.0 while the package requires >=24, so it is not a byte-for-byte match with CI's runtime.

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Aug 1, 2026
@Yigtwxx

Yigtwxx commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Clearing the runtime caveat from my previous comment: c3a43f4 is now verified on Node 24, matching CI's node-version: 24.

Re-ran the same commands on a clean tree at c3a43f4 with Node v24.18.1 (latest 24.x) and pnpm 10.33.2, Windows 11:

node -v                                                v24.18.1
oxfmt --check .                                        ok (352 files)
oxlint --type-aware --deny-warnings --max-warnings=0   ok
tsc --project tsconfig.json --noEmit                   ok
tsc -p tsconfig.build.json                             ok

vitest run tests/error-classifier.test.ts
                                  Tests      31 passed (31)

node scripts/test-runner.js
                                  Test Files 128 passed | 3 skipped (131)
                                  Tests      813 passed | 48 skipped (861)
                                  exit code  0

Same counts as the Node 22 run, so nothing was runtime-dependent. Two notes on top of that:

  • The DOMException: ABORT_ERR teardown noise I reported earlier from tests/runtime-integration.test.ts does not surface on Node 24, and the full suite exits 0 here — so the non-zero local exit I mentioned was a Node 22 artifact, not something this branch introduces.
  • No source changes; the working tree is identical to c3a43f4 and git status is clean after the run.

No new findings on my side. The only open item remains the maintainer decision on the bare-401-plus-transport-signal precedence.

@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Aug 2, 2026
@steipete
steipete merged commit 223c512 into openclaw:main Aug 2, 2026
10 checks passed
@steipete

steipete commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Landed as 223c512.

Maintainer decision on the open precedence question: accepted as implemented. For a status-less message carrying both an offline phrase and a standalone numeric 401, classifying as offline is correct — a bare numeral inside transport wording is far more often a port, duration, or id than a status, and the unambiguous keyword signals (unauthorized, invalid_token, forbidden) still outrank offline patterns, so genuine token rejections wrapped in transport wording keep promoting to auth.

Verification at exact head c3a43f4 before merge (Node 24.18.0 / pnpm 10.33.2, macOS):

  • pnpm exec vitest run tests/error-classifier.test.ts — 31 passed.
  • Full suite — 858 passed / 3 skipped.
  • Live probe against the production analyzeConnectionError: ECONNREFUSED 127.0.0.1:14012 → offline; timed out after 4010ms → offline; HTTP 401 Unauthorized → auth; invalid_token + "timed out" → auth; host401.example.com ENOTFOUND → offline; offline phrase + standalone 401 → offline; code: 404 with "unauthorized" text → http.
  • Codex (ClawSweeper) review on this exact head found no actionable patch defect; the sole remaining item was this maintainer-intent call.

Changelog entry (maintainer-owned) follows on main. Thanks @Yigtwxx — exemplary review turnaround, every finding closed in code with before/after proof.

steipete added a commit that referenced this pull request Aug 2, 2026
…ifier 401 fix (#248)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Yigtwxx
Yigtwxx deleted the fix/error-classifier-transport-vs-auth branch August 2, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 auth-provider 🚨 Merging this PR could break OAuth, tokens, provider routing, model choice, or credentials. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants