@sema-agent/core
Version:
Stateless, task-oriented AI agent core
342 lines (341 loc) • 13.4 kB
JavaScript
import { accessSync, constants as FS, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { createHash } from "node:crypto";
import { atomicWriteFile } from "./fs-atomic.js";
import { createSafeNotifier, observeThenableRejection } from "../../core/safe-notify.js";
import { thrownText } from "../../core/thrown-value.js";
import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_TEXT_BYTES, compileStrategyQuery, resolveCapacityCap, resolveFindLimit, scoreStoredStrategy, storedStrategyShapeIssue, strategyDedupKey, validateStrategyForWrite, } from "../../core/strategy-store.js";
function configRefusal(message, code) {
const e = new Error(message);
e.code = code;
return e;
}
const SCOPE_SLUG_MAX = 40;
const READ_FILE_CAP_FACTOR = 4;
const STALE_TEMP_MS = 60 * 60 * 1000;
const MAX_ENTRY_FILE_BYTES = 65536;
const FILE_RE = /^([A-Za-z0-9_-]{1,64})\.json$/;
export class FileStrategyStore {
root;
maxPerScope;
onIncident;
warnedOps = new Set();
sinkNotifier = createSafeNotifier();
sweptTemps = new Set();
constructor(opts) {
this.maxPerScope = resolveCapacityCap("maxPerScope", opts.maxPerScope ?? 100);
this.onIncident = opts.onIncident;
const root = opts.root;
if (typeof root !== "string" || root.length === 0) {
throw configRefusal("FileStrategyStore: root must be a non-empty path", "config.strategy_root_invalid");
}
try {
mkdirSync(root, { recursive: true, mode: 0o700 });
}
catch (err) {
throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
}
let st;
try {
st = statSync(root);
}
catch (err) {
throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${thrownText(err)}`, "config.strategy_root_invalid");
}
if (!st.isDirectory()) {
throw configRefusal(`FileStrategyStore: root ${root} exists and is not a directory`, "config.strategy_root_invalid");
}
try {
accessSync(root, FS.W_OK);
}
catch {
throw configRefusal(`FileStrategyStore: root ${root} is not writable`, "config.strategy_root_invalid");
}
this.root = root;
}
scopeDirName(scope) {
const slug = scope
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, SCOPE_SLUG_MAX);
const h = createHash("sha256").update(scope, "utf8").digest("hex").slice(0, 24);
return `${slug || "s"}-${h}`;
}
scopeDirPath(scope) {
return join(this.root, this.scopeDirName(scope));
}
assertScopeDirSafe(dir) {
let st;
try {
st = lstatSync(dir);
}
catch (err) {
if (err.code === "ENOENT")
return;
throw err;
}
if (st.isSymbolicLink() || !st.isDirectory()) {
const e = new Error(`FileStrategyStore: scope directory ${dir} is not a plain directory (symlink or non-dir refused)`);
e.code = "strategy.scope_dir_invalid";
throw e;
}
}
incident(i) {
const sink = this.onIncident;
if (sink !== undefined) {
const site = `FileStrategyStore.onIncident.${i.op}`;
this.sinkNotifier.notify(() => observeThenableRejection(sink(i), this.sinkNotifier, site), site);
return;
}
if (!this.warnedOps.has(i.op)) {
this.warnedOps.add(i.op);
console.warn(`FileStrategyStore ${i.op} incident: ${i.error}${i.path !== undefined ? ` (${i.path})` : ""}`);
}
}
sweepStaleTemps(dir) {
if (this.sweptTemps.has(dir))
return;
this.sweptTemps.add(dir);
let names;
try {
names = readdirSync(dir);
}
catch {
return;
}
const cutoff = Date.now() - STALE_TEMP_MS;
for (const name of names) {
if (!name.endsWith(".tmp"))
continue;
const p = join(dir, name);
try {
if (statSync(p).mtimeMs < cutoff)
unlinkSync(p);
}
catch {
}
}
}
loadScope(scope, dir, readCap) {
let names;
try {
names = readdirSync(dir);
}
catch (err) {
if (err.code === "ENOENT")
return [];
throw err;
}
let candidates = names.filter((n) => FILE_RE.test(n)).sort();
if (Number.isFinite(readCap) && candidates.length > readCap) {
this.incident({
op: "find",
error: `scope directory holds ${candidates.length} entry files, over the ${readCap} read cap — parsing the first ${readCap} only (an external writer likely inflated this directory)`,
path: dir,
});
candidates = candidates.slice(0, readCap);
}
const out = [];
for (const name of candidates) {
const p = join(dir, name);
let fst;
try {
fst = lstatSync(p);
}
catch (err) {
if (err.code === "ENOENT")
continue;
this.incident({ op: "find", error: `unstat-able entry skipped: ${thrownText(err)}`, path: p });
continue;
}
if (!fst.isFile()) {
this.incident({ op: "find", error: "non-regular entry skipped (symlink or special file refused)", path: p });
continue;
}
if (fst.size > MAX_ENTRY_FILE_BYTES) {
this.quarantine(p, `entry file is ${fst.size} bytes, over the ${MAX_ENTRY_FILE_BYTES} read bound`);
continue;
}
let raw;
try {
raw = readFileSync(p, "utf8");
}
catch (err) {
if (err.code === "ENOENT")
continue;
this.incident({ op: "find", error: `unreadable entry skipped: ${thrownText(err)}`, path: p });
continue;
}
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
this.quarantine(p, "unparseable JSON");
continue;
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
this.quarantine(p, "top-level JSON value is not an object");
continue;
}
const rec = parsed;
if (rec.v !== 1) {
this.incident({ op: "parse", error: `unknown schema version ${String(rec.v)} — entry skipped, file left in place`, path: p });
continue;
}
const { v: _v, ...candidate } = rec;
const issue = storedStrategyShapeIssue(candidate);
if (issue !== null) {
this.quarantine(p, issue);
continue;
}
const entry = candidate;
const fileId = FILE_RE.exec(name)[1];
if (entry.scope !== scope || entry.id !== fileId) {
this.incident({
op: "find",
error: `entry skipped: scope/id mismatch (file claims scope ${JSON.stringify(entry.scope)}, id ${JSON.stringify(entry.id)})`,
id: fileId,
path: p,
});
continue;
}
out.push({ entry, path: p });
}
const byKey = new Map();
for (const le of out) {
const key = strategyDedupKey(le.entry);
const prev = byKey.get(key);
if (prev === undefined || scoreStoredStrategy(le.entry) > scoreStoredStrategy(prev.entry))
byKey.set(key, le);
}
return [...byKey.values()];
}
quarantine(p, reason) {
try {
renameSync(p, `${p}.bad`);
this.incident({ op: "parse", error: `corrupt entry quarantined (${reason})`, path: p });
}
catch (err) {
this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${thrownText(err)}`, path: p });
}
}
writeEntry(dir, path, entry) {
const record = {
v: 1,
id: entry.id,
problem: entry.problem,
strategy: entry.strategy,
confidence: entry.confidence,
scope: entry.scope,
ts: entry.ts,
...(entry.teacherModel !== undefined ? { teacherModel: entry.teacherModel } : {}),
...(entry.signature !== undefined ? { signature: entry.signature } : {}),
...(entry.origin !== undefined ? { origin: entry.origin } : {}),
};
const bytes = JSON.stringify(record);
if (Buffer.byteLength(bytes, "utf8") > MAX_ENTRY_FILE_BYTES) {
const e = new Error(`FileStrategyStore: entry serializes over the ${MAX_ENTRY_FILE_BYTES}-byte read bound (JSON escaping expanded it past the field caps) — refusing a write the read side would quarantine`);
e.code = "strategy.entry_invalid";
throw e;
}
atomicWriteFile(dir, path, bytes);
}
save(s) {
validateStrategyForWrite(s);
const dir = this.scopeDirPath(s.scope);
this.assertScopeDirSafe(dir);
mkdirSync(dir, { recursive: true, mode: 0o700 });
this.sweepStaleTemps(dir);
const existing = this.loadScope(s.scope, dir, this.retrievalReadCap());
const key = strategyDedupKey(s);
const dup = existing.find((le) => strategyDedupKey(le.entry) === key);
let all;
if (dup !== undefined) {
const refreshed = {
...dup.entry,
...(s.confidence >= dup.entry.confidence ? { confidence: s.confidence, ts: s.ts } : {}),
...(s.teacherModel !== undefined ? { teacherModel: s.teacherModel } : {}),
};
this.writeEntry(dir, dup.path, refreshed);
dup.entry = refreshed;
all = existing;
}
else {
const p = join(dir, `${s.id}.json`);
const byId = existing.find((le) => le.entry.id === s.id);
this.writeEntry(dir, p, s);
if (byId !== undefined) {
byId.entry = s;
all = existing;
}
else {
all = [...existing, { entry: s, path: p }];
}
}
if (all.length > this.maxPerScope) {
this.evict(all, this.maxPerScope);
}
}
evict(all, keep) {
const sorted = [...all].sort((a, b) => scoreStoredStrategy(b.entry) - scoreStoredStrategy(a.entry));
for (const le of sorted.slice(keep)) {
try {
unlinkSync(le.path);
}
catch (err) {
if (err.code === "ENOENT")
continue;
this.incident({ op: "evict", error: thrownText(err), id: le.entry.id, path: le.path });
}
}
}
find(scope, query, limit) {
const cap = resolveFindLimit(limit);
const dir = this.scopeDirPath(scope);
this.assertScopeDirSafe(dir);
const matches = compileStrategyQuery(query);
if (matches === null)
return [];
const ranked = this.loadScope(scope, dir, this.retrievalReadCap())
.map((le) => le.entry)
.filter((e) => matches(e.problem))
.sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a))
.slice(0, cap);
let bytes = 0;
for (let i = 0; i < ranked.length; i++) {
bytes += Buffer.byteLength(ranked[i].strategy, "utf8");
if (bytes > MAX_STRATEGY_INJECTION_TOTAL_BYTES) {
this.incident({
op: "find",
error: `result truncated at ${i} of ${ranked.length} entries — combined strategy text exceeded ${MAX_STRATEGY_INJECTION_TOTAL_BYTES} bytes (per-entry cap ${MAX_STRATEGY_TEXT_BYTES})`,
});
return ranked.slice(0, i);
}
}
return ranked;
}
retrievalReadCap() {
return this.maxPerScope * READ_FILE_CAP_FACTOR;
}
prune(scope, maxSize) {
const n = resolveCapacityCap("maxSize", maxSize);
const dir = this.scopeDirPath(scope);
this.assertScopeDirSafe(dir);
const all = this.loadScope(scope, dir, Number.POSITIVE_INFINITY);
if (all.length > n)
this.evict(all, n);
}
scopeUsage(scope) {
const dir = this.scopeDirPath(scope);
this.assertScopeDirSafe(dir);
return { used: this.loadScope(scope, dir, Number.POSITIVE_INFINITY).length, capacity: this.maxPerScope };
}
hasStrategy(scope, entry) {
const dir = this.scopeDirPath(scope);
this.assertScopeDirSafe(dir);
const key = strategyDedupKey(entry);
return this.loadScope(scope, dir, this.retrievalReadCap()).some((le) => strategyDedupKey(le.entry) === key);
}
}