UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

126 lines (125 loc) • 4.92 kB
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; }