opencode.ai/zen/v1 rejects non-browser clients (urllib) with 403
error_code 1010 while curl on the same key succeeds. The 403 was
treated as an auth-level failure and two of them crystallized a
misleading ALL_ACCOUNTS_INACTIVE on the free pool.
- errorClassifier: new FINGERPRINT_REJECTION type; a 403 carrying
error_code 1010 / browser_signature_banned is the CDN refusing the
client TLS/UA signature, not the account credentials.
- combo/targetExhaustion: fingerprint rejections skip auth-level
exhaustion so remaining targets stay eligible.
- auth: resolveTerminalConnectionStatus no longer treats the
fingerprint rejection as a terminal banned account state.
UA passthrough is deliberately untouched: #5997/#5720 make the
forward-only behavior load-bearing.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Replace literal <app-name> and <org-slug> with HTML entities (< >)
in the denoRelayOrgDomainHint translation key for all 43 locale files.
The React Flight (RSC) protocol parser interprets unclosed angle-bracket
tokens as HTML tags, causing INVALID_MESSAGE: UNCLOSED_TAG errors when
rendering the DenoRelayModal component on /dashboard/system/proxy.
Add regression test suite (tests/unit/i18n-deno-relay-unclosed-tag.test.ts)
covering four axes: valid JSON (no BOM), key existence, no raw angle brackets,
and correct HTML entities in all locales.
Both tables (002_mcp_a2a_tables.sql) store their row timestamp in
created_at; the cleanup queries used WHERE timestamp < ? which does not
exist, so every boot-time cleanup logged:
Error cleaning mcp_tool_audit: SqliteError: no such column: timestamp
Error cleaning a2a_task_events: SqliteError: no such column: timestamp
and retention pruning for these two tables never ran. Fix the DELETE
columns and align the log labels/doc comments with the real table names.
Adds source-level invariant tests (cleanup-column-fix.test.mjs) asserting
the created_at column for both tables.
* fix(memory): allow OMNIROUTE_STRICT_SYSTEM_PROVIDERS to extend the system-first provider list
PROVIDERS_SYSTEM_MUST_BE_FIRST (added in #6225 for #6135) gates both the
memory-injection placement fix and the #7293 hoistLeadingSystemMessage
translator fix, but was hardcoded to xiaomi-mimo/mimo only. Self-hosted
deployments routing other strict backends (e.g. a custom OpenAI-compatible
connection in front of a self-hosted Qwen3.5+/3.6 model, whose chat template
rejects any non-leading system message the same way) had no way to opt in
without forking and rebuilding the image.
Adds OMNIROUTE_STRICT_SYSTEM_PROVIDERS (comma-separated, case-insensitive
provider ids) to extend the built-in set at read time, mirroring the
injectable-env pattern already used in src/lib/memory/typedDecay.ts. No
behavior change for anyone who doesn't set it.
* chore: fix changelog fragment PR number
* fix(i18n): re-escape CC discovery-alias angle brackets for next-intl
Restore #8747 HTML-entity escaping for claude/<provider>/<model> in the
three CC discovery-alias message keys so next-intl stops logging
INVALID_MESSAGE: UNCLOSED_TAG on provider detail pages after the bulk
entity-unescape regression.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(i18n): align conflict context with release
* fix(i18n): cover localized CC alias placeholders
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Restore the shared media detector and the hard-reason set lost by the maintainer cherry-pick. Re-document the two live low-memory controls and cover nested case-insensitive image indicators.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(sse): grace period before finalizing a client disconnect as 499 (#9653)
A client that closes its connection right after reading a fully-completed
SSE stream can race OmniRoute's own completion bookkeeping: the bytes
already reached the client, but the transform stream's own completion
callback (onStreamComplete, which flips streamCompletionRecorded) hasn't
finished bubbling up when the disconnect handler fires, so the request gets
persisted as a false 499 with zero token usage even though it delivered its
full response.
Confirmed live on real traffic before this fix: a request whose server log
showed "disconnect: request_signal_aborted" at 18236ms was persisted with
status 200 and full token usage (82814/1292) once the grace period let the
real completion win the race, matching what the client actually received.
createClientDisconnectGraceHandler (new leaf in
streamFailureFinalization.ts) polls isStreamCompletionRecorded() for up to
STREAM_DISCONNECT_GRACE_PERIOD_MS (default 10s, env-configurable, 0
disables) before finalizing as a failure. If a real completion lands within
the window, handleStreamFailure's own guard is a no-op and the genuine 200
stands.
Covered by tests/unit/stream-disconnect-grace-period-9653.test.ts (fake-timer
driven: already-recorded completion short-circuits, disabled-grace-period
finalizes immediately, a completion landing mid-window skips finalize
entirely, and no completion ever landing finalizes once the deadline
passes).
(cherry picked from commit 5d0fe28c42)
* chore(quality): rebaseline chatCore.ts for the disconnect grace-period fix
Own growth from the disconnect grace-period fix: 5030->5039 (+9, the
createClientDisconnectGraceHandler wiring at the existing
onClientDisconnectFinalize call site).
---------
Co-authored-by: Markus Hartung <mail@hartmark.se>
* fix(sse): persist per-tool-call JSON escape state across SSE delta chunks
escapeJsonStringValues() reset its inString/pendingEscape state on every
call instead of carrying it forward per tool-call index, so a raw newline
byte (or an already-escaped \n) split across two delta chunks got corrupted
in transit — the model's own output was correctly escaped, OmniRoute broke
it. Root-caused via a dispatched investigation into real OpenClaw traffic
that looked like model-generation quality but wasn't.
Fix: escapeJsonStringValues now takes and mutates a persistent per-call
state object (JsonStringEscapeState), keyed per tool-call index in the
translator's init state and cleared when a tool call is superseded.
* chore(quality): rebaseline openai-responses.ts for the escape-state fix
Own growth from the extracted per-tool-call JSON escape-state fix
(previous commit): open-sse/translator/response/openai-responses.ts
1204->1249 (+45).
---------
Co-authored-by: Markus Hartung <mail@hartmark.se>
* fix(compression): add Lite tool truncation toggle
* fix(antigravity): add missing antigravityProjectPersistence.ts module
The quota-strategy engine (quotaStrategies.ts) imports from
antigravityProjectPersistence.ts, but only antigravityProjectPersist.ts
existed in the tree. Add the missing module with the expected
preferAntigravityConnectionsWithStoredProject() helper and re-export
the existing persistDiscoveredAntigravityProjectId().
Co-authored-by: diegosouzapw <diegosouza.pw@outlook.com>
* fix(file-size): rebaseline strategySelector.ts for Lite truncation toggle
The PR adds one line to threading options?.config?.lite into
applyLiteCompression. Update the frozen size from 1060 to 1061.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Refs #9629
---------
Co-authored-by: Xiangzhe <xiangzhedev@gmail.com>
Co-authored-by: xz-dev <xz-dev@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Renumber the CCR block-store migration from 134 to 139, reconcile databases that already applied the legacy slot, and add regression coverage for both upgrade paths.
Co-authored-by: fenix007 <fenix007@users.noreply.github.com>
* fix(adobe-firefly): open browser sign-in and resolve provider slug in /login
POST /api/providers/[id]/login passed the connection DB id to
inAppLoginService.startLogin, but that service looks up the provider by
slug in TOKEN_EXTRACTION_CONFIGS. The lookup always missed and returned
"No extraction config" without launching a browser — so the VibeProxy
"Sign in" button for Adobe Firefly (and every other web-cookie provider)
never opened a browser.
Adobe Firefly additionally had no extraction config because its IMS JWT
is never in cookies/localStorage — it only rides on the Authorization:
Bearer header of firefly-3p.ff.adobe.io XHRs.
- Resolve the provider slug from the connection row and pass the slug
(not the DB id) to inAppLoginService.startLogin.
- Add open-sse/services/adobeFireflyBrowserLogin.ts: a Playwright
service that launches a visible browser at firefly.adobe.com and
intercepts firefly-3p requests to capture the IMS JWT + sherlockToken
cookie. Wire it into the /login route for the adobe-firefly slug.
- Fix latent bug: updateProviderConnection reads camelCase keys
(apiKey, providerSpecificData), so the previous snake_case call never
persisted extracted credentials.
* fix(adobe-firefly): open browser sign-in and resolve provider slug in /login
POST /api/providers/[id]/login passed the connection DB id to
inAppLoginService.startLogin, but TOKEN_EXTRACTION_CONFIGS is keyed by
provider slug — so browser login never launched for web-cookie providers.
Adobe Firefly also cannot use cookie extraction: the IMS JWT only appears
on Authorization headers to firefly-3p.ff.adobe.io. Add a dedicated
Playwright interceptor and persist credentials with camelCase keys that
updateProviderConnection actually reads.
* fix(adobe-firefly): use system Chrome/Edge CDP for browser sign-in
Playwright is not available inside the pkg-packaged VibeProxyServices.exe,
so import('playwright') always failed with 'Playwright not installed' and
never opened a window. Launch Chrome/Edge with --remote-debugging-port and
capture the firefly-3p Authorization Bearer via pure CDP WebSocket instead.
* fix(adobe-firefly): live x-arp-session-id / Arkose wire (stop 408 under load)
Browser generate-async requires x-arp-session-id as base64({sid,ark,ftr}) with a
real Arkose blob (sherlockToken). JWT alone frequently returns colligo HTTP 408
system under load while credits still work.
- Match live ftr magic __UDF43-m4_31ck + Arkose pk in synthetic ARP fallback
- Ranked extract of sherlockToken / x-arp from Cookie, HAR, fetch() paste, and
space-joined JWT+ARP (PasswordBox newline collapse)
- Reuse one ARP for storage upload + generate-async
- Clearer 408 errors when browser ARP is missing vs stale
- Unit suite 42/42
* fix(adobe-firefly): durable session ARP rebuild and aux_sid false-positive
Rebuild x-arp-session-id from forterToken/arkose/ff_session_guid instead of
ranking long Cookie pairs (e.g. aux_sid=…) as opaque ARP, which caused colligo
HTTP 408. Cache IMS JWT + cookie sessions, rotate ARP on 408 retries, and keep
Playwright warm-up opt-in only (headless Forter is rejected).
Also expand synthetic ARP shape with bfp/fpjs to match live successful captures.
* fix(adobe-firefly): durable session, off-screen Chrome recovery, browser sign-in
Rebuild x-arp-session-id from Cookie pieces (sid/ark/forter) so aux_sid is never
sent as ARP. Sticky ARP + submit spacing reduce mid-batch colligo 408 thrash.
Add optional managed Chrome warm (off-screen headed by default; Forter rejects
headless) and POST /api/providers/{id}/login browser sign-in that returns JWT+Cookie
after a fresh SSO. Visible sign-in resets off-screen window placement and clears
prior Adobe session when adding another account.
* fix(adobe-firefly): renew sessions through durable CDP
* fix(adobe-firefly): isolate browser sessions per account
* fix(adobe-firefly): make account login fresh and deterministic
* chore(adobe-firefly): remove obsolete browser fallback
* docs(adobe-firefly): document renewal controls
* fix(adobe-firefly): harden CDP warm, risk session, and browser sign-in
Stop colligo 408 thrash from stale Forter and frozen Google login during
Sign in with browser:
- CDP warm: clear Firefly origin storage + risk cookies (keep SSO); require
forter age under 10 minutes on loop and timeout paths; dual CDP queues;
await Runtime.runIfWaitingForDebugger; profile-lock launch retries
- Session: connectionId fingerprint; write-back JWT+Cookie; warm-fail
cooldown; fail closed risk_session_stale when forter is known-stale
- Client: submit gate around generate-async; max 2 attempts when forter
known-stale; poll 401 one refresh; pass sessionBrowserKey through handlers
- Login route: pure system Chrome/Edge CDP only; camelCase credential persist
- Unit: browser-login + firefly suites green (60)
---------
Co-authored-by: artickc <artur1992123@mail.ru>
* feat(api): add GET /api/resilience/connections for per-account state
The three temporary-failure mechanisms each have their own scope -- the
provider circuit breaker covers a whole provider, connection cooldown covers
one account, model lockout covers a provider/connection/model triple -- and
until now nothing showed them side by side. Diagnosing "why is this key being
skipped" meant reading three separate surfaces and correlating by hand, which
is exactly what the docs' own debugging guidance asks an operator to do.
The route returns all three keyed by connection, plus the breaker's transition
history so a flapping provider is visible as a sequence rather than a single
current state. getStatus() already assembled everything except that history;
it now returns a copy of it and carries an explicit CircuitBreakerStatus type
instead of an inferred one.
Reading raw connection rows for this meant widening getRawProviderConnections'
column projection, so the existing allowlist is exported and the route selects
through it. A test asserts every column the route names is in that allowlist,
which turns a future typo into a failure here rather than a silent empty field.
Each of the three data sources is wrapped independently: one of them throwing
degrades that section and sets meta.degraded rather than failing the whole
response, since a partial view still answers most of the questions the page
exists for.
Loopback-gated. It spawns nothing, unlike every other entry on that list, but
it exposes per-account operational state and the comment says so to keep it
from being read as precedent for gating read-only routes generally.
Tests are real isolated-DB integration tests rather than mocks -- ESM mocking
is unavailable here (no mock.module, non-configurable exports) and the
codebase already has the isolated-DB pattern, which exercises more than a mock
would anyway.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* feat(dashboard): add the per-account resilience connections page
Renders what the API added: every connection with its cooldown, its provider
breaker, and its model lockouts in one table, with a detail view per connection
and the breaker's transitions drawn as a timeline. The timeline is the part that
is hard to get from the existing surfaces -- a breaker sitting at CLOSED right
now looks healthy, and only the sequence shows it has opened four times in the
last hour.
Polls rather than streams. The state it displays changes on the order of
seconds to minutes and the page is loopback-gated, so an SSE channel would buy
nothing over an interval.
ModelCooldownsCard had its own formatRemaining. The new table needs the same
countdown format and two copies would drift, so it moves to
shared/utils/formatRemaining.ts and both import it -- behaviour unchanged, the
extracted version differs from the deleted one only in local variable names.
DataTable's column and row interfaces are exported for the same reason: the new
table types against them rather than restating their shape.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
* fix(i18n): translate new resilience-connections screen strings
PR #9510 added the "Connection Resilience" dashboard screen but the
sync-added i18n keys (sidebar.resilienceConnections/Subtitle and the
full resilienceConnections namespace) were left as __MISSING__: in
every non-English locale, dropping i18nUiCoverage.pct below the 99
ratchet baseline.
Translate all ~78 new leaf strings into all 41 non-English locales.
Pre-existing unrelated __MISSING__ debt (hermesRole*, apiProtocol*,
grokAutoTopUp*, featureFlagExposeFunctionalGatewayMirrorsDescription)
is left untouched — out of scope for this fix.
Co-authored-by: HouMinXi <HouMinXi@users.noreply.github.com>
---------
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: HouMinXi <HouMinXi@users.noreply.github.com>