Skip to content

feat(resilience): surface TLS-fingerprint remediation when Cloudflare 1010 hits - #12531

Closed
alvinveroy wants to merge 16 commits into
diegosouzapw:release/v3.8.51from
alvinveroy:feat/tls-fingerprint-remediation
Closed

alvinveroy wants to merge 16 commits into
diegosouzapw:release/v3.8.51from
alvinveroy:feat/tls-fingerprint-remediation

Conversation

@alvinveroy

@alvinveroy alvinveroy commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #12529
Closes #12657
Closes #12658
Closes #12659

Supersedes #12660, #12661, #12662 (their head branches carried the same cumulative content with an older base; this PR is the consolidated, updated head on current release/v3.8.51).

Summary

One production batch for the 3.8.51 fork, grouped by area. All pieces were deployed and validated on the production instance (see Deployment evidence per area) before this PR was assembled.

1. TLS-fingerprint remediation on Cloudflare 1010 (fixes #12529)

A Cloudflare 403 / error_code 1010 rejection means the CDN banned the CLIENT's TLS/UA signature — not the account. #9929 already classifies it FINGERPRINT_REJECTION so it no longer poisons account state, and the repo ships the fix (Chrome-124 TLS impersonation via optional wreq-js), but nothing connected a 1010 to that one-line remediation.

  • open-sse/services/errorClassifier.ts: throttled (once-per-process) remediation hint when a response classifies FINGERPRINT_REJECTION while ENABLE_TLS_FINGERPRINT is off; suppressed when the transport is enabled.
  • open-sse/utils/proxyFetch.ts: adaptive TLS ladder — plain fetch fast path; wreq-js only on the 1010 arm (1h TTL) with a first-byte watchdog (TLS_FINGERPRINT_FIRST_BYTE_WATCHDOG_MS, default 10s) and direct-dispatcher fallback, fixing the wreq-js zero-byte streaming stall (TTFB 90–600s → 3–6s). New env TLS_FINGERPRINT_FIRST_BYTE_WATCHDOG_MS documented.
  • open-sse/services/errorClassifier.ts + auth path keep the connection active/non-terminal on 1010.
  • Tests: tests/unit/12529-fingerprint-remediation-auth-path.test.ts, tests/unit/error-classifier.test.ts, tests/unit/transport-bounded-abort.test.ts, tests/unit/self-probe-watchdog.test.mjs.

2. opencode family: background-call identity + shape fixes (closes #12657)

Starting 2026-09-06 opencode.ai enforces x-opencode-session on background calls (model discovery, quota fetches went out as UA "Bun fetch" with no session header).

  • open-sse/utils/opencodeHeaders.ts: buildOpencodeBackgroundHeaders() attaches the conversation-stable session identity to discovery (src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts opencode/zen/go entries) and open-sse/services/opencodeQuotaFetcher.ts.
  • open-sse/translator/index.ts + toResponses.ts: reasoning-presence sentinel + same-format user-turn handling for GLM [1214]; explicit-replay missing-reasoning branch.
  • Tests: tests/unit/opencode-background-identity-headers.test.ts, tests/unit/opencode-reasoning-presence-responses.test.ts, tests/unit/glm-1214-same-format-user-turn.test.ts.

3. Combo dispatch: per-target skip diagnostics + tiny-budget reasoning probe (closes #12659)

  • open-sse/services/combo/decisionTrace.ts + combo.ts: persisted per-target skip reasons surface in the ALL_TARGETS_SKIPPED diagnostics instead of a bare 503.
  • Tiny-budget reasoning probes (max_tokens 256) that get truncated return a truncated 200 via buildReasoningProbeTruncatedResponse in both dispatchers (was a quality-validation 502 "reasoning consumed N/N tokens").
  • Tests: tests/unit/12294-skipped-target-diagnostics.test.ts, tests/unit/10281-combo-reasoning-probe.test.ts.

4. Ops robustness (closes #12658)

  • Embedding inputs clamped to model context (both callers) — open-sse/handlers/embeddings.ts, src/lib/memory/embedding/remote.ts; test embeddings-input-clamp.test.ts.
  • Egress-IP lockout provider override for paid plans: OMNIROUTE_EGRESS_IP_LOCK_PROVIDERS (none/off/false/0 disables; CSV replaces the default opencode free-tier family) — open-sse/config/providerErrorRules.ts; test egress-ip-lock-env-override.test.ts.
  • Client-abort crash guard (src/shared/utils/httpClientAbortGuard.mjs + sse handler) and bounded transport abort inside combos.
  • Legacy-provider projection fix so per-provider fetchStartTimeoutCapMs actually reaches the runtime registry entries (open-sse/config/providerRegistry.ts + legacy projection), raising the streaming cap for buffered gateways (opencode-go / command-code).
  • Related upstream note: fix(opencode): synthesize x-opencode-session when client sends none — 2026-09-06 upstream enforcement #12719 (different author) adds x-opencode-session synthesis on the executor client leg — complementary to item 2 (background leg); both touch open-sse/utils/opencodeHeaders.ts but different functions; merge order is safe.

Testing

  • Scoped unit suites (repo loader stack, tsx/esm): fetch-start-cap projection, opencode background identity headers (6/6), opencode reasoning presence, glm-1214 same-format, embeddings clamp, egress-IP env override, skipped-target diagnostics (5/5), combo reasoning probe, transport-bounded abort, self-probe watchdog, error classifier (35/35) — all green.
  • Branch merged current release/v3.8.51 (17 upstream fixes); local gate run: changelog-integrity OK, api-docs-refs OK, docs-all OK (migration counts 169→170, env/docs contract), agent-skills sync OK, file-size gate OK.
  • CI on this head: Vitest fast-path, unit fast-path splits, ESLint, semgrep, typechecks.

Deployment evidence

On the affected production instance:

  • TLS: enabling the fingerprint transport (wreq-js present since install) changed egress to Chrome-124 (ja3_hash 24a38702… h2 vs Node default d67b0948…); 1010 rejections stopped. With the adaptive ladder in place, ENABLE_TLS_FINGERPRINT may stay off until 1010s recur.
  • opencode-go / command-code buffered gateways: long reasoning generations no longer die at the 110s fetch-start cap (daily TPS opencode-go 12→31→54, command-code 23→62→73 after the cap fix).
  • Background identity headers live from 09/06 enforcement date; no more UA "Bun fetch" hard errors on discovery/quota.

… 1010 hits

A Cloudflare 1010 / browser-signature rejection (diegosouzapw#9929) means the CDN
banned the CLIENT's TLS/UA signature — not the account. OmniRoute already
ships the fix for exactly this: a Chrome-124 impersonation transport
(open-sse/utils/tlsClient.ts via the optional wreq-js dependency), gated
behind ENABLE_TLS_FINGERPRINT. But the gate is off by default, the
dependency is optional, and nothing tells the operator any of this — so
repeated 1010s look like flaky upstreams while a one-line env change
would fix them.

Emit a throttled (once-per-process) remediation hint when a response is
classified FINGERPRINT_REJECTION and ENABLE_TLS_FINGERPRINT is not
enabled, naming the exact env vars and the optional dependency. No
behavior change otherwise; the hint is suppressed entirely once the
transport is enabled.
…-fingerprint hint

Maintainer request on diegosouzapw#12531: the classifier-only unit passes both before and
after the change because classifyProviderError already returns
FINGERPRINT_REJECTION — the defect is that resolveTerminalConnectionStatus
silently dropped it. Drive markAccountUnavailable end-to-end and assert the
account stays healthy AND the hint fires exactly once from that path
(throttled; suppressed when ENABLE_TLS_FINGERPRINT=true).

Mirrors the 8200 auth-path test setup (isolated DATA_DIR + real DB rows).
Resolves the PR's dirty merge state: keeps upstream's Kiro IDC profileArn
PROJECT_ROUTE_ERROR block (diegosouzapw#10725) ahead of the Sentinel/Turnstile check and
retains this branch's diegosouzapw#8813 comment refinement on the Sentinel block.
alvinveroy and others added 6 commits September 4, 2026 06:37
…codeProtocol allowlist to L313

Fast Quality Gates carried two reds on this PR:

1. mutation-test-coverage (ours): the new auth-path regression test covers a
   mutated module but was absent from stryker.conf.json tap.testFiles, so its
   mutant kills stopped counting (--strict drift gate). Registered.

2. public-creds (inherited base-red diegosouzapw#12581): upstream diegosouzapw#12179 moved the
   per-process ZCode handshake literal from L302 to L313, leaving the frozen
   file:line:value allowlist pin stale. Re-pin to L313 — same justification
   (local per-process handshake ID, not an upstream credential). Identical to
   the fix upstream's fix/release-v3.8.51-basereds-public-creds must carry.
…ode background identity headers + staged resilience work

# Conflicts:
#	open-sse/executors/base.ts
#	open-sse/handlers/embeddings.ts
#	open-sse/services/combo.ts
#	open-sse/services/opencodeQuotaFetcher.ts
#	src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx
#	src/i18n/messages/in.json
#	src/lib/usage/callLogArtifacts.ts
#	src/shared/constants/visionModels.ts
#	src/shared/utils/httpClientAbortGuard.mjs
#	tests/unit/call-log-cap.test.ts
#	tests/unit/httpClientAbortGuard.test.mjs
…10, with a first-byte watchdog

Root cause of the 2026-09-03/04 outage: with ENABLE_TLS_FINGERPRINT=true every
direct inference dispatch rode the wreq-js (browser-JA3) transport, whose
responses resolved but streamed ZERO bytes for 90-600s on opencode-go /
command-code / bailian (agents aborted -> 499 storms; the then-live 110s
headers-wait default turned the hangs into 504s). Plain node fetch streamed the
same requests in seconds and control-plane calls were unaffected — the stall is
transport+streaming-specific, not DNS/CF slow-walking.

- wreq is no longer eager by default: plain direct fetch is the fast path; a
  Cloudflare fingerprint rejection (403 + 1010 / browser_signature_banned)
  arms a 1h per-provider cache and retries once via wreq (replay-safe requests
  only). TLS_FINGERPRINT_PROVIDERS keeps its legacy eager semantics, now
  guarded.
- Every wreq response (direct, ladder retry, proxied overlay) carries a
  first-byte watchdog (TLS_FINGERPRINT_FIRST_BYTE_WATCHDOG_MS, default 10s):
  no first byte -> cancel and fall back to the direct dispatcher instead of
  hanging the caller.

Verified live: fingerprint off + ladder armed-on-demand restores TTFB 3-6s on
opencode-go (was 90-600s of zero bytes), zero 1010 recurrence in the window.
…10, with a first-byte watchdog; per-provider fetch-start cap override

Root cause of the 2026-09-03/04 outage this PR was filed against: with
ENABLE_TLS_FINGERPRINT=true every direct inference dispatch rode the wreq-js
(browser-JA3) transport, whose responses resolved but streamed ZERO bytes for
90-600s on opencode-go / command-code / bailian (agents aborted -> 499 storms;
the streaming headers-wait default turned the hangs into exact-110000ms 504s).
Plain node fetch streamed the same requests in seconds.

- wreq is no longer eager by default: plain direct fetch is the fast path; a
  Cloudflare fingerprint rejection arms a 1h per-provider cache and retries
  once via wreq (replay-safe requests only). TLS_FINGERPRINT_PROVIDERS keeps
  its legacy eager semantics, now watchdog-guarded.
- Every wreq response carries a first-byte watchdog
  (TLS_FINGERPRINT_FIRST_BYTE_WATCHDOG_MS, default 10s): no first byte ->
  cancel and fall back to the direct dispatcher instead of hanging the caller.
- fetchStartTimeoutCapMs: the per-provider headers-wait override now actually
  reaches the executor (generateLegacyProviders() dropped the field, so the
  110s default fired even with the registry override present); opencode-go and
  command-code raise it to 600s (Console Go gateways buffer entire generations
  — production showed TTFB up to 493s).

Verified live: TTFB 3-6s on opencode-go (was 90-600s of zero bytes); zero
1010 recurrence with the transport off; zero 110000ms timeouts after the cap
fix.
@alvinveroy

Copy link
Copy Markdown
Contributor Author

Updated (head a556549b6): merged current release/v3.8.51 (includes the 16 stream-reliability fixes #12444–#12466) and extended the PR with the two operational fixes discovered while running this hint in production:

  1. Adaptive TLS-fingerprint ladder — the fingerprint transport can no longer hang callers: plain direct fetch is the fast path; a Cloudflare 1010 rejection arms a 1h per-provider cache and retries once via wreq-js; every wreq response carries a first-byte watchdog (10s default) that falls back to the direct dispatcher. This fixes the zero-byte streaming stall (90–600s) observed on opencode-go / command-code / bailian with the flag blanket-on. TLS_FINGERPRINT_PROVIDERS eager semantics preserved, watchdog-guarded.
  2. fetchStartTimeoutCapMs actually reaches the executor — generateLegacyProviders() dropped the field, so the 110s streaming default fired even with the registry override present (the two open-sse TS2353 baseline errors are this undeclared field). opencode-go / command-code raise the cap to 600s (measured TTFB up to 493s on buffered Console Go generations).

New tests: fetch-start-cap-legacy-projection.test.ts + the auth-path regression from the previous update; scoped suites 269/269 green on the branch. Also filed #12655 (cap override) and #12656 (wreq-js stall) with the full evidence.

… watchdog

2026-09-05 wedge incident: opencode h2 stream resets (ERR_HTTP2_STREAM_ERROR,
UND_ERR_SOCKET) fanned out through every combo target (8 fallbacks / 18
decisions per request), then a client abort during combo cleanup left the
event loop wedged — listener accepting, nothing served, nothing logged, for
hours. The existing supervisor only restarts on process exit.

- Bounded transport abort (both dispatchers): consecutive transport-class
  network failures share the same egress — after
  DEFAULT_TRANSPORT_ABORT_BOUND (3), abort the combo with an explicit 502
  instead of burning the remaining fan-out. Non-transport failures reset the
  counter. New predicates isTransportClassNetworkError /
  shouldAbortComboForTransportFailures in comboPredicates.ts.
- Self-probe wedge watchdog (scripts/dev/self-probe-watchdog.mjs, wired in
  standalone-server-ws.mjs after the listener accepts): loopback probe of
  /api/status on the same event loop — a wedged loop cannot answer it.
  N consecutive connect/timeout failures (any HTTP response counts as alive,
  incl. 401) exit(1) so launchd KeepAlive / systemd / Docker relaunch.
  Config: OMNIROUTE_SELF_PROBE(_INTERVAL_MS|_TIMEOUT_MS|_THRESHOLD), default
  60s/15s/3; OMNIROUTE_SELF_PROBE=off disables.
- file-size rebaseline for the touched frozen files with justification.
…in TLS watchdog; stryker tap registration

- dashboard/combos/page.tsx: defer the post-hydration localStorage correction
  one microtask so setShowUsageGuide is not a synchronous setState inside the
  effect body (react-hooks/set-state-in-effect base red on release/v3.8.51
  from diegosouzapw#12448); eslint:json gate green.
- proxyFetch.ts withTlsFirstByteWatchdog: the watchdog race consumed the first
  body chunk from the reader but never re-emitted it — the first SSE frame
  was silently dropped. The rest stream now re-emits the captured chunk
  before continuing the original stream.
- stryker.conf.json: register 10281-combo-reasoning-probe + transport-bounded-abort
  in tap.testFiles (they cover mutated modules; --strict gate).
- callLogArtifacts.ts / earlier dashboard stray: restore pristine remote state
  for files untouched by this branch (resolves unused-var lint).
- take upstream useSyncExternalStore combos usage-guide (diegosouzapw#12671) over our
  queueMicrotask lint defuse (supersedes it; suppression already dropped)
- file-size baseline: combos/page.tsx 5066 (upstream shape); keep both
  _rebaseline entries (bounded-abort/tls-ladder + 12671 external store)
- upstream additions folded in: diegosouzapw#12682 sqlite cleanup, diegosouzapw#12691 tool_calls
  guard, diegosouzapw#12834 codex compression re-enable, diegosouzapw#12707 continuation, diegosouzapw#12710
  video-transcript redaction, diegosouzapw#12699 npmrc shipping, docker/deps/docs
- docs counts: 169 -> 170 migrations (README.md, AGENTS.md, llm.txt + 41 i18n llm.txt mirrors)
- env/docs contract: document OMNIROUTE_EGRESS_IP_LOCK_PROVIDERS and
  TLS_FINGERPRINT_FIRST_BYTE_WATCHDOG_MS in .env.example + ENVIRONMENT.md
- changelog: normalize upstream reset-aware-model-family.md fragment to bullet form
- agent skills: regenerate cli-tunnel SKILL.md (catalog drift from merged CLI-tool commits)
- drop stray changelog.d/.DS_Store
@alvinveroy

Copy link
Copy Markdown
Contributor Author

Closing this consolidated PR. Following the maintainer-friendly stacked-PR pattern
(CONTRIBUTING.md + the split-PR best practice referenced in
Graphite's "How to break up large pull requests" guide), the work is being re-submitted as a
chain of focused PRs that each touch a single concern, each with its own changelog fragment,
its own scoped tests, and its own CI pass.

The 17 commits consolidated here are split into 10 groups; each new PR links back to the
relevant existing issue (when one already exists) and creates a new issue only when none
covers the gap. Net effect for the reviewer: smaller diffs, faster per-PR feedback loop,
clearer failure attribution when CI reddens.

Replacement stack (track these new PRs instead of this one):

  1. fix(resilience): bounded combo abort on transport-class failures + tls-scope guard
    — fixes fix(sse): client-abort crash guard rethrows Error [AbortError]: request_signal_aborted — repeated exit-code-7 crashes on routine client disconnects #12164, ties fix(docker): harden cli profile trust boundary + add SECURITY note (#12570) #12706-class transport errors
  2. fix(ops): self-probe wedge watchdog + pack-artifact closure allowlist
    — registry size 11 → 12 entries; adds dist/self-probe-watchdog.mjs to the required paths
  3. fix(translator): GLM same-format user-turn + opencode reasoning-presence placeholder
    — fixes [1210]/[1214] on glm-5.3-flash via opencode-go; refs fix(ops): clamp embedding inputs to model context; egress-IP lockout provider override for paid plans; glm-5.3 vision fragment #12662
  4. fix(providers): fetch-start cap projection + opencode background identity headers
    — implements Responses->Chat: turn stalls with 120s keepalive then client 499 abort after tool_search is flattened (NVIDIA upstreams) #11526 follow-up + fix(opencode): background calls (model discovery, quota) send UA "Bun fetch" without x-opencode-session — hard errors announced from 09/06 #12657
  5. fix(ops): egress-IP lock provider override + env contract docs
    — implements fix(ops): embeddings 8192-token clamp + egress-IP lockout provider override for paid plans #12658
  6. fix(memory): embedding 8192-token clamp (remote + handler)
    — implements fix(ops): embeddings 8192-token clamp + egress-IP lockout provider override for paid plans #12658
  7. feat(errorClassifier): Cloudflare 1010 remediation hint
    — implements feat(resilience): surface the Cloudflare 1010 TLS-fingerprint remediation hint #12529
  8. fix(combo): per-target skip diagnostics + tiny-budget reasoning probe returns 200
    — implements fix(resilience): ALL_TARGETS_SKIPPED 503 on round-robin combos returns no per-target diagnostics #12294 / fix(combo): ALL_TARGETS_SKIPPED must carry per-target skip reasons; tiny-budget reasoning probes should return truncated 200s #12659
  9. fix(ci): clear merge-gate drift inherited from upstream merge
    — docs counts (170), env docs, changelog fragment normalization, eslint suppressions
  10. chore(build): NEXT_BUILD_CPUS env knob + combos/page useEffect microtask defer
    — small ergonomic wins

Each PR body will include the focused-loop commands run on the VPS, the changed/added test
files, the final coverage result, and the proof-of-green CI link once opened.

Closing this entry to keep the review queue clean. The corresponding tracking issue at the
top of the chain will be linked into each PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment