UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

149 lines (148 loc) • 7.56 kB
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; }