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