UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

124 lines (123 loc) • 4.59 kB
import { join } from "node:path"; import { assertRetentionPolicy } from "../../core/retention-policy.js"; import { WorkflowRunStoreError, isTerminalWorkflowStatus, nextWorkflowRunOnUpdate, queryWorkflowRuns, } from "../../core/workflow-run-store.js"; import { SharedLedgerTable } from "./shared-ledger.js"; import { guardCorruptSink } from "./corrupt-read-sink.js"; import { assertAdoptionBootGate } from "./adoption/marker.js"; const isRunRow = (x) => x !== null && typeof x === "object" && typeof x.id === "string"; const runLedgers = new SharedLedgerTable({ label: "workflow-run ledger", keyOf: (run) => run.id, apply: (runs, ev) => { if (ev.t === "delete") { runs.delete(ev.id); return; } runs.set(ev.id, ev.run); }, judgeRow: isRunRow, judgeEvent: (x) => { const ev = x; if (typeof ev.id !== "string") return false; return ev.t === "delete" ? true : isRunRow(ev.run); }, }); export class FileWorkflowRunStore { fsyncEnabled; compactEvery; ledger; closed = false; get runs() { return this.ledger.rows; } constructor(root, opts = {}) { assertAdoptionBootGate(root, "FileWorkflowRunStore"); this.fsyncEnabled = opts.fsync !== false; this.compactEvery = opts.compactEvery ?? 1000; const dir = join(root, "workflow-runs"); this.ledger = runLedgers.acquire({ dir, tmpDir: join(root, "tmp"), ...(opts.onCorruptRead !== undefined ? { onCorruptRead: guardCorruptSink(opts.onCorruptRead) } : {}), ledgerPath: join(dir, "runs.jsonl"), snapshotPath: join(dir, "runs.snapshot.jsonl"), }); } withLock(id, fn) { return this.ledger.withLock(id, fn); } commit(ev, fsync) { this.ledger.commit(ev, fsync, this.compactEvery); } async put(id, run) { return this.withLock(id, () => { if (this.runs.has(id)) { throw new WorkflowRunStoreError("workflow_run.already_exists", `workflow run id already exists`); } const stored = structuredClone(run); stored.id = id; stored.rev = run.rev ?? 0; this.commit({ t: "put", id, run: stored }, this.fsyncEnabled); }); } async get(id) { const run = this.runs.get(id); return run ? structuredClone(run) : null; } async update(id, scope, run, expect) { return this.withLock(id, () => { const next = nextWorkflowRunOnUpdate(this.runs.get(id), id, scope, run, expect); if (!next) return false; this.commit({ t: "update", id, run: next }, this.fsyncEnabled); return true; }); } async listByScope(scope, opts) { return queryWorkflowRuns(this.runs.values(), scope, opts); } async reap(scope, now, opts) { assertRetentionPolicy("WorkflowRunStore.reap", opts); if (opts?.maxAgeMs === undefined && opts?.keep === undefined) return 0; const terminal = [...this.runs.values()].filter((r) => r.scope === scope && isTerminalWorkflowStatus(r.status)); terminal.sort((a, b) => b.createdAt - a.createdAt); const doomed = []; for (let i = 0; i < terminal.length; i++) { const r = terminal[i]; const tooOld = opts.maxAgeMs !== undefined && (r.endedAt ?? r.createdAt) < now - opts.maxAgeMs; const overKeep = opts.keep !== undefined && i >= opts.keep; if (tooOld || overKeep) doomed.push({ id: r.id, byAge: tooOld }); } let n = 0; for (const { id, byAge } of doomed) { const deleted = await this.withLock(id, () => { const live = this.runs.get(id); if (!live || live.scope !== scope || !isTerminalWorkflowStatus(live.status)) return false; if (byAge && opts.maxAgeMs !== undefined && (live.endedAt ?? live.createdAt) >= now - opts.maxAgeMs) { return false; } this.commit({ t: "delete", id }, this.fsyncEnabled); return true; }); if (deleted) n++; } return n; } compactNow() { this.ledger.compact(); } get size() { return this.runs.size; } close() { if (this.closed) return; this.closed = true; this.ledger.release(); } }