UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

342 lines (341 loc) • 13.4 kB
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); } }