@sema-agent/core
Version:
Stateless, task-oriented AI agent core
42 lines (41 loc) • 2.85 kB
TypeScript
/**
* Bounded await for BEST-EFFORT teardown /
* cleanup legs on a run's settle path.
*
* The defect class: `resume()`/`runTask` settle only when the run body returns, and the body's tail
* (after the event queue closed) plus the prepare-throw cleanup path both awaited external entities
* (adapter `destroy()`, MCP client close, registry rows minted by EARLIER runs, session-store
* `forget`) with no bound. Every one of those sites already caught THROWS ("best-effort … must not
* mask the original error") — but a promise that never settles is not a throw, and one wedged
* adapter turned into: for-await done, `result()` never settles (②型, process alive), or — when the
* wedge happened before the queue closed — a zero-handle pure-logic hang (the 2.12.0 exit-13 shape).
* The session lock releases in the run IIFE's finally, so one wedged run also contaminated every later
* same-session resume.
*
* The remedy is the same doctrine as the workflow journal drain (batch-4 F2): a best-effort leg gets
* a BOUNDED terminal await — generous, disclosed on timeout, never silent (§C1), and the settle path
* proceeds. The abandoned leg keeps running (nothing can cancel a wedged promise); the incident
* report says exactly that, so a leaked env/process is attributable instead of anonymous.
*
* Timer discipline (with-retry.ts same doctrine): the race timer is deliberately NOT
* unref'd — the caller awaits this in FOREGROUND control flow, and an unref'd timer lets an
* otherwise-empty event loop exit mid-wait, which un-settles the very await this module exists to
* bound. The timer is itself bounded, and cleared the moment the leg settles.
*/
/** Generous ceiling for one teardown leg — cleanup that has not settled after this long is wedged,
* not slow (the legs are process kills, fd closes, store forgets; none is legitimately minutes). */
export declare const TEARDOWN_LEG_MAX_MS = 15000;
/** Test hook (fs-atomic TxnLockTimings precedent): shrink the budget so wedge pins need no real 15s. */
export declare function setTeardownLegBudgetForTests(ms: number | undefined): void;
/** The typed timeout incident — `leg` names the site so a leak is attributable from the report alone. */
export declare class TeardownLegTimeoutError extends Error {
readonly leg: string;
constructor(leg: string, budgetMs: number);
}
/**
* Run one best-effort teardown leg with the bound above. NEVER throws: a throw OR a timeout is
* routed to `report` (wire it to `deps.onError` where available) and the caller proceeds —
* exactly the contract every wrapped site already claimed in prose, now including the never-settle
* arm it could not previously keep.
*/
export declare function settleTeardownLeg(leg: () => Promise<unknown> | unknown, label: string, report?: (err: Error) => void): Promise<void>;