@sema-agent/core
Version:
Stateless, task-oriented AI agent core
124 lines (123 loc) • 7.94 kB
JavaScript
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({});
}