Skip to content

fix: a 401 fails over, unclaimed control paths stay local, and eight smaller gaps from the issue tracker - #439

Merged
MagicalTux merged 5 commits into
masterfrom
fix/issue-batch
Sep 20, 2026
Merged

MagicalTux merged 5 commits into
masterfrom
fix/issue-batch

Conversation

@MagicalTux

@MagicalTux MagicalTux commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

Closes #412
Closes #413
Closes #414
Closes #420
Closes #421
Closes #422
Closes #423
Closes #424
Closes #425
Closes #426

Static checks only on this machine: eslint clean, tsc clean, strict ratchet nine below base. The tests run in CI.

🤖 Generated with Claude Code

Added after #419 landed: --bucket on a route is validated against the weekly gating keys (#424), an array switchThreshold reads as the default with one line saying so (#425), the TUI and the status renderer import the probe ceiling and the route colours from config-ops.js (#426), and the MCP route's Host check uses the bound address like the others (#423).

MagicalTux and others added 2 commits September 20, 2026 08:37
…maller gaps from the issue tracker

- #412: a 401 had no handler unless the account carried a refresh token, so it
  was relayed to the client and the account stayed in rotation (4,124 in a row
  in the report). It now fails over like a 403, and an account nothing here can
  repair — an API key, an OAuth account with no refresh token, or a freshly
  refreshed token rejected again — leaves rotation with one log line.
- #420: a path under /teamclaude/ that no control route claims (a typo, or the
  wrong verb) is answered 404 here instead of being forwarded upstream under a
  fleet credential.
- #423: the DNS-rebinding Host check is given the address actually bound, so a
  server bound through TEAMCLAUDE_HOST accepts a caller naming that address.
- #421: sx.org calls carry a 15s timeout and a reply size cap, so a stalled
  api.sx.org cannot hang a reload for good.
- #422: a reload that lands while the TUI is removing an account no longer
  re-adds it from the not-yet-rewritten file; both adopt loops read the
  recorded removals.
- #413: startup output is held while a TUI is coming and replayed into the
  activity pane and its log file once the TUI owns the console; a process that
  exits first gets the held lines on stderr.
- #414: console.warn joins log and error in the TUI's redirect (the constructor
  half of this issue landed in #432).

Closes #412
Closes #413
Closes #414
Closes #420
Closes #421
Closes #422
Closes #423

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ee config-rule issues

The MCP route that landed meanwhile asked its Host check with proxy.host alone,
the same gap #423 closes elsewhere, so keylessMcpRefusal takes the bound host.
With the CLI rules now in config-ops.js: a route bucket is validated against
the weekly gating keys, since a typo stored verbatim silently disabled weekly
gating for that route (#424); an array switchThreshold is read as the default
with one line saying so, instead of being spread into buckets named 0, 1, ...
(#425); and the TUI and the status renderer import the probe ceiling and the
route colours from config-ops.js rather than re-declaring them (#426).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MagicalTux MagicalTux changed the title fix: a 401 fails over, unclaimed control paths stay local, and five smaller gaps from the issue tracker fix: a 401 fails over, unclaimed control paths stay local, and eight smaller gaps from the issue tracker Sep 20, 2026
MagicalTux and others added 3 commits September 20, 2026 09:18
…sts that pinned a relayed 401 follow

CI showed four existing tests encoding the behaviour this change replaces: a
401 relayed to the client, and a non-GET on /teamclaude/dashboard forwarded
upstream. They now assert the proxy's own error and the local 404.

Reading them also narrowed the rule. A second 401 on an account that holds a
refresh token can be stale news: the forced refresh is suppressed for a short
floor after a successful one, so the retry may go out on the same token. That
account now only fails over; an API key or an OAuth account with no refresh
token, which nothing here can repair, is what leaves rotation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MagicalTux
MagicalTux merged commit 72c3943 into master Sep 20, 2026
5 checks passed
@MagicalTux
MagicalTux deleted the fix/issue-batch branch September 20, 2026 00:49
@MagicalTux MagicalTux mentioned this pull request Sep 20, 2026
MagicalTux added a commit that referenced this pull request Sep 20, 2026
Thirty-six commits since 1.1.20. Several change what a client sees on a
failure, so read the first section before upgrading a shared deployment.

Behaviour changes
  #439 a 401 from upstream is no longer relayed to the client: the request
       fails over like a 403, and an API-key account or an OAuth account with
       no refresh token leaves rotation (it used to be picked again on every
       request). With nothing left the client gets the proxy's own error
  #439 a path under /teamclaude/ that no control route claims — a typo, or the
       wrong verb — answers 404 locally instead of being forwarded upstream
       under a fleet credential
  #429 the synthetic 429's retry-after is the real reset of the windows
       blocking the request's candidate accounts, not a flat 60s, and the
       message counts only those candidates; #408 names accounts that need a
       re-login instead of calling them "at quota"
  #438 session pins are per conversation (session id plus a digest of the
       first message), so a session's subagents spread across accounts.
       `sessions.items[].id` in status is the composite key, load is counted
       per conversation, and a persisted concurrency cap re-learns
  #378 a `thread: continue` bound for a per-account third-party upstream is
       refused with the 400 Anthropic gives, so the client resends the whole
       conversation; `messageThreads: true` opts a relay out
  #434 a 429 whose x-codex-* headers show a spent account-wide window holds
       the account like an Anthropic rejection; a spent model-scoped bucket
       only moves the request
  #437 a Codex response head is awaited for five minutes (Anthropic unchanged)
  #411 idle keep-alive connections are held 120s on both listeners
  #389 with session distribution on, requests carrying no session id rotate on
       a cursor of their own instead of all resting on the current account
  #405 `defaultClientMode` ("mitm" | "base-url") sets what `run` and `env` do
       without a flag; `--mitm` / `--no-mitm` decide per launch, and in
       base-URL mode `env` unsets a stale proxy export naming this proxy
  #439 route `--bucket` is validated; an array `switchThreshold` reads as the
       default with one line saying so

Features
  #419 an MCP management endpoint at POST /teamclaude/mcp, off unless
       `proxy.mcp` is "read" or "full"; a named client key is read-only even
       in full mode, and with no proxy key it serves only this machine
  #428 per-account `switchThreshold`, a number or a per-bucket table
  #406 a Claude+Codex pool is drawn as two panes on a wide terminal, each with
       its own current marker; #392 names the provider in a mixed list; #418
       lets the operator arrange the list (`displayOrder`); #376 draws
       loopback-served accounts last; #435 shows the percentage beside a bar's
       countdown; #394 shows the running version in the header
  #430 free Codex rate-limit reset credits in status, the TUI and the dashboard
  #385 `proxy.terminalOnly` tunnels chatgpt.com so ChatGPT Desktop stays out

Fixes
  #404 a TUI paint can no longer block the proxy (stdout non-blocking, frames
       dropped while the terminal is behind); #410 a dead terminal no longer
       takes the proxy with it, and SIGHUP shuts down cleanly
  #433 token usage is booked from Codex Responses streams
  #386 #387 #388 the Codex five-hour window is read from the model-scoped
       family and the usage probe, and extra limits are named from their entries
  #431 a headerless 429 that follows the request is retried once
  #432 #439 startup and collaborator log lines reach the TUI's activity pane
       and log file instead of the covered terminal
  #415 #403 #439 reload mirrors `priority`, `disabled`, `stripRequestFields`
       onto the config entry, and a reload during a removal does not re-add it
  #439 the Host check uses the address actually bound; sx.org calls time out
  #381 the dashboard polls status before asking for a key
  #403 session outcome accounting classifies the decoded path; account names in
       daemon log lines are sanitised

Tooling
  #371 #372 #373 `npm run typecheck` (tsc over the JS sources) in CI, with a
       strict-mode ratchet: per-file strict diagnostics may not grow past the
       pre-merge commit (2006 at introduction, 1735 now)
  #401 docker workflow actions bumped

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
MagicalTux added a commit that referenced this pull request Sep 29, 2026
…enching it for good (#473) (#481)

#439 stopped the 401 loop of #412 by putting an account in `error` on its
first 401. For an API key nothing ever took it back out: the revalidation
probe skips `error` and nothing else re-checks a key. One 401 from a gateway
whose own upstream was unreachable benched a working fallback account for 22
hours, until it was toggled by hand.

An API-key account is now held out of rotation for a cooldown and retried
after it. The hold lengthens while the key keeps being rejected with no
success in between: 1 min, 5 min, 15 min, then 1 h for every one after that.
Any non-error response from the account resets the sequence. It never
escalates to a permanent `error`, so a key that really is revoked costs one
failed-over request per hour and a gateway that recovers comes back by itself.

While held the account is unavailable to selection, to the revalidation probe
and to the soonest-reset fallback, so the loop stays fixed. 401s from requests
already in flight when the hold was armed do not escalate it. A reload that
brings a different key, or re-enabling the account, lifts the hold.

The hold is its own field beside the entitlement and routing cooldowns rather
than the 429 hold, which any non-429 response clears and the probe reopens
after a minute. Status carries `credentialRejectedUntil` and the reason
`credential`; the renderer and the dashboard explain it.

OAuth accounts are unchanged.

Closes #473

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
lifrary added a commit to lifrary/teamclaude that referenced this pull request Sep 29, 2026
Takes KarpelesLab/teamclaude up to ba01b4f while keeping the fork's routing
safeguards (403 cooldown without parking, send-failure fail-over, outbound
content-length, the single predispatch wait budget, cappedMessage, the
overload slot release, the dead-refresh-token guard and refresh retry, the
session home wait from d7810f1, and unranked-priority semantics).

Adopted from upstream, among others: per-conversation session pins (KarpelesLab#438),
401 fail-over without parking (KarpelesLab#439, KarpelesLab#473), real quota-reset retry-after and
candidate counts (KarpelesLab#429, KarpelesLab#408), a headerless 429 retry (KarpelesLab#431), fail-over on a
failing 200 stream (KarpelesLab#470), per-account egress proxies (KarpelesLab#441), extra-usage
fallback (KarpelesLab#427), maxSpend (KarpelesLab#466), stripOverageHeaders (KarpelesLab#478), keep-alive that
outlives the client pool (KarpelesLab#411), a dead terminal not killing the proxy
(KarpelesLab#410), console resolution per call (KarpelesLab#432), config reload and sync fixes
(KarpelesLab#465, KarpelesLab#415), and the dashboard, TUI and Codex work since 1.1.20.

Integration fixes the merge needed beyond conflict hunks:
- upstream request-path code that referenced upstream-only locals (sx,
  route, ctx.tried) rewritten for the fork's forwardRequest
- resolveSwitchThreshold was declared twice after a clean auto-merge
- the fork's warm-up probe now forwards a routed account through its own
  proxy instead of sending its credential direct (new regression test)
- a routing failure during a token refresh arms the routing hold instead of
  parking the account; a pinned request may use an account on routing hold
- the headerless-429 branch no longer writes after headers were sent
- canonical-state allowlists widened for upstream's new quota fields; saves
  use exportState()
- the fork's home wait keys on the conversation pin like selection does

Tests adapted where the fork deliberately differs (warm-up probe on by
default, account-anchored TUI cursor, coordinator-built Prober and Warmer,
unranked priority, per-conversation pin keys, fail-over-only 401, the wait
budget instead of inline waits), each with an in-file note. Full suite
2919/2919, lint, typecheck and the strict ratchet (1411 vs 1465) pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment