UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

146 lines (145 loc) • 8.69 kB
import { CheckpointError } from "../checkpoint-store.js"; import { MAX_TIMER_MS } from "../mods/kernel/index.js"; import { hasDestroy } from "../remote-env.js"; import { inlineUntrusted } from "../untrusted-text.js"; import { describeSuppliedValue } from "./decide-continuation.js"; import { settleTeardownLeg } from "./teardown-bounded.js"; import { asThrownError } from "../thrown-value.js"; async function boundedPreflight(timeoutMs, signal, call) { const controller = new AbortController(); const pending = Promise.resolve(call(controller.signal)); return await new Promise((resolve, reject) => { let settled = false; const finish = (act) => { if (settled) return; settled = true; clearTimeout(timer); signal?.removeEventListener("abort", onAbort); act(); }; const expire = (cause) => finish(() => { void pending.then(undefined, () => { }); resolve({ expired: true, cause }); queueMicrotask(() => { try { controller.abort(); } catch { } }); }); const onAbort = () => expire("aborted"); const timer = setTimeout(() => expire("timeout"), timeoutMs); if (signal?.aborted === true) onAbort(); else signal?.addEventListener("abort", onAbort, { once: true }); pending.then((value) => finish(() => resolve({ expired: false, value })), (err) => finish(() => reject(asThrownError(err)))); }); } const RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS = 10_000; export async function resumePreflight(input) { const { token, cp, resumeSignal, recheckGovernanceWindow, store, runner, locallyClaimedTokens, parentConstraintRegistry, suspendedEnvReaps } = input; if (runner.deps.resumePreflight !== undefined) { const preflight = runner.deps.resumePreflight; const rawDeadline = runner.deps.resumePreflightTimeoutMs; let preflightTimeoutMs = RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS; if (rawDeadline !== undefined) { if (typeof rawDeadline === "number" && Number.isFinite(rawDeadline) && rawDeadline > 0 && rawDeadline <= MAX_TIMER_MS) { preflightTimeoutMs = rawDeadline; } else { try { runner.deps.onError?.(new Error(`RunnerDeps.resumePreflightTimeoutMs is not a positive finite number within the timer ceiling of ${MAX_TIMER_MS}ms (got ${describeSuppliedValue(rawDeadline)}) — the resume preflight falls back to its own ${RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS}ms default`), { phase: "config", sessionId: cp.sessionId }); } catch { } } } const info = { token, sessionId: cp.sessionId, ...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}), ...(cp.principal ? { principal: cp.principal } : {}), ...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}), gateKind: cp.gate.kind, }; const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears"; let verdict; try { const seat = await boundedPreflight(preflightTimeoutMs, resumeSignal, (sig) => preflight(info, sig)); if (seat.expired) { if (seat.cause === "aborted") throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while the deployment's resumePreflight was still screening it — the checkpoint stays pending and is resumable with a live signal"); throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume — refused fail-closed (retry-later arm)${refusalTail}`); } verdict = seat.value; } catch (preflightErr) { if (preflightErr instanceof CheckpointError) throw preflightErr; const err = asThrownError(preflightErr); try { runner.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId }); } catch { } throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight crashed while screening this resume (${inlineUntrusted(err.message)}) — refused fail-closed (retry-later arm)${refusalTail}`); } const verdictOk = verdict !== null && typeof verdict === "object" ? verdict.ok : undefined; if (verdictOk !== true) { const bag = verdict !== null && typeof verdict === "object" ? verdict : undefined; const disposition = bag?.disposition; const suppliedMessage = bag?.message; const retryAfterMs = bag?.retryAfterMs; const readableMessage = typeof suppliedMessage === "string" && suppliedMessage !== "" ? suppliedMessage : undefined; const said = readableMessage !== undefined ? `: ${inlineUntrusted(readableMessage)}` : " (no readable verdict — fail-closed)"; if (disposition === "terminal" && readableMessage !== undefined) { locallyClaimedTokens.add(token); let settled; try { settled = await store.expire(token, cp.scope); } catch (expireErr) { locallyClaimedTokens.delete(token); throw expireErr; } if (!settled) { locallyClaimedTokens.delete(token); const live = await store.get(token); if (live?.status === "pending") { throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said}, but the row moved under this refusal (a concurrent resolve/reopen cycle) — nothing was settled here; the row is pending again, re-resume against the current state (the preflight screens every attempt)`); } throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — a concurrent actor had already settled the row (${describeSuppliedValue(live?.status ?? "consumed")}); nothing was settled by this refusal`); } parentConstraintRegistry.delete(token); const terminalReap = suspendedEnvReaps.get(token); suspendedEnvReaps.delete(token); locallyClaimedTokens.delete(token); if (terminalReap?.env !== undefined && hasDestroy(terminalReap.env)) { const terminalEnv = terminalReap.env; await settleTeardownLeg(() => terminalEnv.destroy(), "terminal-preflight env destroy", (envErr) => { try { runner.deps.onError?.(envErr, { phase: "config", sessionId: terminalReap.sessionId }); } catch { } }); } try { runner.deps.onError?.(new Error(`resumePreflight ruled this row PERMANENTLY blocked${said} — the row was settled terminally (expired) by this refusal's single-shot CAS (token no longer redeemable)`), { phase: "hook", sessionId: cp.sessionId }); } catch { } throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — the row was settled terminally (expired by this refusal's single-shot CAS); the token is not redeemable`); } const waitHint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : undefined; throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight refused this resume${said}${refusalTail}${waitHint !== undefined ? ` (deployment wait hint: ${String(waitHint)}ms)` : ""}`, waitHint !== undefined ? { retryAfterMs: waitHint } : undefined); } if (recheckGovernanceWindow !== undefined) await recheckGovernanceWindow(); } return await input.next({}); }