/** * Where a circuit stands: `closed` lets traffic through, `open` refuses it, `half-open` admits * probes. */ export type CircuitState = 'closed' | 'open' | 'half-open'; /** Configuration for the circuit breaker. */ export interface CircuitBreakerConfig { /** Turns the breaker on. Off by default. */ enabled?: boolean; /** Consecutive failures that open the circuit. Defaults to 5. */ failureThreshold?: number; /** * Failure fraction within the window that opens the circuit, between 0 and 1. * * Catches a provider that fails most calls without ever failing several in a row, which a * consecutive-failure count alone cannot see. Only applied once `minimumThroughput` calls have * been observed, because one failure out of two is not evidence of anything. */ failureRateThreshold?: number; /** Calls needed before `failureRateThreshold` is considered. Defaults to 10. */ minimumThroughput?: number; /** Rolling window for the failure rate. Defaults to 60 seconds. */ windowMs?: number; /** How long the circuit stays open before allowing a probe. Defaults to 30 seconds. */ resetTimeoutMs?: number; /** Concurrent probes allowed while half-open. Defaults to 1. */ halfOpenMaxCalls?: number; /** Probe successes needed to close the circuit again. Defaults to 1. */ successThreshold?: number; /** * Decides whether a failure counts against the circuit. Defaults to counting every failure except * a cancellation by the caller, which says nothing about the provider's health. */ isFailure?: (error: unknown) => boolean; /** Called on every state change. */ onStateChange?: (event: CircuitStateChange) => void; /** * Shares circuit decisions across processes. Without it each worker learns on its own, which is * the right default for one process; with it, a provider that fails in one worker is taken out of * routing in all of them, and only one worker probes it when the cooldown ends. Adapters live in * `nexus-ai-pro/ops/circuit-store` and `nexus-ai-pro/postgres/circuits`. */ store?: CircuitStateStore; /** * Counts every worker's calls together, so nine failures spread over three workers count as nine. * Each worker adds the calls it saw to a rolling window in the `store` when it syncs, and opens the * circuit everywhere once the shared count crosses `failureThreshold` (failures since any worker's * last success) or `failureRateThreshold`. Checks stay synchronous: a worker learns the shared count * on its next sync, at most `syncIntervalMs` late. Needs a store with `observe()`; every bundled one * has it. Off by default, when each worker counts only its own calls. */ shareObservations?: boolean; /** Names this worker in shared state. Defaults to a random id. */ workerId?: string; /** Shared state is refreshed at most this often, on the next check that needs it. Defaults to 1 second. */ syncIntervalMs?: number; /** How long one worker holds the right to probe. Defaults to 30 seconds. */ probeLeaseMs?: number; /** Called when the shared store cannot be read or written. The breaker keeps working locally. */ onStoreError?: (error: unknown) => void; /** Replaces the system clock, for tests. */ now?: () => number; } /** A circuit changing state. */ export interface CircuitStateChange { /** The provider whose circuit changed. */ providerName: string; /** The state it left. */ from: CircuitState; /** The state it entered. */ to: CircuitState; /** ISO-8601 time of the change. */ at: string; /** Why it changed, such as `consecutive failure threshold reached`. */ reason: string; } /** One circuit's state, for a health endpoint or dashboard. */ export interface CircuitSnapshot { /** The provider. */ providerName: string; /** Its state. */ state: CircuitState; /** Failures in a row. */ consecutiveFailures: number; /** Calls counted inside the rolling window. */ windowCalls: number; /** Failures inside the rolling window. */ windowFailures: number; /** Share of calls in the window that failed. */ failureRate: number; /** ISO-8601 time the circuit opened. */ openedAt?: string; /** When a probe will be allowed, while the circuit is open. */ retryAt?: string; /** The most recent error, as text. */ lastError?: string; /** Every worker's calls together, as of this worker's last sync, with `shareObservations`. */ shared?: CircuitWindow; } /** Calls one worker saw since it last reported, which `CircuitStateStore.observe()` adds to the shared window. */ export interface CircuitObservation { /** Calls and failures per window bucket, by the bucket's start in epoch milliseconds. */ buckets: Array<{ at: number; calls: number; failures: number; }>; /** Whether any of the calls succeeded, which ends the shared failure streak before `trailingFailures` is added. */ succeeded: boolean; /** Failures after the last success, or every failure when none succeeded. */ trailingFailures: number; /** The rolling window, in milliseconds. */ windowMs: number; /** The width of one bucket. A bucket that ended before `now - windowMs` is dropped. */ bucketMs: number; /** Epoch milliseconds now, by the breaker's clock. */ now: number; /** * Epoch milliseconds of the circuit's last close. Buckets that started before it are not counted, so * failures that opened a circuit do not reopen it the moment a probe closes it. */ since?: number; } /** A provider's calls across every worker, inside the rolling window. */ export interface CircuitWindow { /** Calls counted. */ calls: number; /** Of those, failures. */ failures: number; /** Failures since any worker last reported a success. */ consecutiveFailures: number; } /** A circuit decision as every worker sees it. */ export interface SharedCircuitState { /** The provider. */ providerName: string; /** Open or closed. Half-open is never shared; it belongs to the worker holding the probe. */ state: 'open' | 'closed'; /** Epoch milliseconds when the circuit opened. Present while open. */ openedAt?: number; /** Epoch milliseconds of the transition that wrote this state. The newest transition wins. */ updatedAt: number; /** The worker that wrote it. */ updatedBy: string; /** Why it changed. */ reason?: string; /** The error that opened it. */ lastError?: string; } /** * Where shared circuit decisions live. * * Only decisions are shared — open, closed, and who may probe. Each worker still counts its own * failures, because a shared counter would put a network round trip on every call's hot path. */ export interface CircuitStateStore { /** Every circuit the store knows about. One entry per provider, so this stays small. */ read(): Promise | SharedCircuitState[]; /** Records a transition. A write older than the stored state must not replace it. */ write(state: SharedCircuitState): Promise | void; /** * Claims the right to send the half-open probe. One worker holds it until `ttlMs` passes, so a * recovering provider is probed once rather than by every replica at the same moment. */ claimProbe(providerName: string, owner: string, ttlMs: number): Promise | boolean; /** * Adds one worker's calls to a provider's shared window, and returns the window across every * worker. Optional; `shareObservations` needs it. Each call must add atomically, so two workers * reporting at once both count. */ observe?(providerName: string, observation: CircuitObservation): Promise | CircuitWindow; } /** @internal A circuit's local state. Shared with the coordinator module, never exported publicly. */ export interface CircuitEntry { state: CircuitState; consecutiveFailures: number; /** Timestamps of calls in the rolling window, paired with whether each failed. */ window: Array<{ at: number; failed: boolean; }>; openedAt?: number; halfOpenInFlight: number; halfOpenSuccesses: number; lastError?: string; /** When this worker last changed or adopted the state, for ordering against shared writes. */ changedAt: number; /** True once this worker holds the shared right to probe the current cooldown. */ probeClaimed: boolean; /** Why the circuit last changed state. */ reason?: string; /** The shared window from the last sync, with `shareObservations`. */ shared?: CircuitWindow; } /** * Trips routing away from a provider that is failing. * * `ProviderHealthMonitor` already scores providers, and the auto-router already penalizes an * unhealthy one. A breaker is the stronger step: while a circuit is open the provider is removed * from routing entirely rather than merely ranked lower, so a hard-down provider stops absorbing * one attempt per request. After a cooldown a limited number of probes decide whether it recovered. * * State is per process unless a `store` is configured. Every check stays synchronous either way: * shared state is refreshed in the background, on the first check after `syncIntervalMs`, so the * breaker never puts a network call in front of a request. */ export declare class CircuitBreaker { private readonly config; private readonly entries; private readonly now; /** Shared-state logic, loaded only when a store is configured. */ private coordinator; private readonly coordinatorReady; constructor(config?: CircuitBreakerConfig); /** Whether the breaker is on. */ get enabled(): boolean; /** * Whether a call may proceed. * * Consumes a half-open probe slot when it returns true in that state, so concurrent callers do * not all rush a provider that has only just come back. The slot is released by * `recordSuccess()` or `recordFailure()`. */ allowRequest(providerName: string): boolean; /** Current state, after applying any elapsed cooldown. */ state(providerName: string): CircuitState; /** Whether the circuit currently refuses traffic. */ isOpen(providerName: string): boolean; /** Providers whose circuit currently refuses traffic. */ openProviders(): string[]; /** Records a successful call. A successful probe closes the circuit. */ recordSuccess(providerName: string): void; /** Records a failed call. Enough failures, or a failed probe, open the circuit. */ recordFailure(providerName: string, error?: unknown): void; /** Every circuit's state, or one provider's. */ snapshot(providerName?: string): CircuitSnapshot[]; /** * Forces a circuit closed, for an operator override or a test. With a shared store the override * is published, so every worker closes the circuit rather than only this one. */ reset(providerName?: string): void; /** * Reads shared state now, adopting decisions other workers made and claiming a probe when this * worker's cooldown has elapsed. Checks call it in the background on their own; await it at * startup so the first request already knows which providers are open elsewhere. */ sync(): Promise; /** Waits for transitions this worker has published. For tests and graceful shutdown. */ flush(): Promise; private refreshState; private transition; private pushWindow; private trimWindow; private failureRate; private entry; }