Merged with admin on local + CI evidence: `tests/unit/i18n-catalogs-no-duplicate-keys.test.ts` red on the tip (59 catalogs) → `pass 3 / fail 0` here; **API Route Typecheck passes on this PR** (it fails on every PR based on the current tip because of the duplicated `ERROR_TYPE_CONTRACT` import this removes); CodeQL, semgrep, Vitest fast-path, Docs gates, Change Classification pass. The remaining red checks (Fast Quality Gates, Merge integrity, Unit Tests fast-path 1/2/4) are the same inherited tip reds every PR on release/v3.8.51 shows right now — #13747 sweeps them. Both removed lines were byte-identical duplicates; nothing parsed or typed changes.
Behind the new `MISTRAL_AMBIGUOUS_401_SOFT_LOCKOUT` flag (default off), a bare Mistral 401 (`{"detail":"Unauthorized"}`, identical for a revoked key and an exhausted quota) gets a retryable cooldown instead of parking the connection as `expired`; after three soft strikes within an hour the next bare 401 parks it, so revocation still converges.
Maintainer rework before merge (kept the idea, no default behavior change):
- The predicate is shared with the connection-test module instead of duplicated; the squeezed 139-char line that dodged the file-size gate is formatted normally and the growth is rebaselined honestly with an annotation.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `OPENCODE_TRANSIENT_FAILOVER_BACKOFF` flag (default off), after two consecutive transient upstream failures the opencode rotation pauses before each later account (1.5s, 3s, 6s, capped at 10s per request) instead of hammering the upstream.
Maintainer rework before merge (kept the idea, no default behavior change):
- The pause honors the client abort signal (no dispatch after a disconnect), the failed attempt's body is cancelled before sleeping, `transientRetryDelayMs` now uses its arguments, and the sleep is injectable so the tests run without real timers.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `OPENCODE_USER_BLOCKED_ROTATION` flag (default off), a 403 or 451 carrying `user_blocked` on a proxied opencode account rotates at most once to the next account instead of being returned as-is.
Maintainer rework before merge (kept the idea, no default behavior change):
- 403 and 451 are handled by one predicate (the original returned 451 without rotation), the refused account gets a cooldown and joins the tried-set, and the response body of the attempt rotated away from is cancelled.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `OPENCODE_RESPONSES_STALL_ROTATION` flag (default off): a streamed Responses reply with no first body byte within `RESPONSES_FIRST_BYTE_TIMEOUT_MS` (15s) cools the account and rotates once; a second stall fails fast instead of waiting the 80s readiness timeout.
Maintainer rework before merge (kept the idea, no default behavior change):
- The TLS first-byte watchdog from #12656 is restored byte for byte (the PR had changed its pump, timer and cancel); the stall guard lives in its own module.
- Proxy-less multi-account setups now rotate the same way as proxied ones (the original threw for them), a client abort during the wait rethrows instead of rotating, and the env var is documented as flag-only.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `PROXY_HEALTH_BLOCKED_RESETS_STREAK` flag (default off), a probe the target refuses (401/403/429) resets the proxy's consecutive-failure streak, so a proxy that clearly relays is not marked dead by spaced-out real failures.
Maintainer rework before merge (kept the idea, no default behavior change):
- The original reversed the deliberate #10654 policy for everyone; with the flag off a refusal stays neutral, and the existing assertions are restored. The stale JSDoc and the wrong "any relayed response resets" comment are fixed (5xx stays inconclusive).
- The source-grep test became a real sweep test: a local relay answering 403 drives fail → blocked → fail with auto-disable, in both flag modes.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `PROXY_POOL_EGRESS_OBSERVATION` flag (default off): a line under each proxy pool showing how many distinct egress IPs actually served it over 24h, backed by `GET /api/settings/proxies/pool/egress-observation`.
Maintainer rework before merge (kept the idea, no default behavior change):
- The route validates its query with Zod (unknown `scope` → 400 instead of silently `global`), error bodies go through `errorResponse()`, the OpenAPI entry documents security, parameters and responses, and the three UI strings exist in every locale.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind `PROXY_SKIP_RECENTLY_FAILED` (from #13578): a provider 429 received through a pool member sets that member aside and a 2xx clears it, for opencode providers.
Maintainer rework before merge (kept the idea, no default behavior change):
- `noteProxyOutcome` ran inside the fire-and-forget `safeLogEvents` after awaited dynamic imports, so a concurrent request could still pick the member; it now runs first, synchronously, before any `await`.
- The duplicate `177_proxy_logs_upstream_status.sql` the stack still carried alongside the renamed 179 was removed; the regression test the PR body named exists as `pool-ip-quota-429-path.test.ts`.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
`proxy_logs` records `upstream_status`, the HTTP status the provider actually returned through the proxy, instead of only success/timeout/error.
Maintainer rework before merge (kept the idea, no default behavior change):
- The migration collided with the tip (177 was already taken): renumbered to `179_proxy_logs_upstream_status.sql`, the runner's already-applied check moved to `case "179"` (the old `"177"` would have skipped the tip's own 177), migration count bumped to 176 in README, AGENTS.md, llm.txt and its mirrors (operator-approved).
- A new test runs the real migration runner on the real SQL files and fails with the old case number.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `PROXY_SKIP_RECENTLY_FAILED` flag (default off), pool rotation and the opencode account rotation remember a proxy that just failed (refused probe or 429) and skip it for a doubling cooldown instead of re-serving it immediately.
Maintainer rework before merge (kept the idea, no default behavior change):
- The original was on by default and re-queried the DB on every request while a member was set aside; selection now caches a refusal sequence number and re-runs the cascade once per set-aside event.
- `src/lib/db` no longer imports the heavy dispatcher for key normalization (a parity test guarantees the same key as `proxyConfigToUrl()`); `.env.example` and `ENVIRONMENT.md` document the default as false.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
GHSA-7pq4-8pvv-rx7r (critical). Every link of the reported chain held on the
release tip:
1. First boot without JWT_SECRET generates one and writes it in cleartext to
$DATA_DIR/server.env.
2. With no password configured, isAuthRequired() returned false for
POST /api/settings/require-login unconditionally — before the loopback
check — so any network peer could switch requireLogin off.
3. With requireLogin off, POST /api/settings/obsidian/webdav accepted an
arbitrary vault root and echoed freshly minted Basic credentials.
4. The WebDAV file service is served by the custom Node layer before Next.js,
outside the authz pipeline.
5. Pointing it at DATA_DIR reads server.env, and JWT_SECRET forges an
`{"authenticated":true}` admin session.
A second, worse problem surfaced while verifying: isLoopbackRequest() decided
"loopback" from nextUrl.hostname / the Host header, which the client controls.
`Host: localhost` from a remote address made the whole fresh-install bootstrap
reachable, not just the write path.
Three cuts, plus the root cause:
- isLoopbackRequest() now reads the trusted peer: the token-stamped real TCP
peer the custom server writes (peerStamp), then the pipeline's own locality
verdict once a stamp token exists, then a real socket peer. The bootstrap
write path honours the same constraint instead of returning false, and
managementPolicy hands down the peerContext verdict explicitly, because at
policy time the original request still carries client-supplied headers.
- Host is consulted only when the process has no stamp token at all — no
stamping server in front, which in practice means route handlers invoked
directly by the unit-test harness. Every supported runtime (run-next dev and
start, standalone-server-ws for Docker, the npm CLI and Electron) calls
ensurePeerStampToken() at boot, so there a signal-less request fails closed.
Without this fallback ~340 route tests that call handlers with
`new Request("http://localhost/…")` turned into 401s.
- /api/settings/obsidian joins ALWAYS_PROTECTED_API_PATHS: issuing and rotating
reusable WebDAV credentials is credential export, the same rationale as the
GHSA-62vw entry for the password reveal.
- enableObsidianVaultSync() refuses a vault that is, sits inside, or contains
DATA_DIR, comparing realpath-resolved paths so a symlink cannot dodge it.
Tests are red-first: remote stamped peer → auth required on the bootstrap
write; Host: localhost plus a forged locality header from a non-loopback
stamped peer → 401 through the full pipeline; the local operator keeps the
first-password flow; obsidian inventory and DATA_DIR overlap cases.
Behind `STREAM_EARLY_EOF_SIBLING_FAILOVER_ENABLED` (default off): after the bounded same-connection retry is spent, a stream that closed early fails over exactly once to a sibling connection.
Maintainer rework before merge (kept the idea, no default behavior change):
- The PR's own failover test was red on its head: the `/v1/chat/completions` route's early-stream keepalive dropped the `X-OmniRoute-Selected-Connection-Id` header on the first cold request. Tests now drive `handleChat()` directly; the assertion was kept.
- "One hop" was one hop per connection (a 3-connection pool made 4 dispatches); it is now a single sibling hop per request, and when the pool runs out the original `STREAM_EARLY_EOF` 502 is returned instead of a generic `bad_gateway`, so combo-level detection keeps working. The source-regex timeout test became a behavioral one; flag description in all 59 locales.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Estimated token usage is now visible to operators: usage a provider marks as `estimated` carries an internal marker through extraction and the call log records `_omniroute.usageEstimated: true` on the logged response.
Maintainer rework before merge (kept the idea, no default behavior change):
- Billing is unchanged: the original skipped cost/budget/quota-share for estimated usage, which would have let streams without upstream usage and eight web executors spend $0 against API-key budgets; that part is reverted and no opt-in flag was added because it cannot be made budget-safe.
- Both open-sse TS2345 errors, the client-visible `estimated_prompt_tokens` field and the `as unknown as` casts are gone; four real `handleChatCore` cases assert the marker and unchanged spend.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Recovery traces for mid-stream continuation: one `onContinueOutcome` hook reports suffix stitched, overlap rejected, terminal, empty, no-stream and refused (with reason), logged through `chatCore` at debug level; warn is reserved for the cases where recovery gives up.
Maintainer rework before merge (kept the idea, no default behavior change):
- The original logged a warn-level latch line on every streamed tool call; nominal and tool-call streams are now silent, and the existing `mid-stream continuation attempt N/4` line keeps its format.
- `chatCore.ts` ends 3 lines shorter than the tip, so the baseline bump the PR carried was removed; the wiring is tested through a real `handleChatCore` continuation.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind `STREAM_RECOVERY_TOOLCALL_ORDER_FIX` (default off), mid-stream continuation becomes tool-call safe: any tool call seen in the stream — in flight or finished — blocks a continuation, and an empty continuation stops after one attempt.
Maintainer rework before merge (kept the idea, no default behavior change):
- The empty-continuation short-circuit also ran with the flag off; it is now gated, so the flag-off path uses the whole budget exactly as before (regression test added).
- The latch re-arm that let a continuation fire after a completed `finish_reason: tool_calls` is gone; index-less tool calls on multi-choice payloads are now blocked too; ~150 lines of dead trace plumbing removed.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Stale provider-pin (`clearStaleLKGP`) clears are no longer silent: the fire-and-forget promise carries a `.catch` that warns with combo, comboId and executionKey, and a `check:routing-error-guard` npm script keeps the inventory of swallowed catches in the routing hot path from growing.
Maintainer rework before merge (kept the idea, no default behavior change):
- The awaited DB writes in the fallback loop were reverted (they added latency and SQLite lock exposure on every skip); the clear stays non-blocking.
- The guard keys its allowlist by file + normalized catch body instead of line numbers (the PR's version broke on any edit) and is wired as an npm script only, not in CI; the unused stats counters were dropped.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `PROTECTED_PRIORITY_INFRA_502_ENABLED` flag (default off), protected-priority combo stops caused by provably non-quota infrastructure (provider circuit open, predictive-TTFT latency) surface as 502 instead of a quota-looking 503.
Maintainer rework before merge (kept the idea, no default behavior change):
- The original branch made 502 the default for every stop, including model lockouts and cooldowns, and removed the #8133/#1731 provider-wide skip for 401/5xx without a connection id; both are restored with their regression tests untouched.
- Nineteen cases cover eight gate causes plus predictive latency, flag off and on.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Behind the new `RETRY_AFTER_PROVENANCE_ENABLED` flag (default off): `unavailableResponse` omits the synthetic `Retry-After: 1` when there is no real retry signal, marks `retry_after_provenance` on its bodies, and both combo drain readers parse prose retry hints from plain-text bodies too. With the flag off, headers and bodies are exactly as before.
Maintainer rework before merge (kept the idea, no default behavior change):
- A past `Retry-After` date is no longer labelled as an upstream signal with `Retry-After: 1`; non-JSON bodies (HTML 502 pages) log at debug instead of warning on every request.
- The provenance claim is narrowed to responses built by `unavailableResponse`, documented in the flag row.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Fixes the DST-gap bug in `nextDailyResetAtMs`: a reset hour that does not exist on the transition day landed one hour early (New York 02:00 came out as 01:00; Havana/Santiago midnight as 23:00 the day before). The walk across the gap is bounded to one day and uses a cached formatter.
Maintainer rework before merge (kept the idea, no default behavior change):
- Dropped the 24h clamp in `getMsUntilTomorrow` (on a 25h fall-back day 24.5h is the correct wait; clamping expired the lock 30 minutes early) and the unreachable `ms <= 0` branch, with their tests; characterization tests pin ordinary and fall-back days.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Daily-quota lockouts on the non-TPD path honor the provider's configured daily-reset clock (`dailyQuotaResetTimezone`/hour) in combo routing instead of the host's midnight.
Maintainer rework before merge (kept the idea, no default behavior change):
- The process-lifetime clock cache is gone: the clock is resolved on each failure through the already-TTL'd `getCachedProviderNodes`, so a timezone change takes effect without a restart and a DB error is never cached as `{}` forever.
- Round-robin combos are threaded too (the PR left them out); an option on `recordModelLockoutFailure` that could never run was removed; tests prove both call sites pass the configured clock.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Both settings routes use the shared `isSocks5ProxyEnabled()` reader instead of a copied check (identical logic, no behavior change).
Maintainer rework before merge (kept the idea, no default behavior change):
- The source-grep tests were replaced by behavioral tests of both routes across the flag on/off matrix (`GET /api/settings/proxies` reports `socks5Enabled`; `PUT /api/settings/proxy` accepts socks5 or returns 400).
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
The provider-page Free badge can require a free tier the provider actually honors: behind the new `FREE_BADGE_REQUIRES_PROVIDER_FREE_TIER` flag (default off) the display-name "free" heuristic, non-boolean `free` fields and `:free` suffixes on registered providers without a documented free tier no longer light the badge. With the flag off the historical rule is unchanged.
Maintainer rework before merge (kept the idea, no default behavior change):
- `:free` models on free-tier providers and on compatible nodes (OpenRouter-style endpoints) keep the badge in both modes — the original change dropped them.
- Test D derives its provider set from `FREE_MODEL_BUDGETS` instead of a hard-coded allowlist; a vitest render covers both sections with the flag endpoint on, off and erroring.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
GHSA-35fw-cv32-2373 and GHSA-jx89-f37j-pq89 — the same defect class as
/api/acp/agents (GHSA-hf57): a route whose handler chain spawns a host process
was classified Tier 3 MANAGEMENT only, and requireManagementAuth() waives auth
when requireLogin=false. Hard Rules #15/#17 require the LOCAL_ONLY gate, which
runs on the stamped real peer before any auth check.
cli-tools (GHSA-35fw): 14 routes reach
getCliRuntimeStatus() -> locateCommand() -> runProcess("sh", ["-c",
'command -v -- "$1"']) -> spawn(), exactly like their six gated siblings
(forge/grok-build/jcode/qwen/omp/letta-settings):
all-statuses, status, and the claude/cline/codewhale/codex/crush/deepseek-tui/
droid/kilo/openclaw/pi/smelt-settings routes. The advisory counted 13; it
missed /api/cli-tools/detect, which is heavier — detectAllTools() runs
execFile(binary, ["--version"]) and execFile("which") per tool.
skills (GHSA-jx89): POST /api/skills/install stores the request's handlerCode
verbatim as the skill handler with no allowlist, so a value equal to a built-in
name (execute_command / eval_code) aliases the real sandboxed built-in;
POST /api/skills/executions then runs it. The sandbox is a real container, but
the spawn is transitive, which is why the 6A.8 source scan never flagged it.
Entries are exact paths, not a /api/cli-tools/ blanket prefix: apply, backups,
config, guide-settings, hermes-agent-settings, keys, logs, openclaw/auto-order
and codex-profiles do not spawn and remote dashboards use them. All 16 are
mirrored into SPAWN_CAPABLE_PREFIXES (no manage-scope bypass) and added to the
route-guard-membership roots so the gate enforces them from now on.
Functional trade-off, same one already accepted for grok/forge/jcode/qwen: a
dashboard served through a tunnel no longer shows the CLI Tools status badges.
Tests are red-first. Two existing negative controls pointed at routes that turn
out to spawn (/api/cli-tools/all-statuses, /api/skills/install); they now point
at routes that genuinely do not (/api/cli-tools/config, /api/skills/marketplace,
/api/skills/skillssh/install), so the non-over-gating assertions are kept.
GHSA-2jm2-mpx8-6523 and GHSA-m3hp-hq9g-fpmv, one root cause.
`getApiKeyRequestScope()` never rejects: with the default REQUIRE_API_KEY=false
the client-api policy admits both a missing and an invalid bearer as anonymous,
and the scope comes back `{ apiKeyId: null, isSessionAuth: false }`. The
`/v1/files` and `/v1/batches` routes then treated "null" as permissive in two
different ways:
- GHSA-m3hp — the list routes coerced `apiKeyId || undefined`, and the DB layer
reads `undefined` as "no owner filter", so an anonymous or invalid-bearer
caller got every tenant's file and batch metadata, the same unfiltered view as
the operator's dashboard.
- GHSA-2jm2 — the single-record checks were `record.apiKeyId !== null && …`, so
a record with no owner short-circuited to "allowed" for any caller: read,
download raw content, delete, cancel, or use as a batch input. Null-owner
records are common — every dashboard-session upload, and every batch output
file inheriting a session batch's owner, which carries model responses.
`api_key_id` has existed since the table was created (migration 028), so a null
owner is not a legacy row; it is an unattributable write. No doc described it
as shared — API_REFERENCE says files are scoped per key — and batch_api.test.ts
pinned the by-id exposure as expected behaviour.
One rule now, in `_helpers/apiKeyScope.ts`:
- `canAccessOwnedRecord(scope, owner)`: a dashboard session is the instance
operator and may act on any record; an API key acts on its own records only;
a null owner is denied to every non-session caller. Applied to files
GET / DELETE / content, batches GET / DELETE / cancel, and the batch-create
input-file check.
- `resolveListScope(scope)`: an explicit union for list/count reads — scoped to
the presented key (a key wins even alongside a session cookie), instance-wide
only for a session without a key, and 401 otherwise, including for a bearer
that does not resolve to a key. There is no default that widens a read.
This follows the GHSA-wvxc shape already used by the delete-completed sweep.
Behaviour change: the anonymous upload → batch → download flow no longer works
without an API key, because a null owner cannot be attributed.
Subsumes #13683: it moved `scopeCheck` into the shared helper so a session can
cancel any batch — kept, and its test ported — but it also kept null-owner
records open on the premise they predate ownership tracking, which migration
028 contradicts.
Tests are red-first. batch_api's by-id case is flipped to 404 with a negative
assertion; batch-deletion-route-logic now imports the real helper instead of a
local copy that had silently diverged from production; the two integration
tests present a real key, since their subject is limits and rate logging, not
auth.
Co-authored-by: Markus Hartung <mail@hartmark.se>
GHSA-34rg-3pqj-35g9. `fetchRemoteImage()` defaults to
`getProviderOutboundGuard()` — the OPERATOR outbound policy, local-first by
design so self-hosted providers on loopback/LAN keep working. Since #11062 added
the `block-metadata` middle tier, a default install resolves to that mode: the
string check only rejects 169.254/16 and the IMDS hostnames, and the DNS
validation step is skipped entirely (it only runs under `public-only`).
Three sinks feed that default with CALLER input, so a request body could make
the server fetch `http://127.0.0.1:…` or any RFC-1918 host and forward the bytes
upstream:
- imageGeneration.ts `resolveImageSource()` — `image_url`, `mask_url`, message parts
- imageUpscale/shared.ts `resolveUpscaleImageSource()` — 14 body aliases,
`provider_options.*`, message parts (Stability, Topaz)
- visionBridgeHelpers.ts `fetchRemoteImageAsDataUri()` — chat `image_url` parts
inlined into the vision self-call
plus the NanoBanana result-URL download, which is upstream-supplied rather than
OmniRoute-controlled.
Same trust confusion as GHSA-3f8g / GHSA-j7j4 on the search base URL: operator
config and caller input must not share a guard. Each site now passes
`guard: "public-only"` (string check + DNS validation of every answer), matching
the siblings that already did it right — embeddings, the audio bridge and the
AI Horde result download.
`pinDns` is set only on the vision bridge. The other three sites use
`globalThis.fetch`, and connection pinning would swap that for a raw undici
fetch — the same reason the AI Horde site leaves it off. On the vision bridge a
`fetchImpl` is injected, so `pinDns` there validates every DNS answer but cannot
pin the connection; commented in place.
Blind SSRF rather than full read: the bytes go upstream or into the vision
self-call, not back to the caller — but the status oracle and upstream
exfiltration are real.
Tests are red-first — per sink, `http://127.0.0.1:1/x.png` and
`http://192.168.1.50/x.png` are rejected with the injected fetch never called,
and a public host whose DNS resolves to a public IP still downloads.
GHSA-r4q7-7f24-m29p. `CREDENTIAL_PATTERNS` (open-sse/utils/credentialPatterns.ts)
is the single catalog iterated in order by both the opt-in credential-masker
guardrail and the public error sanitizer. It had no entry for Groq (`gsk_`) or
xAI (`xai-`), and only knew the exact 48-char OpenAI `sk-` form.
Measured on the release tip before this change:
| Shape | public sanitizer | guardrail |
|------------------------------|------------------|-----------|
| Groq gsk_ + 52 | LEAK | LEAK |
| xAI xai- + 80 | LEAK | LEAK |
| DeepSeek sk- + 32 hex | redacted | LEAK |
| sk- + 20/36/40/51 (not 48) | redacted | LEAK |
The public path already caught every `sk-` shape through STRONG_CREDENTIAL_TOKEN,
so the advisory's "both layers" framing only holds for gsk_/xai-; for the sk-
family the exposure was the guardrail.
Adds `groq` and `xai` after `anthropic_alt`, and a generic `openai_compatible`
`sk-` fallback as the LAST entry. Ordering matters: both consumers replace as
they iterate, so `openai_proj`, `openai` and `anthropic*` stamp their specific
label first and the fallback only sees shapes nothing else claimed. The
lookbehind mirrors STRONG_CREDENTIAL_TOKEN so `risk-…`-style words do not match.
All three regexes are a fixed prefix plus one bounded character class — linear,
no nested quantifiers.
Tests are red-first: the new guardrail cases (bare / sentence / JSON-body
contexts per shape, plus label-ordering and negative cases) and the catalog
coverage array in error-sensitive-redaction both failed on the tip.
Follow-ups deliberately left out of scope: `tskey-auth-` (Tailscale) was never in
the catalog, and the guardrail does not decode `\uXXXX` escapes the way the
public path does.
Dependabot #214 (GHSA-vwc7-r8mq-g2x9 / CVE-2026-76845, moderate): adm-zip
0.5.9–0.6.0 follows a symlink that already exists inside the extraction root
and writes through it, outside the root. The advisory still reports
`first_patched_version: null`, but 0.6.1 (published after the advisory) is the
fix — `util/utils.js` gains `assertPathSafe`, which walks every path component
below the root with `lstat` and throws on a symlink; `extractAllTo` calls it
before every write. Verified by diffing the two tarballs.
Reach in this repo: adm-zip is pulled only by `onnxruntime-node` (an
optionalDependency, itself pinned by override) and used only in its install
script to unpack the vendor's own runtime binary. No request path touches it.
The override already existed at ^0.6.0 (PR #7732, the previous adm-zip CVE);
this just raises the floor. Lockfile moves 0.6.0 → 0.6.1, nothing else.
The WAL busy counter survives restarts: it is persisted in `key_value` and restored at boot, so the health output no longer resets to zero after every restart.
Maintainer rework before merge (kept the idea, no default behavior change):
- `recordBusy()` no longer writes synchronously on the contended path (with `busy_timeout = 2000` that could block the event loop for up to 2s); it accumulates in memory and `flushBusyTotal()` upserts the delta on a clean passive/TRUNCATE tick or best-effort at stop.
- The boot wiring is tested for real: a child Node process drives `startWalMaintenance` against a real SQLite file (restore at boot, zero writes while busy, one flush at stop, restore after restart).
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Seven routing/quota caches (quality states, account buckets, quota-fetcher/saturation/header caches, learned rate limits) sit behind a shared bounded map with LRU/TTL eviction instead of growing without bound. The learned-limits cap of 200 that the tip declared was never enforced.
Maintainer rework before merge (kept the idea, no default behavior change):
- Eviction logging goes through the project logger, aggregated (first eviction, then one summary line per minute per map) instead of a `console.warn` per eviction.
- `refetch-lazy` and `hard-expire` behaved identically and are collapsed into `ttl`; protected entries (saturated account buckets, evaluator quality scores) are never evicted; caps raised to 2048–4096 so normal deployments never evict, with tests showing 300 learned limits and 600 cached entries all kept.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Proxy credentials containing a literal `%` no longer throw `URIError`: every `decodeURIComponent` on proxy user/password is guarded.
Maintainer rework before merge (kept the idea, no default behavior change):
- HTTP proxies still failed because undici's `ProxyAgent` decodes the credentials itself; the dispatcher now builds undici's `Basic` token with the safe decoder and passes it as `token`, so a literal `%` works there too.
- The three remaining unguarded sites (`mappers.ts`, `proxySubscription/parse.ts`, `subscriptionService.ts`) are guarded; tests run the real `createProxyDispatcher` against a local HTTP CONNECT proxy and a local SOCKS5 server that record what they received.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Fixes the production build break from `node:fs` reaching client bundles (`oauth.ts → cursorAgentCliVersion.ts` through the codebuddy-cn registry) and widens the client-bundle guard so it finds any Node builtin, not just the one that broke.
Maintainer rework before merge (kept the idea, no default behavior change):
- The guard was 11× slower (3.8s → ~40s) because resolved edges were not cached; with resolved edges and per-file verdicts cached it runs in ~4.6s.
- Bare builtins that Next's client build polyfills (`path`, `os`, `crypto`, `buffer`, … from Next's own `resolve.fallback` list) are allowed consistently; `node:` imports are always flagged; a drift test fails if Next stops polyfilling an allowlisted name.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Search stats and recent searches stop surfacing ghost rows: NULL and `-` providers are always hidden, and, behind the new `SEARCH_STATS_HIDE_DELETED_CONNECTIONS` flag (default off), traffic of a keyed provider whose connection was deleted is hidden too. Totals use the same guard as the per-provider rows, so they always agree.
Maintainer rework before merge (kept the idea, no default behavior change):
- Keyless providers from the search registry (`duckduckgo-free`, `searxng-search`, anonymous `context7`) and providers served through a credential fallback (`perplexity-search` on a `perplexity` key) stay visible in both modes — the original filter dropped them because they have no `provider_connections` row.
- Tests use real registry ids and cover flag off (historical stats) and flag on, including the analytics route; #13281's changelog fragment restored.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
The labels wvxc-route-401/wvxc-route-500 sat right after a key*.id argument and cleared the gitleaks generic-api-key length and entropy floors; renamed to route401/route500 with a docblock stating the measured rule. Test-only; .gitleaks.toml untouched. Reviewed by 3 rounds of /omni-code-review (37 agents).
A write-boundary guard for `error_type`: `toStoredErrorType()` validates what `saveCallLog` stores against the vocabulary (Zod enum built once), as defense in depth on top of #13281.
Maintainer rework before merge (kept the idea, no default behavior change):
- Dropped the redundant `SCHEMA_SQL` column (migration 158 already creates it) and the string-absence "migration 177" test; the real `PRAGMA table_info` test is back.
- Restored #13281's changelog fragment, which this branch had deleted, and renamed this PR's own fragment to `13441-error-type-write-guard.md`.
- The guard is now exercised for real: the exported function is tested with out-of-vocabulary values and an end-to-end drift test that changes a classifier family at runtime.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
`error_type` becomes a versioned vocabulary: a failure the classifier cannot place is stored as `unknown` instead of NULL, and any stored value outside the vocabulary reads back as `unclassified` in the breakdown.
Maintainer rework before merge (kept the idea, no default behavior change):
- `PROVIDER_ERROR_TYPES` is `as const`, so `ErrorTypeContract` is a real union and the classifier functions return the narrowed type.
- The constants moved above the JSDoc that documents `getErrorTypeBreakdown`; the vocabulary SQL is built on first use so an import cycle cannot read it before it exists.
- Legacy NULL rows keep landing in the `pre_migration`/`unclassified` bucket without vanishing or double counting, and the log export / BigQuery row pass both NULL and `"unknown"` through unchanged — both covered by tests. Duplicate assertions removed; every seeded row is cleaned up in `finally`.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Combo test probes are aborted when the dashboard client disconnects (`AbortSignal.any` over the route's own timeout and `request.signal`), instead of running to completion for nobody.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
A cold `/v1/models` catalog build that exceeds its time bound now answers 503 with `Retry-After` instead of a 500, and the timed-out build stays joinable so the next retry does not start another cold build. Seven cases; the first fails on the tip (500 → 503).
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
`token_limit_exceeded` joins `REQUEST_SCOPED_UPSTREAM_ERROR_CODES`: chatCore's local Tier-2 check answers 429 with that code, but `shouldSkipConnDisable` did not know it and cooled a healthy connection down for a request-sized problem. Combo exhaustion treats it as request-scoped too.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
The plugin v2 on-disk catalog snapshot is written to a temp file and renamed into place, snapshots from a newer format version are refused instead of parsed, and size-cap or I/O give-ups now warn instead of failing silently. Nine cases, four of which fail on the old code.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
The opencode plugin v2 reads its management token from `OMNIROUTE_MANAGEMENT_API_KEY` (the plugin option still wins) and warns once at startup when it has to fall back to the inference key. Eight cases through the real plugin setup, env isolated, asserting the Bearer header on `/api/*`.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Registers Muse Spark 1.3 (with its effort aliases) on OpenCode Zen, OpenCode and OpenCode Go with `targetFormat: openai-responses` and a 1M context window, so the model no longer falls back to `/chat/completions` (#12674, #12698). Superset of #12675, #12973 and #13111, whose authors are credited in the PR.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
A write that does not send a proxy `status` (subscription refresh, bulk import, PATCH) no longer turns a disabled or dead proxy back on; the stored status is kept. Real DB-backed tests through `upsertProxy`, the subscription sync against a local feed and `handleProxyUpdate`.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
The combo PUT route and the dashboard modal stop stripping nine live config keys (`queueDepth`, `maxComboDepth`, `fallbackDelayMs`, `handoffProviders`, `manifestRouting`, `complexityAwareRouting`, `pipeline_enabled`, `shadowRouting`, `evalRouting`, plus `queueTimeoutMs` in the modal); only the three dead keys that nothing reads are removed. Route tests assert every live key survives a PUT.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
The radar-server only consumes the stable asset, so a catalog merge on the active release
branch waited for the Monday 06:17 UTC cron and the feed sat up to a week behind the README
(2026-09-14: the Together signup-credit row removed in d6e62ae only left the feed after a
manual dispatch). Trigger on release/** pushes for the same catalog paths and move the
schedule to 03:17 UTC daily, ahead of the server's 04:23 UTC publish cycle.
Pool validation no longer rewrites `inactive` or `dead` proxies to `active`: `validateProxyPool` only ever touched rows that were already live. Covered by 8 status × probe combinations plus case and null variants.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Partial updates keep the fields the client left out. Update/PATCH schemas that carried `.default()` re-applied those defaults under `.partial()`, so renaming a disabled reasoning-routing rule turned it back on, a playground preset lost its params and a proxy edit reset `family` to `auto`. The new `partialWithoutDefaults` helper strips defaults before `.partial()`; a scan test fails if any exported update schema ever leaks a default again.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
Batch 3 (last) of the locale-expansion plan: ha, yo, ig, am, uz, ka, hy on every surface — dashboard catalog, docs mirror (22-file core + llm.txt + CHANGELOG), CLI catalog, README flag block, locale tables and 🌐 language bars. Also closes the key gap the batch-1 (43 keys) and batch-2 (10 keys) catalogs carried since their base merges, fixes the Igbo "Model" copy and allowlists the Uzbek cognate. Translation-ratio baseline covers 65 locales.
⚠️ base-red inherited: #12732
* fix(combo): rotate unweighted quota-share targets instead of pinning the first
The combo resolver turns an unset step weight into 0 (comboStructure.ts), and
#10881 made normalizeWeight treat 0 as disabled plus return definition order when
the total weight is 0. A quota-share combo without explicit weights therefore had
no DRR quanta and dispatched every request to its first target — the
combo-matrix/quota-share integration suite saw openai six times out of six.
An all-zero set is now an unweighted combo and shares evenly; an explicit 0 still
disables a target when its siblings are weighted.
Refs #12732
* fix(models): resolve auto-combo target metadata once per catalog build
#12046 derives vision/modalities for the built-in auto/* combos by resolving
catalog metadata for every target of every combo. The ~40 auto combos share one
candidate pool and the loop neither memoized nor yielded, so the #9147 fixture
(60 connections, 720 synced models) went from a ~4s build with a 167ms longest
event-loop gap to ~11s and a 860-1070ms gap on an idle box — past the 800ms
contract and past #12628's 8s cold-build bound, which is why the test came back
500 catalog_build_timeout on every release-green run.
Metadata depends only on the target's provider/model/connection scope within a
build, so memoize it per build and yield between misses. Same fixture: 2.3-3.1s
build, 56-72ms longest gap. The 9147 test is unchanged.
Refs #12732
* test(resilience): list credentialHealthCheck in the configuration-only key set
#12043 added credentialHealthCheck.intervalMinutes to DEFAULT_RESILIENCE_SETTINGS
and to the /api/resilience GET projection. It is operator configuration (the
background sweep cadence), not runtime breaker state, but the exact key-set
assertion was never updated, so resilience-http-e2e failed on the release tip.
The providerBreakers/runtime absence checks stay as they were.
Refs #12732
* fix(ci): stamp BUILD_SHA before the release-green pack gate validates
check:pack-artifact assembles dist/ through build:cli, which never writes
dist/BUILD_SHA (only build:release does). #12959 pointed the provenance ref at
HEAD, but the #10427 guard still stops at 'dist/BUILD_SHA is missing' before it
ever reaches the ancestry check — reproduced on tip + #13635 + #13436, the first
tree whose Turbopack build compiles.
ci.yml sequences build -> stamp -> validate; the validator now does the same in
both entry points, keeping PACK_GATE_ENV for the validate step. The guard is
unchanged: an unstamped dist/ or one built from another commit still fails.
On that tree the stamped gate passes: 'BUILD_SHA 5cb3ae5d9 is on the release line'.
Refs #12732
* docs(env): document OMNIROUTE_SYNCED_CATALOG_STALE_AFTER_MS
#13248 (#12849) added the override for when a connection's synced model list
stops being authoritative, but neither .env.example nor ENVIRONMENT.md lists it,
so the env/docs contract gate reports it as code-only. The other five vars that
gate reports are already added by #13635 and #13361; this touches a different
region of .env.example so it does not collide with either.
Refs #12732
* docs: bring the provider count to the live 358 across the reference, diagrams and llm.txt mirrors
* chore(skills): regenerate the cli-tunnel SKILL.md for the tunnel create positional
* test: clear the ESLint errors in the volcengine upsert and resource-pressure tests
* test(autoCombo): complete the mode-pack ProviderCandidate fixtures for the open-sse typecheck
* fix(ci): allow the opencode-plugin-v2 workspace package in the pack artifact policy
* docs: list the WAL, vacuum, sql.js and pressure self-restart env vars in .env.example
* refactor(db): move the synced-model provider purge into its persistence module to break the models/providers cycle
* test(memory): use a plain label for the rerank loopback key fixture so gitleaks stays at zero
* chore(ci): register the eleven covering unit tests in stryker tap.testFiles
* test(grok-cli): run the reset-credit tests on a fixture clock inside the captured token window
* test(combo): seed real provider connections for the reset-aware strategy tests
* fix(db): keep operator custom models out of the listing-only synced catalog reader
* fix(i18n): translate the new settings and combo keys for vi and pt-BR and restore the zh-TW glossary term
* fix(sse): carry the upstream error code and type through the provider execution pipeline
* test(sse): re-point the chatCore and combo source guards at the split modules and refresh the translate-path golden
* fix(oauth): keep the server-only OAuth constants out of the provider detail client bundle
* test: align the sql.js, webpack, injection-scan and error-boundary guards with their merged contracts
* docs(changelog): record the v3.8.51 base-red sweep
* fix(sse): anchor the glued-prefix sk- credential pattern so error redaction scans in linear time
* docs(changelog): note the linear credential scan in the base-red sweep
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
`check:agent-skills-sync` (ci.yml:lint) has been failing on release/v3.8.51
because the generated skills/cli-serve/SKILL.md no longer matches the command
catalog. fc111dc1 (make startup readiness budget configurable) added the
`--ready-timeout <ms>` flag to bin/cli/commands/serve.mjs without regenerating
the skill.
This is the generator's output, unedited: a single added line listing the
flag, which exists at bin/cli/commands/serve.mjs:62. No other skill changed
(Generated: 1 · Unchanged: 45 before, Generated: 0 · Unchanged: 46 after).