/** * Durable exit signals — per-kind, exclusive-create, precedence-safe. * * Each signal kind occupies its own immutable slot: * /signals/human_interrupt.json * /signals/external_event.json * * Atomic publication (crash-safe): * 1. Write complete JSON to a unique tmp file (wx, 0o600) in the signals dir * 2. Sync and close the tmp file * 3. Hard-link tmp to the final per-kind slot (EEXIST → already_exists) * 4. Unlink tmp in finally; best-effort dir sync on POSIX * * The final slot is only visible after a fully written tmp — a crash cannot * leave a permanently malformed final slot. * * Per-kind files let different kinds coexist — an external event filed first * does NOT prevent a later human interrupt. EXIT_PRECEDENCE is always honoured. * * Real-path containment guards against symlink/junction traversal out of the * run store. Diagnostics from malformed signals are propagated — never silently * discarded as "no signal". */ import type { ExitSignalV1 } from "../contracts/index.js"; export interface ExitSignalSource { poll(runId: string): Promise; } export interface SignalReadResult { signals: readonly ExitSignalV1[]; diagnostics: readonly SignalDiagnostic[]; } export interface SignalDiagnostic { kind: "human_interrupt" | "external_event"; error: string; } /** * Typed error surfaced by the monitor when diagnostics are detected and no * onDiagnostic handler is registered. Contains safe diagnostic codes and * signal kinds — no absolute paths or payload secrets. */ export declare class SignalDiagnosticError extends Error { readonly diagnostics: readonly SignalDiagnostic[]; constructor(diagnostics: readonly SignalDiagnostic[]); } /** Path to a per-kind signal file — safe against lexical traversal. */ export declare function exitSignalPath(runsRoot: string, runId: string, kind: "human_interrupt" | "external_event"): string; /** * Write one signal of its kind atomically. Returns "created" on success or * "already_exists" when that kind was already filed (first writer wins). * Throws on IO errors other than EEXIST on the final slot. * * Publication sequence (crash-safe): * 1. Validate signal and size-check the payload * 2. mkdir the signals directory * 3. realpath-check: the real signals dir must stay inside the real root * 4. Write complete JSON to a unique tmp file (wx, 0o600) * 5. Sync and close the tmp file * 6. Hard-link tmp to the final per-kind slot (EEXIST → already_exists) * 7. Unlink tmp in finally; best-effort dir sync on POSIX */ export declare function writeExitSignal(runsRoot: string, signal: ExitSignalV1): Promise<"created" | "already_exists">; /** * Read all present signals for a run (both kinds). Returns a structured * result with any parse diagnostics rather than throwing on malformed files. */ export declare function readAllExitSignals(runsRoot: string, runId: string): Promise; /** Read a single signal kind; returns undefined when absent. */ export declare function readExitSignal(runsRoot: string, runId: string, kind: "human_interrupt" | "external_event"): Promise; export declare function createFileExitSignalSource(runsRoot: string): ExitSignalSource; /** * Returns true when a signal should abort the active attempt and terminate the * run. external_event with disposition "satisfied" is non-terminal: the run * observes the event (for durable audit) but continues to its natural * goal/verification completion. Only "superseded" and "cancelled" dispositions * are terminal external events. */ export declare function isTerminalExitSignal(signal: ExitSignalV1): boolean; /** * Polls for any exit signal and calls onSignal with the full set when any * new signal appears. Returns a dispose function — MUST be called on every * return/throw path in the run harness (invariant: one interval per run). * * Diagnostics (malformed signals, containment failures) are routed to * onDiagnostic when provided; otherwise surfaced as a SignalDiagnosticError * through onError so callers always learn about corruption rather than * silently treating it as "no signal". */ export declare function startExitSignalMonitor(input: { source?: ExitSignalSource; runId: string; controller: AbortController; pollIntervalMs?: number; onSignal: (signals: readonly ExitSignalV1[]) => void; onDiagnostic?: (diagnostics: readonly SignalDiagnostic[]) => void; onError: (error: Error) => void; }): () => void;