UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

233 lines (232 loc) • 8.26 kB
import { createHash } from "node:crypto"; import { readFileSync, readdirSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance, ToolResultRefConflictError, } from "../../core/tool-result-store.js"; import { ensureDir, sanitizePathComponent, writeThenLink } from "./fs-atomic.js"; import { guardCorruptSink } from "./corrupt-read-sink.js"; import { assertAdoptionBootGate } from "./adoption/marker.js"; export class FileToolResultStore { dir; discloseCorrupt; constructor(root, opts) { assertAdoptionBootGate(root, "FileToolResultStore"); this.dir = join(root, "tool-results"); this.discloseCorrupt = guardCorruptSink(opts?.onCorruptRead); ensureDir(this.dir); } disclose(path, reason) { this.discloseCorrupt?.({ kind: "file", path, reason }); } locate(ref) { assertSafeToolResultRef(ref); const encoded = encodeRefFilename(ref); return { dir: encoded.folded ? join(this.dir, FOLDED_SUBDIR) : this.dir, stem: sanitizePathComponent(encoded.stem), }; } pathFor(ref) { const { dir, stem } = this.locate(ref); return join(dir, `${stem}${CONTENT_SUFFIX}`); } ownerPathFor(ref) { const { dir, stem } = this.locate(ref); return join(dir, `${stem}${OWNER_SUFFIX}`); } put(ref, content, provenance) { assertSafeToolResultRef(ref); let won; try { writeThenLink(this.pathFor(ref), content); won = true; } catch (err) { if (err.code !== "EEXIST") throw err; won = false; } if (!won) { const stored = this.readOwnerFile(ref, { loud: true }); assertToolResultProvenanceMatch(ref, stored, provenance); return; } if (provenance === undefined) return; try { this.writeOwnerFile(ref, provenance); } catch (err) { if (err instanceof ToolResultRefConflictError) unlinkIfAbsentOk(this.pathFor(ref)); throw err; } } ownerOf(ref) { return this.readOwnerFile(ref, { loud: false }); } readOwnerFile(ref, opts) { const ownerPath = (() => { try { return this.ownerPathFor(ref); } catch { return undefined; } })(); if (ownerPath === undefined) return undefined; let raw; try { raw = readFileSync(ownerPath, "utf8"); } catch (err) { if (err.code === "ENOENT") return undefined; if (opts.loud) throw err; this.disclose(ownerPath, `unreadable owner sidecar (${err.code ?? "unknown"}) — read as unowned`); return undefined; } const parsed = parseOwnerRecord(raw); if (parsed !== undefined) return parsed; if (opts.loud) { throw new Error(`tool-result store: owner record for ref ${JSON.stringify(ref)} is present but unreadable — refusing to write over it`); } this.disclose(ownerPath, "not-an-object: owner sidecar is present but is not an owner record — read as unowned"); return undefined; } writeOwnerFile(ref, provenance) { try { writeThenLink(this.ownerPathFor(ref), JSON.stringify(normalizeToolResultProvenance(provenance))); } catch (err) { if (err.code !== "EEXIST") throw err; assertToolResultProvenanceMatch(ref, this.readOwnerFile(ref, { loud: true }), provenance); } } get(ref, opts) { let full; try { full = readFileSync(this.pathFor(ref), "utf8"); } catch (err) { if (err.code === "ENOENT") return undefined; if (err instanceof Error && (err.message.startsWith("file store: unsafe path component") || err.message.startsWith("tool-result store: unsafe ref"))) { return undefined; } throw err; } const offset = Math.min(full.length, Math.max(0, intOr(opts?.offset, 0))); const limit = opts?.limit != null ? Math.max(0, intOr(opts.limit, 0)) : undefined; const content = limit != null ? full.slice(offset, offset + limit) : full.slice(offset); return { content, offset, totalChars: full.length }; } deleteBySession(sessionId) { let deleted = 0; let unattributable = 0; for (const dir of [this.dir, join(this.dir, FOLDED_SUBDIR)]) { let names; try { names = readdirSync(dir); } catch (err) { if (err.code === "ENOENT") continue; throw err; } const listed = new Set(names); for (const name of names) { if (!name.endsWith(OWNER_SUFFIX)) continue; const owner = readOwnerAt(join(dir, name)); if (owner === "gone") continue; if (owner === "damaged") { unattributable++; continue; } if (owner.sessionId !== sessionId) continue; const stem = name.slice(0, -OWNER_SUFFIX.length); const sidecarPath = join(dir, name); const contentPath = join(dir, `${stem}${CONTENT_SUFFIX}`); unlinkIfAbsentOk(contentPath); unlinkIfAbsentOk(sidecarPath); const republished = readOwnerAt(sidecarPath); if (republished === "gone") { unlinkIfAbsentOk(contentPath); } else if (republished === "damaged") { unattributable++; } else if (republished.sessionId === sessionId) { unlinkIfAbsentOk(contentPath); unlinkIfAbsentOk(sidecarPath); } deleted++; } for (const name of names) { if (!name.endsWith(CONTENT_SUFFIX)) continue; if (listed.has(`${name.slice(0, -CONTENT_SUFFIX.length)}${OWNER_SUFFIX}`)) continue; unattributable++; } } return { deleted, unattributable }; } } const CONTENT_SUFFIX = ".txt"; const OWNER_SUFFIX = ".owner.json"; function parseOwnerRecord(raw) { const parsed = (() => { try { return JSON.parse(raw); } catch { return undefined; } })(); if (typeof parsed === "object" && parsed !== null) { const { sessionId, taskId } = parsed; if (typeof sessionId === "string" && (taskId === undefined || typeof taskId === "string")) { return taskId === undefined ? { sessionId } : { sessionId, taskId }; } } return undefined; } function readOwnerAt(path) { let raw; try { raw = readFileSync(path, "utf8"); } catch (err) { return err.code === "ENOENT" ? "gone" : "damaged"; } return parseOwnerRecord(raw) ?? "damaged"; } function unlinkIfAbsentOk(path) { try { unlinkSync(path); } catch (err) { if (err.code !== "ENOENT") throw err; } } const NATIVE_FILENAME_CHARSET = /^[A-Za-z0-9_.-]+$/; const FOLDED_SUBDIR = "folded-v2"; const MAX_FILENAME_CHARS = 180; function encodeRefFilename(ref) { if (ref.length <= MAX_FILENAME_CHARS && NATIVE_FILENAME_CHARSET.test(ref)) return { folded: false, stem: ref }; const base = ref.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 64); return { folded: true, stem: `${base || "ref"}-${createHash("sha256").update(Buffer.from(ref, "utf16le")).digest("hex")}` }; } function intOr(x, fallback) { return Number.isFinite(x) ? Math.floor(x) : fallback; }