* fix(sse): grace period before finalizing a client disconnect as 499 (#9653)
A client that closes its connection right after reading a fully-completed
SSE stream can race OmniRoute's own completion bookkeeping: the bytes
already reached the client, but the transform stream's own completion
callback (onStreamComplete, which flips streamCompletionRecorded) hasn't
finished bubbling up when the disconnect handler fires, so the request gets
persisted as a false 499 with zero token usage even though it delivered its
full response.
Confirmed live on real traffic before this fix: a request whose server log
showed "disconnect: request_signal_aborted" at 18236ms was persisted with
status 200 and full token usage (82814/1292) once the grace period let the
real completion win the race, matching what the client actually received.
createClientDisconnectGraceHandler (new leaf in
streamFailureFinalization.ts) polls isStreamCompletionRecorded() for up to
STREAM_DISCONNECT_GRACE_PERIOD_MS (default 10s, env-configurable, 0
disables) before finalizing as a failure. If a real completion lands within
the window, handleStreamFailure's own guard is a no-op and the genuine 200
stands.
Covered by tests/unit/stream-disconnect-grace-period-9653.test.ts (fake-timer
driven: already-recorded completion short-circuits, disabled-grace-period
finalizes immediately, a completion landing mid-window skips finalize
entirely, and no completion ever landing finalizes once the deadline
passes).
(cherry picked from commit 5d0fe28c42)
* chore(quality): rebaseline chatCore.ts for the disconnect grace-period fix
Own growth from the disconnect grace-period fix: 5030->5039 (+9, the
createClientDisconnectGraceHandler wiring at the existing
onClientDisconnectFinalize call site).
---------
Co-authored-by: Markus Hartung <mail@hartmark.se>
requestLogger.ts's cloneBoundedForLog had its own hardcoded depth cap of 6,
independent of the existing configurable getChatLogMaxDepth(). A typical
Chat Completions response body's responseBody.choices[0].message.tool_calls[0].function
sits at exactly depth 6, so every logged tool call's function field
(name+arguments) was silently replaced with the literal string "[MaxDepth]"
before ever being stored — corrupting the data, not just how it renders.
Bumped the shared default 6->20 and switched requestLogger.ts to read it
instead of using its own literal.
(cherry picked from commit a2df6cf289)
Co-authored-by: Markus Hartung <mail@hartmark.se>
* feat(logging): make the chat-log truncation limit configurable, bumped default 128x
The 8KB cap on logged request/response bodies
(open-sse/handlers/chatCore/logTruncation.ts::truncateForLog()) was
hardcoded — trivially exceeded by any real multi-turn agentic
conversation, meaning the dashboard's "Full Conversation" panel could
only ever show a placeholder instead of the actual messages for nearly
every logged row of any conversation with real substance.
- Added CHAT_LOG_MAX_BODY_KB env var (src/lib/logEnv.ts::
getChatLogMaxBodyBytes()), default 1024 KB (1MB) — a 128x bump from
the old hardcoded 8KB — following the same configurable-limit pattern
as the sibling CHAT_LOG_TEXT_LIMIT/CHAT_LOG_ARRAY_TAIL_ITEMS/etc. vars.
- Documented in .env.example and docs/reference/ENVIRONMENT.md.
estimateSizeFast() (open-sse/utils/estimateSize.ts) has been
substantially rewritten upstream since this bug was first found (now an
iterative Frame-based walker with a separate node-visit budget, not the
simple stack loop originally patched) — re-implemented the fix against
the current algorithm rather than porting the old diff: the byte
early-exit was unconditionally the module-level ESTIMATE_SIZE_BYTE_LIMIT
(256 KiB) with no way for a caller to raise it, so any caller comparing
against a bigger configured threshold could never see a size above
~256 KiB — every payload between 256 KiB and the caller's real limit
looked "under threshold" and truncation never fired, the opposite of
intended. Added an optional byteLimit parameter (default unchanged at
ESTIMATE_SIZE_BYTE_LIMIT, so isSmallEnoughForSemanticCache's existing
behavior is untouched) threaded through both the byte-check early-exit
and the node-budget-exhaustion fail-closed fallback, with
truncateForLog() now passing its own configured getChatLogMaxBodyBytes()
value through.
* feat(dashboard): show conversation session tag in request detail metadata
Adds a "Conversation" field to the request detail panel's metadata
grid (after "Combo"), showing the request's conversation id
(sessionTag) for quick reference/copy.
---------
Co-authored-by: Markus Hartung <mail@hartmark.se>
copyOpenAICompatibleReasoningFields only stripped the sentinel
(NON_ANTHROPIC_THINKING_PLACEHOLDER = "(prior reasoning summary
unavailable)") from reasoning_content and reasoning. Non-standard
reasoning fields (reasoning_text, thinking, thought) and
reasoning_details items passed through raw, leaking the internal
replay sentinel to clients on providers that use those fields
(e.g. Venice), where the model echo surfaces as a bogus thought block
and can degrade into empty turns.
Strip the sentinel from every forwarded reasoning field, including
per-item text/content inside reasoning_details; drop items/fields that
strip to nothing while preserving non-text details such as
reasoning.encrypted.
Fixes#9765
Refs #8081, #9606
Co-authored-by: safeer <asafeer1994@gmail.com>
* fix(cursor): hydrate SelectedImage via blobIdWithData + JPEG soft-cap
Cursor vision expects SelectedImage.blob_id_with_data (field 9) backed by
the session blobStore, and large clipboard PNGs need JPEG soft-cap prep
rather than a hard 1 MiB reject before encode.
* docs(changelog): add fragment for Cursor SelectedImage blobIdWithData fix
* refactor(cursor): split image protobuf encoding
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com>
* fix(ci): drop unused RadarReferrals type export — dead-code ratchet back to 227 baseline
The radar referral-links feature (#9697) exported the inferred type
RadarReferrals from feedSchema.ts but nothing imports it (the singular
RadarReferral is the consumed type). knip counts it as a new dead export,
pushing the dead-code ratchet to 228 > 227 and failing Fast Quality Gates
on every PR born after the merge. RadarReferralsSchema itself stays — it
is used by RadarFeedSchema.
Refs #9737
* fix(ci): clear the 08-08 base-red layer — prod crash in chat.ts, Responses API payload regression, born-red stdio test, gate drifts
Six independent base-reds from the 08-07 evening merge batch, each verified
against the pure release/v3.8.50 tip:
- src/sse/handlers/chat.ts: #9467's squash carried a refactor hunk that
renamed the all-rate-limited breaker guard to an UNDEFINED variable
(isAllRateLimited) — a production ReferenceError on the all-accounts-429
path (chat.ts is outside typecheck:core scope, so only tests caught it).
Restore credentials?.allRateLimited. Guard: chat-rate-limit-body-lock (2/2),
also un-breaks batch_api and chat-combo-live-test.
- open-sse/utils/stream.ts: #9315 switched providerPayload summaries to the
accumulated responseBody, but in passthrough paths that body is synthesized
in chat-completion shape — Responses API lost its `response` object in the
dashboard payload. Keep the events-derived summary for OPENAI_RESPONSES
only. Guard: stream-utils + stream-collector-9315 suites (51/51).
- tests/unit/mcp-stdio-json-purity.test.ts: born red — the full CLI chain
takes ~10s (2x tsx import + DB init) and the test slept a fixed 4s. Poll
for the first stdout line with a 60s deadline instead.
- tests/unit/plugins-route-error-sanitization.test.ts: register #9445's new
marketplace/install route in PLUGIN_ROUTES (route already sanitizes) (33/33).
- tests/unit/provider-models-route-codex.test.ts: realign pinned GPT-5.6
input limit to #9432's deliberate 272000→922000 bump (7/7).
- lint: fix 11 no-explicit-any errors in repro-9630 + specialty-9293 tests,
prune 1 orphaned suppression, allowlist the opencode-ai devDependency
(#8869, publisher-verified), and reword a doc line the fabricated-docs
gate misread as an env var.
Gates re-verified locally: lint:json --max-warnings 0 exit 0, dead-code 227,
typecheck:core clean, check:deps OK, check:fabricated-docs OK.
Refs #9737
* fix(ci): clear the third 08-08 base-red layer — invalid ru rule pack, stale event pin, orphaned UI repro test, pack/mutation/file-size drifts
Follow-up to the previous layer: the serial fast-gates chain unmasked one
more stratum after file-size/dead-code went green, all verified against the
merged release/v3.8.50 tip:
- compression rules ru/ultra.json (#9581): two rules shipped
minIntensity "notes", which is not a valid CavemanIntensity
(lite|full|ultra) — loading ANY language pack list threw and killed the
rtk-loader suite. Mapped both to "ultra" (they are the most aggressive
punctuation/case rules, matching the en pack tiers). 2/2.
- plugins-welcome-banner-e2e: #9668 added the onStreamComplete builtin
event (real emission path via runOnStreamCompleteHooks) and missed this
pinned-list sibling. 35/35.
- tests/unit/free-pool-frontend-repro (#9046): landed as .tsx with
node:test semantics — no runner collects tests/unit/*.tsx, so it NEVER
ran (test-discovery NEW-orphan). It contains zero JSX; renamed to .test.ts
so the unit runner's existing glob collects it. 5/5 (first real run).
- pack-policy: allow + require bin/mcpStdioConsoleGuard.mjs (#9281) — it is
preloaded via node --import by bin/mcp-server.mjs, so a published artifact
without it crashes 'omniroute --mcp' at startup.
- stryker.conf.json: add 5 covering unit tests from the batch (#8779/#9204/
#9330/#9630/openrouter-passthrough) to tap.testFiles (--strict drift).
- file-size-baseline: consolidate the base-drift rebaseline for the 12
files grown by the 08-06..08-08 batches (#9616's entries never reached the
base; measured on this branch's tree — this PR's own source edits add zero
lines to any frozen file).
Local battery: file-size/deps/test-discovery/mutation/pack-policy/dead-code/
duplication/docs-all/secrets/vuln/workflows ratchets all exit 0; full lint
gate --max-warnings 0 exit 0.
Refs #9737
* fix(types): clear the 3 uncovered open-sse-typecheck regressions + realign combo skip-code siblings
Fourth base-red layer unmasked by the serial gates. The other 4 typecheck
regressions (codex.ts, kiro.ts, tierResolver.test.ts, translator/index.ts)
already have dedicated open [TS7] PRs (#9748/#9753/#9742/#9747) — not
duplicated here. This commit covers only what no open PR owns:
- devin-agentic/serializer.ts TS2367: drop the dead 'role === "system"'
branch — the guard above already narrows role to user|assistant (system
throws unsupported_role). Devin suites 104/104.
- raycast.ts TS2416: the buildHeaders 'override' never matched the base
signature (2nd param is the signed payload string, not the stream
boolean) — renamed to a private buildRaycastRequestHeaders helper so a
polymorphic buildHeaders(credentials, true) call can never bind here.
- modelMetadataRegistry.ts TS2352: PricingByProvider → nested-record cast
now goes through unknown (shape is runtime-guarded by findInsensitive).
- combo-routing-engine.test.ts: realign 2 pre-dispatch-skip expectations to
#9630's deliberate ALL_TARGETS_SKIPPED contract (87/87).
Refs #9737
* fix(ci): clear the fifth 08-08 base-red layer — reasoning-placeholder contract sweep, GPT-5.6 limits sweep, vi key parity
The 08-08 merges (#9610 reasoning replay, #9432 GPT-5.6 limits, #9630 combo
skip codes, #9336 provider key links) each changed a contract and left
sibling tests pinning the old one. Full grep sweep per contract, not just
the shard that happened to go red:
- reasoning placeholder (#9573/#9610): the fix DELIBERATELY removed
NON_ANTHROPIC_THINKING_PLACEHOLDER injection on cache miss — the model
echoed the placeholder as its own reasoning (empty stop) and re-poisoned
cache + client history; DeepSeek's 400 is specific to an EMPTY STRING, not
an absent field. Realigned reasoning-cache (2 cases, renamed to describe
omission) + tool-request-sanitization (1 case + dead import). 60/60.
- GPT-5.6 Codex limits (#9432, 272000 -> 1050000 ctx / 922000 input):
realigned vscode-token-routes-gpt56 (2) + vscode-token-routes (3). 43/43
together with t23-t24.
- combo skip codes (#9630): t23-t24-fallback-resilience T24 now expects
ALL_TARGETS_SKIPPED like the combo-routing-engine siblings.
- vi.json key parity: #9336 added providers.getApiKey/getApiKeyDescription
to en.json without syncing vi (the only locale with a parity gate).
Translated both; providers block reordered to match en key order. 5/5.
- pack-artifact-policy.test.ts: sibling of this PR's own required-paths
change (bin/mcpStdioConsoleGuard.mjs). 10/10.
- combo-routing-engine.test.ts: dropped the 6 comment lines added in the
previous commit so the frozen test file-size stays at its baseline (the
rationale lives in that commit message, not the test body).
Gates: file-size, test-discovery, mutation-test-coverage, pack-policy,
open-sse-typecheck, dead-code all exit 0.
Refs #9737
* fix(translator): keep the reasoning_content placeholder for Xiaomi MiMo — #9610 traded one live 400 for another
The xiaomi-mimo replay test (9router#1321) went red on the base after #9610
removed the NON_ANTHROPIC_THINKING_PLACEHOLDER injection globally. That test
is NOT stale — it guards a documented upstream 400 ('Param Incorrect: The
reasoning_content in the thinking mode must be passed back to the API'), so
realigning it would have masked a reintroduced production bug.
Two real bugs conflict here:
- #9573: forwarding the placeholder makes the model continue its chain of
thought FROM that text (echo -> empty stop) and re-poisons cache/history.
- 9router#1321/#1337: omitting reasoning_content on a plain replay turn makes
Xiaomi MiMo reject the request outright.
#9610's evidence for omitting is provider-specific — it verified that
deepseek-v4-flash accepts an ABSENT field. It does not extend to MiMo. So the
omission stays for every provider #9610 covered, and the placeholder survives
the cache miss only for xiaomi-mimo (new requiresReasoningContentPresence
predicate next to isReasoningOnlyReplayTarget). The echo that comes back is
still stripped on the way in by isInternalReasoningPlaceholder(), so #9573's
cache/history poisoning stays fixed for MiMo too.
Both contracts now hold simultaneously: xiaomi-mimo replay + reasoning-cache +
tool-request-sanitization 61/61; placeholder-strip/responses/translator/combo
regression sweep 168/168. Gates: file-size, open-sse-typecheck, dead-code,
mutation-test-coverage exit 0; typecheck:core clean.
A live check on the VPS (Hard Rule #18 path 2) is the only way to confirm the
DeepSeek half of #9610's empirical claim; flagging it in the PR rather than
widening this fix on speculation.
Refs #9737
* test(translator): pin the reasoning-placeholder provider scope so neither half of the conflict can silently re-break
#9610 removed the placeholder globally on the strength of ONE provider's
observed behavior (deepseek-v4-flash accepting an absent reasoning_content),
which re-opened the MiMo 400 (9router#1321). The previous commit scoped the
placeholder to xiaomi-mimo; this pins BOTH directions in one test so the next
global edit fails loudly instead of trading the bugs again:
- xiaomi-mimo plain replay turn, cache miss -> reasoning_content present
(narrowing the scope away from MiMo re-opens 9router#1321)
- deepseek plain replay turn, cache miss -> reasoning_content absent
(widening it back to DeepSeek re-opens the #9573 echo bug)
Guard verified by mutation: forcing requiresReasoningContentPresence() to
return true makes the DeepSeek half fail (1 pass / 1 fail), and the file was
restored from the pre-probe copy before committing.
Also checked kimi-coding/kimi-coding-apikey, the other strict-contract entries
in REASONING_REPLAY_PROVIDERS: their originating PR (#7673) fixes capture and
replay of REAL reasoning and documents no 400 on an absent field, so they stay
out of the placeholder scope — evidence-scoped, not speculatively widened.
Reasoning suites together: 87/87. Gates: file-size, test-discovery,
mutation-test-coverage, dead-code exit 0; eslint clean.
Refs #9737
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* test(base): realign six suites with contracts that #9100/#8990/#9009 deliberately changed
Continuing the base-red drain — every one of these reproduces on the pure tip.
- tests/snapshots/provider/translate-path.json: regenerated via UPDATE_GOLDEN=1.
The diff is ADDITION-ONLY — the unorouter block from #9009; no existing
provider entry changed. 3/3.
- tests/unit/provider-models-route.test.ts: ff012ff420 added onboardUser as a
bootstrap fallback next to loadCodeAssist; the mock now excludes it from the
discovery-URL ledger like it already excluded loadCodeAssist, otherwise it
consumed the injected 503 and the retry assertion misfired. 59/59.
- tests/unit/responses-commentary-passthrough-6199.test.ts: #8990 (c996dc93c2)
deliberately preserves `tools` on the TERMINAL response.completed snapshot
(Codex CLI rebuilds its tool list from it); the assertion now pins the echoed
tools instead of their absence. Still stripped on created/in_progress. 7/7.
- tests/unit/vision-compression-authoritative-capability-7237.test.ts:
68cb678780 added the 'gpt-5' fragment, so the heuristic-vs-spec DRIFT this
suite documented no longer exists; the cases now guard the agreement, keep a
conservative-for-unknown-ids probe, and reproduce the strip-bug shape with an
explicit false instead of deriving it. 4/4.
- tests/unit/provider-limits-proxy-fail-closed.test.ts +
tests/unit/image-generation-route.test.ts: #9100 made the proxy reachability
probe NON-BLOCKING (optimistic dispatch; the probe aborts only in-flight
requests — its own t14 sibling was updated to this exact pattern). Instant
mocks therefore won the race and the PROXY_UNREACHABLE 503 became unobservable
(a success or a generic 502). The mocks now stay in flight (never-resolving,
so the aborted continuation cannot reach the restored real fetch), and the
fail-closed proof is the settled rejection itself plus zero egress AFTER the
fast-fail. Production fail-closed semantics are unchanged — the proxy dispatch
path still throws; only the mock timing was stale. 3/3 and 20/20.
Refs #9298
* fix(guardrails): forward the router deps seam through callVisionModel
tests/unit/guardrails/vision-bridge-sse-and-reasoning.test.ts was 7/7 red on any
clean box (CI shard 3/4): callVisionModel() called getBestVisionModel()/
getFallbackModels() WITHOUT the routers' existing VisionBridgeRouterDeps seam,
so the credential check always hit the live connections DB — no vision-capable
connection meant 'No vision-capable provider connected' before the mocked fetch
was ever reached, and on a dev box auto-selection could swap the fixed model
under the assertions.
The routers already accepted deps; only the forwarding was missing. Added the
optional 5th param (backward compatible — the sole production caller,
visionBridge.ts, injects its own callVisionModel and is unaffected) and the
suite now pins selection with hasUsableCredentials: async () => null
(indeterminate → the fixed model is honored, DB untouched). 7/7.
Sibling suites re-run green: vision-bridge-callmodel 2/2, visionBridge 25/25,
visionBridgeHelpers.callVisionModel 8/8, visionBridgeRouter 10/10,
vision-bridge-cc-no-reroute 8/8.
Refs #9298
* fix(db,combo): clear the NEW base-reds the 08-06 merge batch introduced
The tip moved while the first sweep PR (#9600) was in review, and three fresh
base-reds landed with it — same classes as before, all reproduced on the pure
tip 9995bc4893:
1. ANOTHER migration collision: #9061 shipped 134_ccr_blocks.sql onto the slot
134_proxy_logs_egress_ip.sql (#9291) has held since 08-04. getMigrationFiles()
throws on collision, so every DB-touching test died at bootstrap again.
Renumbered to 139 (next free slot). No retroactive guard needed this time:
both statements are IF NOT EXISTS, and no DB can have applied it as 134 —
the runner refused to run at all while the collision existed.
2. BROKEN IMPORT killing the combo module graph: #8894 imported
preferAntigravityConnectionsWithStoredProject from
../antigravityProjectPersistence.ts — a module that exists NOWHERE in the
repo (it came from an unmerged sibling branch). Anything importing
quotaStrategies.ts died with ERR_MODULE_NOT_FOUND. Implemented the helper in
the real persistence module (antigravityProjectPersist.ts, #8491) with the
semantics the call site needs — prefer connections that already carry a
stored projectId, never emptying the pool — and pointed the import there.
New regression suite tests/unit/antigravity-prefer-stored-project.test.ts
(5/5), including an import-graph probe that reproduces the break shape.
3. Sibling-test drift from #9106 (gemini-3.1-pro-high now user-callable): its
own suites were updated but provider-models-route.test.ts was not. Expected
discovery list realigned; testFrozen 1784->1787 justified in the baseline
(irreducible +2 after comment compression; gate counts split-newlines).
Also regenerated tests/snapshots/provider/translate-path.json — addition-only:
devin-cli-agentic, raycast, regolo (today's provider merges), zero removals.
image-generation-route 20/20 (was import-dead), provider-models-route 59/59,
antigravity-prefer-stored-project 5/5, provider-translate-path-golden 3/3.
Refs #9298
* fix(changelog): convert the #9415 fragment to the required bullet shape
Another base-red from the 08-06 batch: bd4407cb64 landed
changelog.d/features/9415-newapi-sub2api-aggregator-balance.md as YAML
frontmatter + a prose paragraph. Every other fragment in changelog.d/ is a
single markdown bullet, and both consumers enforce that —
scripts/check/check-changelog-integrity.mjs:97 and the release aggregator
(scripts/release/aggregate-changelog.mjs:57) reject anything that does not
start with '- ', so 'Merge integrity (changelog + generated skills)' was red
for every PR targeting the release branch.
Rewritten as a bullet with the standard issue link, preserving the feature
description (aggregator gateway toggle, /api/user/self balance read, dashboard
badge, quota-preflight skip, NEWAPI_AGGREGATOR_BALANCE flag default off,
quotaPerUnit override). Swept the rest of changelog.d/ — this was the only
malformed fragment.
check:changelog-integrity OK.
Refs #9298
* fix(types,docs): clear the 5 typecheck errors and the fabricated env vars on the base
Third pass over the base-reds, from the 2026-08-06T22:51Z verdict on #9298 —
it reported "Typecheck (core)" with only the FIRST error; there are five, all on
the pure tip 9995bc4893. Two are real production defects.
**Real bugs**
- open-sse/services/compression/engines/ccr/index.ts:295 called
enforceGlobalBudget(entry.bytes) against an (owner, bytes) signature. The
`bytes` argument arrived undefined, so `ccrTotalBytes + undefined` is NaN,
`NaN > MAX` is false (the eviction loop exits immediately) and `NaN <= MAX` is
false (the re-admit is refused). The #9061 durable tier therefore NEVER
repopulated its in-memory map: every retrieve after a restart or an eviction
re-read from SQLite forever, and evictions could not prefer the owning
principal. Fixed and pinned by a new case in
tests/unit/ccr-durable-store-9061.test.ts (11/11) — verified failing against
the buggy call and passing against the fix.
- open-sse/services/combo/fusionPanel.ts:54 read `step.model` after #8894
widened ComboStep with ComboProviderWildcardStep (which carries modelPattern,
not model), so a wildcard step in a fusion panel pushed `undefined` onto the
panel. Now resolved through getComboModelString(), which already handles every
step shape and returns null for the ones without a concrete model id.
**Type-only**
- accountSemaphore.ts:203 — isBypassed() returns a plain boolean and cannot
narrow `number | null` (an `x is null | undefined` predicate would be unsound:
0 bypasses too). Added resolveActiveCap(), the narrowing companion isBypassed
is now defined in terms of; the acquire path uses the narrowed value.
- comboStructure.ts:140 — same #8894 widening: `prompt` only exists on a model
step, so it is now read under a kind check.
- firecrawlQuotaFetcher.ts:136 — the function returns full FirecrawlQuota
objects but was annotated Promise<QuotaInfo | null>, which made the
custom-base literal an excess-property error. Widened to the accurate type
(FirecrawlQuota extends QuotaInfo, so callers are unaffected).
**Fabricated docs (the "Docs sync + fabricated-docs (strict)" HARD failure)**
docs/ops/VM_DEPLOYMENT_GUIDE.md recommended OMNIROUTE_MAX_POOL_SIZE and
OMNIROUTE_DB_POOL_SIZE (#9471). Neither is read anywhere in the codebase.
Replaced with the two knobs that do exist and are already documented in
ENVIRONMENT.md: OMNIROUTE_MEMORY_MB and OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT.
typecheck:core 5 errors -> 0. check:fabricated-docs + check:env-doc-sync OK.
accountSemaphore 6/6, ccr-durable-store 11/11, ccr-protocol 9/9,
combo-fusion-strategy 10/10, combo-fusion-comboref 5/5, combo-fusion-warn 4/4,
firecrawl-executor 7/7, executor-firecrawl-fetch 4/4.
Refs #9298
* fix(tests): type the #3440 vertex helpers instead of `any` (the 3 base ESLint errors)
The "ESLint errors: 3 error(s)" HARD failure in the #9298 verdict is
tests/unit/vertex-functioncall-id-3440.test.ts lines 32/41/50: the three
find*(result: any) walkers. `@typescript-eslint/no-explicit-any` is an ERROR in
tests/ (and open-sse/) since #6218, and this file landed on 2026-08-04 without a
suppressions entry, so every run of `lint:json --max-warnings 0` failed. That
step prints nothing on failure, which is why the gate looked like a silent
crash across the open PRs.
Replaced with a GeminiRequestLike interface describing exactly what the three
walkers traverse (contents[].parts[]), so the assertions keep their meaning and
nothing is cast away.
eslint on the file: clean. Suite: 6/6.
Refs #9298
* docs(proxy): use an RFC 5737 documentation IP in the proxy examples
The #9298 verdict headlines its docs failure with
`L810 [stale-version] 1.2.3: const removed = await failOneproxyProxy("1.2.3.4", 8080)`.
That is a false positive: check-deprecated-versions.mjs matches
`/\bv?[12]\.\d+\.\d+\b/`, and the example IP literal 1.2.3.4 contains "1.2.3".
Swapped both occurrences in PROXY_GUIDE.md (and its pl mirror) for 203.0.113.7,
from the RFC 5737 documentation range that exists precisely for examples — it
cannot collide with a version pattern and is the correct thing to print in docs
regardless. Drift count 64 -> 62; no gate threshold was touched.
The gate that actually FAILED under "Docs sync + fabricated-docs (strict)" was
check:fabricated-docs (the invented pool env vars), fixed in the previous
commit; this one removes the misleading line the verdict quotes.
* test(base): allowlist probeUtils and realign the #7849 suite to the replacement bound
Two more base-reds, both visible only after the migration collision stopped
killing the shards.
**check-db-rules — src/lib/db/probeUtils.ts not classified**
#9541 added probeUtils.ts (transient-error retry for the SQLite corruption
probe). It is imported ONLY by src/lib/db/core.ts, exactly like its siblings
schemaColumns / optimizationSettings / providerNodeSelect, so re-exporting it
through localDb.ts would push callers toward the barrel-import anti-pattern the
gate exists to prevent. Added to INTENTIONALLY_INTERNAL with that rationale.
check-db-rules 22/22, check:db-rules exit 0.
**session-dedup-memory-7849 — pinned a mechanism that was replaced**
7f36b192f0 (#7855 follow-up) swapped the shared "suffix work budget" for the
MAX_SUFFIX_STARTS / MAX_TOTAL_BLOCK_BYTES guards and deleted both the budget and
its SUFFIX_WORK_BUDGET_WARNING string. It updated session-dedup.test.ts but not
this sibling, so 3 of its 4 cases asserted a warning that can no longer be
emitted.
Realigned to the contract that actually survives — which is the invariant #7849
was opened for, not the mechanism:
- the pathological pair must stay BOUNDED (completes in <4s, body intact) —
measured at ~280ms on the current guards;
- it must FAIL OPEN — original body returned by identity, compressed false,
stats null (the explanatory zero-savings stats belonged to the removed
budget path, which skipped before producing any);
- the 512 MiB child fixture must still exit 0 with the full engine chain
(session-dedup, lite, rtk, headroom, caveman) — that IS the OOM guard — and
session-dedup must still report its skip, now pinned by prefix since the
reason string moved with the mechanism.
No threshold was loosened and no case was deleted: 4/4 here, 8/8 on the sibling
session-dedup.test.ts.
Refs #9298
* docs(mcp): bump the tool count to 105 and realign two vitest count pins
Three more base-reds from the same 08-06 batch, all count/contract drift that
the merged PRs left in sibling files.
**Docs Gates (fast-path) — 3 STRICT drifts**
check:docs-counts measures the MCP tool set from live code: it is 105 now
(#8925 added omniroute_create_combo), while README.md, AGENTS.md and
docs/frameworks/MCP-SERVER.md still claimed 104. Updated all five occurrences
(two of them inside SVG alt text). check:docs-all exits 0.
**Vitest (fast-path) — 2 failures**
- open-sse/mcp-server/__tests__/essentialTools.test.ts pinned 11 phase-1 tools;
#8925 shipped omniroute_create_combo as phase 1, making it 12. Verified by
enumerating MCP_ESSENTIAL_TOOLS directly.
- tests/unit/autoCombo/provider-family-combos.test.ts pinned the auto/glm
provider set to [auggie, glm, zai]. #8914 (Devin ACP bridge) added
devin-cli-agentic, whose catalog (registry/devin/catalog.ts:90-93) advertises
the glm-5-2* line — so it belongs in the family pool for exactly the reason
the test's own comment gives for auggie: a no-auth backend that genuinely
serves a family model is a legitimate member. Expected set updated, invariant
unchanged.
npm run test:vitest 36/36 files, 340/340 tests.
Refs #9298
* fix(combo,usage,oauth): drain the base-reds the shard fix exposed
With the migration collision and the broken import out of the way the four unit
shards actually run, and a further layer of base-reds became visible on the pure
tip 9995bc4893. Three are production defects.
**Production defects**
- open-sse/services/combo/runtimeUnitCapacity.ts:58 called resolveComboTargets()
WITHOUT the hidden-model snapshot, so it fell back to the default
getHiddenModelsByProvider() — a fresh full key_value read PER nested combo-ref
unit, on every request. #8878 threaded the snapshot through the other call
sites and missed this one. Threaded it from executeRuntimeUnitCombo (and from
the dispatchPrelude call site), restoring the one-snapshot-per-request
invariant combo-hidden-leaf-routing.test.ts pins. 9/9.
- open-sse/services/usage/firecrawl.ts silently ignored its own `apiKey`
parameter: 91bb6aa619 moved the fetch to
fetchFirecrawlQuota(connectionId, connection), which reads the key off the
connection record, so any caller passing the key directly got "Firecrawl API
key not available". The explicit key is now merged into the connection passed
down. firecrawl-usage 8/8.
- src/lib/oauth/constants/oauth.ts was missing a RAYCAST entry in PROVIDERS
while src/lib/oauth/providers/index.ts registers `raycast` (#8895), so every
consumer reading PROVIDERS did not know Raycast Pro exists. Also added its
OAUTH_TEST_CONFIG entry (checkExpiry only — it is an `import_token` provider
with refreshToken always null), which #8408's guard explicitly requires rather
than grandfathering. oauth-providers-config 25/25, oauth-test-config-8408 2/2.
**Count / contract drift from the same batch**
- feature flags 45 -> 46, APIKEY_PROVIDERS 197 -> 198 (Raycast Pro #8895),
unique MCP tools 107 -> 108. Each re-derived from the source of truth.
- vi + pt-BR locales: translated the 8 keys #9415 added
(providers.newApiAggregator* and providers.modelTestQuotaTooltip) instead of
relaxing the parity guard. i18n-vi 5/5, i18n-pt-br 3/3.
- login-bootstrap-route: #9491 added `authenticated` to the require-login
payload so /login can redirect an active session; the three deepEqual bodies
now carry it. 10/10.
**Flaky-by-construction, made deterministic**
tests/unit/chat-combo-live-test.test.ts asserted the early-keepalive frame with
a 100ms mocked upstream while resolveKeepaliveThreshold() is 2000ms for
openai/*. It only ever passed while unrelated handler latency happened to push
the total past the threshold — incidental, not deterministic, and it stopped
holding once the handler got faster. The mock now sleeps 2400ms so the slow path
is guaranteed and the assertion means what it says. 5/5.
typecheck:core exit 0. check:file-size (base-relative) OK.
Refs #9298
* test(base): run the orphaned #8890 suite and realign three mechanism pins
**check:test-discovery — a suite that had NEVER executed**
#8890 landed open-sse/services/__tests__/fail-fast-concurrency-gate.test.ts into
a directory no runner collects (only one explicit file from that folder is in
vitest.mcp.config.ts), so it ran zero times since it merged. Wired it into the
runner AND into check-test-discovery.mjs's mirrored collector list, which the
gate keeps in sync deliberately. It passes 4/4 now that it actually runs —
test:vitest goes 36 -> 37 files, 340 -> 344 tests.
**check-db-rules-classification** — 37 -> 38 audited modules, adding probeUtils
alongside the INTENTIONALLY_INTERNAL entry from the previous commit.
**ratelimit-reservoir-refresh** — #9604 (rolling RPM leases) DELETED Bottleneck's
fixed-window reservoir, so currentReservoir() is null and the poll for
`reservoir === 2` could never settle. It updated several sibling suites but not
this one. The pin on the removed mechanism is gone; what remains is the
invariant the original Bottleneck heartbeat bug actually broke and that #9529
opened this test for — after a header-learned updateSettings() the limiter must
keep admitting work, proven by racing a post-exhaustion request against a 5s
timer. 1/1.
**translator-openai-to-gemini** — #9568 (c9a3361e5a) made
buildChangedToolNameMap emit IDENTITY entries too, because Gemini lowercases
tool names in functionCall responses and the response translator needs a key to
map them back. Any request carrying tools therefore carries `_toolNameMap` in
the Antigravity envelope now. Expected key list updated and the map's contents
asserted explicitly rather than left implicit. 45/45.
Refs #9298
* fix(db): restore node-backed synced catalogs and realign the #8944 context hints
**Production regression from #9294 (d69f521491)**
lookupModelMeta moved from getSyncedAvailableModels(providerId) to
getActiveSyncedCatalog(providerId). The new reader unions models only from rows
in `provider_connections` with isActive = 1 — but a provider NODE lives in
`provider_nodes` and NEVER has a connections row, so filtering by active
connection ids silently dropped every node's synced catalog.
The consequence was not just a missing list: lookupModelMeta reads that catalog
for RUNTIME METADATA, so for openai-compatible nodes it took out
- `supportedThinkingEfforts`, which is what splitSyncedEffortSuffix needs — so
`<prefix>/<model>-high` stopped resolving to the base id and the effort was
never derived (#7694), and
- `contextWindow` / `maxInputTokens`, used by the combo context-window filter.
getActiveSyncedCatalog now falls back to the provider-wide key_value set — the
exact pre-#9294 source — when no active connection carries a catalog, and marks
that fallback explicitly NON-authoritative. #9294's live-catalog gating is about
what an active connection actually serves, so a node-backed catalog informs
metadata while never being able to reject a model as unavailable. `available`
therefore stays fail-open for nodes, as it was before.
sync-reasoning-supported-efforts-7694 23/23 (was 21/2).
live-model-catalog-reconciliation-8926 11/11 and combo-provider-wildcard 23/23
confirm #9294's own coverage is untouched.
**#8944 sibling-test drift**
714a315a1a ("Treat context metadata as a routing hint") deliberately turned the
context-window check from a HARD filter into an ordering hint: a catalog-too-small
target is demoted, not removed, because a stale catalog entry must never delete
the only target that could accept the request at runtime. The PR updated one case
in this suite and left three asserting the old drop behaviour. Realigned to the
new contract — the too-small target must lose the ordering to the fitting one
while remaining present — and renamed them from "still rejects"/"still dropped"
to "is demoted"/"ordered last" so the names stop describing the removed
behaviour. 14/14.
**file-size**
tests/unit/translator-openai-to-gemini.test.ts testFrozen 1616 -> 1619: the
frozen value sat exactly at the base size, so the 3 lines the previous commit's
_toolNameMap alignment needs could not fit. Justified in the baseline.
typecheck:core exit 0.
Refs #9298
* chore(stryker): register the two covering suites missing from tap.testFiles
check:mutation-test-coverage flags any unit test that covers a mutated module but
is absent from stryker.conf.json tap.testFiles — without the entry its mutant
kills do not count toward the module's score.
- tests/unit/antigravity-prefer-stored-project.test.ts covers
open-sse/services/combo/quotaStrategies.ts (added earlier in this PR).
- tests/unit/executor-devin-cli-agentic-acp.test.ts covers
src/sse/services/auth.ts — pre-existing drift, same gate, same fix.
Inserted in alphabetical position only; the rest of the file is byte-identical
(it is not prettier-formatted upstream and reformatting it is out of scope here).
Refs #9298
* fix(db): drop the never-wired getSessionModelUsageCounts (knip regression)
The dead-code ratchet only ran once the earlier Fast Quality Gates steps stopped
failing, and it lands at 228 vs baseline 227.
The extra symbol is src/lib/db/contextHandoffs.ts::getSessionModelUsageCounts,
added by #8894 "for least-used strategy" and never wired: the least-used branch
in applyStrategyOrdering.ts uses the pre-existing sortTargetsByUsage(), and the
helper has no caller in src/, open-sse/ or tests/. It is the same incomplete-PR
shape as that PR's import of a module which does not exist in the repo (fixed
earlier in this branch).
Removed rather than baselined — bumping the ratchet would loosen the gate, and
removal is exactly the remedy the gate prescribes. Same treatment the Dario
installer's never-wired uninstall() got in #9600. The implementation is
recoverable from a598fbb090 whenever someone actually wires a session-aware
least-used strategy.
check:dead-code 228 -> 227 (baseline untouched). check:db-rules exit 0.
context-handoff 13/13, db-context-handoffs 7/7, service-context-handoff 11/11.
Refs #9298
* fix(security): embed the Raycast signature secret via resolvePublicCred (HR#11)
The secret-scan ratchet only ran once the earlier Fast Quality Gates steps
stopped failing, and it lands at 1 finding vs baseline 0.
The finding is open-sse/services/raycast.ts:19 —
RAYCAST_DEFAULT_SIG_SECRET, a 64-hex request-signature secret that #8895
committed as a bare string literal. It is genuinely public (community-extracted
from the Raycast macOS client; the SAME value ships to every install, it is not
a per-user credential), which is exactly the category Hard Rule #11 governs:
public upstream credentials MUST go through resolvePublicCred()
(open-sse/utils/publicCreds.ts), never a literal — see
docs/security/PUBLIC_CREDS.md.
So the fix is the mandated pattern, not a .gitleaks.toml allowlist entry: added
`raycast_sig_secret` to EMBEDDED_DEFAULTS as the XOR-masked byte sequence and
resolved it with the existing RAYCAST_SIG_SECRET env override. The
providerSpecificData.sigSecret override is untouched. Verified the decoded value
is byte-identical to the literal it replaces.
check:secrets secretFindings 1 -> 0. check:public-creds exit 0.
publicCreds 12/12, raycast-auth 6/6, raycast-local-extract 1/1,
trae-publiccred 3/3. typecheck:core exit 0.
Refs #9298
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* feat(dashboard): add RADAR_ENABLED flag (default off)
* feat(db): radar feed cache + settings with encrypted supporter key
* feat(radar): signed feed sync with pinned key and version floor
- feedSchema.ts: Zod v4 schema mirroring the server feed format
(discriminated union on budget.kind, enum constraints, etc.)
- pinnedKeys.ts: Ed25519 SPKI-DER pinned key + env override for forks
- verify.ts: signature verification over exact wire bytes, never throws
- sync.ts: full download/verify/validate/cache pipeline with injectable
deps, feature-flag gate, opt-in gate, version floor (numeric compare),
and sanitized error reasons (no stack traces)
- 40 tests covering: contract hash, key handling, sig verification,
schema validation, version compare, all sync paths (disabled, opt_out,
invalid_signature, invalid_schema, stale, updated, error), auth header
injection, and cache-untouched assertions for every failure mode
* feat(radar): read-time overlay merge rules over the free catalog
Pure function applyFeed() merges the cached Radar feed over the static
baseline catalog at read time, honoring 4 rules:
1. Feed never overwrites a local override field.
2. enabled:false disables the entry with disabledBy:"radar" provenance.
3. User-added entry NOT in the feed survives untouched.
4. User deletion tombstone prevents feed resurrection.
getRadarCatalog() accessor in index.ts: flag off / no cache / corrupt
payload all fall back to baseline. Valid cache applies the overlay and
returns feed metadata (version, tier, fetchedAt).
TDD: 19 tests (4 rules + dedup + origin + accessor flag/cache/corrupt/
valid/bad-feed + baselineToMergedEntries converter).
* feat(dashboard): radar catalog and guided setup screens
- API routes: GET /api/radar/catalog, POST /api/radar/sync, POST /api/radar/settings
- All gated on RADAR_ENABLED flag (404 when off)
- Error responses via buildErrorBody(), never raw stack/message
- Settings never echoes clear supporter key (masked omr_****<last4>)
- Sync delegates to syncRadar() server-side, never proxies feed URL
- Dashboard pages:
- /dashboard/radar: 4 states (flag off, opt-in pending, empty, populated)
- /dashboard/radar/setup?provider=X: guided setup with steps, key URL, test connection
- Uses existing Card component and next-intl patterns
- Sidebar: radar entry in costs group with icon
- i18n: pt-BR and en keys for radarPage and radarSetupPage namespaces
- Tests:
- radar-api-routes.test.ts: 11 tests (flag-off 404, flag-on shape, error sanitization)
- radar-page-state.test.ts: 5 tests (pure state logic)
- All 90 radar tests pass (including prior 74)
* docs(radar): module doc and flag-off inertia test
Add docs/frameworks/RADAR.md covering the flag gate, the separate data-sync
opt-in and privacy promise, the Ed25519 signature/pinned-key security model,
tiers, the read-time overlay merge rules, and the self-hosting env vars —
plus index entries in CLAUDE.md/AGENTS.md/docs/README.md/REPOSITORY_MAP.md.
Document RADAR_FEED_URL and RADAR_FEED_PUBKEY in .env.example and
docs/reference/ENVIRONMENT.md to satisfy check:env-doc-sync, which was
failing on this branch since the sync.ts commit added the reads.
Add tests/unit/radar-inertia.test.ts as the single canonical place asserting
the "RADAR_ENABLED off => zero behavioral delta" claim end to end: the three
/api/radar/* routes 404, the flag resolves to the definition default with no
override, getRadarCatalog() returns exactly the baseline without touching the
cache, and computeFreeModelTotals() keeps its pinned values with the Radar
module imported alongside it.
* fix(db): renumber radar migration to 135 after collision with 134
The base branch introduced 134_proxy_logs_egress_ip while this branch carried
134_radar_cache_settings; the migration runner rejects duplicate numeric prefixes.
This migration has never been applied to a real database (the PR is unmerged), so
no retroactive isSchemaAlreadyApplied guard is needed.
* i18n(radar): translate radar catalog and setup strings to all locales
The UI-coverage ratchet measures (present - placeholder) / total_en, so the
__MISSING__ sentinels that i18n:sync-ui writes do not count as covered — only
real translations restore the metric. Scoped to this PR's namespaces
(radarPage, radarSetupPage, sidebar.radar*) instead of a bulk sync, which would
have pulled ~978 unrelated pending keys into this diff.
Placeholders and code identifiers verified preserved across all 1682 strings.
* fix(radar): trust the served-tier header instead of the signed body field
The signed feed body always carries tier:"live" by design (one signed
artifact per version — rewriting the field server-side per request
would break the exact-bytes Ed25519 signature). The server now returns
the tier ACTUALLY served via the x-omniroute-feed-tier response
header, so free users on a delayed community snapshot no longer see
"Ao vivo (tempo real)" in the UI.
sync.ts now reads and validates that header (falling back to the
body's tier only when the header is absent or holds an unrecognized
value) and stores the served tier in the cache; index.ts already
surfaces cache.tier to the UI unchanged.
* test(combo): shorten an assert message that exceeded the line limit
The assertion added by #9507 was 104 chars, so prettier reformatted it into
five lines on the next commit that touched the file, pushing it past its
frozen size (3449) and failing check:file-size. The message is shortened
(the issue reference stays in the comment directly above); the assertion
itself is unchanged, and the file is back to 3448 lines and prettier-clean.
* i18n(radar): use the canonical zh-TW glossary terms
The machine translation produced retired renderings the glossary gate blocks:
供應商 for provider (canonical 提供者) and 文檔 for documentation (canonical 文件).
Fixed across the 11 affected radar strings; tests/unit/i18n-glossary-consistency-check.test.ts
is back to 17/17.
* fix(radar): point the default feed URL at the domain that exists
radar.omniroute.dev was a placeholder for a domain that was never registered,
so an out-of-the-box sync would fail DNS resolution for every user. The live
feed is served from radar.omniroute.online (the subdomain the design always
specified), now behind Cloudflare TLS. Forks still override it via
RADAR_FEED_URL.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Acquire admission once after API-key policy, preserve lazy raw-request snapshots, and bind lease settlement to JSON, SSE, abort, deadline, and failure lifecycles. Expose a low-cardinality health summary and preserve non-SSE Ollama errors unchanged.
Add bounded weighted admission with fair queuing, deadline and cancellation handling, exact lease accounting, and a default-shadow runtime. Keep asynchronous resource-pressure shedding as an independent safety fuse and bound request feature estimation.