@sema-agent/core
Version:
Stateless, task-oriented AI agent core
126 lines (125 loc) • 4.92 kB
JavaScript
import { readFileSync, readdirSync, mkdirSync, unlinkSync } from "node:fs";
import { join, resolve } from "node:path";
import { assertJsonMetadata, normalizeTaskShape } from "../../tools/task-list.js";
import { atomicWriteFile } from "../file/fs-atomic.js";
import { physicalPathOf } from "../../core/physical-path.js";
import { withCcLock } from "./lockfile.js";
export function createCcFileTaskListStore(listDir) {
const dir = resolve(listDir);
mkdirSync(dir, { recursive: true });
const hwmPath = join(dir, ".highwatermark");
const lockTarget = physicalPathOf(dir);
const CC_TASK_ID_RE = /^[A-Za-z0-9_-]+$/;
const validId = (id) => CC_TASK_ID_RE.test(id);
const taskPath = (id) => {
if (!validId(id))
throw new Error(`cc task-list store: invalid task id ${JSON.stringify(id)} (CC id charset [A-Za-z0-9_-] required)`);
return join(dir, `${id}.json`);
};
const scanIds = () => readdirSync(dir)
.filter((f) => f.endsWith(".json"))
.map((f) => f.slice(0, -".json".length))
.filter((id) => validId(id));
const scanNumericIds = () => scanIds()
.map((id) => Number.parseInt(id, 10))
.filter((n) => Number.isFinite(n));
const orderIds = (ids) => {
const num = (id) => (/^\d+$/.test(id) ? Number.parseInt(id, 10) : Number.NaN);
return [...ids].sort((a, b) => {
const [na, nb] = [num(a), num(b)];
if (Number.isFinite(na) && Number.isFinite(nb))
return na - nb || a.localeCompare(b);
if (Number.isFinite(na))
return -1;
if (Number.isFinite(nb))
return 1;
return a.localeCompare(b);
});
};
const readHwm = () => {
try {
const raw = readFileSync(hwmPath, "utf8").trim();
const n = Number.parseInt(raw, 10);
return Number.isFinite(n) ? n : 0;
}
catch {
return 0;
}
};
const readTask = (id) => {
if (!validId(id))
return undefined;
try {
return JSON.parse(readFileSync(taskPath(id), "utf8"));
}
catch (err) {
if (err.code === "ENOENT")
return undefined;
throw err;
}
};
const writeTask = (id, item) => {
atomicWriteFile(dir, taskPath(id), JSON.stringify(item, null, 2));
};
const snap = (t) => structuredClone(t);
const store = {
get: (id) => withCcLock(lockTarget, () => {
const t = readTask(id);
return t ? snap(t) : undefined;
}),
set: (id, item) => withCcLock(lockTarget, () => {
if (item.metadata)
assertJsonMetadata(item.metadata);
const prior = readTask(id);
writeTask(id, { ...(prior ?? {}), ...normalizeTaskShape(snap(item)) });
}),
delete: (id) => withCcLock(lockTarget, () => {
if (readTask(id) === undefined)
return false;
unlinkSync(taskPath(id));
return true;
}),
list: () => withCcLock(lockTarget, () => orderIds(scanIds())
.map((id) => readTask(id))
.filter((t) => t !== undefined)
.map(snap)),
allocateId: () => withCcLock(lockTarget, () => {
const scanned = scanNumericIds();
const next = Math.max(readHwm(), scanned.length > 0 ? Math.max(...scanned) : 0) + 1;
atomicWriteFile(dir, hwmPath, String(next));
return String(next);
}),
mutate: (fn) => withCcLock(lockTarget, () => {
const tx = {
get: (id) => {
const t = readTask(id);
return t ? snap(t) : undefined;
},
set: (id, item) => {
if (item.metadata)
assertJsonMetadata(item.metadata);
const prior = readTask(id);
writeTask(id, { ...(prior ?? {}), ...normalizeTaskShape(snap(item)) });
},
delete: (id) => {
if (readTask(id) === undefined)
return false;
unlinkSync(taskPath(id));
return true;
},
list: () => orderIds(scanIds())
.map((id) => readTask(id))
.filter((t) => t !== undefined)
.map(snap),
allocateId: () => {
const scanned = scanNumericIds();
const next = Math.max(readHwm(), scanned.length > 0 ? Math.max(...scanned) : 0) + 1;
atomicWriteFile(dir, hwmPath, String(next));
return String(next);
},
};
return fn(tx);
}),
};
return store;
}