@sema-agent/core
Version:
Stateless, task-oriented AI agent core
112 lines (111 loc) • 6.43 kB
JavaScript
import { StoredSession, isSessionConflict } from "../session.js";
import { reconcileInterruptedSession } from "../session-reconcile.js";
import { detectConflicts, settleBranchWriteBack } from "./branch-writeback.js";
import { settleTeardownLeg } from "./teardown-bounded.js";
const RECONCILE_MAX_RETRIES = 3;
export async function prepareAcquireReconcile(input) {
const { sessions, spec, resume, toolEffects } = input;
let acquired;
let session;
let conflictRef;
let wakeRecovered = [];
let branch;
for (let attempt = 0;; attempt++) {
try {
acquired = await sessions.acquire(spec.sessionId, spec.requireExistingSession ? { requireExisting: true } : input.placement !== undefined ? { placement: input.placement } : undefined);
}
catch (err) {
if (spec.requireExistingSession && err?.code === "not_found") {
const e = new Error(`requireExistingSession: session "${spec.sessionId}" does not exist — refusing a silent fresh run (design/114 Phase3)` +
(input.durableParkGap !== undefined ? `. Note the assembly: ${input.durableParkGap}` : ""));
e.code = "resume.session_not_found";
throw e;
}
throw err;
}
conflictRef = { hit: false };
const own = { ids: undefined };
session = new StoredSession(detectConflicts(acquired.session.getStorage(), conflictRef, own));
if (!spec.sessionId) {
break;
}
try {
if (resume) {
await session.getStorage().setLeafId(resume.leafId);
}
else if (spec.resumeAt !== undefined) {
const entry = await session.getEntry(spec.resumeAt);
if (!entry) {
const e = new Error(`resumeAt entry "${spec.resumeAt}" not found in session "${spec.sessionId}"`);
e.code = "resume_at.not_found";
throw e;
}
if (entry.type !== "message" && entry.type !== "custom_message") {
const e = new Error(`resumeAt entry "${spec.resumeAt}" is a "${entry.type}" entry; resume-at requires a message boundary`);
e.code = "resume_at.not_a_message";
throw e;
}
if (entry.type === "message") {
const msg = entry.message;
if (msg?.role === "toolResult") {
const e = new Error(`resumeAt entry "${spec.resumeAt}" is a tool-result (mid-turn); resume-at requires a settled turn boundary (a user message or a finished assistant turn)`);
e.code = "resume_at.not_a_message";
throw e;
}
if (msg?.role === "assistant" && Array.isArray(msg.content) && msg.content.some((p) => p?.type === "toolCall")) {
const e = new Error(`resumeAt entry "${spec.resumeAt}" ends a turn mid-tool-call; resume-at requires a settled turn boundary`);
e.code = "resume_at.not_a_message";
throw e;
}
}
if (spec.resumeAtMode === "before") {
const role = entry.type === "message" ? entry.message.role : undefined;
if (role !== "user") {
const kind = entry.type === "message" ? `role "${String(role)}" message` : `"${entry.type}" entry`;
const e = new Error(`resumeAt entry "${spec.resumeAt}" is a ${kind}; resumeAtMode "before" supports only USER-message targets (the exclusive-rewind shape: the removed message is a user message)`);
e.code = "resume_at.before_target_not_user";
throw e;
}
let ancestorId = entry.parentId;
let sawConversationAbove = false;
while (ancestorId !== null) {
const ancestor = await session.getEntry(ancestorId);
if (!ancestor)
break;
if (ancestor.type === "message" || ancestor.type === "custom_message") {
sawConversationAbove = true;
break;
}
ancestorId = ancestor.parentId;
}
if (!sawConversationAbove) {
const e = new Error(`resumeAt entry "${spec.resumeAt}" is the session's first message; resumeAtMode "before" cannot rewind past the session root — start a NEW session instead`);
e.code = "resume_at.before_root_unsupported";
throw e;
}
}
const storage = session.getStorage(), leafBefore = await storage.getLeafId(), leafSet = spec.resumeAtMode === "before" ? entry.parentId : spec.resumeAt;
await storage.setLeafId(leafSet);
own.ids = new Set();
branch = { storage, leafBefore, leafSet, own };
}
wakeRecovered = (await reconcileInterruptedSession(session, toolEffects, resume?.suspendedBatch)).recovered;
break;
}
catch (err) {
if (isSessionConflict(err) && attempt < RECONCILE_MAX_RETRIES) {
if (sessions.forget) {
const forget = sessions.forget.bind(sessions), id = spec.sessionId;
await settleTeardownLeg(() => Promise.resolve(forget(id)).catch(() => undefined), "forget (acquire-reconcile re-wake leg)", (e) => input.report?.(e, id));
}
branch = undefined;
continue;
}
if (branch !== undefined)
await settleBranchWriteBack(branch, acquired.sessionId, "resumeAt branch write-back (acquire-reconcile failure leg)", (e) => input.report?.(e, acquired.sessionId));
await settleTeardownLeg(() => Promise.resolve(sessions.forget ? sessions.forget(acquired.sessionId) : undefined).catch(() => undefined), "forget (acquire-reconcile failure leg)", (e) => input.report?.(e, acquired.sessionId));
throw err;
}
}
return { acquired, session, conflictRef, wakeRecovered, branch };
}