* docs(i18n): full Russian README rewrite for native readers
Rewrite docs/i18n/ru/README.md from an outdated English dump into a proper Russian manual aligned with the modern product README (v3.8.x structure, free-tier budget, combos, compression, CLI/MCP). Fix relative screenshot/doc links for the i18n path.
* fix(8217): changelog fragment bullet prefix
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(logs): stop an async EPIPE becoming an uncaughtException loop
A raw process.stderr.write into a broken pipe fails asynchronously, so the
try/catch around it never sees the failure. The stream emits 'error'; with no
listener on process.stderr Node re-throws it as an uncaughtException; the
framework's handler logs that through console.error; and the patched console
writes back into the same dead stream. That closes a self-sustaining loop.
Attach an 'error' listener to process.stdout and process.stderr. Node only
converts a stream 'error' into an uncaughtException when the emitter has no
listener, so the listener alone terminates the cycle. Measured in a spawn
harness over 1.5s: 3,387 uncaught exceptions before, 0 after.
Absorb EPIPE only. Attaching a listener otherwise makes every stream error on
those streams non-fatal process-wide, so ENOSPC, EBADF and the rest are
re-raised on a fresh stack to preserve today's crash semantics. The
accompanying test asserts that in a child process, because node:test
attributes any in-process uncaughtException to the running test.
Add a test-only reset() to undo the patched console and the listeners:
test:unit:fast runs --test-isolation=none, so leaked state would reach every
subsequent test file.
Refs #8181
* fix(logs): bound interceptor disk writes and self-heal a missing log dir
Two write-path defects in the same file, both independent of the loop itself.
writeEntry appended with no rate limit, so while the loop spun it wrote
unbounded lines to disk (4.3 GB in 90 minutes in the reported incident). Apply
the same policy #1006 established in structuredLogger -- 50 writes/sec, a 5s
dedup window, a bounded tracking map -- but scoped to `error` entries only.
That scoping is deliberate: structuredLogger applies its limiter solely to
error() and fatal(), whereas writeEntry serves all five of
log/info/warn/error/debug across ~800 non-error call sites. Capping those would
silently drop routine logging from the Console Log Viewer's file. A test
asserts non-error levels stay unlimited.
ensureDir() ran once in initConsoleInterceptor and never again, so a log
directory removed while the process was alive made every later append throw
ENOENT into a bare catch -- console file-logging then stopped permanently with
nothing surfaced anywhere. Recreate the directory and retry once, and report
the failure exactly once through the unpatched stderr so it cannot recurse
through the patched console or become a flood of its own.
Refs #8181
* fix(logs): skip raw stderr writes to a stream already known dead
error() and fatal() write with a raw process.stderr.write wrapped in
try {} catch {}. The comment on that line says the raw write exists to avoid
Next.js console patching "that triggers EPIPE loops" -- but on a broken pipe
the write fails asynchronously, so the catch never sees it, and the resulting
stream error is what ignites the loop.
Skip the write when the stream is already destroyed or ended, falling through
to the file sink as before. The listener added earlier is what breaks the
cycle; this stops the ignition point firing into a dead stream in the first
place. The existing try/catch is retained for the synchronous cases it always
covered.
The #1006 suppression policy and its call sites are untouched.
Refs #8181
* fix(logs): install the stdio guard independently of console interception
initConsoleInterceptor() returns early when APP_LOG_TO_FILE=false, and when the
log directory cannot be created. The stdio 'error' listeners were installed
after that return, so in those supported configurations no listener was
attached at all.
structuredLogger's raw stderr writes still happen there, and an ordinary broken
pipe raises an async EPIPE without destroyed or writableEnded being set first,
so the guard in safeStderrWrite does not cover it either. The loop this change
exists to prevent was therefore still reachable with file logging turned off.
Extract installStdioErrorGuard() and call it before the early return. It is
idempotent and cleared by reset(). A new test asserts, in a child process, that
both listeners are present when APP_LOG_TO_FILE=false.
Also restore APP_LOG_TO_FILE and APP_LOG_FILE_PATH in the test's after() hook.
test:unit:fast runs with --test-isolation=none, so the previous top-level
mutations leaked into later test files, leaving file logging enabled against a
path this file deletes.
Refs #8181
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.
Live AGY model discovery (isDiscoverableAgyModelId + filterUserCallableAntigravityModels)
composing with the #8013 antigravity discovery rewrite. Reconstructed onto the current
release tip (branch was ~977 commits behind); updated the test to the renamed version-cache
API (seedAntigravityVersionCache -> seedAntigravityIde/CliVersionCache) after the fusion.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
persistOAuthConnection gated its whole dedup step behind if(tokenData.email).
The matcher (findExistingOAuthConnectionMatch) already matches by explicit
connectionId first, but it was never reached when the payload had no top-level
email. GitHub Copilot's device-code flow keeps identity under
providerSpecificData.githubEmail, so tokenData.email is undefined — a refresh
(which passes the existing connectionId) skipped the match and fell through to
createProviderConnection, producing a duplicate connection.
- Widen the gate to if(connectionId || tokenData.email) so an explicit
connectionId is honored regardless of email.
- Guard the matcher's email branch with if(!tokenData.email) return false, so a
widened gate can't false-match an email-less connection via
safeEqual(undefined, undefined).
Fixes#8059.
Two accuracy problems in buildCodexUsageQuotas (open-sse/services/codexUsageQuotas.ts):
1. ChatGPT Codex's /wham/usage advertises a latent per-feature ceiling for the
spark feature (metered_feature codex_bengalfox) to accounts by default. A
never-used bucket is unanchored (used_percent 0, reset_after_seconds ==
limit_window_seconds), so it recomputes its reset as now + full_window on
every fetch and was rendered as a permanent GPT-5.3-Codex-Spark row at 100%
for a model the operator never used. Skip latent windows (isLatentWindow);
they reappear once the feature is actually used. The label now comes from the
payload's own limit_name, falling back to the constant.
2. primary_window/secondary_window were labeled session/weekly purely by
position, ignoring limit_window_seconds, so a 7-day primary_window showed
'Session'. The session/weekly keys (routing semantics) stay unchanged; only
the display label is corrected from the real window duration
(windowDurationLabel), so a 7-day window shows 'Weekly'.
Fixes#8051.
Point dashboard provider cards at current Baidu developer landings instead of the deprecated yiyan nag page and the 301ing wenxinworkshop path.
Co-authored-by: LandLord64 <ulofeuduokhai@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Explain release/* as the active cycle, main as the published line, and tags as ship markers so contributors know where to aim PRs.
Co-authored-by: Ulofe Uduokhai <179733406+c4usal@users.noreply.github.com>
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.
Pollinations image requests with no configured apiKey/accessToken (the
common free case) were sent with no Authorization header AND no
fingerprint headers, so Pollinations' own upstream legitimately
rejected them with a real 401 even for a valid OmniRoute key. The
chat path already has an anonymous fingerprint-pool fallback
(PollinationsExecutor.execute()'s isAnonymous branch); the image
path never reused it.
Adds open-sse/handlers/imageGeneration/pollinationsAnonAuth.ts,
mirroring the chat executor's anonymous session-pool fallback for
handleOpenAIImageGeneration, and fixes the pre-existing bug where
Authorization was set to the literal string "Bearer undefined"
when no token was configured (now correctly gated by if (token)).
Regression test: tests/unit/pollinations-image-anon-fallback-8085.test.ts
* fix(providers): add missing poe registry baseUrl entry (#8082)
The built-in poe provider (passthroughModels:true, NAMED_OPENAI_STYLE_PROVIDERS)
had no open-sse/config/providers/ REGISTRY entry, so model discovery's
getRegistryEntry("poe")?.baseUrl resolved to undefined and GET
/api/providers/[id]/models always failed with {"error":"No base URL
configured for provider"} even though credentials and inference worked fine
(the validation/inference path already had a hardcoded https://api.poe.com/v1
fallback). Adds a real REGISTRY entry mirroring moonshot/byteplus, and points
the audioMiscProviders.ts hardcoded fallback at the same POE_DEFAULT_BASE_URL
constant so both paths agree going forward.
* test: regenerate provider translate-path golden for poe (#8082)
src/domain/quotaCache.ts kept its quota state (cache Map, refreshingSet,
refreshTimer, tickRunning) in bare module-scope variables. In a Next.js 16
`output: "standalone"` build, code reachable only from instrumentation-node.ts
(providerLimitsSyncScheduler's write path) and code reachable from an
API-route/SSE-handler chunk (auth.ts::evaluateQuotaLimitPolicy()'s read path)
can be compiled into separate server chunks, each independently instantiating
this module's top-level state. A quota renewal written by the sync scheduler
was invisible to the routing read path, leaving accounts stuck exhausted until
a full process restart.
Anchors all quota-cache state on a single globalThis-held object, following
the same pattern already used in src/lib/credentialHealth/cache.ts and
src/lib/db/core.ts, and the identical fix already shipped for this exact
failure mode in src/lib/pricingSync.ts (#6325 / commit de9d748dac).
Regression test: tests/unit/repro-8065-quota-cache-cross-instance.test.ts
imports the module twice under distinct query-string specifiers to force two
separate module instances, proving a write from one instance is now visible
to a read from the other.
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).
Windows console-window close delivers CTRL_CLOSE_EVENT, which Node/libuv maps
to a JS-visible SIGHUP event. initGracefulShutdown() only listened for
SIGTERM/SIGINT, so closing the window never ran cleanup() (WAL checkpoint +
closeDbInstance()), leaving storage.sqlite's WAL un-checkpointed for the next
launch.
Separately, process.kill(pid, "SIGTERM") on win32 unconditionally
force-terminates the target process instead of delivering an interceptable
signal. The CLI's own stop paths (ServerSupervisor.stop() and
runStopCommand()) sent it immediately on every stop, racing and beating the
child's own async graceful shutdown before the WAL checkpoint could run.
Fix:
- src/lib/gracefulShutdown.ts: register a SIGHUP handler alongside
SIGTERM/SIGINT.
- src/shared/platform/windowsProcess.ts (new): stopProcessGracefully() skips
the immediate SIGTERM on win32 (letting the target's own CTRL_C/CTRL_CLOSE
handling run) and polls before escalating to SIGKILL; unchanged immediate
SIGTERM behavior on POSIX.
- bin/cli/runtime/processSupervisor.mjs and bin/cli/commands/stop.mjs: use
stopProcessGracefully() instead of an unconditional process.kill(SIGTERM).
Regression tests: tests/unit/graceful-shutdown-sighup-8045.test.ts (reuses
the RED probe from the triage analysis) and
tests/unit/windows-process-stop-8045.test.ts.
checkRunnable() built the healthcheck spawn's minimalEnv.PATH from the
caller's PATH only, never merging in this Node's own bin dir the way
locateCommand's known-path search already does. npm-installed CLIs like
codex are `#!/usr/bin/env node` shebang scripts, so when the server is
launched with a minimal PATH (systemd/docker/PM2/Electron) lacking
node's dir, the healthcheck spawn fails even though the binary was
correctly located, and the tool shows as undetected.
Extracted the merge into a new buildHealthcheckPath() helper
(cliRuntimeHealthcheckPath.ts) to keep cliRuntime.ts within its frozen
file-size ceiling.
getDbInstance()'s probe-then-reopen pattern (written for per-open-handle
drivers like better-sqlite3/node:sqlite) was calling .close() on a
throwaway probe connection before opening the "real" connection right
after. For sql.js, openSqliteDatabase()'s fallback path always returns
the SAME module-global cached singleton for a given filePath, so closing
"the probe" closed the ONLY connection that file would ever get until
process restart — every subsequent query threw sql.js's raw "Database
closed" string, matching the reported crash-loop on every boot once
storage.sqlite already exists and both sync drivers are unavailable.
Adds closeProbeIfSafe() (src/lib/db/core.ts) and uses it at every
probe-close site in getDbInstance()/captureCriticalDbState() — it skips
the close for sql.js-backed adapters and lets the same live adapter flow
through, while still closing real per-handle drivers normally.
Also makes sqljsAdapter.ts's gracefulClose() remove its 3 process-level
listeners (beforeExit/SIGINT/SIGTERM) so a closed adapter's closure (raw
sql.js Database + buffers) can actually be garbage collected instead of
being pinned forever — addresses the compounding-OOM sub-finding as a
consequence of the same defect.
Regression test: tests/unit/db-sqljs-close-poison-7494.test.ts
Security-review follow-up to #7900. The notion-web thread-session cache was keyed only
by Notion spaceId (space-, not user-scoped) and accepted arbitrary client-supplied thread
ids, so two users of the same space could pin/read each other's thread. Now: (1) the cache
key includes hashNotionCallerCookie(cookie) so each caller gets an isolated namespace, and
(2) readClientThreadId rejects any value that is not a well-formed Notion UUID.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(grok-cli): require full auth.json on OAuth paste import (#7610)
The Grok Build paste path told operators to paste only the JWT "key"
field, which creates connections with refresh_token=null that can never
auto-refresh. Require the full ~/.grok/auth.json object (with
refresh_token) in OAuthModal, and reject bare JWT pastes with a clear
error.
* fix(grok-cli): add behavioral test coverage for auth.json paste-import (#7610)
Replace the source-regex-only test for the OAuth paste-import path with a
real behavioral suite (bare JWT rejected, auth.json missing refresh_token
rejected, multi-entry auth.json accepted, valid auth.json POSTed) using the
existing grok-device-oauth-modal.test.tsx jsdom harness. Extract
parseGrokCliPasteToken() into its own src/lib/oauth/utils/grokCliAuthJson.ts
module so it is directly unit-testable and to keep OAuthModal.tsx's frozen
file-size gate from growing (bump 1080->1100, justified in
file-size-baseline.json, mirroring the existing extraction precedent on
this file). Also fixes two pre-existing "JWT Token" label assertions that
this PR's own tab rename ("Import auth.json") had left stale.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(grok-cli): sanitize function_call_output before Grok Build dispatch (#7611)
Grok Build cli-chat-proxy rejects Responses bodies when tool-result
outputs contain incomplete \u escapes or other malformed JSON text.
Sanitize function_call_output.output values in GrokCliExecutor so
large agent tool transcripts no longer fail intermittently with 400
body-parse errors.
* fix(grok-cli): type test credentials instead of casting through any (#7611)
tests/ has no-explicit-any as an ESLint error; the 3 `{ accessToken: "tok" }
as any` casts passed to transformRequest() were the proven, non-drift cause
of this PR's own "No new ESLint warnings" CI failure. Replace them with a
single properly-typed ProviderCredentials literal (all fields on that type
are optional, so no cast is needed).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ravi Tharuma <RaviTharuma@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat: provider tab account search + mirrored top pagination (#7937)
Two client-side UI improvements to the provider connections/accounts list
(all data already loaded in memory; PAGE_SIZE=50):
- Mirror the pagination bar ABOVE the list (previously bottom-only) in both
the flat/untagged branch and the tagged/grouped branch.
- Add a case-insensitive substring account search input (id/tag/name/email)
to the left of the status filter pills, searching across ALL accounts
(not just the current page), resetting pagination to page 0 on change.
- Add pagination to the tagged/grouped view, which previously had none.
New pure helper `connectionsSearchFilter.ts` keeps the substring matcher
testable and out of the already-large ConnectionsListPanel.tsx.
Closes#7937
* i18n(vi): add providers.accountSearchPlaceholder for locale parity (#7937)
abort(reason) rejects the upstream fetch with the raw reason, which is
often a bare string ("request_signal_aborted", "Client disconnected: ...")
carrying no `name` or `status`. The chatCore catch block only recognized
`error.name === "AbortError"`, so those aborts fell through to the 502
provider-failure default and were surfaced as `FAILED 502 / Bad Gateway`
in the client response, request logs, and usage records.
Classify the caught error with the existing isLocalStreamLifecycleError
helper (expanded by #7908 to cover AbortError plus the known abort reason
strings) so every client-abort shape maps to `499 Request aborted`.
Status-normalization follow-up to #7908, which already excluded these
aborts from provider circuit-breaker and cooldown accounting.
Co-authored-by: xiaolong.835 <xiaolong.835@bytedance.com>
* feat: narrow mcp:connect scope + per-key HTTP tool-scope binding (#7895)
Adds MCP_CONNECT_SCOPE ("mcp:connect"), a narrow additive API-key scope
(kept out of MANAGEMENT_API_KEY_SCOPES, same precedent as SELF_USAGE_SCOPE)
that authorizes ONLY the /api/mcp/ LOCAL_ONLY route-guard carve-out --
remote MCP-only callers no longer need broad manage/admin scope just to
reach the transport routes. Scoped strictly to /api/mcp/; every other
LOCAL_ONLY bypass prefix still requires hasManageScope() unchanged.
Also resolves the caller's real api_keys.scopes over HTTP/SSE
(httpAuthContext.ts::resolveMcpCallerAuthInfo) and passes it to the MCP
SDK's transport.handleRequest(req, { authInfo }), so extra.authInfo.scopes
reaching tool calls reflects the Bearer key's own scopes instead of the
OMNIROUTE_MCP_SCOPES env fallback -- scopeEnforcement.ts already prioritized
authInfo, it was simply unfed over HTTP. Does not flip the
OMNIROUTE_MCP_ENFORCE_SCOPES default; stdio is unaffected (no per-caller
identity, stays on the meta/env fallback chain).
Closes#7895
* test(mcp): register mcp-connect-scope test in stryker tap.testFiles (#7895)
Two base-reds on the v3.8.49 tip from already-merged PRs, both blocking the merge-train
test:unit gate:
- #7528 WS rewrite dropped the ecto_1_sess cookie hint from the 401 message (guard #5449).
- #7734 (hailuo) + #7997 (M365 variants) grew provider count to 278; docs still said 271.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
duckduckgo-web returned 400 ERR_BAD_REQUEST on every request because the model
catalog advertised ids DuckDuckGo has retired from the free Duck.ai lineup
(gpt-4o-mini, gpt-5-mini, llama-4-scout, mistral-small-2501, o3-mini,
claude-3-5-haiku-20241022). duckchat/v1/chat validates `model` server-side and
rejects retired ids, and normalizeDuckDuckGoModel() defaulted to / passed through
gpt-4o-mini, so the retired id reached the wire verbatim.
Update all three id sources to the current free wire ids captured live from
duckchat/v1/models (2026-07-22): gpt-5.4-mini, gpt-5.4-nano, claude-haiku-4-5,
mistral-small-2603, tinfoil/gpt-oss-120b, tinfoil/gemma4-31b —
- executor: default gpt-4o-mini -> gpt-5.4-mini; legacy ids aliased to the
nearest current model via a lookup map; dropped the invalid gpt-5-mini
"minimal" reasoningEffort;
- freeModelCatalog.data.ts + providers/registry/duckduckgo-web: current ids.
Regression test duckduckgo-web-model-catalog-8000.test.ts asserts no retired id
ever reaches the wire and all three catalogs match the current set (RED on the
old default/passthrough + retired catalogs). Live 200 confirmation remains a
recommended VPS smoke per the plan-file.
* feat(oauth): add browser login for Grok Build provider (#7013)
* feat(oauth): grok-build supports device_code AND browser-PKCE side-by-side (#7013)
Reworks #7735 so the browser PKCE login is added ALONGSIDE the device_code flow
(#7358) instead of replacing it; the OAuthModal lets the user pick either method.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
The KimiSponsorBanner (use client) version gate imported isNewer/normalizeVersion
from versionCheck.ts, whose top-level 'import { execFile } from child_process'
cannot be tree-shaken out of a client bundle — Turbopack next build failed with 33
'Module not found' errors (child_process, fs, net, dns, module). Move the pure
helpers to a dependency-free versionCompare.ts; versionCheck.ts re-exports them.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
isDdnProjectPromptQlToken() (jwt.ts) and isLikelyDdnToken() (usage/promptql.ts) used
`iss.includes("auth.pro.hasura.io")`, which a spoofed issuer like
"https://auth.pro.hasura.io.evil.com/ddn/token" also satisfies
(js/incomplete-url-substring-sanitization, 2 open CodeQL high alerts).
Adds a shared issuerHostIsTrusted() helper in jwt.ts that parses `iss` with `new URL()`
and compares the hostname (exact match or trusted subdomain), and points both call
sites at it, de-duplicating the previously copy-pasted predicate.