import type { AgentChatEvent } from "./types.js"; /** * Max time without a heartbeat before a "running" run is considered dead. * The run-manager heartbeats every 1.5s, so 15s tolerates ~9 missed writes. * Widened from 6s to absorb real-world DB latency spikes and GC pauses that * caused false-positive reaps: a live run whose heartbeat lagged 6s+ would be * reaped and a zombie would keep running, eventually clobbering the new row. */ export declare const RUN_STALE_MS = 15000; /** * Stale window for runs dispatched into a Netlify background function * (`dispatch_mode = 'background'`). The design doc flags the 15s reaper vs a * background cold-start as the #1 false-failure risk: the foreground POST * inserts the `running` row, then `fireInternalDispatch` returns 202 and the * background function may take >15s to cold-start and emit its first heartbeat. * With the normal 15s window the reaper would falsely kill that freshly- * inserted-but-not-yet-heartbeaten row. 90s tolerates a slow background * cold-start while still reaping a genuinely dead background worker promptly. * Claimed background workers heartbeat during long work; the stale watchdog is a * liveness timeout, not the Netlify background-function execution budget. * * Only applied to rows explicitly marked background-dispatched; ordinary * foreground runs keep the tight 15s window unchanged. */ export declare const BACKGROUND_RUN_STALE_MS = 90000; /** * A row is `background` only while the platform may still be cold-starting the * worker, so it needs the full 90s handoff allowance above. Once that worker * atomically claims the row (`background-processing`), it has already proved * it started and should be reaped sooner if both heartbeat and real progress * stop. This keeps a silent post-claim worker death from holding the client * for the entire cold-start window before the durable successor is created. * * A real long-running tool or nested agent call still sets `in_flight_since`, * which grants the bounded `IN_FLIGHT_RUN_STALE_GRACE_MS` below. Healthy model * work keeps the normal heartbeat moving every 1.5s, so this is only a faster * recovery path for a worker that has genuinely gone silent. */ export declare const BACKGROUND_PROCESSING_RUN_STALE_MS = 45000; export declare const STALE_RUN_ERROR_EVENT: { readonly type: "error"; readonly error: "The agent stopped before it could finish. It may have hit a server timeout or the worker may have been interrupted."; readonly errorCode: "stale_run"; readonly recoverable: true; readonly details: "The run heartbeat stopped while the run was still marked running. Partial output and tool calls were preserved when available."; }; /** * Terminal error for a background-dispatched run whose worker NEVER claimed it * (the foreground fired the self-dispatch, Netlify acked it async with a 202, * but the `_process-run` worker never ran far enough to flip * `dispatch_mode background → background-processing`). Distinct errorCode so the * client (and prod triage) can tell "the worker died silently" apart from "a * claimed worker's heartbeat went stale". Recoverable so the client surfaces a * retry affordance and re-drives the turn. See `reapUnclaimedBackgroundRun`. */ export declare const UNCLAIMED_BACKGROUND_RUN_ERROR_EVENT: { readonly type: "error"; readonly error: "The agent run was handed off to a background worker that never started. It was recovered so you can try again."; readonly errorCode: "background_worker_never_started"; readonly recoverable: true; readonly details: "A background-dispatched run was acknowledged (HTTP 202) but its worker never claimed the run, so no progress was produced. The run was reaped early (it had no live worker to protect) so the turn can be retried."; }; /** * Terminal error for a background worker that DID claim the run, then failed * during route/handler setup before `startRun` could emit its own error event. * Claimed runs are no longer eligible for foreground inline recovery, so the * route boundary must fail them loudly instead of leaving subscribers to wait * for stale-run recovery. */ export declare const CLAIMED_BACKGROUND_WORKER_FAILED_ERROR_EVENT: { readonly type: "error"; readonly error: "The background agent worker stopped before it could start the turn. You can retry from the preserved chat context."; readonly errorCode: "background_worker_failed"; readonly recoverable: true; readonly details: "The durable background worker claimed the run but threw during setup before it could emit agent events."; }; /** * Grace period before a never-claimed background run (dispatch_mode still * 'background', no worker claim) is treated as a dead handoff and reaped. * * This is intentionally tighter than `BACKGROUND_RUN_STALE_MS`. That wider * window protects cold-starting or temporarily delayed background dispatches, * while claimed workers stay alive by heartbeat/progress updates. A run that is * still `dispatch_mode = 'background'` has, by definition, NO worker — nothing * to protect — so once a Netlify * background function has had a reasonable cold-start window to claim it and * hasn't, the handoff is dead and should surface promptly instead of leaving * the user staring at a spinner for the durable-worker window. 25s comfortably exceeds a normal * Netlify Lambda cold start while still failing fast on a silent worker death. */ export declare const UNCLAIMED_BACKGROUND_RUN_GRACE_MS = 25000; /** * Backstop ceiling — measured from the row's ORIGINAL `started_at`, which never * changes — after which the unclaimed-background-run sweep stops attempting to * redispatch a lost handoff and instead reaps it via `reapUnclaimedBackgroundRun` * (loud, attributable `errored`). This is what keeps redispatch recoverable * WITHOUT becoming a silent hang: a handoff that cannot be delivered within this * window (a genuinely dead platform, not a transient blip) still fails loudly, * it just gets a few sweep-cycle chances first. 5 minutes comfortably allows * multiple 2-minute sweep ticks (see `agent-chat-plugin.ts`'s * "Unclaimed background-run sweep") while staying well inside both the 40s * foreground chunk clamp and the ~13min background soft-timeout ceiling that * bound how long a real user turn is worth waiting on before failing loud. */ export declare const UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS: number; /** * Tick interval for the DEDICATED fast redispatch sweep in * agent-chat-plugin.ts (distinct from that file's general-purpose 2-minute * orphan/reap sweep). Only attempts redispatch for rows still inside * `UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS` — it never reaps, so it * cannot race the loud-failure fallback onto an earlier trigger. * * This constant exists because the general sweep's 2-minute cadence puts the * FIRST redispatch attempt uncomfortably close to (and on a slow tick, past) * `BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS` (150s, agent-chat-adapter.ts) — the * client following a deferred successor would give up and report a fatal * error for a turn the server was silently about to recover. The whole * budget is a derived chain, each bound following from the one before it: * * UNCLAIMED_BACKGROUND_RUN_GRACE_MS (25s) row must look abandoned * + UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS (20s) worst-case tick latency * = ~45s worst-case time-to-first-redispatch-attempt, ~65s to a second * attempt if the first fails — both comfortably under the client's 150s * idle timeout, which additionally no longer counts a known-deferred row * against its idle window at all (see `awaitingRedispatch` surfaced by * `/runs/active` and consumed by the client follow loop). * < BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS (150s) client's own backstop * < UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS (300s) hard, unresettable * ceiling — untouched by this constant — past which the slow sweep's * existing loud reap (`background_worker_never_started`) still fires. */ export declare const UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS = 20000; /** * Maximum time the stale reapers (`reapIfStale`, `reapAllStaleRuns`, * `cleanupOldRuns`'s heartbeat-stale pass) will suspend reaping a "running" * row that is marked in-flight (`in_flight_since`, see `setRunInFlightMarker`) * even though its heartbeat/progress liveness basis * (`livenessBasisSql`/`backgroundAwareStaleCutoffSql`) has gone stale. * * WHY a marker column at all: `inFlightWorkCount` in run-manager.ts (the * no-progress backstop's guard) is in-memory, per-isolate — but all three * reapers above can run in a DIFFERENT isolate than the one holding the * producing run (a client's SQL-subscription poll, a sibling isolate's * opportunistic `cleanupOldRuns` after ITS OWN run completes, or a fresh * boot's `reapAllStaleRuns`). None of them can read another isolate's * in-memory counter, so the counter's 0->1 / 1->0 transitions are mirrored * into this column (`setRunInFlightMarker`, called from * run-manager.ts's `trackInFlightWork`) so it is observable from SQL. This is * exactly the gap that let a demonstrably-alive run holding a long tool call * or A2A `call-agent` delegation get reaped: the heartbeat WRITE can fail * silently (Neon pooler saturation) for the whole `BACKGROUND_RUN_STALE_MS` * window while the run is provably still doing work. * * BOUNDED, not a silent hang — derived from two independent ceilings already * in the codebase, not picked by feel: * - `DEFAULT_TOOL_TIMEOUT_MS` (12 min, production-agent.ts) is the longest * any SINGLE tool call or `agent_call` (A2A delegation) may legitimately * stay in flight — past that its own `AbortSignal.timeout` forces a * tool_done/error and clears the marker. * - `BACKGROUND_SOFT_TIMEOUT_CEILING_MS` (13 min, run-manager.ts) is the * background chunk's OWN soft-timeout ceiling. Unlike the no-progress * backstop, this timer is NOT gated on in-flight work (see the "secondary" * hazard documented next to the soft-timeout timer in run-manager.ts) — * it fires unconditionally and checkpoints/continues the run, so by 13 * minutes the row leaves status='running' via that path regardless of * what the marker says. * This grace is the LARGER of the two (13 min) plus one `BACKGROUND_RUN_STALE_MS` * (90s) buffer for that checkpoint's own completion write to land under the * same DB pressure that could have caused the heartbeat to lapse in the first * place: 780_000 + 90_000 = 870_000ms (14.5 min). Past that, a "running" row * that still shows in-flight work AND a stale liveness basis is not a slow * producer anymore — every backstop that should have ended it has ALSO failed * to write, and it is reaped loud like any other stale run. * * Never applied when a caller passes an explicit `maxStaleMs` override to * `reapIfStale` — that escape hatch is an exact, caller-chosen window and * stays exact. Never weakens the no-in-flight case: a row with no marker set * evaluates this grace clause to a no-op and is reaped at the original * `BACKGROUND_RUN_STALE_MS` / `RUN_STALE_MS` exactly as before. */ export declare const IN_FLIGHT_RUN_STALE_GRACE_MS: number; /** * Ceiling on how far the liveness basis (`livenessBasisSql`) may lag before * `IN_FLIGHT_RUN_STALE_GRACE_MS` stops applying at all. The grace exists for * ONE scenario: a demonstrably-alive producer whose heartbeat WRITE is failing. * It was never meant to cover a producer that has stopped writing anything — * but as originally written it did, because the marker is set on tool_start and * only cleared on tool_done, so a worker that dies mid-tool leaves the marker * latched and inherits the full 14.5 minutes. Prod: 23 such corpse rows across * five apps sat the entire grace before being reaped (mean age 715-908s), which * made the one mechanism protecting slow tools the reason a dead worker took a * quarter hour to surface. * * Requiring recent liveness for the grace to hold collapses that to this * window. 120s = 80 consecutive missed 1.5s heartbeat writes, and 30s past the * widest normal window (`BACKGROUND_RUN_STALE_MS`) — a write outage that * outlasts it is not "the heartbeat lagged", it is the whole producer being * gone. Beyond it the row falls back to the normal background-aware window and * is reaped like any other stale run. */ export declare const IN_FLIGHT_GRACE_MAX_LIVENESS_GAP_MS = 120000; /** * Persist a zombie tool-call completion to the ledger. Called by the detached * promise continuation after `Promise.race` abandons it. Best-effort — never * throws so a ledger write failure doesn't break any caller. */ export declare function writeLedgerEntry(threadId: string, toolKey: string, resultSummary: string): Promise; /** * Look up a prior zombie completion for this thread + tool key. Returns the * persisted result summary, or `null` when no entry exists. */ export declare function readLedgerEntry(threadId: string, toolKey: string): Promise; /** * Delete ledger entries for a thread. Called after a turn fully completes so * old entries don't bleed into the next turn's disambiguation. * Best-effort — never throws. */ export declare function clearLedgerForThread(threadId: string): Promise; export declare function insertRun(id: string, threadId: string, turnId?: string, options?: { dispatchMode?: "foreground" | "foreground-self-chain" | "background"; /** * JSON-serialized request body for a background dispatch. Persisted on the * run row so the self-POST to the background function carries only the * tiny `__backgroundRun` marker (Netlify caps background-function request * bodies at 256KB); the worker rehydrates the body from this column. */ dispatchPayload?: string; }): Promise; /** * Atomically claim a background-dispatched run for processing. The foreground * POST inserts the run row with `dispatch_mode = 'background'`; the FIRST * delivery of the background dispatch flips it to `background-processing` and * wins the claim. A duplicate Netlify delivery (background functions can be * retried) sees `background-processing` and loses, so it no-ops — mirroring * `claimAgentTeamRun` returning null. Returns true when this caller won. * * Idempotent and conditional: the WHERE clause only matches the unclaimed * `background` state AND a still-running row, so a reaped/terminal row can't be * re-claimed. */ export declare function claimBackgroundRun(runId: string): Promise; /** * Read the claim/lifecycle state of a single run by id — for the foreground * circuit-breaker that confirms a background worker actually CLAIMED a run that * was dispatched with a Netlify async 202. A 202 only means the invocation was * ENQUEUED; if the generated background-function wrapper fails to import/hand off * to the route it never reaches `claimBackgroundRun`, leaving the row stuck at * `dispatch_mode = 'background'`. `'background-processing'` means a worker won * the claim; a terminal `status` means the run already resolved. Returns null if * the row is missing. */ export declare function readBackgroundRunClaim(runId: string): Promise<{ dispatchMode: string | null; status: string | null; diagStage: string | null; workerStage: string | null; lastLivenessAt: number | null; } | null>; /** * Read the persisted dispatch payload for a background-dispatched run. The * worker rehydrates its request body from this column when the dispatch marker * carries `payloadRef: true` (the self-POST itself stays under Netlify's 256KB * background-function body cap). Returns null when the row is missing or the * payload was already cleared (terminal run). */ export declare function readRunDispatchPayload(runId: string): Promise; /** * Clear a run's persisted dispatch payload once the worker has claimed and * rehydrated it — the payload can be large (full chat history) and has no use * after the handoff. Best-effort; terminal status writes also clear it. */ export declare function clearRunDispatchPayload(runId: string): Promise; /** * List background-dispatched runs that were never claimed by a worker within * the unclaimed grace window. These are handoffs that were lost in flight — * the async 202 (or the dispatching worker) died before any worker reached * `claimBackgroundRun`. The periodic sweeper reaps them via * `reapUnclaimedBackgroundRun` so a lost handoff becomes a loud, attributable * error instead of a silent forever-hang. The foreground circuit-breaker * already covers initial dispatches while the client is connected; this sweep * exists for server-chained continuation handoffs, which have no foreground * watching them. */ export declare function listUnclaimedBackgroundRunIds(): Promise; /** A row returned by `listUnclaimedBackgroundRunRows`. */ export interface UnclaimedBackgroundRunRow { id: string; /** The row's ORIGINAL `started_at` (never bumped by heartbeats), so a * caller can measure total elapsed time since the handoff was first * pre-inserted — independent of any liveness bump a redispatch attempt * makes along the way. */ startedAt: number; } /** * Same eligibility as `listUnclaimedBackgroundRunIds`, but also returns each * row's original `started_at` so a caller can bound total redispatch time * (see `UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS`) independent of the * liveness bumps a redispatch attempt makes along the way. Used by the * unclaimed-background-run sweep's redispatch pass; `listUnclaimedBackgroundRunIds` * is kept as the simpler, pre-existing surface for callers that only need ids. */ export declare function listUnclaimedBackgroundRunRows(): Promise; /** * Pure decision for the unclaimed-background-run sweep: should THIS row get * another redispatch attempt, or has it exceeded * `UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS` and must fall back to the * loud reap (`reapUnclaimedBackgroundRun`)? Measured from the row's ORIGINAL * `started_at` (never bumped by a redispatch's heartbeat write), so this is * the total-elapsed-time backstop that keeps recovery bounded — a handoff * that cannot be delivered within the window is not spinning forever, it * fails loud. Exported as a pure function (no DB access) so the bound is unit * -testable independent of the sweep's setInterval wiring. */ export declare function shouldRedispatchUnclaimedBackgroundRun(row: { startedAt: number; }, now?: number): boolean; /** * Count how many runs (chunks) a logical turn has consumed so far. This is the * durable per-turn recovery ledger: unlike the in-marker `continuationCount` * (which resets whenever a fresh client POST starts a new chain for the same * turn), the SQL count survives every recovery path, so it bounds pathological * turn loops regardless of which layer initiated each chunk. */ export declare function countRunsForTurn(threadId: string, turnId: string): Promise; /** * Resolve the authenticated owner email for a run by joining it to its chat * thread. The durable background worker's self-dispatch is cookieless * (HMAC-only — see `AGENT_CHAT_PROCESS_RUN_PATH`), so it has no session for the * normal owner resolution and would otherwise be treated as unauthenticated. * The thread's `owner_email` was written by the authenticated foreground when it * created the thread, so it is a trusted, non-forgeable owner source: only the * HMAC-signed `runId` selects the row, and the caller cannot influence which * owner that row maps to. Returns null when the run (or its thread) is missing. */ export declare function getRunOwnerEmail(runId: string): Promise; /** * Atomically acquire a run lease for a thread. Succeeds (returns true) only * when no other run for the same thread is currently status='running' with a * fresh heartbeat. Works for both Postgres and SQLite: the stale-cutoff * comparison lets a dead producer's run be replaced without waiting for the * reaper, mirroring the logic in `reapIfStale`. * * Callers that win the claim then insert the run row normally; callers that * lose skip the run and return the existing active runId to the caller. */ export declare function tryClaimRunSlot(threadId: string, maxStaleMs?: number): Promise<{ claimed: boolean; activeRunId: string | null; }>; /** * Record terminal failure classification for a run so cut-off / errored runs * can be surfaced for pattern analysis (see listErroredRuns). Best-effort — * never throws, since it runs on the completion path that must not fail the run. */ export declare function setRunError(runId: string, errorCode: string | undefined, errorDetail: string | undefined): Promise; /** * Record why a run reached its terminal status, and correct the status when the * reason says the run did not actually finish. * * Every writer sets the status first and the reason second, so this is the one * place that sees both — correcting it here keeps all of them honest instead of * making each caller re-derive "was that completion real?". Only `completed` is * corrected: an `errored`/`aborted` row must never be softened, and a still * `running` row must not be terminated early (`persistRunCheckpointEvent` * records the reason mid-run, before the boundary is final). */ export declare function setRunTerminalReason(runId: string, terminalReason: string | undefined): Promise; /** * Repair a run whose terminal event was durably appended but whose final * `agent_runs.status` write lost a race with reconnect/reaper code. * * The event ledger is the durable transcript users see. If its latest event is * terminal, the run is no longer alive and must not be converted into a stale * error later. This keeps `agent_runs` and `agent_run_events` from telling two * different stories after delayed DB writes or background function teardown. */ export declare function reconcileTerminalRunFromEvents(runId: string): Promise; /** * Diagnostic stage names recorded onto a background run as it moves through the * `_process-run` worker pipeline. Each value is the LAST stage successfully * reached, so a stuck run's `diag_stage` reveals exactly where it died. Ordered * roughly by execution; the literal strings are the client-readable contract. */ export declare const RUN_DIAG_STAGE: { /** The `_process-run` route handler was entered (the request reached Nitro). */ readonly routeEntered: "route_entered"; /** HMAC auth + body validation in prepareProcessRunRequest FAILED. */ readonly authFailed: "auth_failed"; /** HMAC auth + body validation PASSED; about to invoke the worker handler. */ readonly authPassed: "auth_passed"; /** The re-entered agent-chat handler recognized itself as the bg worker. */ readonly workerEntered: "worker_entered"; /** The worker won the atomic claim (it owns the run). */ readonly workerClaimed: "worker_claimed"; /** The worker LOST the claim (a duplicate delivery already owns the run). */ readonly workerClaimLost: "worker_claim_lost"; /** The agent loop started (startRun fired). */ readonly workerStarted: "worker_started"; /** Last worker setup stage reached before startRun (progressive hang localizer). */ readonly workerSetupStep: "worker_setup_step"; /** Pre-claim setup timing breakdown (diagnostic). */ readonly setupTimings: "setup_timings"; /** The worker threw before/while running the loop (message carried in detail). */ readonly workerThrew: "worker_threw"; /** The route handler caught an error from the worker invocation. */ readonly routeThrew: "route_threw"; /** * The foreground circuit-breaker fired: a Netlify async 202 was returned but * no background worker CLAIMED the run within the foreground grace window * (the generated function wrapper never reached the route), so the foreground * recovered by running the turn inline. The run still completes for the user. */ readonly foregroundInlineRecovery: "foreground_inline_recovery"; /** * FIX 3 (durable-background incident): a stale-run reaper (`reapIfStale` / * `reapAllStaleRuns`) found this background chat-turn run dead (heartbeat * stale, no terminal event) and attempted server-owned recovery — detail * carries the outcome (a recovered successor's runId, or why recovery was * declined: not eligible, payload missing, a newer run already exists, or * the per-turn budget is exhausted). See `attemptStaleRunRecovery`. */ readonly staleRunRecoveryAttempted: "stale_run_recovery_attempted"; }; export type RunDiagStage = (typeof RUN_DIAG_STAGE)[keyof typeof RUN_DIAG_STAGE]; /** * Record the last reached pipeline stage (+ optional short detail) for a run. * * PURPOSE: a Netlify background function's logs are not readable from the build * tooling, so when its worker dies silently the run just times out with no clue * WHY. This writes the failure stage straight onto the `agent_runs` row, which * `/runs/active` and `listRunsForThread` surface to the client — so the next * prod run's death cause is readable WITHOUT bg-fn logs. Cheap, additive, and * best-effort: it must never throw or perturb the run (it is called on the auth * path BEFORE a 401 is returned, and around the worker body). * * The stored value is a compact JSON `{ stage, detail?, at }` capped to 2 KB so * a long stack can't bloat the row. */ export declare function recordRunDiagnostic(runId: string, stage: RunDiagStage, detail?: string): Promise; /** Update the run's liveness heartbeat. Called periodically by run-manager. */ export declare function updateRunHeartbeat(runId: string): Promise; /** * Bump `last_progress_at` — call this whenever the agent actually emits an * event (token, tool call, message). Distinct from `heartbeat_at` so the * stuck-detector can tell "process alive but nothing happening" from * "process dead." Callers should throttle (run-manager debounces to ~1/s). */ export declare function bumpRunProgress(runId: string): Promise; /** * Mirror run-manager's in-memory `inFlightWorkCount` 0<->N transitions into * SQL so a stale reaper running in a DIFFERENT isolate can tell a * demonstrably-alive run (holding a tool call or A2A `agent_call` delegation) * apart from a genuinely dead one — see `IN_FLIGHT_RUN_STALE_GRACE_MS`'s doc * comment for the full reasoning. * * `inFlight: true` only writes when the row is still `NULL` — a defense-in- * depth belt-and-suspenders against a nested 1->2 transition clobbering the * ORIGINAL start time with a later one (the caller's own counter already * dedupes 0->1 transitions; this WHERE just makes the write itself * idempotent/order-independent too). `inFlight: false` always clears * unconditionally — if it races a fresh 0->1 write from a *different* tool * finishing/starting back to back, worst case is losing a few seconds of * grace, never gaining an incorrect one. * * Best-effort: callers fire-and-forget (`.catch(() => {})`) so a write * failure here never blocks event emission or aborts the run. If this write * itself fails (the same DB pressure that could be starving the heartbeat), * the row simply gets no grace — never worse than today's behavior. */ export declare function setRunInFlightMarker(runId: string, inFlight: boolean): Promise; /** * If the given run is marked "running" in SQL but its heartbeat is stale * (producer likely crashed), flip it to "errored" so watchers stop waiting. * Returns true if the row was reaped. */ export declare function reapIfStale(runId: string, maxStaleMs?: number): Promise; /** * FALLBACK HARDENING for the "dispatched with 202 but the worker never started" * case. A background-dispatched run sits in `dispatch_mode = 'background'` until * the worker wins `claimBackgroundRun` (which flips it to * `background-processing`). If the worker silently dies (e.g. the bg-fn 401s * before it can claim), the row stays `background`, never heartbeats again, and * — because dispatch returned 202 — the foreground already returned the SSE * stream, so the existing fast-fail inline fallback never engaged. The run would * otherwise hang for the full durable background window and then error opaquely. * * This reaps such a run EARLY and DISTINCTLY: a row that is still unclaimed * (`dispatch_mode = 'background'`) past the tight `UNCLAIMED_BACKGROUND_RUN_GRACE_MS` * grace is a dead handoff — there is no live worker to protect with the wide * window — so we flip it to `errored` with the recoverable * `background_worker_never_started` code. The client's existing recoverable-error * path then lets the user (or auto-recovery) re-drive the turn. Idempotent and * conditional: only an unclaimed, still-running, grace-exceeded row matches, so a * claimed worker, a fresh dispatch, or a terminal row is never touched. * * Returns true when this call reaped the run. */ export declare function reapUnclaimedBackgroundRun(runId: string): Promise; export declare function updateRunStatus(runId: string, status: "completed" | "truncated" | "errored" | "aborted"): Promise; /** * Conditional terminal status write: only updates if the row still belongs to * this run AND is still status='running'. Returns true when the update landed. * * This is the safe variant used by the producer's finally block so a zombie run * (reaped while executing) can never clobber the status written by the reaper * or a replacement run. */ export declare function updateRunStatusIfRunning(runId: string, status: "completed" | "truncated" | "errored" | "aborted"): Promise; /** Read the current status of a run row. Returns null when the row is missing. */ export declare function getRunStatus(runId: string): Promise; /** * Truthful terminal event for an aborted run. * * A synthetic `done` is indistinguishable from a real finish on the wire, so * every recovery abort used to render as "The agent stopped without sending a * final message" with the streamed work apparently thrown away. Recoverable * chunk-boundary reasons ride the `auto_continue` channel the client already * routes into continuation; anything else that isn't a user stop surfaces as a * reason-coded error. Only known infrastructure aborts are recoverable; * every other abort is terminal and non-recoverable. */ export declare function terminalEventForAbortReason(reason: string | undefined): AgentChatEvent; export declare function markRunAborted(runId: string, reason?: string): Promise; /** Records Stop before a foreground request has created its real run row. */ export declare function markTurnAborted(threadId: string, turnId: string, reason?: string): Promise; export declare function isTurnAborted(threadId: string, turnId: string): Promise; export declare function isRunAborted(runId: string): Promise; export declare function getRunAbortState(runId: string): Promise<{ aborted: boolean; reason?: string; }>; export declare function insertRunEvent(runId: string, seq: number, eventData: string): Promise; /** * Reserved seq for a checkpoint terminal event written BEFORE the agent loop * unwinds. Stream events use contiguous 0-based seqs, so a value this high can * never collide with one and always sorts last — which is what lets * `reconcileTerminalRunFromEvents` / `getLastTerminalRunEvent` read the * checkpoint as the run's real terminal state even when the process is killed * mid-unwind, and what keeps `appendTerminalRunEvent` from stamping a * `stale_run` lie over it. */ export declare const CHECKPOINT_TERMINAL_EVENT_SEQ = 1000000000; /** * Durably record a chunk-boundary terminal event the moment the boundary is * decided, not after the loop unwinds. Wind-down regularly overruns the * remaining serverless budget; without this the auto_continue is never * persisted and the row is reaped as a `stale_run` lie instead of a * sweep-continuable checkpoint. */ export declare function persistRunCheckpointEvent(runId: string, event: AgentChatEvent, terminalReason: string): Promise; export declare function getRunEventsSince(runId: string, fromSeq: number): Promise>; export declare function getRunById(runId: string): Promise<{ id: string; threadId: string; status: string; startedAt: number; errorCode: string | null; errorDetail: string | null; terminalReason: string | null; } | null>; /** * Read the latest terminal event already persisted for a run, if any. * Used by SSE reconnect when the client cursor is already past that event * (so `getRunEventsSince` returns empty) but the row is terminal — we must * replay the REAL error instead of inventing a stale_run card. */ export declare function getLastTerminalRunEvent(runId: string): Promise<{ seq: number; event: Record; } | null>; /** * Build the terminal error payload to stream when an `errored` run has no * in-cursor terminal event. Prefer the real last terminal event, then the * row's error_code/error_detail, and only then the generic stale_run card. */ export declare function resolveErroredRunTerminalEvent(run: { errorCode?: string | null; errorDetail?: string | null; }): { event: Record; shouldPersist: boolean; }; export declare function getRunByThread(threadId: string, options?: { includeTerminal?: boolean; }): Promise<{ id: string; threadId: string; turnId?: string | null; status: string; startedAt: number; heartbeatAt: number | null; completedAt: number | null; lastProgressAt: number | null; dispatchMode: string | null; terminalReason: string | null; diagStage: string | null; /** * Raw `in_flight_since` marker (see `setRunInFlightMarker`) — non-null * exactly when run-manager's in-memory `inFlightWorkCount` was last known * (from THIS row's own producer) to be > 0: a tool call or A2A `agent_call` * delegation is open and has not yet resolved. Callers that want the * authoritative "does this run currently hold live work" signal (e.g. the * `hasInFlightWork` wire field on `/runs/active`) should test this for * non-null, not re-derive their own notion of in-flight — see * `getActiveRunForThreadAsync` in run-manager.ts. */ inFlightSince: number | null; } | null>; export interface AgentRunSummary { id: string; threadId: string; turnId: string | null; status: string; startedAt: number; heartbeatAt: number | null; completedAt: number | null; lastProgressAt: number | null; errorCode: string | null; abortReason: string | null; dispatchMode: string | null; terminalReason: string | null; /** Last reached `_process-run` worker stage (JSON `{stage,detail?,at}`). */ diagStage: string | null; } export declare function listRunsForThread(threadId: string, options?: { limit?: number; }): Promise; /** * Read the current logical turn's recorded events for a thread, parsed into * `AgentChatEvent`s in seq order, for per-turn tool-call journal classification * (see `tool-call-journal.ts`). Read-only and additive — reuses the existing * `agent_runs` / `agent_run_events` ledger with no schema change. * * A logical turn may span several continuation runs (each chunk is its own run * sharing one `turn_id`), so we union the events of every run that belongs to * the latest turn for this thread. Events are ordered by (started_at, seq) so * earlier chunks come before later ones and the positional `tool_start` → * `tool_done` matching in the classifier stays correct across chunk boundaries. * * Returns an empty array when the thread has no run yet or no parseable events. * Best-effort on parse: malformed ledger rows are skipped rather than thrown. */ export declare function getCurrentTurnEventsForThread(threadId: string, knownTurnId?: string): Promise; /** * Expire any "running" rows whose heartbeat is stale — producer died. * Safe to call at server startup on multi-isolate deployments: only rows * without a fresh heartbeat get reaped, so runs owned by OTHER live * isolates (which keep heartbeating) are left alone. */ export declare function reapAllStaleRuns(): Promise; /** * Read the daily terminal-outcome counters rolled up from pruned runs. Pair * with live `agent_runs` rows for a complete picture — see * `pruneAndRollUpPrunedRunOutcomes`. */ export declare function getRunOutcomeCounters(options?: { /** Inclusive lower bound as YYYY-MM-DD. */ sinceDay?: string; }): Promise>; /** Delete old runs and expire stale "running" rows that haven't had activity * (e.g. worker crashed before updating status). Genuinely completed runs are * pruned at `olderThanMs`; errored/aborted/truncated runs are kept until * `erroredOlderThanMs` (a longer window, falling back to `olderThanMs`) so * their event log survives for cut-off pattern analysis via listErroredRuns. */ export declare function cleanupOldRuns(olderThanMs: number, erroredOlderThanMs?: number): Promise; /** * List recent unsuccessful runs (errored, aborted, and truncated) for cut-off * pattern analysis. Read-only, bounded, and ordered newest-first. Surfaced via * the list-errored-runs action so the team can see why chats are failing * (terminal error code, duration, turn linkage) instead of discovering it ad * hoc. Truncations belong here: a run that stopped at a budget boundary is a * cut-off, and they outnumbered genuine completions on Plan in prod while being * invisible to this query. */ export declare function listErroredRuns(options?: { limit?: number; sinceMs?: number; }): Promise>; /** * Idempotently append a terminal event to a run's event stream. No-op if the * stream already ends in a terminal event. Used by reapers AND by SSE * reconnect paths that discover an `errored` run row with no terminal event * (e.g. an earlier reaper's silent `.catch(() => {})` swallowed the append). * * Persisting from the reconnect path is what keeps the system self-healing: * subsequent reconnects replay the proper terminal event from SQL instead of * synthesizing a fresh one each time. */ export declare function ensureTerminalRunEvent(runId: string, event: Record): Promise; //# sourceMappingURL=run-store.d.ts.map