feat: unify tinfoil retry stack, add timeouts, surface real errors - #1151
Conversation
Semgrep Security ScanNo security issues found. |
|
Preview environment destroyed 🧹 Stack |
PR Metrics
Updated Thu, 30 Jul 2026 19:49:48 GMT · run #2518 |
There was a problem hiding this comment.
🔭 thunder-deep-review (advisory)
Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 44eb88983a0d · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)
There was a problem hiding this comment.
🔭 thunder-deep-review (advisory)
Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: bf2cd1e4eaff · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)
There was a problem hiding this comment.
🔭 thunder-deep-review (advisory)
Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: a7b3c96abc49 · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)
There was a problem hiding this comment.
🔭 thunder-deep-review (advisory)
Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 2afaa16f2091 · mode: deep · deferred 0 item(s) already reported by other bots (best-effort dedup)
…unify-retry-stack-add-timeouts
GLM 5.2 via Tinfoil intermittently needed multiple retries; failures were invisible and the UI showed a generic error. This PR unifies the retry stack, bounds every hang vector, and surfaces the real failure cause.
What changed
Backend — Tinfoil proxy timeouts (
backend/src/tinfoil/routes.ts)504with a stabletinfoil upstream timeoutmarker the frontend classifies.capStream(onIdle: 'terminate' | 'error', optional byte cap) instead of forking a second idle-relay implementation.AbortSignal.any), so cancelled sends stop upstream work.Frontend — SecureClient lifecycle (
src/ai/tinfoil-client.ts, extracted fromfetch.ts)ready()) is now bounded (15s): the SDK has no timeout/AbortSignal anywhere, and a chat send's abort signal does not cover the attestation await — a hungatc.tinfoil.shpreviously hung the send forever. The bound applies to every call (not just the first) because an SDK-internal reset re-attests on the nextready().ready()now evicts the cached client, so the next send reconstructs instead of inheriting a wedged instance.TypeError: … null (reading 'fetch')(no exported error class), which now also triggers eviction alongsideKeyConfigMismatchError. All classification is byerr.name— the tinfoil browser entry does not export the error classes, soinstanceofis unusable.prewarm-system-model,model-catalog, and the voice engine now import the lighttinfoil-clientmodule instead of dragging the whole AI-provider pipeline into the startup chunk.Unified per-turn retry budget (
src/ai/retry-budget.ts)maxRetriesdefault 2) × empty-response retry (×2) × outer chat auto-retry (×3).maxRetries: 0) so every request is counted. A typed consumer/probe split prevents layers from double-drawing.sendAutomaticallyWhen) previously shared one budget until exhaustion.Error UX + telemetry
kindthrough serialization (attestation/timeout/rate-limit/provider/network) and the chat error state shows cause-specific guidance instead of "Something went wrong" for every failure.tinfoil_attestation(outcome/duration/client, deduped to one success per client instance),chat_auto_retrygains areason, pluschat_retry_successandchat_retries_exhausted.Fault-injection coverage (all deterministic, DI-based, no module mocks)
ready()rejection/hang/timeout, construction failure, key-config mismatch, the null-transport race, re-attestation hang after internal reset, TTFB timeout, mid-stream stall, client abort propagation, connection reset cleanup, 429/4xx/5xx/timeout/network classification, budget exhaustion across layers, backoff progression, and budget reset semantics (including the automation path).Verification
bun run check(type-check, lint, format, license): cleanbun run test: full frontend suite greenbun run test:backend: 938 pass, 0 failtinfoil@1.1.11dist (ready() semantics, internal retry counts, missing timeout/signal support, error names, EHBP framing)