@sema-agent/core
Version:
Stateless, task-oriented AI agent core
233 lines (232 loc) • 8.26 kB
JavaScript
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;
}