Commit Graph

378 Commits

Author SHA1 Message Date
Paijo
2ddbbc61a6 [v3.8.50] feat(memory): MemoryBackend provider pattern with generic HTTP connector (#8752)
Validated in local merge-train T7 (ungrouped batch 2)
2026-08-06 06:06:29 -03:00
Diego Rodrigues de Sa e Souza
e7f6b1d130 feat(radar): flag-gated signed free-model catalog overlay (#9515)
* 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>
2026-08-06 05:58:58 -03:00
Diego Rodrigues de Sa e Souza
04683029a6 fix(build): exec native esbuild binary directly in prepublish (dast-smoke base-red) (#9558)
* fix(build): exec native tool binaries directly in runBuildTool

#8858 routed every resolved local bin through process.execPath to avoid
Windows .cmd shims — but esbuild >=0.25 ships bin/esbuild as the NATIVE
platform executable (ELF on Linux), so Node parsed machine code as JS and
build:cli died with 'SyntaxError: Invalid or unexpected token', turning
dast-smoke red for every PR.

runBuildTool now sniffs the entry's magic bytes (ELF / Mach-O / PE) and
execs native binaries directly; JS entries keep going through this Node
binary (the .cmd-shim avoidance #8858 wanted).

Validation (RED->GREEN on this box):
- RED: node node_modules/esbuild/bin/esbuild --version -> SyntaxError (ELF)
- GREEN: the exact failing CI step reproduced via the new logic bundles
  open-sse/mcp-server/server.ts successfully (4.2MB output, 1.3s).

* fix(docs): add MDX frontmatter to the 20 remaining docs without it

Same failure class as AGENTROUTER_WAF (#9503) and DOCKER_RELEASE_CHANNELS
(this run's dast-smoke red): any doc without frontmatter breaks the
fumadocs MDX loader during next build, killing build:cli/dast-smoke for
every PR. Swept ALL of docs/ (i18n mirrors excluded) in one pass so this
class cannot recur one file at a time.

* docs(env): document OMNIROUTE_INTERNAL_SERVICE_TOKEN(+_FILE), OPENROUTER_PROVIDER_STATS_* and embedded-Redis binding vars

Pre-existing env/docs contract drift from recently merged features made
check:env-doc-sync red for any docs-touching PR. Values and defaults read
from the defining modules (internalServiceAuth.ts, openrouterProviderStats.ts).

* fix(build): resolve bundled npm-cli.js in the standard Unix layout + safe npm fallback off-Windows

The opencode-plugin step hard-failed on GitHub runners because
resolveBundledNpmEntry only looked next to the node binary (Windows zip
layout); hostedtoolcache Node keeps npm at <prefix>/lib/node_modules/npm.
Added that candidate, and when neither exists on non-Windows the step now
falls back to plain 'npm' — the .cmd-shim hazard #8858 avoids is
Windows-only.

* test(mutation): register xai-agent-tools-passthrough.test.ts in stryker tap.testFiles

The test landed on release/v3.8.50 covering
open-sse/handlers/chatCore/passthroughHelpers.ts without the stryker
registration, so Fast Quality Gates' drift detection reds any PR that
carries it. Mechanical registration so its mutant kills count.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 02:19:57 -03:00
Diego Rodrigues de Sa e Souza
e12d2d546b fix(build): resolve npm-cli.js on POSIX layouts in the shim-free prepublish resolver (#9553)
* fix(build): resolve npm-cli.js on POSIX layouts in the shim-free prepublish resolver

The #8858 resolver only tried <dir(node)>/node_modules/npm/bin — the
Windows layout. On POSIX (GitHub hosted runners, nvm, system installs)
npm lives at <prefix>/lib/node_modules/npm while node is <prefix>/bin/
node, so resolveBundledNpmEntry returned null and npm run build:cli
died installing @omniroute/opencode-plugin deps on every fresh checkout
('npm-cli.js not found next to the running Node binary') — redding Fast
Production Build and dast-smoke for the whole PR queue.

Extract the resolver to scripts/build/resolveNpmEntry.ts with injectable
seams and try, in order: npm_execpath (exported by npm run itself), the
Windows beside-the-binary layout, the POSIX <prefix>/lib layout.

TDD: tests/unit/build/resolve-npm-entry.test.ts — the POSIX-layout and
npm_execpath cases plus a live regression guard fail against the old
single-candidate logic (2/5) and pass with the fix (5/5).

* docs(env): register the 7 env vars orphaned by the 08-05 merge batch

The Docs Gates env/docs contract went red on the release tip: #9260
added OMNIROUTE_INTERNAL_SERVICE_TOKEN(_FILE) and #9324 added
OPENROUTER_PROVIDER_STATS_ENABLED/_TTL_MS without .env.example entries,
and the #9286 Redis sidecar vars (REDIS_BIND_HOST, REDIS_PORT,
OMNIROUTE_REDIS_BIND_HOST) never reached ENVIRONMENT.md. Inherited
base-red on every open PR. Defaults and descriptions taken from the
consuming source files.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 01:13:42 -03:00
Tech Guy
9751821338 chore(quality): add an RTL layout ratchet (#8828)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:46 -03:00
Mauricio Antonio Sevilla Britto
ad82c81c38 fix(build): support npm v11 allowScripts for optional native deps (#8877)
Validated in local merge-train T5 (base49+contributors+pacocartones)
2026-08-06 00:08:23 -03:00
Aman
d2f3c1abf5 fix(docker): bundle LLMLingua optional dependencies (#9185)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:52:10 -03:00
Aman
1b2a72ebc8 feat(docker): publish next from active release branches (#9181)
Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc)
2026-08-05 23:51:50 -03:00
Diego Rodrigues de Sa e Souza
8180b49ce1 fix(quality): 2 production bugs + 24 unit base-reds + measured gate ceilings (#9529)
* fix(quality): resolve net-new lint errors and allowlist #9343 assert rewrite

Two `no-explicit-any` errors landed with #9407 and #9320 after the
suppressions inventory was generated. Project policy is to fix new
violations rather than freeze them, so both are typed instead:
  - #9407: `executor as unknown as Record<string, unknown>`
  - #9320: `(k: { name?: string })`

Also allowlists the net-assert reduction in web-tools-translation-2820
(39->35). #9343 inverted the contract — bare JSON must no longer be
promoted to tool_calls without an explicit <tool> envelope — so the
tests were rewritten to assert non-promotion, which costs fewer asserts
than validating a promoted object. More restrictive, not weaker.

* fix(quality): raise integration ceiling to 40min and unpin codex-cli version in test

The integration gate's 20min ceiling killed a healthy run: measured 22m08s
hermetic on an idle 16-core box (935 tests across 112 files, strictly serial
at --test-concurrency=1 because ~16 of them bind a port or share a DB). The
"~3-10min" estimate in the code was stale by ~3x. 40min keeps the ceiling's
real purpose — turning a genuine hang into a visible failure — without
failing a long-but-healthy suite.

Also fixes a base-red in chat-pipeline: 564c204efe bumped
DEFAULT_CODEX_CLIENT_VERSION to 0.146.0 but the User-Agent assertion still
pinned 0.144.1. The line two above already read the constant via
getCodexClientVersion(); this one duplicated the literal. Deriving it from
the same source stops the next bump from breaking the test again.

* fix(ratelimit): re-arm Bottleneck reservoir heartbeat after updateSettings

Bottleneck 2.19.5 (frozen upstream dependency, no release since 2019) has a
bug in LocalDatastore#_startHeartbeat() (node_modules/bottleneck/lib/
LocalDatastore.js:29,56): the guard `if (this.heartbeat == null && ...)`
only (re)creates the periodic reservoir-refresh interval the first time it
runs. Every later call -- including the one updateSettings() itself
triggers internally -- falls into the else branch and does
clearInterval(this.heartbeat) WITHOUT resetting the reference back to
null. Because the stale reference sticks around, every future
_startHeartbeat() call keeps taking the same dead else branch: the
periodic reservoir refresh is gone forever after the first manual
updateSettings() call on a limiter.

Every limiter created by this file starts with a live heartbeat
(buildLimiterDefaults() always sets reservoirRefreshInterval/
reservoirRefreshAmount), so the very first updateFromHeaders() /
updateFromResponseBody() / applyRequestQueueSettings() call against a
limiter permanently kills its refresh. In production this wedges the
request queue once the reservoir hits 0: an auto-enrolled apikey
connection accumulates its default 60 requests, the reservoir zeroes, the
queue freezes for ~120s, the watchdog fires a synthetic 502
(RATE_LIMIT_QUEUE_WEDGED), the connection cools down and gets excluded
from weighted combo pools -- turning a configured 70/30 split into
~50/50.

Add applyLimiterSettings(), a module-local wrapper around
limiter.updateSettings() that nulls the stale heartbeat reference and
re-invokes _startHeartbeat() afterward so it takes the "start a fresh
interval" branch again. Route all 5 updateSettings() call sites through
it (updateAllLimiterSettings, both updateFromHeaders() branches,
loadPersistedLimits(), and updateFromResponseBody()). updateAllLimiterSettings
is now async and awaited by its two callers (initializeRateLimits,
applyRequestQueueSettings); the sync call sites use the existing
trackAsyncOperation() fire-and-forget tracking pattern.

tests/integration/combo-matrix/weighted.test.ts is the E2E proof: the
"weighted: 70/30" case now passes with zero WEDGED/RATE_LIMIT_QUEUE/502
log lines across 200 sequential requests (previously the wedge/recovery
cycle inflated its runtime and skewed the distribution toward ~50/50).

Refs #8213

* fix(tests): remove stray TDD probes committed by accident in f4e93f339d

Three TDD repro/probe test files landed on the release tip via
f4e93f339d (docs: add management authentication terminology guide,
files from a worktree. Each file is a pre-fix TDD probe that belongs
to a *different*, still-in-flight fix branch/PR and duplicates a file
path that PR already owns and will properly update on merge:

- tests/unit/authz/probe-9033-repro.test.ts: probe for #9033 (IP
  blacklist direct-connection bypass). 3/4 asserts fail against this
  tree (D1, D2, Bonus — all assert the not-yet-implemented target
  behavior); D3 passes (pre-existing behavior). Owned by PR #9385
  (open, unmerged), which modifies this exact path.
- tests/unit/repro-8522.test.ts: probe for #8522 (absolute file-size
  baseline reds innocent PRs on inherited drift). First test fails
  against this tree's evaluateFileSizes (still absolute-only); second
  (sanity: real growth still flags) passes. #8522 is actually CLOSED
  upstream — PR #9355 merged the real fix into release/v3.8.50 today
  (2026-08-05T15:53Z) modifying this exact path — but this branch's
  merge-base with release/v3.8.50 (6b0e11e378) predates that merge,
  so the fix has not synced into this tree yet.
- tests/unit/repro-8956.test.ts: probe for #8956 (resolveProjectRoot
  stops at synthetic Next.js standalone package.json). First test
  fails against this tree; second (sanity: named package.json still
  resolves) passes. Owned by PR #9354 (open, unmerged), which
  modifies this exact path.

Each deleted file's real implementation + passing version already
exists in its owning PR and will land normally through that PR's own
merge — deleting the premature copy here does not lose any coverage.

No config/quality/test-masking-allowlist.json entry was added: the
_deletedWithReplacement schema only supports `replacement` (a test
file that must already exist in this tree's HEAD — none does, the
real versions live in the unmerged sibling PRs above) or `sourceRemoved`
(production files that must be absent from HEAD — they are not, none
of the three issues are implemented in this tree). Neither shape fits
an "owned by an in-flight sibling PR" deletion, so the CI test-masking
gate will flag these 3 deletions for mandatory human review on this
branch's next PR diff against release/v3.8.50 — flagged for the owner
rather than inventing a new allowlist shape.

Refs #9033, #8522, #8956, #7786

* fix(tests): align 8189-classifier-compat with #9276 always-mode semantics

tests/unit/8189-classifier-compat-auto-narrow.test.ts was a test-sibling
forgotten when #9276 (commit 6b531fbacd) removed the unconditional
`if (mode === "always") return true` branch from
shouldDefaultAllowClassifier(). tests/unit/claude-classifier-compat.test.ts
was updated in that same commit; this file was not.

Old contract: 'always' mode short-circuited every Claude-format request
unconditionally (operator opt-in was treated as sufficient on its own).
New contract: 'always' now requires the same SECURITY_MONITOR_MARKER
system-prompt text as 'auto' — the marker-optional behavior let a normal
chat request through /v1/messages be silently swallowed by an operator's
'always' opt-in.

The single 'always' test (1 assert, no-marker body expecting true) is
replaced by two tests mirroring the depth already used for 'auto' mode
in the same file: no-marker/false and marker-present/true. Net effect is
+1 assert, not a reduction — the new pair verifies both directions of
the narrowed contract instead of only the now-incorrect unconditional
case.

Before: 3/4 pass (the 'always' test failed: expected true, got false).
After: 5/5 pass.

Refs #9276

* fix(tests): align deepseek-web-tools-execute with #9343 tool envelope contract

tests/unit/deepseek-web-tools-execute-2820.test.ts (executor level) was a
test-sibling forgotten when #9343 (commit d969555417) hardened tool-call
parsing: bare JSON with no explicit <tool>/<tool_call> envelope is never
promoted to tool_calls anymore (previously it was, whenever a tools[] set
was requested — a security gap allowing prose/code-fenced JSON echoed
back by the model, or a copy-attack, to trigger real tool execution).

Three siblings were updated in the same commit: web-tools-translation.test.ts
and web-tools-translation-2820.test.ts (parseToolCallsFromText, the shared
translator), and deepseek-web-tools-variants.test.ts (parseDeepSeekToolCalls,
deepseek-specific parser) — all inverted their bare-JSON assertions to
`toolCalls === null` + `content === text` (preserved verbatim, not stripped).
This file calls the executor's execute() (full HTTP round trip through
buildToolAwareResult), so it was not touched by that diff and kept
asserting the old contract (finish_reason: "tool_calls", content: null).

Verified against source (open-sse/executors/deepseek-web.ts
buildToolAwareResult): when parseDeepSeekToolCalls returns toolCalls=null,
hasCalls is false, so finish_reason is "stop", message.tool_calls is never
set, and message.content is the parser's returned content — which for text
with no <tool>/<tool_call> tag at all is the original string, unchanged
(parseToolCallsFromText's early-return branch). The test now asserts
exactly that shape, at the same executor level as the rest of the file's
tool_calls that make sense at that level as the rest of the file's tool_calls
Refs #9343

* fix(tests): align visionBridge tests with #8430 contract (partial — see note)

Two test-siblings were forgotten when #8430 (commit 7e55abbc41) hardened
Vision Bridge's vision-model selection: getBestVisionModel() now validates
that a candidate has a usable active connection (hasUsableCredentialsForModel,
DB-backed) before returning it, instead of unconditionally returning the
fixedModel or a hardcoded "openai/gpt-4o-mini" default. Three siblings were
updated in the same commit (visionBridgeRouter.test.ts, the new
repro-8430.test.ts, vision-bridge-preserve-on-failure-4012.test.ts); these two
were not.

tests/unit/guardrails/visionBridgeHelpers.callVisionModel.test.ts (8 failures,
all "No vision-capable provider connected"): callVisionModel()'s `routerConfig`
param only merges into getBestVisionModel's CONFIG argument, never its `deps`
argument, so there is no way to inject a credentials stub through this
function's public signature (unlike the guardrail class and getBestVisionModel
itself, which do accept an injectable `hasUsableCredentials`). These tests
exercise callVisionModel's own request/response handling, not credential
routing (already covered elsewhere), so the fix seeds one real usable
`provider_connections` row per provider the file exercises (openai, anthropic)
via createProviderConnection in a test.before() hook, with resetDbInstance()
in test.after() per the DB-handle-cleanup convention. All 8 now pass.

tests/unit/guardrails/visionBridge.test.ts (7 failures): 1 of the 7 (VB-S03)
is a genuine forgotten-contract case, fixed here — same semantic flip already
applied to vision-bridge-preserve-on-failure-4012.test.ts: in the combo
describe path, when EVERY describe call fails, the raw image is now replaced
with an "(unavailable)" stub instead of preserved, because that path is only
reached for confirmed non-vision targets. Assertions inverted to match
(imagePart undefined, unavailable-stub present), same assert count, no
weakening.

*** THE OTHER 6 (VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10) ARE
DELIBERATELY LEFT FAILING. *** These are NOT a #8430 contract change — root-
caused to what looks like a separate, unintentional regression: the ONE call
to getBestVisionModel() in visionBridge.ts's whole-request-reroute path
(line 244, `getBestVisionModel({ fixedModel: configuredModel })`) does not
pass a `deps` second argument, so it always uses the real DB-backed
hasUsableCredentialsForModel instead of this.deps.hasUsableCredentials — even
though the two adjacent checks in the very same function (`checkCreds(model)`
at line 226, `checkCreds(bestModel)` at line 246) DO honor the injectable
override. In this suite's empty-but-readable isolated test DB, that real
check deterministically returns `false` (not the indeterminate `null` the
file's own createGuardrail() comment says these tests rely on: "Fail-open
(null) so classic VB-S01/S07/S10 reroute tests keep working without a live
credential DB"), so getBestVisionModel silently returns null, the reroute
branch's `if (bestModel && ...)` guard never fires, and every test that
expects a reroute observes a silent no-op instead.

Evidence this is a source gap, not a test that needs updating:
- The file's own pre-existing comment names VB-S01/S07/S10 as tests the
  `null` fail-open default is SUPPOSED to keep green.
- VB-CRED-01/02 (the file's only two tests that actually inject a non-default
  hasUsableCredentials mock) both pass today, but neither one's assertions
  distinguish "mock honored" from "mock ignored, real check also says no" —
  they don't prove the threading works, they just don't happen to notice it's
  missing.
- visionBridgeRouter.test.ts, repro-8430.test.ts, and
  vision-bridge-preserve-on-failure-4012.test.ts (22 tests, all green) all
  either call getBestVisionModel directly with explicit deps, or mock
  callVisionModel wholesale (bypassing getBestVisionModel entirely) — none of
  them exercises this exact call site through the guardrail's own deps.

Per instructions, this was intentionally NOT "fixed" by weakening these 6
tests' assertions (that would mask the gap) or by seeding fake DB credentials
to route around it (that would hide a real production DI inconsistency behind
a test-only workaround) or by touching src/lib/guardrails/visionBridge.ts
(a production behavior change outside a test-alignment task's scope, and
Hard Rule #18 requires its own TDD/validation cycle). Flagging for the owner:
the likely one-line fix is threading `{ hasUsableCredentials:
this.deps.hasUsableCredentials }` as getBestVisionModel's second argument at
visionBridge.ts:244, mirroring the two adjacent call sites in the same
function.

Before: 15 failures (7 + 8). After: 9 pass added (1 + 8), 6 still fail
(unchanged, by design).

Refs #8430

* feat(quality): add strayFromCommit deletion allowlist form to test-masking gate

The deletion allowlist supported two shapes: replacement (test rewritten
elsewhere) and sourceRemoved (feature deleted). Neither fits a third
legitimate case surfaced today: test files that entered the repo BY
ACCIDENT — commit f4e93f339d (#7786 docs) swept another session's
worktree artifacts into the release, including TDD probes owned by open
fix PRs (probe-9033-repro -> PR #9385, repro-8956 -> PR #9354,
repro-8522 -> PR #9355). Those probes fail by design until their owning
PR merges, so every unit run on the release tip broke on them.

The new strayFromCommit form is verified, not trusted: the gate asks git
which commit actually ADDED the file (git log --diff-filter=A) and only
exempts the deletion when it matches the declared hash; a non-empty
reason naming the owning PR/issue is mandatory. Also allowlists the
deepseek-web-tools-execute assert reduction (23->21) from ed661f2126 —
same #9343 contract-inversion class as the existing
web-tools-translation entry.

Gate unit tests: 55/55 pass. Full gate vs main: OK.

* fix(guardrails): pass credential deps to getBestVisionModel at reroute call site

The individual-model reroute path in VisionBridgeGuardrail.preCall() calls
getBestVisionModel({ fixedModel: configuredModel }) without its second
`deps` argument, so the router always falls back to the real DB-backed
hasUsableCredentialsForModel instead of an injected
`deps.hasUsableCredentials` override. The two adjacent credential checks in
the same function (the original-model check and the best-model check,
both via the local `checkCreds` binding) already thread deps correctly —
only this middle call, added in #8430, was left out.

Pass the same resolved `checkCreds` used by those two adjacent checks as
`getBestVisionModel`'s deps argument so all three credential checks in this
reroute path stay consistent.

Fixes 6 tests in tests/unit/guardrails/visionBridge.test.ts that depended
on the injected hasUsableCredentials mock being honored on this path:
VB-S12, VB-S12b, VB-S01, VB-S13, VB-S07, VB-S10.

Refs #8430

* fix(quality): raise unit ceiling to 100min and align 2 more forgotten sibling tests

Unit ceiling 45->100min: a hermetic-env measurement on the loaded devbox
(load 7-26) was still inside invocation 1 of 3 at 76min when killed;
contention factor 2-3x measured, no idle measurement exists. The
pre-flight's real condition is exactly that contended one (unit runs in
Promise.all with integration+vitest), and there 45min provably killed a
healthy suite and fabricated a false base-red. The 45min value came from
v3.8.43 as an estimate never validated by measurement. TODO in-code:
re-tighten after an idle run on the .113 box.

Also aligns the 6th and 7th occurrences of the same systemic pattern
(behavior change merged updating only part of the sibling tests):
- issue-7859-gemini-web-redirect-valid: #9407 refined ServiceLogin
  redirects to mean expired session; the #7859 regression coverage is
  preserved via a non-ServiceLogin public redirect variant.
- provider-validation-specialty claude-web 429: #9406 inverted the
  contract (rate-limited session is unhealthy); the dedicated repro file
  owns the full contract, this sibling now matches it.

Also carries the file-size rebaseline for #9323's base.ts growth
(1578->1623, WAF retry + burst guard) and the eslintWarnings baseline
tightened 5000->0 (real measured value with the TS7 suppressions in
place — 5000 left the ratchet inert).

Refs #9407, #9406, #9323

* fix(tests): restore the 3 TDD probes now owned by merged fixes and drop their stray allowlist entries

The base advanced while this PR was open: the real fixes for the three
issues behind the stray probes all merged into release/v3.8.50 —
#9385 (issue 9033), #9355 (issue 8522) and #9354 (issue 8956).

- probe-9033-repro / repro-8522: the base rewrote both probes into the
  regression tests of their merged fixes, so the delete side of the
  rebase conflict was dropped and the base versions kept.
- repro-8956: #9354 only realigned one fixture line in
  auto-update.test.ts (package.json marker now needs a name field) and
  added no test for the new skip-synthetic behavior — the probe is the
  ONLY regression coverage of that merged fix (2/2 green on the base),
  so deleting it would remove real coverage. Restored.

With no test-file deletions left in the PR diff, the three
strayFromCommit allowlist entries are stale and removed. The
strayFromCommit form support in check-test-masking.mjs stays (covered
by its own fixtures).

* fix(quality): rebaseline file-size for PR #9529 own growth

The base sits exactly at the old frozen values, so the base-relative
mode (#8522) does not cover this growth — it is this PR's own:

- open-sse/services/rateLimitManager.ts 1060->1105: the
  applyLimiterSettings() helper that re-arms the reservoir heartbeat
  after updateSettings (Bottleneck 2.19.5 fix, TDD in
  ratelimit-reservoir-refresh.test.ts).
- tests/integration/chat-pipeline.test.ts 1592->1598: codex User-Agent
  derived from getCodexClientVersion() instead of a pinned literal.
- tests/unit/provider-validation-specialty.test.ts 2980->2985: new
  claude-web 429 -> valid:false coverage (#9406).

* fix(docs): sync provider count to 291 in README and CLAUDE

The live catalog counts 291 providers but README.md/CLAUDE.md still
said 290, so the STRICT 'Docs Gates (fast-path)' check reds EVERY open
PR against release/v3.8.50 (verified on #9537/#9539 as well — inherited
base-red, not introduced by this PR). Updated all provider-count
mentions including the section anchor.

* fix(tests): align launch-codex 6312 guard with the async #9454 spawn contract

#9454 made resolveCodexSpawn async (PATH-probes a native codex.exe before
the .cmd shim) and updated its own tests, but left this older sibling
calling the function synchronously — destructuring the Promise yields
undefined and reds Unit fast-path (1/4) for EVERY open PR against the
release (verified on #9537/#9539; inherited base-red). Realigned to the
async contract with an injected probe; keeps the original #6312 fallback
guard plus the only non-Windows codex coverage (now also asserting the
probe never runs off Windows).

* fix(translator): move state-mutating reasoning summary helper out of the pure leaf

#9500 added buildResponsesReasoningSummaryDelta(state, ...) to
pureHelpers.ts, but the function reads AND mutates stream state
(reasoningSummaryIndex map) — violating the leaf contract declared in
the file header ('no host imports, no stream state') and guarded by
response-openai-responses-purehelpers-split.test.ts, which reds Unit
fast-path (4/4) for every open PR (inherited base-red, verified on
#9537/#9539). Moved verbatim to the host next to the other stream-state
helpers (markResponsesReasoningDeltaEmitted); the host was its only
consumer. Behavior unchanged: repro-9500-reasoning-separator 3/3 green,
leaf/host architecture tests green.

* fix(quality): rebaseline openai-responses.ts for the leaf-state relocation

The #9500 helper moved from pureHelpers.ts into the host (previous
commit) grows the host file 1174->1204 while the leaf shrinks by the
same amount — net-zero LOC across the pair, but the per-file frozen
ratchet only sees the growing side.

* fix(tests): let the 9442 cert-mode test see past the harness trust-store guard

tests/_setup/isolateDataDir.ts sets OMNIROUTE_SKIP_SYSTEM_TRUST=1
globally, which makes installCert() return before issuing any command —
so the #9442 install-gap test captured nothing and could NEVER pass
under npm run test:unit (it only passed invoked directly, harness-less;
inherited base-red on Unit fast-path 3/4, verified on #9537/#9539).
Clear the flag for this file only (restored in test.after): safe because
every spawned command is a logging stub on PATH and OMNIROUTE_NO_SUDO=1
strips sudo, so nothing touches the real trust store. 6/6 under the CI
harness including system-trust-test-guard.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-05 22:52:50 -03:00
Diego Rodrigues de Sa e Souza
103bab99d5 fix(cli): prefer IPv4 DNS for spawned servers (#9209)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake).
2026-08-05 22:40:06 -03:00
jowimila
34251a1c37 chore(dev): bump better-sqlite3 and add DB query scripts for provider_connections (Anthropic/Claude debugging) (#9325)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:46:27 -03:00
yutuknown
19181567d4 fix(docker): move entrypoint script to /app to avoid tmpfs masking (#8999)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:42:10 -03:00
Kemji
8b6dbe2a67 fix: add Termux/Android support for playwright-core and better-sqlite3 (#8922)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:41:47 -03:00
Marco
035512585e [v3.8.50] fix(build): prepublish no longer spawns .cmd shims on Windows (#8858)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:41:05 -03:00
Diego Rodrigues de Sa e Souza
1efb94b102 docs: centralize agent instructions in AGENTS.md (CLAUDE/GEMINI point to it) (#9508)
* docs: centralize agent instructions in AGENTS.md; CLAUDE/GEMINI point to it

- AGENTS.md becomes the single source of truth: full CLAUDE.md content (main
  data) merged with the AGENTS.md-only sections (documentation accuracy,
  repository map, review focus, upstream contributions) and the GEMINI.md-only
  file-placement/root-hygiene and local-access rules. Adds the base-green
  check section (PRs must not be born red) and fixes the stale
  _tasks/release-flow path in Hard Rule 21.
- CLAUDE.md: @AGENTS.md pointer + Claude-Code-only operational deltas
  (EnterWorktree, subagent stash-ban replication, superpowers path overrides,
  base-green/sweep-reds pointers).
- GEMINI.md: pointer + Gemini-only notes; the stale 10-item hard-rule mirror
  is removed (source is the 22-rule list in AGENTS.md).
- ci(release-green): label the not-green tracking issue with base-red.

* docs: retarget docs-sync provider/MCP claims to AGENTS.md and fix test placeholder

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-05 19:20:16 -03:00
Diego Rodrigues de Sa e Souza
51efc71af5 fix(docker): ship MITM _internal/ shims and selfsigned package in standalone bundle (#9451)
Closes #9451
2026-08-05 16:47:02 -03:00
Diego Rodrigues de Sa e Souza
7d5e8235da fix(quality): add base-relative file-size check so inherited drift does not red innocent PRs (#8522) (#9355)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-05 12:53:05 -03:00
diegosouzapw
6b0e11e378 refactor: update quality baseline and test masking allowlist
- Updated the quality baseline to set eslintWarnings value to 5000, reflecting the migration to TypeScript 7 and the new warning thresholds.
- Modified the test masking allowlist to account for removed tests and sources, ensuring proper tracking of deprecated features.
- Enhanced ESLint configuration to ignore additional directories containing non-source files.
- Removed the .npmignore file as its contents are now managed in package.json.
- Adjusted KimiWeb model configuration to correctly map K3 to the K2D5 scenario, reflecting changes in the underlying logic.
- Updated artifact packing policy to prevent nested node_modules from being published, ensuring a leaner package size.
- Added tests to verify the exclusion of node_modules from published artifacts and to ensure the integrity of the package.json files array.
2026-08-05 08:46:22 -03:00
nguyenha935
712910612b fix(db): bundle and verify the sql.js fallback (#9044)
Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
2026-08-04 18:06:55 -03:00
MumuTW
2e5854906d docs: slim AGENTS.md (#8839) 2026-08-04 10:05:29 -03:00
Diego Rodrigues de Sa e Souza
b38f3a4c02 feat(test:scoped): add TIA-based local test runner (#8084 D1) (#9143)
- npm run test:scoped: runs only tests impacted by your changes
- npm run test:scoped:staged: for staged changes (pre-commit)
- Uses select-impacted-tests.mjs with impact map when available
- Falls back to heuristic (changed test files) when no map
- Hub file changes suggest full suite
- 7 unit tests for the selection logic

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-01 11:32:04 -03:00
Diego Rodrigues de Sa e Souza
0ef50886ef feat(g1): rewrite combo-strategy check to runtime-import approach (#9131)
G1 (v3.8.51): section (2) of check-known-symbols no longer regex-scans
strategy === "..." literals from combo source. The handled set now comes
from a runtime-imported dispatch registry (open-sse/services/combo/
strategyDispatch.ts) that imports the real ordering functions and enumerates
which strategies they implement. This keeps the canonical-not-handled gate
correct under the upcoming R0.3 registry dispatch, which removes the
strategy === branches the regex relied on.

- Adds HANDLED_COMBO_STRATEGIES registry (all 20 canonical strategies) + binds
  the real dispatch leaves (applyStrategyOrdering, resolveAutoStrategyOrder,
  tryFusionDispatch, tryPipelineDispatch, resolveComboTargetPipeline).
- main() imports the registry instead of reading/sourcing combo files.
- extractHandledStrategies + diffComboStrategies stay exported (pure, tested).
- New TDD test proves the runtime enumeration covers canonical exactly.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-01 11:08:49 -03:00
Diego Rodrigues de Sa e Souza
9b3efef806 fix(ci): five workflow defects, one of them shipping the wrong dmg to Intel Macs (#8988)
* fix(ci): five workflow defects, one of them shipping the wrong dmg to Intel Macs

Gaps 31, 19, 16, 30 and 12 of the v3.8.49 process dossier.

## 31 — LIVE BUG: an Intel Mac downloads the ARM dmg

electron-builder runs once per macOS job and each run emits its own
`latest-mac.yml` listing only its own dmg — measured at 338 and 350 bytes,
different content, identical filename. `download-artifact` with
`merge-multiple: true` resolves that collision by ARRIVAL ORDER, so one silently
overwrites the other. arm64 won in the published v3.8.48.

Why that breaks Intel, from electron-updater's own selection code
(out/providers/Provider.js):

    files.find(it => [...].some(n => n.includes(process.arch))) ?? files.shift()

The Intel dmg is `OmniRoute-X.Y.Z.dmg` — no arch suffix. On Intel `process.arch`
is "x64", nothing matches, and the fallback takes the FIRST entry. With an
arm64-only manifest that is the ARM build.

So ORDER is the fix, not tidiness: the un-suffixed entry must be first, because
it is the only one reachable through that fallback. `merge-multiple` is now off
(per-artifact subdirectories) and a new
`scripts/release/merge-mac-update-manifest.mjs` merges them deliberately. It
refuses to write when the inputs disagree on version — a manifest stitched from
two builds points at files that were never published together, which is worse
than no manifest.

Validated against the REAL v3.8.49 manifests, not just fixtures: the script
reproduces byte-for-byte the manifest I hand-merged and published, including
both sha512 values and the newer releaseDate.

## 19 — one variable, two opposite machines

`USE_VPS_RUNNER` governed the build and the test jobs together. The build needs
the .113's RAM; the tests need the hosted runner's link. Measured 2026-07-29:
`actions/setup-node` took 20m06s on .113 with 4 concurrent runners versus 16s
hosted (npm cache restore saturating the link), while the tests themselves tied
— 2m54 vs 2m31.

Self-hosted is therefore strictly worse for tests, so rather than add a second
variable to configure, `test-unit`, `test-vitest`, `fast-unit` and `fast-vitest`
are pinned to `ubuntu-latest`. `quality.yml`'s `fast-gates` deliberately keeps
the variable — I have no measurement for it, and guessing is what produced this
gap.

## 16 — a flaky shard sent the publish into the 40-minute build

The artifact reuse filter required `conclusion == "success"` on the whole run, so
any unrelated red shard discarded a perfectly good tree. The artifact is only
uploaded if the Build job succeeded, so its PRESENCE is the accurate signal. Now
it takes the 5 most recent candidate runs and tries each download until one
works. `head_repository.full_name == env.REPO` stays — that clause is the
artifact-poisoning guard, not a filter refinement.

## 30 — the gate that could be bypassed at merge

`check:agent-skills-sync` lived only in quality.yml's PR-only Merge-integrity
job, because the CHANGELOG half of that job needs a base to diff against. This
half does not. Keeping it PR-only left a real hole: this cycle's merge trains
landed with `--admin`, which bypasses required checks, so three SKILL.md files
drifted, rode the release squash into `main`, and the sync-back turned them into
a base-red blocking EVERY PR into release/v3.8.50 until #8954. It now also runs
in ci.yml's lint job, which runs on push to `main`.

## 12 — a cancelled gate reads like a passing one

The dashboard already renders ` CANCELLED` per job, so my dossier entry was
imprecise: they do not vanish, they sit buried mid-table. A cancelled job
reported no verdict at all, and this cycle the Vitest job was cancelled in rounds
1, 2 and 3 — it finished only in round 4, revealing a suite broken the whole
cycle plus two production bugs. The summary now opens with a banner naming every
cancelled job and saying plainly that nothing was checked.

    node --import tsx/esm --test tests/unit/mac-update-manifest-merge.test.ts   # 11 pass
    merge against the real v3.8.49 manifests → both dmgs, Intel first
    all four workflows parse; check:workflows --ratchet → 178, baseline 190

* docs(changelog): fragment for #8988

* test(ci): align the artifact-provenance guard with the gap-16 criterion

My own assertion from #8953 encoded the criterion this PR deliberately removes:
it required `.conclusion == "success"` on the whole CI run, which discarded a
perfectly good build tree whenever any unrelated shard went red — pushing the
publish into the 40-minute build the fast path exists to avoid.

Inverted rather than deleted, and the replacement is strictly stronger. It now
pins three things where the old one pinned one: that the loose criterion is gone,
that the step actually probes for the artifact (the accurate signal, since it is
only uploaded when the Build job succeeded), and that it probes MORE THAN ONE
candidate run — without which a single miss still falls back to a full build.

The provenance clause it was originally written to protect
(head_repository.full_name == env.REPO) is untouched and still asserted above.

* fix(ci): finish gap 19 — pin fast-gates and give USE_VPS_RUNNER one meaning

This was left deliberately partial because `fast-gates` had never been measured,
and guessing is what produced gap 19 in the first place. Measured now, and the
evidence is cleaner than expected:

    fast-gates, 160 quality.yml runs .... ZERO self-hosted samples
                                          every non-skipped one is "GitHub Actions NNNN"
    median duration, 72 successful runs .. 5.6 min hosted

The classifier is not at fault — in the same window ci.yml's Build demonstrably
ran on omniroute-113-7 and omniroute-113-6, so self-hosted runs are visible when
they happen. The USE_VPS_RUNNER expression on this job was dead configuration.

And had it ever fired it would have inherited the measured penalty, because this
job's first two steps are exactly the bottleneck:

    actions/setup-node on .113 with 4 concurrent runners .... 20m06s
    actions/setup-node hosted ..............................     16s

So it is pinned rather than switched, and the second variable the gap proposed
(USE_VPS_RUNNER_BUILD / _TESTS) turns out to be unnecessary. After this the
variable governs exactly five jobs, all of them build-like:

    ci.yml:build · quality.yml:build · npm-publish:publish
    nightly-release-green: release-green, main-green

One variable, one meaning: "this job needs the .113's memory". A guard test pins
that — it fails if the variable is ever attached to a test-like job again, and it
also asserts the build KEEPS it, so nobody closes this gap by removing the
variable outright.

    node --import tsx/esm --test tests/unit/vps-runner-variable-scope.test.ts   # 3 pass
    check:workflows --ratchet → 178, baseline 190

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-07-30 12:49:52 -03:00
Diego Rodrigues de Sa e Souza
494b1c961a fix(ci): close six release-process gaps from the v3.8.49 run (#8985)
* fix(ci): stop the reconciliation range and the fragment sweep from hiding work

Two release-tooling defects found during the v3.8.49 run (gaps 4 and 7 of the
process dossier). Both fail by hiding work rather than announcing themselves,
which is why each one had already cost a real mistake.

## The reconciliation range was 62× too wide

`list-uncovered-commits.mjs` bounded its scan with `git describe --tags`. Releases
reach `main` by SQUASH, so no commit on a release branch is ever an ancestor of the
tag, and `vPREV..HEAD` re-lists the un-squashed history of every earlier cycle.
Measured on release/v3.8.50 @ 7eca04fd12:

    v3.8.49..HEAD ....... 1361 commits
    cycle open..HEAD .....   22 commits

The report drowns in noise, and that is how a previous reconciliation let ~200 PRs
through with no CHANGELOG bullet.

The base is now resolved by CONTENT — the oldest commit that introduced this
version string into package.json — deliberately NOT by commit subject, because the
subject has already changed format once:

    chore(release): bump v3.8.49 (development cycle version)     older
    chore(release): open v3.8.50 development cycle               current

A message-matching resolver would have silently reverted to the broken tag base the
first time someone reworded the bump. The fallback now writes a WARNING to stderr
explaining that a tag range re-lists previous cycles, so a shallow clone degrades
loudly instead of quietly reproducing the bug. One of the five tests asserts
exactly that the warning says "squash" and "noise".

## The back-merge resurrects fragments that already shipped

The release lands on `main` as one squash commit, so `main` still carries every
`changelog.d/` fragment the reconciliation folded in and deleted. Back-merging
`main` restores all of them — 191 in the v3.8.49 run. Nothing breaks at that
instant; the next aggregation folds them in a SECOND time and the section grows
duplicates that have to be hand-unpicked.

New `scripts/release/sweep-stale-fragments.mjs` (`npm run sweep:stale-fragments`)
reports them, and `--apply` removes them. Report mode exits 1 so the back-merge
step can gate on it.

The identity rule took two attempts, and the second one exists because running the
script against the live repo refuted the first. Matching on any `#N` in the bullet
flagged `changelog.d/features/8980-deprecate-gemini-cli-provider.md` as stale,
because that bullet cites issue **#7034** for context and #7034 shipped in an
earlier cycle — it would have deleted an unreleased fragment and dropped its
credit. A bullet routinely cites issues it merely references; only the
`<PR-number>-<slug>.md` filename says which PR the fragment *is*. That case is now
a regression test.

Every ambiguous case resolves toward KEEPING: no number in the filename falls back
to normalized text, text shorter than 12 chars is never matched, and anything
matching neither is kept. A surviving duplicate is a nuisance someone notices; a
deleted fragment silently costs a contributor their credit.

    node --import tsx/esm --test tests/unit/release-cycle-base-resolver.test.ts   # 5 pass
    node --import tsx/esm --test tests/unit/sweep-stale-fragments.test.ts         # 11 pass
    node scripts/release/list-uncovered-commits.mjs --json
      → base ed2db6cb19, baseSource "cycle-open", total 22   (was 1361)
    node scripts/release/sweep-stale-fragments.mjs
      → 4 fragments, 0 stale, exit 0

* docs(changelog): fragment for #8985

* fix(ci): four quality gates that punished the wrong thing

Gaps 6, 9, 10 and 23 of the v3.8.49 process dossier. Each one either blocked
work it should have waved through, or reported a number that was never the
code's.

## 6 — test-masking is unusable at release scale

My own dossier entry for this was WRONG and the measurement says so:

    tracked test files ............ 3977      (I had written 1277)
    absolute tautology scan ....... ~1 s      (I had written >30 min)
    the diff uses base...HEAD                 three dots — already merge-base
    diff vs release branch ........ 0 files, 0 s
    diff vs main (today) .......... 3 files, 0 s

The base choice was never the problem, and it cannot be reproduced today at
all: `main` has since received the v3.8.49 squash, so the merge-base is recent.
The pathology only exists DURING a release, in the window before `main` gets the
squash — then the merge-base is the PREVIOUS cycle's fork point and the diff
legitimately spans the whole cycle (~1277 changed test files, each costing a
`git show` process plus a full regex pass). That is the same squash-merge
topology as gap 4, and it is why the check ran twice without finishing.

Fix: above 300 changed test files the per-file diff subchecks are skipped, since
every one of those files was already gated by this check on its own PR. The
absolute tautology scan still runs unconditionally over all 3977 files, so the
floor is untouched. The skip is deliberately loud — a silent skip is gap 12,
which cost two production bugs this cycle. `shouldSkipDiffSubchecks` never skips
on unparseable input, so a broken count cannot disable the gate.

## 9 — a capital letter invalidated 41 translations

`"Reset Defaults"` → `"Reset defaults"` marked the key stale in 41 locales. Every
translation was still correct, and in locales with no letter case the "fix" is
not expressible. Worse, the escape hatch (`__MISSING__:`) is BANNED in `vi` by
tests/unit/i18n-vi-completeness.test.ts, so `vi` had no legitimate way out.

`isCosmeticRewrite` folds case, whitespace runs and trailing punctuation — and
nothing else. Most of the nine tests exist to pin what is NOT cosmetic: a changed
word, an added word, and any edit inside an interpolation like `{count}` all
still flag. Two end-to-end tests hold both directions: a cosmetic edit leaves
every locale alone, a real rewrite still flags all of them.

## 10 — the ratchet compared numbers from two different auditors

`pipx install zizmor` was unpinned, so the runner installed whatever PyPI served
that day and measured 1 finding MORE than the devbox on the identical commit
(190 vs 189) — a second rebaseline push per release, chasing a number that was
never the code's. Pinned to 1.25.2 (what the devbox runs), and
check-workflows.mjs now prints `zizmorVersion=` next to the count so any future
rebaseline is traceable to the tool that produced it.

## 23 — a PR pointed at its own branch

#8912 has head == base == release/v3.8.50: no diff, can never merge, and it sits
in the queue with a full check board on every push to that branch. It survived
because nothing looks wrong — the checks pass, since there is nothing to check.

New guard in the `changes` job (one field comparison, before anything is spent).
The distinction that makes it safe to block on: an equal head/base BRANCH is
conclusive, an equal head/base SHA is NOT — a branch cut moments ago has an
identical tip and is legitimate, so that case warns instead of failing. Half a
signal never fails either.

    node --import tsx/esm --test tests/unit/test-masking-release-scale.test.ts   # 6 pass
    node --import tsx/esm --test tests/unit/ui-value-drift-cosmetic.test.ts      # 9 pass
    node --import tsx/esm --test tests/unit/pr-self-target-guard.test.ts         # 7 pass
    check:workflows --ratchet → 178 findings, zizmorVersion=zizmor 1.25.2, baseline 190
    the i18n suite is unaffected (5 files re-run, all green)

* fix(ci): allowlist the four CI-only env vars the new gates read

The env-doc-sync gate failed three unit shards plus Docs Gates on this PR, and it
was right to: it requires every `process.env.X` read in code to be documented in
`.env.example`, and this PR introduced four new reads.

They do not belong in `.env.example`. That file is OmniRoute's runtime
configuration; these are CI signals with no meaning in a user's `.env`:

    HEAD_REF / HEAD_SHA / BASE_SHA   the `changes` job passes github.head_ref,
                                     github.base_ref and the PR head/base SHAs to
                                     the self-targeting-PR guard
    TEST_MASKING_MAX_CHANGED_TESTS   the escape hatch that raises the test-masking
                                     gate's release-scale skip threshold

So they go in IGNORE_FROM_CODE, which exists for exactly this and already carries
the precedent one line above: `BASE_REF`, allowlisted because CI passes it to the
OpenAPI breaking-change gate. `BASE_REF` being already listed is also why only
four of my five reads failed.

Each entry carries its justification and the script that reads it, per the
allowlist policy.

    node --import tsx/esm --test tests/unit/issue-7793-env-doc-sync-repro.test.ts   # 1 pass
    npm run check:env-doc-sync → all three directions in sync

* fix(i18n): narrow the cosmetic-rewrite exemption to the scope actually reported

The gap-9 fix folded whitespace in addition to case, and that collided with a
pre-existing test which pins the opposite — tests/unit/i18n-ui-value-drift.test.ts,
"a value that only changes whitespace still counts as an edit". Its comment states
the reasoning:

    Conservative on purpose: trailing-space churn is rare, and treating it as a
    no-op would let a real reword slip through behind an innocuous-looking diff.

That is a documented decision by whoever wrote it. The problem actually reported
was CASE — `"Reset Defaults"` → `"Reset defaults"` invalidating 41 correct
translations — and whitespace was scope I added on my own. Reversing someone
else's reasoned call, silently, to fix something nobody reported is not this
change's job, so the exemption is narrowed to case + trailing terminal
punctuation. No test pins either of those.

The reported case is still fixed, verified end to end: that rewrite invalidates 0
locales. And whitespace is now asserted NON-cosmetic in my own test file too, so a
later tidy-up cannot quietly fold it back in.

    node --import tsx/esm --test tests/unit/i18n-ui-value-drift.test.ts     # 11 pass (pre-existing)
    node --import tsx/esm --test tests/unit/ui-value-drift-cosmetic.test.ts # 10 pass

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-07-30 12:41:34 -03:00
Diego Rodrigues de Sa e Souza
7eca04fd12 feat(ci): gate the publish on clean-install AND upgrade-over-previous (#8953)
* feat(ci): gate the publish on clean-install AND upgrade-over-previous

`check:pack-boot` proves a fresh install boots. It does not prove the path that actually
broke us: installing over an existing version, where ~110 SQLite migrations run against a
populated database. v3.8.48 shipped as a hotfix because the published 3.8.47 crashed on
boot, and the v3.8.49 upgrade path was only ever exercised end-to-end by hand — on VPS .16,
against a real 3.8.48 install with a 165 MB database, AFTER publishing. That is backwards.

New gate (`scripts/check/check-install-upgrade.mjs`), wired into npm-publish.yml as step 12,
BEFORE `npm stage publish` — so a broken upgrade never reaches the registry and a staged
package that is never approved simply expires, with no `npm deprecate` needed:

- Phase A: fresh prefix + fresh DATA_DIR, install the packed tarball, boot, health.
- Phase B: fresh prefix + fresh DATA_DIR, install the PREVIOUS published version, boot it
  (creates + migrates the DB), stop, install the tarball over the SAME prefix, boot against
  the SAME DATA_DIR. Asserts no table present before the upgrade was dropped.
- Schema convergence, and its DIRECTION is the whole point:
    fresh − upgraded ≠ ∅  → FAIL. Structure a clean install creates but an upgrade does not
                            means every existing user is missing it. Not allowlistable.
    upgraded − fresh ≠ ∅  → residue; fails only when NEW (allowlist carries the known ones).

A naive symmetric check would either block every release on harmless residue or, if relaxed,
let the dangerous direction through. Measured on VPS .16 (2026-07-30): a real 3.8.48 install
upgraded to 3.8.49 ended with 117 tables against 116 for a clean 3.8.49 install — the extra
being `cache_metrics`, recorded in config/quality/install-upgrade-allowlist.json with the
measurement. Both installs healthy, zero `no such table` in 150 log lines.

`evaluateConvergence` is exported and pure so the asymmetry is testable without packing,
installing or booting anything (same reason check-test-masking exports its helpers):
tests/unit/check-install-upgrade-convergence.test.ts, 8 cases, ~6ms.

A previous version that fails to boot degrades to a warning — a historically bad publish
must not block the current one. Uses node:sqlite (Node 24, already the publish job's
runtime): no new dependency.

* fix(ci): require the reused next-build artifact to come from this repository

CodeQL raised actions/artifact-poisoning/critical on the `next-build` fast path
this PR builds on (#8941). The finding is real and it sits on the path that
produces the published npm tarball.

The step picks a CI run by querying the runs API for `head_sha` and filtering on
`name == "CI" and conclusion == "success"`. That query also returns
`pull_request` runs from FORKS: they execute in this repository's context and
upload their own `next-build`, built from fork-controlled source. Measured
today, 57 runs in this repo have a `head_repository` other than the repo itself.
So the selection trusted bytes by coincidence of commit SHA — anything that made
a fork's head commit coincide with the publish commit could put attacker-built
bytes on npm.

Adds `and .head_repository.full_name == env.REPO` to the selection. Provenance
is now explicit; `head_sha` still carries tree-equality. Verified against the
live API using the expression extracted from the workflow itself — the same
single run (30518663668) is selected either way for the current tip, so the fast
path keeps working while every fork run is excluded.

Not a dismissal (hard rule #14) — the clause removes the flagged trust.

    node --import tsx/esm --test tests/unit/npm-publish-artifact-provenance.test.ts
    # 3 pass, 0 fail        (base: 2 pass, 1 fail)

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-07-30 09:07:57 -03:00
Diego Rodrigues de Sa e Souza
ed6a19e05b fix(ci): raise the git ls-files buffer in check:tracked-artifacts (#8844)
* fix(ci): raise the git ls-files buffer in check:tracked-artifacts

`execFileSync` defaults to a 1 MiB stdout buffer and throws ENOBUFS past it.
`git ls-files -s` on this repo is already at 1,042,494 bytes across 11,091
tracked files — 6,082 bytes from the ceiling. Any PR adding roughly sixty files
crosses it.

That matters more than a failing script: the check runs on pre-commit, so once
the listing crosses 1 MiB, committing breaks for everyone working the repo, not
just for the change that happened to cross it. It is not a hypothetical — the
private EE fork hit it this week when a sync landed ~214 translation files and
pushed the listing 504 bytes over; every commit there failed the hook until
this same fix landed.

Both call sites now share a GIT_LS_OPTS with a 64 MiB ceiling — far above any
plausible tree, rather than just above today's, since the listing only grows.

* docs(changelog): add fragment for #8844

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-28 10:25:21 -03:00
NBN-N3
ff168ab086 fix(electron): use NEXT_DIST_DIR when stripping stale native modules (#8794)
removeNativeModules() was called with a hardcoded ".next" path while the actual
distDir is NEXT_DIST_DIR (".build/next" by default). Because the function
early-returns when the directory does not exist, the cleanup silently no-opped
and the plain-Node-ABI better-sqlite3 copy produced by `next build` survived
into the packaged app.

At runtime the standalone server runs under ELECTRON_RUN_AS_NODE, so it needs
the Electron ABI (148 for electron 43). Loading the ABI-137 copy fails with
ERR_DLOPEN_FAILED, the app falls back to the sql.js WASM driver, the connection
is closed and retried in a loop, WASM memory is never reclaimed and the process
OOMs -> HTTP 500 on every route.

Also adds assertNoStaleHashedNatives() so a wrong baseDir fails the build
instead of silently shipping a broken installer. This has regressed at least
twice (#1497 with ABI 127 vs 145, #7082/#7681 with 137 vs 148).

Refs #7082, #7681, #1497, #8792. Supersedes the abandoned #7123.
2026-07-27 22:38:18 -03:00
MumuTW
0eeb8f45c0 refactor(sse): extract combo target resolution into combo/targetResolution.ts (#8592)
* refactor(sse): extract combo dispatch prelude into combo/dispatchPrelude.ts

Pure move, no behaviour change. First of ~7 PRs decomposing the combo.ts
god-file (#3501).

handleComboChat evaluates a series of dispatch branches before it ever
reaches target resolution or the sequential attempt loop. None of them
iterate targets in priority order or need the failover/retry/credential
gate machinery that follows, so they move to a leaf:

  - context-cache pin routing (Fix #679), including the
    pinIsDurablyUnhealthy / isPinnedModelDurablyUnhealthy health gate
  - fusion panel dispatch + the #6455 misconfiguration warn
  - pipeline chaining
  - nested combo-ref execute-mode runtime-unit dispatch

Only the chaos and round-robin hand-offs stay inline (11 and 13 lines);
extracting those would be pure indirection.

open-sse/services/combo.ts 3642 -> 3341 (-301)
open-sse/services/combo/dispatchPrelude.ts: 619 (under the 800 cap)

Each helper keeps the fall-through protocol the inline blocks had: return
a Response to OWN the request, return null to fall through. A flipped
null/Response would silently bypass the whole combo strategy, so the new
tests pin both directions for every branch.

combo.ts re-exports pinIsDurablyUnhealthy so combo-pin-health-gate.test.ts
keeps resolving. The leaf takes handleComboChat as a `runCombo` parameter
instead of importing it, so combo/ keeps zero back-edges into combo.ts.

Complexity-neutral: the first cut added +3 violations (two
max-lines-per-function, one complexity) inside the new leaf, so
evaluatePinnedResponse, orderRuntimeUnits, recordRuntimeUnitStickySuccess
and buildBaseOptions were split out. check:complexity now measures 2169
and check:cognitive-complexity 956 — identical to the pristine base.

* test(sse): close the dispatch-prelude coverage holes found by mutation testing

An adversarial mutation audit of the suite added in the previous commit
found it guarded the fall-through protocol well but asserted almost
nothing about what the helpers do once they OWN the request. 5 of 12
seeded mutations survived. Worst case: deleting the pinned-model
dispatch call outright left all 12 tests green.

Three holes, now closed (8 tests -> 20):

Hole A — the honored-pin path had zero coverage. Both existing pin tests
DROP the pin, so the dispatch, the 200-but-empty quality gate, the
[408, 429, 500, 502, 503, 504] failover list and the catch(pinErr)
branch were unguarded — exactly the logic the 2026-06-21 / 2026-06-22
incident comments call load-bearing. Adds five tests over a seeded
healthy provider connection so the pin is actually honored.

Hole B — orderRuntimeUnits was only ever driven with `priority`, which
is a no-op through it. Four of five strategy branches could be deleted
with nothing failing. Adds round-robin rotation and weighted sticky
ordering tests.

Hole C — recordRuntimeUnitStickySuccess never did anything under test:
both its guards need weighted/round-robin, so an early return changed
nothing. Covered by the new sticky-batch test.

Verified by re-running the mutations rather than assuming: all 7 that
previously survived (delete-pin-dispatch, serve-despite-failed-quality,
never-fail-over-on-transient, rr-counter-not-advanced, rotation-removed,
weighted-sticky-skipped, sticky-recording-no-op) are now killed.

The first sticky-batch test I wrote was itself vacuous — asserting "same
unit twice" holds equally when the recording helper is stubbed out, since
nothing advances the counter either. It now asserts the batch runs out
and rotation resumes on the third dispatch, which is what actually
distinguishes the two.

Also restores API_KEY_SECRET in test.after; it was set at module load and
never put back, inconsistent with the DATA_DIR handling beside it.

* fix(ci): teach known-symbols gate the relocated fusion/pipeline dispatch

The combo sub-check of check:known-symbols asserts every canonical routing
strategy has a real dispatch branch. It scanned a hardcoded file list and
matched only `strategy === "..."`, so the prelude extraction tripped it twice:

  [combo] 2 estratégia(s) canônica(s) sem branch de despacho em combo.ts:
      ✗ fusion
      ✗ pipeline

Both branches are still wired — they just moved to combo/dispatchPrelude.ts and
took the early-return guard form `if (strategy !== "fusion") return null;` that
extracting a branch into a `tryXDispatch()` leaf naturally produces.

Two changes, both extending existing precedent (the list already carries the
Block J leaves for the same reason):

- register combo/dispatchPrelude.ts in comboDispatchFiles
- widen the extractor to `strategy [!=]== "..."` so the inverted guard counts

Loose `==`/`!=` stay rejected, and no `handledNotCanonical` fallout: the gate
now reports 20 canonical strategies, all 20 via despacho.

* chore(ci): register combo-dispatch-prelude test in stryker tap.testFiles

check:mutation-test-coverage --strict failed once the known-symbols fix let
Fast Quality Gates advance to it:

  ✗ 2 covering unit test(s) across 2 module(s) are missing from
    stryker.conf.json tap.testFiles
      open-sse/services/combo/comboStructure.ts
      open-sse/services/combo/rrState.ts

The new tests/unit/combo-dispatch-prelude.test.ts exercises both modules, and
both are already in stryker's mutate list, so without the registration its
mutant kills would not have counted toward the nightly mutation gate.

Note (unchanged, still out of scope): combo/dispatchPrelude.ts itself is not in
stryker's `mutate` list. Adding it would widen the nightly mutation surface,
which is a separate call from fixing this drift.

* docs(changelog): add fragment for #8582 combo dispatch prelude

* refactor(sse): extract combo target resolution into combo/targetResolution.ts

Pure move, no behaviour change. Lifts the target-resolution stage of
handleComboChat — everything between the dispatch prelude and the attempt
loop — into a new leaf, open-sse/services/combo/targetResolution.ts.

Moved verbatim: provider-wildcard expansion, weighted step-group resolution
+ sticky-weighted eligibility, request-tag routing, the known-context-overflow
early return, the smart/pipeline-enabled auto dispatch, auto-strategy
ordering, per-strategy ordering, cache-strategy affinity, session stickiness,
eval scores, request-compatibility + context-requirement filters, task-aware
reordering, prompt-cache affinity, and the priority-strategy pre-screen.

The three early exits become an { earlyResponse } result so the host decides
to return them (same pattern as resolveAutoStrategyOrder). The values the
attempt loop still reads — orderedTargets, stickyWeightedLimit,
getWeightedStepKeyForTarget, the session-stickiness result and preScreenMap —
are returned instead of closed over. Loop config (maxRetries, retryDelayMs,
fallbackDelayMs, maxSetRetries, setRetryDelayMs) stays in combo.ts.

buildAutoCandidates is dependency-injected because it lives in combo.ts, so
the leaf keeps zero back-edges into its host.

combo.ts 3640 -> 3321 lines; new leaf 484 lines (under the 800 cap).
Part of the #3501 god-file decomposition campaign.

* refactor(sse): split targetResolution into stage helpers, ratchet combo.ts file-size baseline

Follow-up to the target-resolution extraction: the moved region landed as one
311-line function, which converted inline code inside the (already-violating)
handleComboChat into a NEW separately-counted violating function — check:complexity
2169 -> 2171 and check:cognitive-complexity 956 -> 957.

Split resolveComboTargetPipeline along its natural stage boundaries into 14
helpers (wildcard expansion, weighted eviction/eligibility/sticky-key/selection,
step-key mapper, context-overflow response, pool-size log, smart-pipeline dispatch
and its fall-through logger, strategy ordering, continuity filters, task-aware
ordering, prompt-cache enablement/first-target protection/affinity stage). Each
stage takes the previous stage's output and returns the next; still a pure move.

The leaf now contributes ZERO complexity, max-lines-per-function and
cognitive-complexity violations. Both ratchets are back at base 4053e2314 values:
check:complexity 2169, check:cognitive-complexity 956. (Both still print RED
against their frozen ceilings 2130/951 — pre-existing base-red per #8580.)

Also ratchets ONLY the open-sse/services/combo.ts entry in
config/quality/file-size-baseline.json from 3642 to 3322, with a justification
note in the file's existing style. No sweep of unrelated entries.

* chore: stack targetResolution on dispatchPrelude tip, rebank + skills

Rebased onto refactor/combo-dispatch-prelude. Keep both leaves in
check-known-symbols. Regenerate file-size baseline; sync agent skills.

* fix(sse): restore #8494 capability fail-closed after targetResolution extract

Stacking targetResolution onto the dispatchPrelude tip dropped the #8488/#8494
compatFilterFailOpen wiring: hard capability filters emptied the pool into a
generic 404 no_executable_targets, and fail-open never re-admitted the pool.

Restore describeCapabilityFilterExhaustion earlyResponse in
applyContinuityFilters and the matching round-robin path, then rebank the
file-size baseline for tip growth the incomplete prior rebank missed.

* fix(sse): realign model-lockout cooldown options with the post-#8254 type

This branch predates #8254, which renamed the recordModelLockoutFailure option
`exactCooldownVerified` -> `exactCooldownIsUpstreamReset` and changed combo.ts's
predicate from `lockoutHintVerified` (#8393's `lockoutHintMs > 0`) to
`lockoutHintMs > mlSettings.baseCooldownMs`. Rebasing onto the current tip brought
the renamed type without updating these two call sites, so typecheck:core failed
with TS2353 at both.

Restores the base expression verbatim rather than re-wiring `lockoutHintVerified`
under the new name. The base predicate is the correct one: selectLockoutCooldownMs
returns the parsed hint ONLY when `lockoutHintMs > baseCooldownMs`, and otherwise
returns 0 or a synthetic baseCooldownMs — so `lockoutHintMs > 0` would mark a
synthetic cooldown as an upstream reset and let it bypass the #7940 maxCooldownMs
cap, which is the bug #8254 fixed.

---------

Co-authored-by: MumuTW <johnsxn.us@gmail.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-27 19:13:31 -03:00
AmirHossein Rezaei
bb5cb51f3e fix(docker): honor OMNIROUTE_BASE_PATH behind reverse-proxy subpaths (#8615)
* fix(docker): honor OMNIROUTE_BASE_PATH behind reverse-proxy subpaths

Next.js basePath is compile-time state; Docker now records the baked value,
forwards the env var as a build-arg, patches root-path images at container
start when needed, and probes health under the active subpath.

Hard Rule #13: scripts/docker/patch-basepath.sh and the entrypoint invoke Node
with a fixed argv; OMNIROUTE_BASE_PATH is read from process.env only — never
interpolated into sed/awk.

Closes #8600

* fix(docs): unblock CI for Docker basePath guide

Describe the build-time basePath marker as a sentinel file instead of a
fabricated env var, and replace the unsupported ```env fence with bash so
fumadocs/Shiki can compile DOCKER_GUIDE.md during DAST smoke.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(docker): add changelog fragment for #8615 basePath bundle patch

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-27 19:07:58 -03:00
MumuTW
b676c5f826 feat(ci): automate ratchet shrink-banking so caps stop outliving their files (#8584) (#8612)
* feat(ci): automate ratchet shrink-banking on the release branch (#8584)

The quality ratchet is only half automatic, and it is the wrong half. Raising a
cap is a manual JSON edit that takes ten seconds and is the fastest way to unblock
a red PR. Lowering one requires someone to run `--update` and commit the result —
and no workflow does: grepping `.github/workflows/` for `--update` finds only
wiki-sync.yml (unrelated) and ci.yml's check-quality-ratchet.mjs --require-tighten
(a different script against a different metric).

Measured on release/v3.8.49 at 4053e2314: 18 frozen files already at or under the
800-line new-file cap, the worst at 132x (src/shared/validation/schemas.ts, 19
lines carrying a 2,523 cap); the complexity ceiling walked 1794 -> 2169 across ~37
rebaseline notes with exactly one decrease (-1); "tighten via --update next cycle"
written 31 times and honoured once in six weeks. A cap that outlives the code that
earned it converts every completed decomposition into a growth allowance for
whoever edits that file next.

New job `bank-ratchet-shrinks` in nightly-release-green.yml measures the active
release branch, runs the two shrink-only `--update` paths, and opens ONE
always-current PR with the result. Schedule/dispatch only, deliberately not on
push: banking has no latency requirement, while a per-merge run would rebuild the
PR branch during merge campaigns and pay for a full ESLint walk each time.
Detection stays on push (release-green); only banking is batched. The job never
pushes to release/* — a human merges, so a bad measurement cannot land unreviewed.

scripts/quality/verify-ratchet-bank.mjs is the hard guarantee that the automation
can only ever write downward. It diffs the post-`--update` tree against HEAD and
aborts the job before a commit exists — opening no PR — unless every change is a
frozen/testFrozen entry lowered or removed, `count` lowered, or
cognitiveComplexity.value lowered. Raising a number, adding an entry, changing
cap/testCap, or deleting/rewriting a `_rebaseline_*` note all fail. A bot that
could raise a cap would be strictly worse than the status quo.

Verified both directions against the real baselines: --update + verifier reports
77 lowered / 14 removed / nothing raised, exit 0; hand-raising chatCore.ts to 9999
and cap to 1200 is rejected with exit 1. 22 unit tests cover each way the
automation could go wrong.

No product code changes.

* chore(skills): sync cli-backup-sync SKILL.md after rebase onto tip
2026-07-27 19:07:51 -03:00
AmirHossein Rezaei
df9550fce9 fix(cli): prepare Next.js cache dir on Android/Termux before serve (#8593)
* fix(cli): ensure `~/.cache` is created and `XDG_CACHE_HOME` is set before Next.js loads on Android/Termux to prevent silent HTTP 500 errors due to instrumentation hook failures (#8519)

* chore(quality): ignore XDG_CACHE_HOME in the env/docs contract scanner

XDG_CACHE_HOME is an XDG Base Directory spec variable set by the OS or the
operator, never OmniRoute product config — the same reason XDG_CONFIG_HOME is
already ignored. The Android/Termux cache-dir preparation added here reads it
to honor an operator-set cache location, which made check-env-doc-sync demand
an .env.example/ENVIRONMENT.md entry for a variable we do not own.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* build(pack): require bin/cli/utils/ensureAndroidCacheDir.mjs in the tarball

bin/omniroute.mjs imports this module at startup to prepare the Next.js cache
dir before serve on Android/Termux. bin/cli/ is only an allowlist PREFIX, so a
file missing from the tarball would not fail the unexpected-paths check — it
would ship a CLI that throws ERR_MODULE_NOT_FOUND on the very platform this
change targets. Registering it makes the absence loud, same guard class as
storageKeyProvision.mjs and versionFastPath.mjs.

Caught by tests/unit/pack-artifact-entrypoint-closures.test.ts in the v3.8.49
merge-train.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-27 19:07:19 -03:00
MumuTW
1d44ee00f8 refactor(sse): extract combo dispatch prelude into combo/dispatchPrelude.ts (#8582)
* refactor(sse): extract combo dispatch prelude into combo/dispatchPrelude.ts

Pure move, no behaviour change. First of ~7 PRs decomposing the combo.ts
god-file (#3501).

handleComboChat evaluates a series of dispatch branches before it ever
reaches target resolution or the sequential attempt loop. None of them
iterate targets in priority order or need the failover/retry/credential
gate machinery that follows, so they move to a leaf:

  - context-cache pin routing (Fix #679), including the
    pinIsDurablyUnhealthy / isPinnedModelDurablyUnhealthy health gate
  - fusion panel dispatch + the #6455 misconfiguration warn
  - pipeline chaining
  - nested combo-ref execute-mode runtime-unit dispatch

Only the chaos and round-robin hand-offs stay inline (11 and 13 lines);
extracting those would be pure indirection.

open-sse/services/combo.ts 3642 -> 3341 (-301)
open-sse/services/combo/dispatchPrelude.ts: 619 (under the 800 cap)

Each helper keeps the fall-through protocol the inline blocks had: return
a Response to OWN the request, return null to fall through. A flipped
null/Response would silently bypass the whole combo strategy, so the new
tests pin both directions for every branch.

combo.ts re-exports pinIsDurablyUnhealthy so combo-pin-health-gate.test.ts
keeps resolving. The leaf takes handleComboChat as a `runCombo` parameter
instead of importing it, so combo/ keeps zero back-edges into combo.ts.

Complexity-neutral: the first cut added +3 violations (two
max-lines-per-function, one complexity) inside the new leaf, so
evaluatePinnedResponse, orderRuntimeUnits, recordRuntimeUnitStickySuccess
and buildBaseOptions were split out. check:complexity now measures 2169
and check:cognitive-complexity 956 — identical to the pristine base.

* test(sse): close the dispatch-prelude coverage holes found by mutation testing

An adversarial mutation audit of the suite added in the previous commit
found it guarded the fall-through protocol well but asserted almost
nothing about what the helpers do once they OWN the request. 5 of 12
seeded mutations survived. Worst case: deleting the pinned-model
dispatch call outright left all 12 tests green.

Three holes, now closed (8 tests -> 20):

Hole A — the honored-pin path had zero coverage. Both existing pin tests
DROP the pin, so the dispatch, the 200-but-empty quality gate, the
[408, 429, 500, 502, 503, 504] failover list and the catch(pinErr)
branch were unguarded — exactly the logic the 2026-06-21 / 2026-06-22
incident comments call load-bearing. Adds five tests over a seeded
healthy provider connection so the pin is actually honored.

Hole B — orderRuntimeUnits was only ever driven with `priority`, which
is a no-op through it. Four of five strategy branches could be deleted
with nothing failing. Adds round-robin rotation and weighted sticky
ordering tests.

Hole C — recordRuntimeUnitStickySuccess never did anything under test:
both its guards need weighted/round-robin, so an early return changed
nothing. Covered by the new sticky-batch test.

Verified by re-running the mutations rather than assuming: all 7 that
previously survived (delete-pin-dispatch, serve-despite-failed-quality,
never-fail-over-on-transient, rr-counter-not-advanced, rotation-removed,
weighted-sticky-skipped, sticky-recording-no-op) are now killed.

The first sticky-batch test I wrote was itself vacuous — asserting "same
unit twice" holds equally when the recording helper is stubbed out, since
nothing advances the counter either. It now asserts the batch runs out
and rotation resumes on the third dispatch, which is what actually
distinguishes the two.

Also restores API_KEY_SECRET in test.after; it was set at module load and
never put back, inconsistent with the DATA_DIR handling beside it.

* fix(ci): teach known-symbols gate the relocated fusion/pipeline dispatch

The combo sub-check of check:known-symbols asserts every canonical routing
strategy has a real dispatch branch. It scanned a hardcoded file list and
matched only `strategy === "..."`, so the prelude extraction tripped it twice:

  [combo] 2 estratégia(s) canônica(s) sem branch de despacho em combo.ts:
      ✗ fusion
      ✗ pipeline

Both branches are still wired — they just moved to combo/dispatchPrelude.ts and
took the early-return guard form `if (strategy !== "fusion") return null;` that
extracting a branch into a `tryXDispatch()` leaf naturally produces.

Two changes, both extending existing precedent (the list already carries the
Block J leaves for the same reason):

- register combo/dispatchPrelude.ts in comboDispatchFiles
- widen the extractor to `strategy [!=]== "..."` so the inverted guard counts

Loose `==`/`!=` stay rejected, and no `handledNotCanonical` fallout: the gate
now reports 20 canonical strategies, all 20 via despacho.

* chore(ci): register combo-dispatch-prelude test in stryker tap.testFiles

check:mutation-test-coverage --strict failed once the known-symbols fix let
Fast Quality Gates advance to it:

  ✗ 2 covering unit test(s) across 2 module(s) are missing from
    stryker.conf.json tap.testFiles
      open-sse/services/combo/comboStructure.ts
      open-sse/services/combo/rrState.ts

The new tests/unit/combo-dispatch-prelude.test.ts exercises both modules, and
both are already in stryker's mutate list, so without the registration its
mutant kills would not have counted toward the nightly mutation gate.

Note (unchanged, still out of scope): combo/dispatchPrelude.ts itself is not in
stryker's `mutate` list. Adding it would widen the nightly mutation surface,
which is a separate call from fixing this drift.

* docs(changelog): add fragment for #8582 combo dispatch prelude
2026-07-27 19:06:56 -03:00
Diego Rodrigues de Sa e Souza
803e7373de feat(ci): block stale UI translations when an English value is rewritten (#8574)
Closes the gap that let #8463 ship. `oauthModal.googleOAuthWarning`'s English value
was rewritten when the Antigravity login helper landed (#5203); 39 of 43 locales kept a
translation of the PREVIOUS English, which told operators to "copy the full URL and
paste it below" — a flow that cannot complete for that provider family. Non-English
users read confident, wrong instructions for months and no gate noticed.

None of the three existing gates can see this class:

- `sync-ui-keys.mjs` only backfills keys that are ABSENT, never ones that are STALE;
- `check-ui-keys-coverage.mjs` counts key PRESENCE, so a stale translation scores as
  fully covered (all 43 locales sat at 99.6% throughout);
- `check-translation-drift.mjs` tracks the `docs/i18n/<locale>/**.md` documentation
  mirrors — it never reads `src/i18n/messages/*.json` at all. (Its `.i18n-state.json` is
  also absent, so it self-skips, but bootstrapping it would not have helped: wrong
  surface.)

New gate `scripts/i18n/check-ui-value-drift.mjs` is DIFF-AWARE rather than
baseline-backed: it compares `en.json` at the merge base against the working tree, and
for every key whose English value changed, reports any locale still holding an untouched
translation.

That choice deliberately freezes pre-existing debt — a diff cannot reveal which old
English a long-standing translation came from, so the gate judges only what the current
change touches, and unrelated PRs never pay for historical drift. The alternative, a
per-key hash baseline over 11207 keys, would have cost a ~600 KB generated file (3x the
largest existing baseline) churning on every i18n PR.

Two ways to satisfy it: refresh the translations, or set them to
`__MISSING__:<new english>` so the runtime serves the corrected English (#7258) while
the key queues for translation. When the string's MEANING changes, renaming the key is
better still — a new key cannot inherit a stale translation, which is what #8463 did.

Wired blocking into the `i18n-ui-coverage` job (the `i18n` job is
`continue-on-error: true`, so a gate there could not block anything). That job gains
`fetch-depth: 0` because the gate needs the base ref; without it the gate self-skips with
`base-unresolved`, mirroring `check-openapi-breaking`. `BASE_REF` is passed via `env:`
and reaches git only through `execFileSync` argv — never a shell string.

Verified against the real defect: rewriting an English value with translations left
behind reports exactly 39 stale locales and exits 1; `--warn` exits 0; an unresolvable
base exits 0 with `SKIP reason=base-unresolved`.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-27 19:06:48 -03:00
Shixi Li
82bed08404 docs(podman): clarify Podman Machine deployment (#8569)
* docs(podman): clarify Podman Machine deployment

* style(podman): format guidance and regression test
2026-07-27 19:06:41 -03:00
MumuTW
1cbe8c44f5 Train 1D: merge via --admin on .113 validation
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
2026-07-27 11:30:49 -03:00
MumuTW
4c9292e66e chore(i18n): normalize zh-TW terminology and sync stale README figures (#8554)
The zh-TW catalog was machine-translated with mainland-habit vocabulary and
simplified->traditional conversions that picked the wrong homophone, and its
README still advertised the v3.7-era figures.

Terminology (docs/i18n/zh-TW/ + src/i18n/messages/zh-TW.json):
- wrong-character conversions: 上遊->上游, 後臺->後台, 儀錶板->儀表板
- mainland habits: 默認->預設, 緩存->快取, 模塊->模組, 調用->呼叫,
  字符串->字串, 全局->全域, 文檔->文件, 響應->回應
- consistency: 供應商/提供商->提供者 (提供者 was already 71% dominant),
  型別->類型 for UI labels, 不活躍->未啟用

README figures synced to the English source: 231->290 providers,
17->19 routing strategies, 1.6B->1.53B free tokens, 50+->90+ free tiers,
11->40+ free forever, 87->104 MCP tools, 30->31 scopes.

Root cause — the generator's post-translation pass was a hardcoded list that
duplicated the glossary and was wired only into the deprecated
generate-multilang.mjs, so the active run-translation.mjs pipeline applied
nothing. Worse, its blanket /代碼/g -> 程式碼 rule would corrupt 控制代碼
(handle), 語系代碼 (locale code) and 錯誤代碼 (error code) on the next
regeneration.

Both scripts and the drift gate now share scripts/i18n/glossary-normalize.mjs,
driven by scripts/i18n/glossary/<locale>.json as the single source of truth.
Ambiguous terms carry blockedPrefixes so 型別->類型 can stay enforced without
mangling 模型別名 (model alias) or 基本型別 (a programming data type); terms
whose synonym is also a legitimate rendering elsewhere (代碼, 項目) are seeded
with no synonyms and documented instead of blanket-rewritten.

CI now runs the glossary gate for zh-TW alongside zh-CN.
2026-07-26 03:52:50 -03:00
MumuTW
4bf47c9b80 chore(perf): add deterministic request-body heap benchmark (#7847) (#8549)
#7847 reports a 3.05 MiB request (729 messages / 86 tools) reaching ~12,282 MiB of V8
heap, and asks for "a regression benchmark that records peak heap for representative
500-800-message, tool-rich requests" before any fix lands. There is currently no memory
baseline in the repo at all (bench:compression is the only benchmark), so a clone-reduction
change could neither be justified nor regression-guarded.

npm run bench:heap-body attributes retained heap to each copy the chat path makes:

  | mechanism                          | call site                          | retained | x wire |
  | cloneLogPayload (unbounded)        | chat.ts buildClientRawRequest      | 3.18 MiB |  1.04x |
  | cloneBoundedForLog (bounded)       | requestLogger.logClientRawRequest  | 0.04 MiB |  0.01x |
  | structuredClone x3 (combo targets) | combo.ts attemptBody               | 9.53 MiB |  3.12x |
  | JSON.stringify (token estimate)    | combo.ts estimateTokens            | 3.06 MiB |  1.00x |
  | per request (sum)                  |                                    |15.81 MiB |  5.17x |

It measures the real production helpers rather than reimplementations, so a change to the
log bounds or the clone strategy is reflected directly.

Design notes:
- Deterministic: fixed-seed LCG, no Math.random(). Verified byte-identical across three
  consecutive runs — without that, a before/after delta measures noise, not the change.
- Corpus lives in its own side-effect-free module so the unit test can import it without
  booting SQLite (requestLogger transitively opens the DB at import time).
- Hermetic: DATA_DIR is redirected to a temp dir before importing, so the benchmark never
  touches the operator's real ~/.omniroute store.
- Node, not bun: --expose-gc and V8 heap accounting are the measurement; another engine's
  heap number would not describe the production runtime.
- --max-retained-mib exits non-zero, so this can become a CI gate once a target is agreed.

Reports only; wires nothing into CI and changes no production code.
2026-07-26 03:52:42 -03:00
MumuTW
c447be4329 fix(db): classify compressionDetailNormalizers as db-internal in check-db-rules (#8534)
* fix(db): classify compressionDetailNormalizers as db-internal in check-db-rules

check:db-rules fails on release/v3.8.49 at its own HEAD: the module added
by #8404 is neither re-exported from localDb.ts nor listed in
INTENTIONALLY_INTERNAL, so the gate blocks every PR->release run and
tests/unit/check-db-rules.test.ts fails its live-repo case.

Its only importer is its sibling src/lib/db/compression.ts, via a
relative import inside src/lib/db/ — the db-internal classification the
list already uses for apiKeyColumnFallbacks and caseMapping. Re-exporting
it from localDb.ts would instead advertise pure normalizer helpers as
part of the compat surface, which Hard Rule #2 discourages.

* test(db): mirror compressionDetailNormalizers in the INTENTIONALLY_INTERNAL audit

The classification guard asserts the exact audited set. Adding the module to
check-db-rules.mjs without the mirror left the exact-list/exact-size assertion
red; both assertions stay exact (37 entries).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-25 11:10:48 -03:00
backryun
1930b09c6a chore(sse): drop deprecated baseUrl from open-sse tsconfig for TS 7.0 (#8473)
TypeScript 6.x raises TS5101 on `open-sse/tsconfig.json`: `baseUrl` is
deprecated and stops functioning in TypeScript 7.0. It was paired with
`ignoreDeprecations: "5.0"`, which no longer silences it under TS 6 (the
compiler now demands "6.0").

Remove `baseUrl: ".."` and rewrite the `paths` mappings relative to the
tsconfig's own directory, which is how TypeScript resolves them with no
baseUrl set:

  "@/*"                     ./src/*       -> ../src/*
  "@omniroute/open-sse"     ./open-sse    -> ../open-sse
  "@omniroute/open-sse/*"   ./open-sse/*  -> ../open-sse/*

`ignoreDeprecations` goes with it — baseUrl was the only deprecated option
it was suppressing.

Verified by diffing the full tsc error set against the previous config (the
base run used `ignoreDeprecations: "6.0"` so compilation proceeds past the
config error, which otherwise aborts type-checking and masks everything):
zero new errors, 28 fewer. All 28 were in `electron/*.js`, which
`baseUrl: ".."` had been dragging into the open-sse program via
root-relative resolution. Scoping the program back to open-sse also moves
`check:type-coverage` from 92.17% to 94.01%; the ratchet direction is up so
the gate passes, and the baseline is deliberately left alone because the
gain is a measurement-scope change rather than new typing work.

The guard test asserts no tsconfig reintroduces `baseUrl` or
`ignoreDeprecations`, and that every `paths` target still resolves to a real
directory — the second half is the part that matters, since dropping baseUrl
silently changes what those mappings point at.
2026-07-25 02:53:01 -03:00
Diego Rodrigues de Sa e Souza
875de01de7 chore(quality): drop stale muse-spark-web allowlist entry + sync sidebar order snapshots (#8383)
Two independent "code is right, bookkeeping lagged" base-reds:

1. #8233 made open-sse/executors/muse-spark-web.ts import
   sanitizeErrorMessage from utils/error.ts (a real Rule #12 fix), but
   left its KNOWN_MISSING_ERROR_HELPER allowlist entry in
   scripts/check/check-error-helper.mjs in place. The gate's own
   stale-allowlist enforcement (assertNoStale) correctly flagged the now
   -obsolete entry: `npm run check:error-helper` failed with "1 entrada(s)
   obsoleta(s)", and tests/unit/check-error-helper.test.ts's "the shipped
   allowlist freezes exactly the known current violators" test expected
   an empty Set. Removed the entry (kept the assertNoStale machinery and
   the general scope-header comments untouched).

2. #8064 added the "compression-exclusions" sidebar item right after
   "compression-studio" in COMPRESSION_CONTEXT_GROUP (deliberate,
   complete feature) but didn't update two order-snapshot tests written
   before that item existed:
   - tests/unit/sidebar-visibility.test.ts expected the "omni-proxy"
     section's flattened id list to end the compression block at
     "compression-studio".
   - tests/unit/ui/sidebar-engine-items.test.ts asserted "Studio must be
     last" in COMPRESSION_CONTEXT_GROUP.
   Updated both to the real, intentional order: Settings -> Combos ->
   engines -> Studio -> Exclusions (Studio now second-to-last,
   Exclusions last).

Validation (red -> green):
- check:error-helper gate: red ("1 entrada(s) obsoleta(s)") -> green
  ("OK (898 files scanned, 0 known-missing frozen)")
- tests/unit/check-error-helper.test.ts: 31/32 -> 32/32
- tests/unit/sidebar-visibility.test.ts: 6/7 -> 7/7
- tests/unit/ui/sidebar-engine-items.test.ts: 13/14 -> 14/14

Refs #8233
Refs #8064
2026-07-24 09:58:58 -03:00
backryun
b84f86ad4f fix(runtime): isolate unique 8177 repairs (#8298) 2026-07-24 09:35:10 -03:00
Moseyuh333
d43e71613e optimize(chaos+ponytail): i18n ponytail, dedupl dispatch, provider diversity (#8264)
* feat(chaos+ponytail): parallel chaos-mode dispatch + ponytail output style (rebased on v3.8.49)

- Chaos mode: new auto/chaos variant fans the prompt out to the top-N
  stable models in parallel and returns a single merged SSE stream.
  - Progressive streaming: each panel model's answer is enqueued as it
    lands (omni-chaos-part event), instead of awaiting the whole panel.
  - withTimeout now aborts the underlying request (modelAbortSignal) on
    timeout so the connection is released, not leaked.
  - concatSseText parses both OpenAI and Anthropic SSE wire formats.
  - autoPrefix/modePacks add the chaos-mode weight pack; virtualFactory
    materializes auto/chaos with fusion strategy + chaos config flag.
- Ponytail (lazy-senior-dev mode) integrated into the existing
  OUTPUT_STYLE_CATALOG registry (id 'ponytail') so it rides the production
  output-style injector, instead of a bespoke duplicate module. Dev-only
  scripts and the duplicate ponytail/ module are removed.
- Tests: chaosEngine/chaosVirtualCombo cover panel dispatch, progressive
  broadcast, timeout abort, and Anthropic parsing; autoCombo pack count
  updated to 6.

Rebased onto release/v3.8.49 (no provider-registry or validation changes —
those are split out per review).

* optimize(chaos+ponytail): i18n ponytail, dedupl chaos dispatch, provider diversity

- Ponytail: add vi/ja/pt-BR/id i18n with lite/full/ultra levels
- chaosEngine: extract dispatchOnePanelModel (shared), add onResult for
  progressive SSE streaming, fix withTimeout anti-pattern
- virtualFactory: deduplicate chaos panel by provider, add tuning overrides
- dispatchChaosFromCombo: accept ChaosTuning, enforce minPanel
- Add/port 8 node-runner tests for ponytail i18n + catalog integrity
- Add muse-spark-web.ts to KNOWN_MISSING_ERROR_HELPER (pre-existing)

* fix(8264): use HandleSingleModel type in chaosEngine dispatch (base-drift)

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-23 11:21:53 -03:00
小妍儿 ✨
fbb8d45757 docs(db): add reproducible SQLite coupling inventory (#8262)
Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com>
2026-07-23 09:51:06 -03:00
backryun
18f1f667bf fix(claude-web): align session transport and fallback (#8230) 2026-07-23 05:24:22 -03:00
Markus Hartung
4ea08f520b fix(sse): Gemini malformed function-call handling + tool_choice translation (#8211)
* fix(sse): synthesize tool_calls for Gemini's malformed function-call abort reasons

Live incident (dashboard log id 1784489701456-d8c0e9): Gemini terminates a
stream with finishReason MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL when its
own parser rejects an attempted tool call — there's no real functionCall part,
only a human-readable finishMessage. gemini-to-openai.ts passed this through
raw as finish_reason (9router#2462's fix, correctly keeping it off a clean
"stop"/Claude end_turn), but a raw "malformed_function_call" isn't one of
OpenAI's 5 documented finish_reason values, so a real OpenAI-format client
(OpenClaw) has no handling for it at all and silently never notices the turn
failed — confirmed live via tests/integration/live-gemini-workload.test.ts's
[28] streaming case after the Gemini TPM/rebase work on this branch.

Fix: synthesize a tool_calls entry (arguments carry the error code + Gemini's
finishMessage, valid JSON) and finish_reason: "tool_calls" instead, routing
the failure into the ordinary "tool call arguments didn't parse" path every
OpenAI-compatible agent loop already handles. Defers to a real tool call if
one already completed earlier in the same turn — the real call wins, no
synthetic entry piles on top of it.

Tests (TDD, each confirmed red-before-green):
- 5 new unit tests in the existing 9router#2462 regression file, covering the
  synthesis itself, UNEXPECTED_TOOL_CALL, the real-tool-call-wins edge case,
  and no-regression on a clean STOP.
- New fixture (tests/fixtures/translation/gemini-malformed-function-call-stream.json):
  the real 6-chunk event series from the live incident, sanitized (personal
  paths/URLs replaced with generic placeholders, structure preserved exactly).
- New integration test chains the real translator into the real Responses API
  transformer using that same fixture, proving correct behavior on BOTH
  /v1/chat/completions and /v1/responses from one shared ground-truth event
  series.

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>

* fix(sse): don't drop a malformed tool-call failure when it lands beside a real one

Live incident (dashboard log id 1784589106014-2a42f8), analyzing why the
prior malformed-function-call fix (3568c7259) still wasn't reaching the
client in this case: Gemini can emit a REAL, valid functionCall AND finish
the SAME candidate with MALFORMED_FUNCTION_CALL — the model attempted
multiple tool calls in one turn (here: a real status-check call plus a
malformed "exec"+"cron" multi-call attempt), one parsed cleanly and the
other didn't.

The first fix version skipped synthesizing a failure signal whenever a real
tool call already existed (state.toolCalls.size > 0), on the assumption
that meant the model was retrying a LATER, separate attempt after an
earlier one already succeeded. That's indistinguishable, from the
translator's state, from this same-turn case — so it silently discarded
the malformed attempt's information entirely: the client saw the real call
succeed and never learned the other tool calls were attempted and rejected.

Fix: always synthesize the failure entry when a malformed abort reason is
seen, appending it alongside any real tool call rather than skipping it.
Multiple tool_calls in one response is normal, well-supported OpenAI
behavior (parallel tool calls), so this adds the failure as an additional
entry instead of replacing or hiding the real one.

Tests (TDD, confirmed red-before-green):
- Rewrote the unit test that encoded the old (wrong) assumption to assert
  both the real and synthesized calls are present.
- New fixture (gemini-malformed-function-call-parallel-real-call-stream.json):
  the real event series from this incident, sanitized.
- New integration tests (same file as 3568c7259's) prove both
  /v1/chat/completions and /v1/responses surface both tool calls correctly
  from this fixture.

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>

* feat(sse): honor tool_choice when translating OpenAI requests to Gemini

Investigating a live report that gemini-3.1-flash-lite frequently narrates
an intended tool call in plain text instead of actually emitting one
(dashboard log id 1784591483850-49c408 — 9 raw provider chunks, all plain
text, zero functionCall parts, clean finishReason STOP): body.tool_choice
was never read anywhere in the OpenAI->Gemini request translator.
result.toolConfig was unconditionally hardcoded to
{ functionCallingConfig: { mode: "VALIDATED" } } whenever tools were
present, regardless of what the caller sent. VALIDATED lets the model
respond with plain text OR a schema-validated function call at its own
discretion — it never forces a call the way OpenAI's tool_choice:
"required" (Gemini's ANY mode) does, so a caller had no way to compel a
tool call even when explicitly requesting one.

Added convertOpenAIToolChoiceToGemini(), mirroring the existing
convertOpenAIToolChoice() in openai-to-claude.ts for the same OpenAI
tool_choice shapes (string "auto"/"none"/"required", or
{type:"function",function:{name}} to force one specific tool):
  - unset/"auto"        -> VALIDATED (unchanged default, no regression)
  - "required"/"any"    -> ANY (forces a call)
  - "none"              -> NONE (disables function calling)
  - {type:"function",...} -> ANY + allowedFunctionNames: [name]

Wired into both Gemini request paths: the direct/base translator
(openaiToGeminiBase) and the Antigravity/Cloud Code envelope
(wrapInCloudCodeEnvelope), which now reuses the base translator's already-
computed toolConfig instead of re-deriving its own hardcoded VALIDATED.

This unblocks (but does not itself resolve) the live question — a
tool_choice: "required" A/B test against gemini-3.1-flash-lite follows to
confirm ANY mode actually changes the narrate-vs-act behavior in practice.

Also updates the T11 any-budget allowlist for this file: the "any" string
comparisons (tool_choice value "any", not a TypeScript type) are the same
documented false-positive pattern already carved out for executors/base.ts.

Tests (TDD, confirmed red-before-green): 9 new unit tests covering all
tool_choice shapes on both the direct and Antigravity/Cloud Code paths,
plus the no-tools and unset-default no-regression cases.

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>

* chore(quality): file-size baseline for own-growth (#8211)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Markus Hartung <markus.hartung@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-23 05:03:44 -03:00
Markus Hartung
40ee0847d9 feat(sre): add tcp-close-analyzer.py for debugging client-vs-server TCP close order (#8208)
Dependency-free (stdlib-only) libpcap/Ethernet/IPv4/TCP parser that answers
one question: does OmniRoute or the far end (Caddy, on behalf of whichever
client it's proxying) close the TCP connection first? Dashboard-level 499s
only tell us OmniRoute detected a dropped connection, not which side's FIN/
RST actually landed first -- this settles it from the raw packets.

Handles classic Ethernet and both "Linux cooked" linktypes (SLL/SLL2, what
`tcpdump -i any` produces) since rootless Podman has no host-visible bridge
interface to capture on directly -- the capture instructions in the script
document the nsenter-into-container-netns workaround.

Adds --find to grep every reassembled stream for a literal marker string --
in practice the reliable way to locate one specific request (the
x-correlation-id header isn't echoed on every hop) is dropping a fresh UUID
into an actual chat message and searching for it, then cross-referencing
the matched stream's timing against data/call_logs/<date>/*.json.

Co-authored-by: Markus Hartung <markus.hartream@gmail.com>
2026-07-23 05:03:26 -03:00
Diego Rodrigues de Sa e Souza
08129cfb0c fix(ci): merge-train --fast mirrors test:unit subdir allowlist (#7688)
The fast bucket fed every changed tests/unit file to node:test; vitest-only
subdirs (autoCombo) always fail under that runner and redden the train. The
classifier now carries the same {api,auth,…} allowlist as package.json's
test:unit (guarded by a sync test) and stops excluding ui/*.test.ts, which
test:unit does run.
2026-07-23 01:33:13 -03:00
Diego Rodrigues de Sa e Souza
f23d7770ec feat: zh-CN terminology glossary + consistency gate + normalization pass (#8038) (#8166)
One-shot 提供商->提供者 normalization across src/i18n/messages/zh-CN.json
(679 substitutions) and bin/cli/locales/zh-CN.json (54), mirroring #8024's
zh-TW pass. Adds a versioned terminology glossary
(scripts/i18n/glossary/zh-CN.json), a protected-names list
(scripts/i18n/glossary/protected-terms.json), and a pure-function
consistency check (scripts/i18n/check-glossary-consistency.mjs,
npm run i18n:check-glossary) wired into CI as the i18n-glossary-zhcn job.
zh-CN added to the visual-QA harness default locales. Complements the
existing parity (check-ui-keys-coverage.mjs) and ICU (validate_translation.py)
gates without replacing them.
2026-07-22 15:54:26 -03:00
Diego Rodrigues de Sa e Souza
98b1aa34b5 fix(sse): run compression pipeline per turn in Codex Responses WS bridge (#8052) (#8154)
The Codex Responses-over-WebSocket bridge bypassed the whole prompt-compression
pipeline (and its analytics writes) that the HTTP/SSE path (chatCore.ts) runs on
every request, via two gaps:

1. prepare() in codex-responses-ws/route.ts never called anything from
   open-sse/services/compression/* — it authenticated, injected memory, applied
   reasoning-routing, then went straight to executor.transformRequest().
2. scripts/dev/responses-ws-proxy.mjs memoized the upstream connection in
   ensureUpstream() and only called the internal "prepare" action on the FIRST
   response.create of a WS session — every subsequent turn on a reused
   connection bypassed prepare() (and therefore compression) entirely.

Fix: a new compression.ts module wires the core compression pipeline (settings
resolution -> selectCompressionStrategy -> applyCompressionAsync ->
compression_analytics/compression_engine_breakdown writes, reusing
adaptBodyForCompression's existing Responses-API input[] adapter) into
prepare(); responses-ws-proxy.mjs now re-runs prepare() (via a new shared
runPrepare() helper) for every logical response.create turn on a reused
connection, not just the first, without recreating the upstream socket.

Regression test: tests/unit/responses-ws-proxy-compression-parity.test.ts
proves the reused-connection bypass by execution (RED: 1 prepare call for 2
turns; GREEN after the fix: 2 prepare calls for 2 turns).
2026-07-22 11:27:57 -03:00
Diego Rodrigues de Sa e Souza
287802cf86 fix: repair pre-existing red gates on the release/v3.8.49 tip (#8055)
* fix(dashboard): resolve Kimi banner casing collision + shrink frozen test file (release tip)

- Rename src/app/(dashboard)/dashboard/kimiSponsorBanner.ts to
  kimiSponsorBannerGate.ts so it no longer differs from
  KimiSponsorBanner.tsx only by the first letter's case (breaks next
  build on case-insensitive filesystems). Updates the sole importer
  (KimiSponsorBanner.tsx) and the two tests that reference it.
- Extract the 8 Kimi/Moonshot featured-ordering tests out of the
  frozen tests/unit/providers-page-utils.test.ts (grown 3 lines past
  its 1294 cap by #8039's rebrand-comment update) into a new sibling
  file tests/unit/providers-page-utils-kimi.test.ts. No assertions
  dropped; both files pass in full (24 + 8 = 32 tests).

* fix(sse): register PromptQlExecutor in the executor registry (release tip)

getExecutor("promptql") had no entry in open-sse/executors/index.ts, so it
silently fell through to DefaultExecutor's provider fallback, which issues a
raw fetch() and returns the bare upstream Response instead of the executor
wrapper shape {response, url, headers, transformedBody}. The real
PromptQlExecutor class (open-sse/executors/promptql.ts) already honors the
contract correctly — it was just never wired into the registry.

Fixes tests/unit/executor-web-cookie-sweep.test.ts "promptql executor
returns wrapper shape".

* fix(i18n): backfill 2220 missing pt-BR keys to restore en.json parity (release tip)

pt-BR.json fell behind after #7935 restored +2220 keys into en.json and
vi.json but left pt-BR.json unmodified. Translated all missing entries to
Brazilian Portuguese, preserving ICU/interpolation placeholders and existing
terminology, and merged them mirroring en.json's key order so the diff is
additions-only (the small comma-only deletions are pure JSON reformatting
from new sibling keys).

* fix(providers): repair 4 pre-existing catalog/registry reds on release tip

- providers-constants-split.test.ts: APIKEY_PROVIDERS grew 182->187 (PR #7887
  added 5 free-tier providers: ainative/aion/sealion/routeway/nara). Verified
  no dup/loss (6-family partition sums exactly to 187) and updated the stale
  expected count + comment trail to match.
- cline registry: added the missing minimax/minimax-m3 free OpenRouter entry
  (#3321) and fixed the neighbouring nemotron-3-ultra-550b-a55b entry, which
  carried a stray ":free" id suffix and an imprecise 1_000_000 contextLength
  instead of the 1_048_576 the test (and every sibling 1M-context entry in
  this catalog) expects.
- promptqlModels.ts / registry/promptql/index.ts: PROMPTQL_FALLBACK_MODELS's
  minimax-m3 entry was missing supportsVision, and the registry mapping
  dropped it entirely (only id/name were passed through) — it was the sole
  minimax-m3 entry across the whole registry not flagged multimodal, despite
  every other provider (minimax, minimax-cn, ollama-cloud, trae, bazaarlink,
  clinepass, codebuddy-cn, opencode-zen/go, synthetic, huggingchat, lmarena)
  agreeing MiniMax-M3 supports vision. Added the field to the PromptQlModel
  type and threaded it through.
- tests/snapshots/provider/translate-path.json: regenerated the golden via
  UPDATE_GOLDEN=1. Diffed old vs new — zero providers removed, 5 added
  (ainative/aion/nara/routeway/sealion, matching #7887), and the only
  changed entry (cline) reflects the already-merged #7914 ClinePass header
  protocol change (Cline/<version> User-Agent + X-Task-ID) that a prior
  narrow golden touch-up missed capturing.

* fix(docs): repair docs-sync/env-sync/repo-contract gates (release tip)

Six pre-existing reds on release/v3.8.49, all "repo drifted from its own
documented contract":

- check-docs-counts-sync: free-tier headline was stale (~1.4B/~2.0B) vs the
  live catalog (~1.53B steady / ~2.15B first month, 43 pools). Updated
  README.md and docs/reference/FREE_TIERS.md to the live numbers and added a
  v3.8.49 correction note explaining the pool-count delta (39->43, #7840).
  Also fixed a soft executors-count drift in ARCHITECTURE.md (84->86,
  268->271 providers) while touching that line.
- release-green-docs-drift-7253: docs/proxy-subscriptions.md referenced a
  fabricated migration filename (123_proxy_subscriptions.sql); the real file
  is 131_proxy_subscriptions.sql. Fixed all 3 occurrences.
- check-env-doc-sync + issue-7793-env-doc-sync-repro: OMNIROUTE_DATA_DIR
  (DATA_DIR fallback alias read by
  open-sse/executors/promptql/threadSticky.ts) was undocumented. Added to
  .env.example and docs/reference/ENVIRONMENT.md.
- check-db-rules: src/lib/db/proxySubscriptions.ts (#7299) is a db-internal
  split of proxies.ts (kept under the frozen file-size cap) whose one export
  is already re-exported via proxies.ts -> localDb.ts. Added it to
  INTENTIONALLY_INTERNAL with the same db-internal justification used for
  identical split modules (apiKeyColumnFallbacks, providerNodeSelect,
  webSessionDedup) rather than a redundant direct re-export from localDb.ts.
- mcp-server-hollow-dist-deps: the sanity test expected better-sqlite3 among
  the MCP bundle's static top-level external imports. That's been stale
  since the pre-#7878 migration to a cascading SqliteAdapter driver factory
  (createRequire()-based lazy require, not a static import); better-sqlite3
  already has its own native-asset copy guarantee in assembleStandalone.mjs,
  unrelated to this test's EXTRA_MODULE_ENTRIES concern. Updated the
  assertion to a still-genuinely-static external (zod) with a comment
  explaining the change.

No production runtime behavior changed — docs, .env.example, and a checker
allowlist/test-expectation only.

* fix(dashboard): repair stale UI component-shape test assertions (release tip)

Two pre-existing reds in the dashboard UI component-contract cluster were
caused by test assertions that had gone stale after intentional, correct
refactors — not by real defects in the components:

- quota-pool-wizard-multi.test.ts: the step-3 preview assertion required
  the literal single-line substring "connectionIds.map((cid)". Prettier
  (100-char width, project config) legitimately breaks the
  connectionIds.map(...).filter(...) chain across lines because of the
  multi-line callback body, so the literal never matches. PoolWizard.tsx
  still builds previewByProvider correctly by mapping over connectionIds;
  updated the assertion to a regex that tolerates the line break.

- v388-phase1-screen-fixes.test.ts: the shared Select placeholder-guard
  assertion required the literal "!children && placeholder". An earlier,
  intentional i18n commit changed the hardcoded "Select an option" default
  to a translated fallback (`placeholder ?? t("selectOption")`), which
  requires parens around the ?? expression for operator precedence. The
  guard behavior is unchanged (still gated on !children); updated the
  assertion to match the current, correct guard shape.

Both fixes are read-only test-file changes; no production behavior changed.

review-reviews-v3814-fixes.test.ts still has one pre-existing, unrelated
red (LEDGER-4: minimax-m3 registry entries missing supportsVision) that
requires editing the promptql provider registry/catalog — out of this
cluster's scope, left untouched and reported separately.

* fix(providers): reconcile cline catalog contradictions + deterministic golden (release tip)

The first tip-green pass introduced 3 regressions caught by CI on sibling guard tests:

- clinepass-provider + cline-catalog-models-3321 encoded OPPOSITE expectations of
  the same cline model list (minimax presence, nvidia :free suffix). Reference
  upstream (OpenRouter free lineup) confirms nvidia/nemotron-3-ultra-550b-a55b:free
  (with :free, 1M ctx) is correct, so restore that id and fix #3321's stale no-:free
  assertion; add minimax/minimax-m3 (the real #3321 gap) to clinepass-provider's list.
- check-db-rules-classification froze INTENTIONALLY_INTERNAL at 35; proxySubscriptions
  was the intentional 36th entry — add it + bump the count.
- provider-translate-path golden stored a LITERAL Cline/3.8.49: clineAuth resolves the
  version from APP_CONFIG.version (stable), but the golden sanitizer collapsed only
  process.env.npm_package_version (unset under `node`, set under `npm run`) — so the
  golden was shard-dependent. Resolve APP_VERSION from APP_CONFIG.version like clineAuth
  and regenerate; now Cline/<APP> normalizes identically in every shard.

* fix(services): type execFile signal/killed in classifyError + ratchet dashboard baseline (release tip)

Pre-existing base-red on the tip's Fast Quality Gates (dashboard-typecheck), missed
in the first inventory:

- src/lib/services/installers/utils.ts TS2339 — `err.signal` was read off a value typed
  as NodeJS.ErrnoException, which @types/node does not declare `signal`/`killed` on
  (those belong to execFile's ExecFileException). Widen classifyError's param to type
  both, and drop the now-redundant `(err as … { killed })` cast.
- Ratchet config/quality/dashboard-typecheck-baseline.json down: 5 baselined errors were
  fixed by already-merged PRs but never ratcheted (OAuthModal TS2769 4→3 / TS2345 4→3,
  CliproxyModelMappingEditor TS2339, CompressionPreviewAccordion TS4104, MonacoEditor
  TS2307). Baseline now 254, matching live — gate exits 0.
2026-07-21 21:25:00 -03:00