UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

42 lines (41 loc) • 2.85 kB
/** * 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>;