UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

124 lines (123 loc) • 7.94 kB
import { CheckpointError } from "../checkpoint-store.js"; import { MAX_HOOK_TIMEOUT_MS, runHookSeat } from "../hooks.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"; 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_HOOK_TIMEOUT_MS) { preflightTimeoutMs = rawDeadline; } else { try { runner.deps.onError?.(new Error(`RunnerDeps.resumePreflightTimeoutMs is not a positive finite number within the seat ceiling of ${MAX_HOOK_TIMEOUT_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 runHookSeat("resumePreflight", { timeoutMs: preflightTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true, onBadTimeout: (badErr) => { try { runner.deps.onError?.(asThrownError(badErr), { phase: "hook", sessionId: cp.sessionId }); } catch { } }, owner: runner.deps, }, (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({}); }