* fix(api): validate request bodies with Zod in 4 routes — restores the t06 gate
The release-green verdict (#9737) lists check:route-validation:t06 as a HARD
failure and it is STILL red on the current tip: four routes call
request.json() and hand-roll `typeof x === "string"` checks instead of using
Zod, which Hard Rule #7 requires and the gate enforces (it scans source and
has no allowlist).
- src/app/api/plugins/marketplace/install (#9445): InstallBodySchema; the
400 'Missing or invalid name field' response is preserved verbatim.
- src/app/api/services/dario/admin/accounts (#8523): DeleteAccountBodySchema
for the optional { alias } DELETE body; query-param path untouched.
- src/app/api/services/dario/admin/login-start (#8523): LoginStartBodySchema;
trimming now happens in the schema, so the forward body is unchanged.
- src/app/api/services/dario/admin/import-from-omniroute (#8523):
ImportBodySchema for connectionId/alias; invalid shapes fall back to the
same 'connectionId is required' 400 as before.
All four keep their exact status codes and messages — this is a validation
mechanism swap, not a contract change (plugins route suite still 33/33).
Adds tests/unit/route-body-validation-t06.test.ts, which runs the gate's own
rule inside the unit suite so the next such route fails on ITS OWN PR instead
of surfacing weeks later in a base-red sweep. Guard verified by mutation:
renaming .safeParse( in one route makes it fail (1 fail), restored from a
pre-probe copy.
Gates: route-validation:t06, file-size, test-discovery, mutation-test-coverage,
dead-code exit 0; typecheck:core clean; eslint clean.
Refs #9737
* fix(memory): register the sqlite backend on the /api/memory/[id] route — every handler 500'd
GET/PUT/DELETE /api/memory/[id] threw `Primary backend "sqlite" not
registered` and returned 500. #8752 (MemoryBackend provider pattern) wired the
route to `@/lib/memory/manager` directly, but the registry is populated by an
import-time side effect in the module INDEX (src/lib/memory/index.ts:23,
`memoryManager.register(sqliteBackend)`). Importing the bare manager gives an
empty registry.
In production the failure is order-dependent, which is why it went unnoticed:
if /api/memory (which imports the index) is hit first in the same process, the
singleton is already populated and [id] works. Reached first — the common case
for a client that edits a known memory id — every request 500s. The sibling
route is the only other consumer and already imports the index; this was the
lone direct-manager import in src/.
- Fix: import from `@/lib/memory` (index) with a comment stating WHY the
indirection matters, so the next refactor does not simplify it back.
- Guard: tests/integration/memory-route-put.test.ts already covered this and
was failing 2/5 on the base (it only surfaced now because the integration
suite runs on the release-PR CI, not per-PR). Now 5/5.
Also fixes a test-isolation defect in the same run:
tests/integration/combo-matrix/context-relay-codex.test.ts reused one combo
name across both tests, and the control failed with `UNIQUE constraint failed:
combos.name` — resetStorage() unlinks the DB file but the previous
better-sqlite3 handle keeps writing to the same inode. Gave the control its own
combo name and parameterized the request builder; the assertion is unchanged
(it never depended on the name). 2/2.
Integration suite on this tip: 936 tests, 32m19s — under the 40min ceiling the
old verdict reported as exceeded (#9737 item 6), which the migration-135
collision was causing.
Refs #9737
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
The release-green verdict (#9737) lists check:route-validation:t06 as a HARD
failure and it is STILL red on the current tip: four routes call
request.json() and hand-roll `typeof x === "string"` checks instead of using
Zod, which Hard Rule #7 requires and the gate enforces (it scans source and
has no allowlist).
- src/app/api/plugins/marketplace/install (#9445): InstallBodySchema; the
400 'Missing or invalid name field' response is preserved verbatim.
- src/app/api/services/dario/admin/accounts (#8523): DeleteAccountBodySchema
for the optional { alias } DELETE body; query-param path untouched.
- src/app/api/services/dario/admin/login-start (#8523): LoginStartBodySchema;
trimming now happens in the schema, so the forward body is unchanged.
- src/app/api/services/dario/admin/import-from-omniroute (#8523):
ImportBodySchema for connectionId/alias; invalid shapes fall back to the
same 'connectionId is required' 400 as before.
All four keep their exact status codes and messages — this is a validation
mechanism swap, not a contract change (plugins route suite still 33/33).
Adds tests/unit/route-body-validation-t06.test.ts, which runs the gate's own
rule inside the unit suite so the next such route fails on ITS OWN PR instead
of surfacing weeks later in a base-red sweep. Guard verified by mutation:
renaming .safeParse( in one route makes it fail (1 fail), restored from a
pre-probe copy.
Gates: route-validation:t06, file-size, test-discovery, mutation-test-coverage,
dead-code exit 0; typecheck:core clean; eslint clean.
Refs #9737
Co-authored-by: diegosouzapw <diegosouzapw@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>
* feat(radar): sync referral links from standalone /v1/referrals/latest feed
Referral links previously came from the catalog feed cache, which on the
community tier can be up to 30 days stale -- a newly-added referral would
not reach a free/community user for up to a month. Adds a new sync module
(syncRadarReferrals), Ed25519-verified feed schema, and a dedicated
radar_referrals_cache table (migration 142) so referrals sync on their own,
much shorter cadence instead of inheriting the catalog's delay.
getRadarReferrals()/getDefaultReferralFor() now read the new cache instead
of the catalog feed's embedded referrals field (kept on RadarFeedSchema for
backward-compat with already-cached catalog feeds, but no longer read).
* feat(radar): wire sync-on-read + scheduler side-sync for referrals
GET /api/radar/referrals now triggers syncRadarReferrals() inline whenever
the cache is missing or older than 1h (shouldSyncReferralsOnRead), so fixed
links show up promptly on the next dashboard load instead of waiting on a
background timer. The route itself still never talks to the upstream feed
server directly -- syncRadarReferrals() remains the only network touchpoint.
radarSchedulerTick() also evaluates referrals staleness on the same hourly
tick used for the catalog, independent of the catalog's own due-ness, as a
best-effort side effect that never changes RadarTickResult's shape and is
swallowed on error.
* docs(radar): document the standalone referrals feed sync
Explains the /v1/referrals/latest feed, its no-tier-field-in-body design
(x-omniroute-feed-tier header is the only tier source), the sync-on-read +
scheduler side-sync triggers, and the self-hosting note for forks that only
serve the catalog feed.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* feat(radar): shared supporter-key format validator
Extract the "omr_" + 40 hex supporter-key regex out of the
POST /api/radar/settings Zod schema into a pure, client-safe helper
(src/lib/radar/supporterKey.ts) so the format rule lives in exactly one
place and the upcoming activation-screen input can reuse it for a
UX-only pre-check. Server-side Zod validation stays authoritative.
Adds regression coverage: both directions of the format check, a
combined opt-in+supporterKey POST persisting both fields with the key
always masked (never raw) in either the POST or GET response body, and
a flag-off inertia case for the same combined payload shape.
* feat(dashboard): paste-key input on the Radar activation screen
The Radar activation screen had opt-in and the two "get a key" claim
buttons, but nowhere to paste a key someone already has — the last
piece of the supporter flow. Add the field to the activation screen
itself, as the primary path: pasting a key and submitting sends
POST /api/radar/settings with { optIn: true, supporterKey } together,
so pasting a valid key both sets it and unlocks the screen in one step.
Client-side format validation (via the shared isValidSupporterKeyFormat
helper) is a UX nicety only; the server's Zod schema already validates
authoritatively. When a key is already set (hasSupporterKey from
GET /api/radar/settings — e.g. set out of band before this UI existed),
the screen shows the masked form instead of an empty input, with a
"change key" control to paste a new one; the raw key is never
displayed. The existing plain "Activate" button (no key, community
tier) and the two claim/plans buttons are unchanged and still present
below, so all three paths to this screen coexist.
Adds 4 new i18n keys (keySectionTitle, keyInvalidFormatError,
activateWithKeyButton, changeKeyButton) with an English fallback across
all 43 locale files (172 entries) — no __MISSING__ sentinel, no price.
* docs(radar): close the paste-key-input known gap
RADAR.md documented a known gap: the activation screen had no
dedicated key-paste input, only the two claim/plans buttons. That gap
is closed — describe the new input, the combined opt-in+supporterKey
submission, and the masked-key "already activated" state instead.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Two files both claimed migration version 135:
135_connection_runtime_state.sql (#9449, landed 2026-08-07) and
135_migrate_model_capability_max_token.sql (#8908, landed 2026-08-05).
#9449 branched before #8908 merged and never got renumbered before
landing on release/v3.8.50.
This is not cosmetic: getMigrationFiles() throws "Migration version
collision detected" the moment ANY code path first touches the
database (getDbInstance() -> runMigrations()), which means a
completely fresh install/deploy from this branch cannot even boot —
confirmed live against a freshly built container while testing
unrelated live-verification tooling.
Renumbered the later-landing file to 140 (the next free slot) and
added the matching isSchemaAlreadyApplied("140") retroactive guard in
migrationRunner.ts, so a DB that already ran this migration under the
old 135 number isn't treated as needing a fresh application. This
matches the established pattern already used for the prior 135/136 ->
137/138 renumber in the same file (also caused by the same recurring
branch-before-merge numbering race).
Test plan:
- TDD: new tests/unit/migration-135-numbering-collision.test.ts (2/2)
— spins up a hermetic fresh DB and confirms getDbInstance() applies
every real on-disk migration without throwing, plus confirms both
formerly-135 migrations' effects are present. Confirmed failing
(reproducing the exact live crash) with the pre-fix colliding
filenames restored, passing after the rename.
- npm run typecheck:core — clean
- npm run lint — clean
- npm run check:file-size — clean (migrationRunner.ts rebaselined
1084->1094 for the new guard case)
- Full migration-runner + migration-numbering test suites (64 tests
across 6 files) — all pass, no regressions