/** * Deadlines and a last-resort exit backstop for CLI teardown. * * ## Why this module exists (T12115 — the 12.9-hour `cleo` process) * * {@link ../shutdown.ts | shutdownCliRuntime} tears down the process-lifetime * workers so a one-shot CLI command can drain its event loop and exit rc:0. * Every step was best-effort — wrapped so a *throw* could not abort the rest — * but nothing bounded a step that never settles at all. A promise that simply * does not resolve is not an error, so `safely()` waited on it forever. * * Measured on a live box 2026-09-12: `cleo update T1690` sat in `ep_poll` for * **12.9 hours**, holding ~100 MB RSS and open SQLite descriptors on two * databases. Its envelope had been printed hours earlier; the command was DONE. * Sibling `cleo add` / `cleo update` processes from other agent sessions had * been resident 9.3 hours each. Thirty-one such processes held 3.25 GiB between * them while the host ran 13 GiB into swap with 3 GiB free of 62 GiB. * * The descriptors are the tell: `closeAllDatabases()` is step 3 of 4, so if the * DB handles are still open the teardown never got past step 1 or 2 — both of * which await a worker thread. A hang there is invisible: the envelope is on * stdout, the exit code is never written, and the caller cannot distinguish * "still working" from "wedged forever" (issues #1228, #1229, #1237, #1241). * * ## The two guarantees * * 1. {@link withDeadline} — no single teardown step can stall the exit path. * A step that misses its deadline is abandoned, not awaited. Abandonment is * only a stop in waiting: it does not cancel the underlying operation or * prove that resources are safe to close. The shutdown coordinator must * report incomplete work and preserve resources still owned by that work. * * 2. {@link armExitBackstop} — an **unref'd** timer that force-exits if the * loop is still alive after the grace period. Unref'd means the timer does * not itself hold the loop open, so a process that drains promptly exits * naturally and never reaches it. * * It does NOT mean the timer only fires on leaks. An unref'd timer still * *fires* if the loop is alive for any other reason when the grace period * elapses — including legitimate unawaited work. `cleo memory observe` * schedules a fire-and-forget embedding whose first call loads a ~22 MB * model, which will not finish inside the grace window on a cold cache. So * the backstop can and does cut short work that was going to succeed. That * is an accepted trade: a bounded exit with a named remedy beats an * unbounded hang. It is why the message below says what was abandoned * rather than claiming the process was idle. * * Neither is a substitute for fixing a leak. Both exist so that the next leak * costs a log line instead of a wedged host. * * @module * @task T12115 */ import type { ShutdownStepOutcome as StepOutcome } from '@cleocode/contracts/jobs'; /** Compatible historical type export; canonical shared declaration lives in jobs contracts. */ export type { ShutdownStepOutcome as StepOutcome } from '@cleocode/contracts/jobs'; /** Per-step teardown budget, in ms. */ export declare const STEP_DEADLINE_MS = 2000; /** Grace period after teardown before the backstop force-exits, in ms. */ export declare const EXIT_BACKSTOP_MS = 3000; /** * Run a teardown step, abandoning it if it outlives `deadlineMs`. * * Swallows rejections as well as stalls — a teardown step must never be able to * abort the exit path, whether by throwing or by never settling. * * @param label - step name for diagnostics. * @param step - the teardown work. * @param deadlineMs - budget before the step is abandoned. * @returns what happened, for the caller's diagnostics. * @remarks Waiting is bounded cooperatively; synchronous work is not preempted. * A timed-out receipt is a snapshot and never becomes successful when abandoned * work later settles. Known operation cancellation retains its original reason. * * @example * ```ts * const outcome = await withDeadline('brain-writer', () => shutdownBrainWriter()); * if (!outcome.settled) console.error('brain-writer did not stop in time'); * ``` */ export declare function withDeadline(label: string, step: () => Promise | void, deadlineMs?: number): Promise; /** * Render assessed shutdown diagnostics without inferring reasons from legacy flags. * @param outcomes - Existing stage receipts, including compatible older callers. * @returns Stderr text, or an empty string when all resource steps completed. * @remarks A settled registry barrier does not establish producer success, so an * unassessed drain still discloses that limit. An assessed drain reports the number * of producers that actually failed, and is silent when none did. Error * messages are JSON-escaped so they cannot inject additional diagnostic lines. * The committed command result is preserved regardless of optional teardown. * @example * ```ts * const text = formatShutdownOutcomes(await shutdownCliRuntime()); * process.stderr.write(text); * ``` */ export declare function formatShutdownOutcomes(outcomes: readonly StepOutcome[]): string; /** * Names of the resources currently keeping the event loop alive. * * Wraps `process.getActiveResourcesInfo()`, which is the only supported way to * answer "what is holding this process open" without a native inspector. Values * are coarse type names (`'Timeout'`, `'MessagePort'`, `'TCPSocketWrap'`), which * is exactly the granularity needed to route the next report to a subsystem. * * @returns a tally of active-handle type names, most frequent first. */ export declare function activeHandleSummary(): string; /** * Arm the last-resort exit backstop. * * Call once, AFTER the envelope has been written and teardown has been * attempted. The returned timer is unref'd, so a healthy process that drains * its loop exits normally and never reaches this code path. * * When it does fire, it writes one diagnostic line to **stderr** — never stdout, * which must carry exactly one LAFS envelope per call (ADR-086) — naming what * kept the loop alive and warning that unawaited background work was cut short, * then exits with `code`. * * The exit code stays the command's own result. A killed *tail* is not a failed * *command*: the envelope on stdout is already correct and the row is already * written. Exiting non-zero here would turn a slow-but-correct invocation into * a failure, which is the false-red defect (gh#1270) in a new place. * * @param code - exit code to use; the command's own result, not an error code. * @param graceMs - how long to let the loop try to drain on its own. * @returns the armed timer, so tests can inspect or clear it. * * @example * ```ts * await shutdownCliRuntime(); * armExitBackstop(); // inherits process.exitCode; healthy commands never reach it * ``` */ export declare function armExitBackstop(code?: number, graceMs?: number): NodeJS.Timeout; //# sourceMappingURL=shutdown-deadline.d.ts.map