import { DeeplineError } from './errors.js'; /** * Shared reconnect policy for the canonical run SSE stream. * * Server stream windows are finite: the platform ends them cleanly at the * function ceiling even while the run keeps executing. Consumers that wait for * a terminal status (`plays run --watch`, `runs tail`, `client.runs.tail`) * therefore reconnect with full-jitter exponential backoff instead of treating * a window end as a failure. */ /** Base delay for the full-jitter exponential backoff between reconnects. */ export const STREAM_RECONNECT_BASE_DELAY_MS = 500; /** Upper bound for a single reconnect delay. */ export const STREAM_RECONNECT_MAX_DELAY_MS = 15_000; /** * A connection that stayed open at least this long (or delivered any event) * counts as healthy and resets the backoff sequence. */ export const STREAM_HEALTHY_CONNECTION_MS = 30_000; /** Full-jitter exponential backoff: uniform in [1, min(cap, base * 2^attempt)]. */ export function streamReconnectDelayMs(attempt: number): number { const cappedExponentialMs = Math.min( STREAM_RECONNECT_MAX_DELAY_MS, STREAM_RECONNECT_BASE_DELAY_MS * 2 ** Math.max(0, attempt), ); return Math.max(1, Math.floor(Math.random() * (cappedExponentialMs + 1))); } export function isTransientPlayStreamError(error: unknown): boolean { if ( error instanceof DeeplineError && error.code === 'PLAY_STREAM_NETWORK_ABORTED' ) { return true; } if (error instanceof DeeplineError && typeof error.statusCode === 'number') { // Server-shaped errors with a definite status code are NOT transient by // pattern — only network-level failures are. 5xx counts as transient // since the server may recover, but 4xx (especially 404 = run gone) is // terminal and should not be hidden behind a silent retry loop. return error.statusCode >= 500 && error.statusCode < 600; } const text = error instanceof Error ? error.message : String(error); return /auth validation backend timed out|coordinator \/submit(?:\?[^ ]*)? 5\d\d|Worker threw exception|Internal Server Error|Service Unavailable|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test( text, ); }