@sema-agent/core
Version:
Stateless, task-oriented AI agent core
149 lines (148 loc) • 7.56 kB
JavaScript
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { relative, sep } from "node:path";
import { inlineUntrusted } from "../untrusted-text.js";
import { scanEntryFiles } from "./entry-scan.js";
import { entryFromFile, parseEntryFile } from "./frontmatter.js";
import { ControlPlaneCorruptError, canonicalize, lockedStrictUpdate, readStrictSidecar, scopeDirFor } from "./layout.js";
export const LAYER_OBSERVATIONS_FILE = "layer-observations.json";
const WHAT = "memory layer-observation table";
const str = (v) => typeof v === "string" && v.length > 0;
function coerce(raw) {
if (raw === undefined)
return { version: 1, rows: [] };
const rec = raw;
if (typeof rec !== "object" || rec === null || rec.version !== 1 || !Array.isArray(rec.rows))
throw new ControlPlaneCorruptError(`${WHAT} has the wrong shape or an unrecognized version — refusing (fail-closed)`);
const rows = rec.rows;
for (const r of rows) {
const row = r;
if (typeof row !== "object" || row === null || !str(row.scope) || !str(row.rel) || (row.entryId !== undefined && !str(row.entryId)) || !str(row.observedRev) || (row.rev !== undefined && (row.entryId === undefined || !str(row.rev))) || (row.sessionId !== undefined && !str(row.sessionId)) || typeof row.observedAt !== "number" || !Number.isFinite(row.observedAt) || (row.undecided !== undefined && (row.undecided !== true || row.entryId === undefined || row.rev !== undefined)) || (row.observationId !== undefined && (!str(row.observationId) || row.undecided !== true))) {
throw new ControlPlaneCorruptError(`${WHAT}: a row is malformed — refusing (fail-closed; an unevaluable row must not read as "no unexpected write")`);
}
}
return rec;
}
const keyOf = (o) => o.entryId === undefined
? JSON.stringify([null, o.scope, o.rel, o.observedRev])
: o.undecided === true
? JSON.stringify([o.entryId, { undecided: true }, o.observedRev, o.scope, o.rel, o.sessionId ?? null, o.observedAt, o.observationId ?? null])
: JSON.stringify([o.entryId, o.rev ?? null, o.observedRev]);
const contentKey = (entryId, observedRev) => JSON.stringify([entryId, observedRev]);
export function recordLayerObservations(controlDir, rows) {
if (rows.length === 0)
return;
lockedStrictUpdate(controlDir, LAYER_OBSERVATIONS_FILE, WHAT, coerce, (rec) => {
const byKey = new Map(rec.rows.map((r) => [keyOf(r), r]));
for (const r of rows)
if (!byKey.has(keyOf(r)))
byKey.set(keyOf(r), { ...r });
return { next: { version: 1, rows: [...byKey.values()] }, result: undefined };
});
}
export function layerObservationLookup(controlDir) {
const rows = coerce(readStrictSidecar(controlDir, LAYER_OBSERVATIONS_FILE, WHAT)).rows;
const ids = new Set(rows.flatMap((r) => (r.entryId !== undefined ? [r.entryId] : [])));
const byKey = new Map(rows.map((r) => [keyOf(r), r]));
const undecidedByContent = new Map(rows.flatMap((r) => (r.undecided === true && r.entryId !== undefined ? [[contentKey(r.entryId, r.observedRev), r]] : [])));
return {
covers: (entryId) => ids.has(entryId),
find: (entryId, rev, observedRev) => ids.has(entryId) ? (byKey.get(keyOf({ scope: "", rel: "", entryId, rev, observedRev })) ?? undecidedByContent.get(contentKey(entryId, observedRev))) : undefined,
findNote: (scope, rel, digest) => byKey.get(keyOf({ scope, rel, observedRev: digest })),
};
}
export function observedDivergenceNotice(rel, served, undecided = false) {
const file = JSON.stringify(inlineUntrusted(rel));
if (undecided)
return `change NOT adopted yet: ${file} changed while a session held its layer read-only, and whether a commit backs it could not be established — re-judged once a writing session can read the committed account`;
return served
? `change NOT adopted: ${file} was written while a session held its layer read-only — memory serves the committed version, not these bytes`
: `file NOT adopted: ${file} appeared while a session held its layer read-only — it is not memory; left on disk for review`;
}
export function undecidedLayerObservations(controlDir) {
return coerce(readStrictSidecar(controlDir, LAYER_OBSERVATIONS_FILE, WHAT)).rows.filter((r) => r.undecided === true);
}
function ordinaryRow(r, rev) {
return {
scope: r.scope,
rel: r.rel,
...(r.entryId !== undefined ? { entryId: r.entryId } : {}),
...(rev !== undefined ? { rev } : {}),
observedRev: r.observedRev,
...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
observedAt: r.observedAt,
};
}
export function settleUndecidedLayerObservations(controlDir, verdicts) {
if (verdicts.size === 0)
return;
const byKey = new Map([...verdicts].map(([row, v]) => [keyOf(row), v]));
lockedStrictUpdate(controlDir, LAYER_OBSERVATIONS_FILE, WHAT, coerce, (rec) => {
const next = new Map();
for (const r of rec.rows) {
const v = r.undecided === true ? byKey.get(keyOf(r)) : undefined;
if (v === "backed")
continue;
const settled = v === undefined ? r : ordinaryRow(r, v.rev);
if (!next.has(keyOf(settled)))
next.set(keyOf(settled), settled);
}
return { next: { version: 1, rows: [...next.values()] }, result: undefined };
});
}
export function layerDigest(text) {
return createHash("sha256").update(text, "utf8").digest("hex");
}
function readText(path) {
try {
return readFileSync(path, "utf8");
}
catch {
return undefined;
}
}
export function readOnlyLayerCensus(homes) {
const files = new Map();
for (const home of homes) {
for (const f of scanEntryFiles(home)) {
const path = canonicalize(f.path);
const text = readText(path);
if (text !== undefined)
files.set(path, layerDigest(text));
}
}
return files;
}
export function readOnlyLayerCandidates(memoryDir, controlDir, handle) {
const census = handle.layerBaseline;
if (census === undefined)
return [];
const out = [];
for (const scope of new Set(handle.scopes)) {
if (scope === handle.writeScope)
continue;
const home = canonicalize(scopeDirFor(memoryDir, controlDir, scope));
for (const f of scanEntryFiles(home)) {
const path = canonicalize(f.path);
const text = readText(path);
if (text === undefined || (path !== home && !path.startsWith(`${home}${sep}`)))
continue;
const digest = layerDigest(text);
if (census.get(path) === digest)
continue;
const id = parseEntryFile(text).id;
const rel = relative(home, path).split(sep).join("/");
out.push({ path, scope, rel, slug: f.slug, digest, ...(id !== undefined ? { id, observedRev: entryFromFile(text, id, f.slug, scope).rev } : { observedRev: digest }) });
}
}
return out;
}
export function landedUndecided(batch) {
return batch?.landed === true ? batch.undecided : [];
}
export function sameObservation(a, b) {
return keyOf(a) === keyOf(b);
}
export function observationBacked(row, committed) {
return committed !== undefined && committed.scope === row.scope && committed.slug === row.rel.replace(/\.md$/, "") && committed.rev === row.observedRev;
}