/** * Per-facade circuit breaker. * * The failure this exists for: one facade's dependency wedges — Mother's * Postgres stops accepting connections, Supabase hangs, a disk mount goes * away — and every call into that facade sits on a socket until it times out. * With six clients sharing one runtime that is enough to make the whole * runtime feel dead, even though twenty other facades are fine. The breaker * turns a slow, repeated failure into a fast, local one. * * WHAT COUNTS AS A FAULT is the whole design, because Decibel's tools rarely * throw. `senken_trade_summary` catches its own Postgres error and returns * `{isError: true}` (src/tools/senken.ts:117), so a breaker that only counted * exceptions would never trip on the exact case it was built for. But counting * every `isError` is worse: `create_issue` missing a title is an `isError` * too, and five bad calls in a row from one confused agent must not take the * sentinel facade offline for everyone. * * So the signal is *unresponsiveness*, not *unhappiness*: * * - a thrown exception is always a fault (the handler didn't handle it) * - an `isError` result that took longer than `slowFailureMs` is a fault * (a dependency that times out is slow; a rejected argument is not) * - an `isError` result that came back fast is neither fault nor proof of * health — it leaves the counters exactly where they were * - a success closes the circuit and clears the count * * State machine: closed → (threshold consecutive faults) → open → (cooldown * elapses) → half-open → success closes it, fault re-opens it. Half-open * admits exactly one probe; concurrent callers are rejected until it lands, so * a recovering database gets one connection attempt rather than six. * * The breaker never converts a working call into a failure — it only refuses * calls it has good reason to believe would fail slowly. */ export type CircuitState = 'closed' | 'open' | 'half_open'; export interface CircuitBreakerOptions { /** Consecutive faults before the circuit opens. Default 5. */ failureThreshold?: number; /** How long the circuit stays open before admitting a probe. Default 30s. */ cooldownMs?: number; /** An `isError` result slower than this counts as a fault. Default 2s. */ slowFailureMs?: number; /** Injectable clock — tests drive this instead of sleeping. */ now?: () => number; } export interface CircuitSnapshot { state: CircuitState; consecutive_faults: number; opened_at?: string; retry_after_ms?: number; last_error?: string; } /** What the caller learns when it asks permission to run. */ export type CircuitDecision = { allowed: true; } | { allowed: false; retryAfterMs: number; lastError?: string; openedAt?: string; }; export declare class CircuitBreakerRegistry { private readonly circuits; private readonly failureThreshold; private readonly cooldownMs; readonly slowFailureMs: number; private readonly now; constructor(opts?: CircuitBreakerOptions); /** * Ask whether a call to `key` may proceed. Transitions open → half_open when * the cooldown has elapsed, and reserves the half-open probe for the caller * it says yes to. */ beforeCall(key: string): CircuitDecision; /** * Report what happened. `threw` and `durationMs` decide whether this was a * fault; see the module comment for why `isError` alone is not enough and * not too much. */ afterCall(key: string, outcome: { threw: boolean; isError: boolean; durationMs: number; error?: string; }): void; recordSuccess(key: string): void; recordFault(key: string, error?: string): void; state(key: string): CircuitState; /** Open circuits only — a healthy runtime reports `{}`. */ snapshot(): Record; reset(key?: string): void; private close; } //# sourceMappingURL=circuitBreaker.d.ts.map