UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

901 lines (900 loc) • 47.3 kB
import { createHash } from "node:crypto"; import { thrownText } from "./thrown-value.js"; import { isAbsoluteForFamily, isAbsolutePathForm, isWinFormPath, pathFamilyOf } from "../tools/fs/safety.js"; import { fileHistoryBoundariesToKeep, resolveFileHistoryRetention } from "./file-history-retention.js"; const normSlash = (p) => p.replace(/\\/g, "/"); const stripTrailSep = (p) => (isWinFormPath(p) ? p.replace(/[\\/]+$/, "") : p.replace(/\/+$/, "")); const foldDrive = (p) => (/^[A-Za-z]:(\/|$)/.test(p) ? p[0].toLowerCase() + p.slice(1) : p); function relUnder(root, child) { const fold = isWinFormPath(root) ? normSlash : (p) => p; const r = foldDrive(fold(stripTrailSep(root))); const c = foldDrive(fold(child)); if (c === r) return ""; if (!c.startsWith(r + "/")) return null; return fold(child).slice(fold(stripTrailSep(root)).length + 1); } function joinUnder(root, rel) { const win = isWinFormPath(root); return stripTrailSep(root) + (win ? "\\" : "/") + (win ? rel.replace(/\//g, "\\") : rel); } function isUnsafeRelPathLike(relN) { return isAbsolutePathForm(relN) || /^[A-Za-z]:/.test(relN) || relN.split("/").includes(".."); } function parentDirOf(absPath) { const win = isWinFormPath(absPath); const norm = win ? normSlash(absPath) : absPath; const cut = norm.replace(/\/+$/, ""); const i = cut.lastIndexOf("/"); if (i <= 0) return undefined; const parent = cut.slice(0, i); return win ? parent.replace(/\//g, "\\") : parent; } export function trackKeyFamilyOf(root) { return isWinFormPath(root) ? "win" : "posix"; } export function describeTrackKey(root, absPath) { const key = trackKeyOf(root, absPath); return { key, kind: key === absPath ? "absolute" : "root-relative", family: trackKeyFamilyOf(root) }; } export function trackKeyOf(root, absPath) { const rel = relUnder(root, absPath); if (rel === null || rel === "") return absPath; const win = isWinFormPath(root); const relN = win ? normSlash(rel) : rel; if (win ? isUnsafeRelPathLike(relN) : isAbsoluteForFamily("posix", relN) || relN.split("/").includes("..")) return absPath; return rel; } export function resolveTrackKey(root, key, minted) { const win = isWinFormPath(root); if (minted !== undefined && minted.family !== trackKeyFamilyOf(root)) { if (minted.kind === "absolute") { return { ok: false, reason: `foreign-family absolute key cannot be restored here (recorded under a ${minted.family} root, this root is ${trackKeyFamilyOf(root)}): ${key}` }; } if (isUnsafeRelPathLike(normSlash(key)) || key.includes("\\")) { return { ok: false, reason: `foreign-family key is ambiguous across path families (recorded under a ${minted.family} root, this root is ${trackKeyFamilyOf(root)}): ${key}` }; } } const keyN = win ? normSlash(key) : key; if (isAbsoluteForFamily(pathFamilyOf({ root }), keyN) || (win && /^[A-Za-z]:/.test(keyN))) return { ok: true, abs: key }; if (win ? isUnsafeRelPathLike(keyN) : keyN.split("/").includes("..")) return { ok: false, reason: `unsafe recorded key escapes root: ${key}` }; return { ok: true, abs: joinUnder(root, keyN) }; } export const FILE_HISTORY_DIFF_LINE_BUDGET = 2000; export function countRestoreLineDiff(current, target) { const isBinary = (b) => b !== undefined && b.includes(0); if (isBinary(current) || isBinary(target)) return { insertions: 0, deletions: 0 }; const linesOf = (b) => { if (b === undefined) return []; const text = new TextDecoder().decode(b); if (text === "") return []; return text.endsWith("\n") ? text.slice(0, -1).split("\n") : text.split("\n"); }; const a = linesOf(current); const bLines = linesOf(target); const common = longestCommonSubsequenceLength(a, bLines); return { deletions: a.length - common, insertions: bLines.length - common }; } function longestCommonSubsequenceLength(a, b) { const n = a.length; const m = b.length; if (n === 0 || m === 0) return 0; const max = Math.min(FILE_HISTORY_DIFF_LINE_BUDGET, n + m); const offset = max; const v = new Int32Array(2 * max + 1); for (let d = 0; d <= max; d++) { for (let k = -d; k <= d; k += 2) { let x; if (k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1])) x = v[offset + k + 1]; else x = v[offset + k - 1] + 1; let y = x - k; while (x < n && y < m && a[x] === b[y]) { x++; y++; } v[offset + k] = x; if (x >= n && y >= m) { return (n + m - d) / 2; } } } return 0; } export function fileHistoryExportDigest(data) { const canonical = { formatVersion: data.formatVersion, tracked: [...data.tracked] .map((t) => ({ relOrAbsPath: t.relOrAbsPath, keyKind: t.keyKind, keyFamily: t.keyFamily, baseAvailable: t.baseAvailable, versions: [...t.versions] .map((v) => ({ version: v.version, blobHash: v.blobHash, mode: v.mode ?? null, parentIdent: v.parentIdent ?? null, backupTime: v.backupTime })) .sort((a, b) => a.version - b.version), })) .sort((a, b) => (a.relOrAbsPath < b.relOrAbsPath ? -1 : a.relOrAbsPath > b.relOrAbsPath ? 1 : 0)), boundaries: [...data.boundaries] .map((b) => ({ entryId: b.entryId, files: [...b.files].map((f) => ({ relOrAbsPath: f.relOrAbsPath, version: f.version })).sort((x, y) => (x.relOrAbsPath < y.relOrAbsPath ? -1 : x.relOrAbsPath > y.relOrAbsPath ? 1 : 0)), })) .sort((a, b) => (a.entryId < b.entryId ? -1 : a.entryId > b.entryId ? 1 : 0)), }; return createHash("sha256").update(JSON.stringify(canonical)).digest("hex"); } export function validateFileHistoryExport(data) { const bad = (message) => ({ ok: false, error: { code: "unsupported_format", message } }); if (data === null || typeof data !== "object") return bad("file-history import payload is not an envelope"); if (data.formatVersion !== "file-history/1") return bad(`unsupported file-history envelope format ${JSON.stringify(String(data.formatVersion))} — this engine reads "file-history/1" and refuses rather than reinterpreting a foreign epoch`); if (!Array.isArray(data.tracked) || !Array.isArray(data.boundaries)) return bad("file-history envelope is missing its tracked/boundaries arrays"); const known = new Set(); for (const t of data.tracked) { if (typeof t?.relOrAbsPath !== "string" || t.relOrAbsPath === "") return bad("file-history envelope carries a tracked row with no path"); if (t.keyKind !== "root-relative" && t.keyKind !== "absolute") return bad(`tracked row ${t.relOrAbsPath} declares an unknown key kind ${JSON.stringify(String(t.keyKind))} — a key whose shape cannot be judged is refused, never guessed`); if (t.keyKind === "absolute") { return bad(`tracked row ${t.relOrAbsPath} is an ABSOLUTE key — an imported graph may not carry paths outside the importing root (it would grant this envelope restore authority over an arbitrary path); same-store carry across sessions is adoptScope's seat`); } if (isUnsafeRelPathLike(normSlash(t.relOrAbsPath)) || t.relOrAbsPath.includes("\\")) { return bad(`tracked row ${t.relOrAbsPath} is not a safe root-relative key (absolute form, drive spelling, '..' segment, or a separator that means different things across path families) — refused, never coerced`); } if (t.keyFamily !== "posix" && t.keyFamily !== "win") return bad(`tracked row ${t.relOrAbsPath} declares an unknown key family ${JSON.stringify(String(t.keyFamily))} — a key whose provenance cannot be judged is refused, never guessed`); if (typeof t.baseAvailable !== "boolean" || !Array.isArray(t.versions)) return bad(`tracked row ${t.relOrAbsPath} is missing its base-availability state or version chain`); for (const v of t.versions) { if (typeof v?.version !== "number" || !Number.isInteger(v.version) || v.version < 1) return bad(`tracked row ${t.relOrAbsPath} carries a non-integer version number`); if (v.blobHash !== null && (typeof v.blobHash !== "string" || !/^[0-9a-f]{64}$/.test(v.blobHash))) return bad(`tracked row ${t.relOrAbsPath} carries a blob reference that is not a sha256 digest`); if (v.mode !== undefined) { if (typeof v.mode !== "number" || !Number.isInteger(v.mode) || v.mode < 0 || v.mode > 0o7777) { return bad(`tracked row ${t.relOrAbsPath} carries a mode outside the permission-bit domain [0, 0o7777] — refused, never coerced`); } if ((v.mode & 0o6000) !== 0) { return bad(`tracked row ${t.relOrAbsPath} carries a set-ID mode (0o${v.mode.toString(8)}) — a restore replaying setuid/setgid bits would mint a privilege-escalation primitive; refused`); } } } if (new Set(t.versions.map((v) => v.version)).size !== t.versions.length) return bad(`tracked row ${t.relOrAbsPath} carries the same version number twice — a chain whose versions are ambiguous cannot be published`); for (let i = 1; i < t.versions.length; i++) { if (t.versions[i].version <= t.versions[i - 1].version) { return bad(`tracked row ${t.relOrAbsPath} carries a chain whose versions are not strictly increasing in order (…${t.versions[i - 1].version}, ${t.versions[i].version}…) — a later local boundary would then reference an ambiguous version`); } } if (known.has(t.relOrAbsPath)) { return bad(`file-history envelope carries the tracked row ${t.relOrAbsPath} twice — duplicate keys make the published graph differ from the validated one`); } if (t.baseAvailable && t.versions.length > 0 && t.versions[0].version !== 1) return bad(`tracked row ${t.relOrAbsPath} claims an available base but its chain does not start at v1`); if (!t.baseAvailable && t.versions.some((v) => v.version === 1)) return bad(`tracked row ${t.relOrAbsPath} carries a v1 record while declaring its base UNAVAILABLE — the DV-14 refusal state and a pristine baseline are mutually exclusive`); known.add(t.relOrAbsPath); } const seen = new Set(); for (const b of data.boundaries) { if (typeof b?.entryId !== "string" || b.entryId === "") return bad("file-history envelope carries a boundary with no entry id"); if (seen.has(b.entryId)) return bad(`file-history envelope carries the boundary ${b.entryId} twice`); seen.add(b.entryId); if (!Array.isArray(b.files)) return bad(`boundary ${b.entryId} carries no file mapping`); if (new Set(b.files.map((f) => f?.relOrAbsPath)).size !== b.files.length) return bad(`boundary ${b.entryId} maps the same path twice — the published mapping would differ from the validated one`); for (const f of b.files) { if (typeof f?.relOrAbsPath !== "string" || typeof f.version !== "number") return bad(`boundary ${b.entryId} carries a malformed file mapping`); if (!known.has(f.relOrAbsPath)) return bad(`boundary ${b.entryId} references ${f.relOrAbsPath}, which the envelope's tracked set does not carry`); const chain = data.tracked.find((t) => t.relOrAbsPath === f.relOrAbsPath); if (!chain.versions.some((v) => v.version === f.version)) return bad(`boundary ${b.entryId} references ${f.relOrAbsPath}@v${f.version}, which the envelope's version chain does not carry`); } } return { ok: true }; } export function boundaryPublishVerdict(attempt) { return attempt.aborted ? { publish: false, reason: "aborted" } : { publish: true }; } async function parentIdentityViaEnv(env, absPath, root, signal) { const parent = parentDirOf(absPath); if (parent === undefined) return undefined; const canon = await env.canonicalPath(parent, signal); if (!canon.ok) return undefined; let rel = relUnder(root, canon.value); if (rel === null) { const canonRoot = await env.canonicalPath(root, signal); if (canonRoot.ok) rel = relUnder(canonRoot.value, canon.value); } return rel === null ? canon.value : rel === "" ? "." : rel; } export async function captureFileBackupViaEnv(env, absPath, root, signal) { if (signal?.aborted) return { state: "aborted" }; const parentIdent = await parentIdentityViaEnv(env, absPath, root, signal); const rb = await env.readBinaryFile(absPath, signal); if (!rb.ok) { if (signal?.aborted) return { state: "aborted" }; if (rb.error.code === "not_found") return { state: "absent", ...(parentIdent !== undefined ? { parentIdent } : {}) }; return { state: "unreadable", cause: `read ${absPath}: ${rb.error.code}` }; } const hash = createHash("sha256").update(rb.value).digest("hex"); const mode = await (async () => { const info = await env.fileInfo(absPath, signal); return info.ok && typeof info.value.mode === "number" ? info.value.mode & 0o7777 : undefined; })(); return { state: "file", bytes: rb.value, hash, ...(mode !== undefined ? { mode } : {}), ...(parentIdent !== undefined ? { parentIdent } : {}) }; } export async function worldStillMatchesMint(env, absPath, root, baseAvailable, mintedBlobHash, signal) { if (!baseAvailable) { return { unchanged: false, reason: "the first touch recorded a base-UNAVAILABLE marker, whose whole purpose is to refuse a fake pristine later — an ambiguous failure may not erase it" }; } const cap = await captureFileBackupViaEnv(env, absPath, root, signal); if (cap.state === "absent") { return mintedBlobHash === null ? { unchanged: true } : { unchanged: false, reason: "the file recorded at the first touch is gone now" }; } if (cap.state === "file") { if (mintedBlobHash === null) return { unchanged: false, reason: "the path was recorded as NOT EXISTING at the first touch but holds bytes now" }; return cap.hash === mintedBlobHash ? { unchanged: true } : { unchanged: false, reason: "the file's bytes differ from the ones recorded at the first touch" }; } return { unchanged: false, reason: `the path could not be re-read to prove it is unchanged (${cap.state === "aborted" ? "aborted" : cap.cause})` }; } export function modeProvenDifferent(recorded, current) { return recorded !== undefined && current !== undefined && recorded !== current; } export async function applyFileRestoreViaEnv(env, absPath, target, opts) { const signal = opts.signal; if (signal?.aborted) return { outcome: "failed", reason: "aborted" }; const guard = await restorePreflight(env, absPath, opts); if ("refused" in guard) return guard.refused; const cur = await env.readBinaryFile(absPath, signal); const curAbsent = !cur.ok && cur.error.code === "not_found"; if ("absent" in target) { if (curAbsent) return { outcome: "identical" }; const rm = await env.remove(absPath, { force: true, ...(signal !== undefined ? { abortSignal: signal } : {}) }); if (!rm.ok) return { outcome: "failed", reason: `remove: ${rm.error.code}` }; return { outcome: "applied" }; } if (cur.ok && createHash("sha256").update(cur.value).digest("hex") === target.hash) { const modeOnly = await applyRecordedMode(env, absPath, opts, guard.currentMode); if (modeOnly === "unchanged") return { outcome: "identical" }; if (modeOnly === "applied") return { outcome: "applied" }; if (modeOnly === "applied_unpreserved") return { outcome: "applied", modeNotPreserved: true }; return { outcome: "identical", modeNotPreserved: true }; } const w = await writeRestoredBytes(env, absPath, target.bytes, signal); if (!w.ok) return { outcome: w.refused ? "refused" : "failed", reason: `write: ${w.code}` }; const modeAfter = await applyRecordedMode(env, absPath, opts, undefined); return { outcome: "applied", ...(modeAfter === "unpreserved" || modeAfter === "applied_unpreserved" ? { modeNotPreserved: true } : {}) }; } async function restorePreflight(env, absPath, opts) { const info = await env.fileInfo(absPath, opts.signal); if (info.ok && info.value.kind !== "file") { return { refused: { outcome: "refused", reason: info.value.kind === "symlink" ? "restore target is a SYMLINK — the recorded bytes belong to the file that was edited, not to whatever the link now points at" : `restore target is a ${info.value.kind}, not a regular file — the recorded bytes are not this object's`, }, }; } const currentMode = info.ok ? info.value.mode : undefined; if (opts.parentIdent === undefined) return { currentMode }; const now = await parentIdentityViaEnv(env, absPath, opts.root, opts.signal); if (now === undefined) return { currentMode }; if (now === opts.parentIdent) return { currentMode }; return { refused: { outcome: "refused", reason: `restore target's parent chain changed identity since it was recorded (recorded ${JSON.stringify(opts.parentIdent)}, now ${JSON.stringify(now)}) — writing here could land outside the task root`, }, }; } async function writeRestoredBytes(env, absPath, bytes, signal) { if (env.writeFileGuarded !== undefined) { const r = await env.writeFileGuarded(absPath, bytes, { canonicalPath: absPath, noFollow: true }, signal); return r.ok ? { ok: true } : { ok: false, code: r.error.code, refused: r.error.code === "precondition_failed" }; } const w = await env.writeFile(absPath, bytes, signal); return w.ok ? { ok: true } : { ok: false, code: w.error.code, refused: false }; } export async function previewFileDelta(env, absPath, targetHash, targetBytes, targetMode) { const cur = await env.readBinaryFile(absPath); const curBytes = cur.ok ? cur.value : undefined; if (curBytes === undefined && targetBytes === undefined) return undefined; if (curBytes !== undefined && targetHash !== null && createHash("sha256").update(curBytes).digest("hex") === targetHash) { if (targetMode === undefined) return undefined; const info = await env.fileInfo(absPath); const currentMode = info.ok ? info.value.mode : undefined; const replayMode = (targetMode & 0o6000) !== 0 ? targetMode & 0o1777 : targetMode; return modeProvenDifferent(targetMode, currentMode) && modeProvenDifferent(replayMode, currentMode) ? { insertions: 0, deletions: 0 } : undefined; } return countRestoreLineDiff(curBytes, targetBytes); } async function applyRecordedMode(env, absPath, opts, currentMode) { const want = opts.mode; if (want === undefined) return "unchanged"; if (currentMode === (want & 0o7777)) return "unchanged"; if ((want & 0o6000) !== 0) { const stripped = want & 0o1777; if (currentMode === stripped) return "unpreserved"; if (env.setFileMode === undefined) return "unpreserved"; const r = await env.setFileMode(absPath, stripped, opts.signal); return r.ok ? "applied_unpreserved" : "unpreserved"; } if (env.setFileMode === undefined) return "unpreserved"; const r = await env.setFileMode(absPath, want & 0o7777, opts.signal); return r.ok ? "applied" : "unpreserved"; } export class InMemoryFileHistoryStore { scopes = new Map(); boundaryKeep; blobs = new Map(); inFlightBlobs = new Map(); constructor(opts) { this.boundaryKeep = resolveFileHistoryRetention("InMemoryFileHistoryStore", opts?.retention); } applyBoundaryRetention(scope, justCommitted) { if (this.boundaryKeep === null) return; try { const sc = this.scopes.get(scope); if (sc === undefined) return; const keepIds = fileHistoryBoundariesToKeep([...sc.boundaries.keys()], justCommitted, this.boundaryKeep); if (keepIds === null) return; this.reap(scope, keepIds).catch(() => { }); } catch { } } leaseBlob(hash) { this.inFlightBlobs.set(hash, (this.inFlightBlobs.get(hash) ?? 0) + 1); } releaseBlob(hash) { const n = this.inFlightBlobs.get(hash); if (n === undefined) return; if (n <= 1) this.inFlightBlobs.delete(hash); else this.inFlightBlobs.set(hash, n - 1); } scope(scope) { let sc = this.scopes.get(scope); if (sc === undefined) { sc = { tracked: new Map(), boundaries: new Map(), spentAttempts: new Set(), activeAttempts: new Set(), reapedEntries: new Set() }; this.scopes.set(scope, sc); } return sc; } persistFirstTouch(scope, key, state) { this.scope(scope).tracked.set(key, state); } publishBoundary(scope, entryId, staged) { const sc = this.scope(scope); for (const [key, cap] of staged) { if (cap.kind !== "unchanged") continue; const still = sc.tracked.get(key)?.versions.some((v) => v.version === cap.version) ?? false; if (!still) throw new Error(`staged reference ${key}@v${cap.version} no longer exists (reaped mid-capture)`); } const mapping = new Map(); const now = new Date().toISOString(); for (const [key, cap] of staged) { if (cap.kind === "unchanged") { mapping.set(key, cap.version); continue; } const t = sc.tracked.get(key); if (t === undefined) continue; const last = t.versions[t.versions.length - 1]; const version = last !== undefined ? last.version + 1 : t.baseAvailable ? 1 : 2; t.versions.push({ version, blobHash: cap.blobHash, backupTime: now, ...(cap.mode !== undefined ? { mode: cap.mode } : {}), ...(cap.parentIdent !== undefined ? { parentIdent: cap.parentIdent } : {}) }); mapping.set(key, version); } sc.boundaries.set(entryId, mapping); } async trackEdit(scope, absPath, env, root, signal) { try { const { key, ...keyDesc } = describeTrackKey(root, absPath); const sc = this.scope(scope); const already = sc.tracked.get(key); if (already !== undefined) { already.observedByLaterTouch = true; return { ok: true, minted: false }; } const cap = await captureFileBackupViaEnv(env, absPath, root, signal); const raced = sc.tracked.get(key); if (raced !== undefined) { raced.observedByLaterTouch = true; return { ok: true, minted: false }; } if (cap.state === "aborted") { return { ok: false, error: { code: "aborted", message: `first-touch backup of ${absPath} aborted` } }; } if (cap.state === "unreadable") { this.persistFirstTouch(scope, key, { baseAvailable: false, keyDesc, versions: [] }); return { ok: true, minted: true }; } if (cap.state === "file") { this.leaseBlob(cap.hash); try { if (!this.blobs.has(cap.hash)) this.blobs.set(cap.hash, cap.bytes); this.persistFirstTouch(scope, key, { baseAvailable: true, keyDesc, versions: [{ version: 1, blobHash: cap.hash, backupTime: new Date().toISOString(), ...(cap.mode !== undefined ? { mode: cap.mode } : {}), ...(cap.parentIdent !== undefined ? { parentIdent: cap.parentIdent } : {}) }], }); } finally { this.releaseBlob(cap.hash); } return { ok: true, minted: true }; } this.persistFirstTouch(scope, key, { baseAvailable: true, keyDesc, versions: [{ version: 1, blobHash: null, backupTime: new Date().toISOString(), ...(cap.parentIdent !== undefined ? { parentIdent: cap.parentIdent } : {}) }], }); return { ok: true, minted: true }; } catch (err) { return { ok: false, error: { code: "track_failed", message: `first-touch record for ${absPath} could not be persisted: ${thrownText(err)}` } }; } } async annulTrack(scope, absPath, root, verify) { try { const key = trackKeyOf(root, absPath); const sc = this.scopes.get(scope); const t = sc?.tracked.get(key); if (sc === undefined || t === undefined) return { ok: true }; const ineligible = () => { const live = sc.tracked.get(key); if (live === undefined) return undefined; if (live !== t) return `first-touch record for ${absPath} was replaced while this retraction was being proven`; for (const mapping of sc.boundaries.values()) { if (mapping.has(key)) return `first-touch record for ${absPath} is referenced by a committed boundary — it is history now, not a retractable mint`; } if (live.versions.length > 1) return `first-touch record for ${absPath} has grown past its first version — only the mint itself is retractable`; if (live.observedByLaterTouch === true) { return `first-touch record for ${absPath} has since been served to another edit of the same path — it is that edit's baseline now, not a retractable mint`; } return undefined; }; const before = ineligible(); if (before !== undefined) return { ok: false, error: { code: "conflict", message: before } }; const proof = verify === undefined ? undefined : await worldStillMatchesMint(verify.env, absPath, root, t.baseAvailable, t.versions[0]?.blobHash ?? null, verify.signal); if (proof !== undefined && !proof.unchanged) { return { ok: false, error: { code: "conflict", message: `first-touch record for ${absPath} is NOT retractable: ${proof.reason} — the write this retraction compensates for may have changed the file, and deleting the only baseline of a modified file is worse than keeping a stale record` } }; } const after = ineligible(); if (after !== undefined) return { ok: false, error: { code: "conflict", message: after } }; sc.tracked.delete(key); return { ok: true }; } catch (err) { return { ok: false, error: { code: "track_failed", message: `annulling the first-touch record for ${absPath} failed: ${thrownText(err)}` } }; } } async snapshot(scope, entryId, env, root, signal) { const sc = this.scope(scope); if (sc.boundaries.has(entryId)) return { ok: true }; if (sc.reapedEntries.has(entryId)) { return { ok: false, error: { code: "snapshot_failed", message: `entry "${entryId}" already had a boundary and it was reaped by retention — a fresh capture now would record post-turn bytes under a pre-turn key, so this entry has no boundary and cannot get one back` } }; } if (sc.spentAttempts.has(entryId)) { return { ok: false, error: { code: "snapshot_failed", message: `capture lease for entry "${entryId}" is closed (a prior attempt failed); no complete pre-close staging exists, so this turn has no boundary` } }; } if (sc.activeAttempts.has(entryId)) { return { ok: false, error: { code: "snapshot_failed", message: `a capture attempt for entry "${entryId}" is already in flight — concurrent same-key capture is refused (the in-flight attempt owns the lease)` } }; } const stagedBlobHashes = new Set(); sc.activeAttempts.add(entryId); try { const staged = new Map(); for (const [key, t] of [...sc.tracked]) { if (signal?.aborted) { sc.spentAttempts.add(entryId); return { ok: false, error: { code: "aborted", message: `boundary capture for entry "${entryId}" aborted` } }; } const resolved = resolveTrackKey(root, key, t.keyDesc); if (!resolved.ok) { sc.spentAttempts.add(entryId); return { ok: false, error: { code: "snapshot_failed", message: resolved.reason } }; } const cap = await captureFileBackupViaEnv(env, resolved.abs, root, signal); if (cap.state === "aborted") { sc.spentAttempts.add(entryId); return { ok: false, error: { code: "aborted", message: `boundary capture for entry "${entryId}" aborted` } }; } if (cap.state === "unreadable") { sc.spentAttempts.add(entryId); return { ok: false, error: { code: "snapshot_failed", message: cap.cause } }; } const last = t.versions[t.versions.length - 1]; if (cap.state === "absent") { if (last !== undefined && last.blobHash === null) staged.set(key, { kind: "unchanged", version: last.version }); else staged.set(key, { kind: "changed", blobHash: null, ...(cap.parentIdent !== undefined ? { parentIdent: cap.parentIdent } : {}) }); continue; } if (last !== undefined && last.blobHash === cap.hash && !modeProvenDifferent(last.mode, cap.mode)) { staged.set(key, { kind: "unchanged", version: last.version }); continue; } if (!stagedBlobHashes.has(cap.hash)) { stagedBlobHashes.add(cap.hash); this.leaseBlob(cap.hash); } if (!this.blobs.has(cap.hash)) this.blobs.set(cap.hash, cap.bytes); staged.set(key, { kind: "changed", blobHash: cap.hash, ...(cap.mode !== undefined ? { mode: cap.mode } : {}), ...(cap.parentIdent !== undefined ? { parentIdent: cap.parentIdent } : {}) }); } const verdict = boundaryPublishVerdict({ aborted: signal?.aborted === true }); if (!verdict.publish) { sc.spentAttempts.add(entryId); return { ok: false, error: { code: "aborted", message: `boundary capture for entry "${entryId}" aborted (fenced — never committed)` } }; } try { this.publishBoundary(scope, entryId, staged); } catch (err) { sc.spentAttempts.add(entryId); return { ok: false, error: { code: "snapshot_failed", message: `boundary for entry "${entryId}" could not be committed: ${thrownText(err)}` } }; } this.applyBoundaryRetention(scope, entryId); return { ok: true }; } catch (err) { sc.spentAttempts.add(entryId); return { ok: false, error: { code: "snapshot_failed", message: thrownText(err) } }; } finally { sc.activeAttempts.delete(entryId); for (const h of stagedBlobHashes) this.releaseBlob(h); } } async restore(scope, entryId, env, root, signal) { const empty = { filesChanged: [], identicalSkipped: [], refused: [], failed: [] }; try { const sc = this.scopes.get(scope); const boundary = sc?.boundaries.get(entryId); if (sc === undefined || boundary === undefined) { return { outcome: "failed", ...empty, error: { code: "not_found", message: `no history boundary for entry "${entryId}"` } }; } const ledger = empty; let modeNotPreserved = 0; for (const key of [...sc.tracked.keys()].sort()) { const t = sc.tracked.get(key); if (signal?.aborted) { ledger.failed.push({ path: key, reason: "aborted before attempt" }); continue; } let targetVersion = boundary.get(key); if (targetVersion === undefined) { if (!t.baseAvailable) { ledger.refused.push({ path: key, reason: "base_unavailable" }); continue; } targetVersion = 1; } const rec = t.versions.find((v) => v.version === targetVersion); if (rec === undefined) { ledger.failed.push({ path: key, reason: `version ${targetVersion} record missing (store integrity)` }); continue; } const resolved = resolveTrackKey(root, key, t.keyDesc); if (!resolved.ok) { ledger.refused.push({ path: key, reason: resolved.reason }); continue; } let target; if (rec.blobHash === null) target = { absent: true }; else { const bytes = this.blobs.get(rec.blobHash); if (bytes === undefined) { ledger.failed.push({ path: key, reason: `blob ${rec.blobHash} missing (store integrity)` }); continue; } target = { bytes, hash: rec.blobHash }; } let applied; try { applied = await applyFileRestoreViaEnv(env, resolved.abs, target, { root, ...(rec.mode !== undefined ? { mode: rec.mode } : {}), ...(rec.parentIdent !== undefined ? { parentIdent: rec.parentIdent } : {}), ...(signal !== undefined ? { signal } : {}), }); } catch (err) { applied = { outcome: "failed", reason: thrownText(err) }; } if (applied.outcome === "applied") ledger.filesChanged.push(key); else if (applied.outcome === "identical") ledger.identicalSkipped.push(key); else if (applied.outcome === "refused") ledger.refused.push({ path: key, reason: applied.reason }); else ledger.failed.push({ path: key, reason: applied.reason }); if ((applied.outcome === "applied" || applied.outcome === "identical") && applied.modeNotPreserved === true) modeNotPreserved++; } return { outcome: ledger.refused.length > 0 || ledger.failed.length > 0 ? "partial" : "complete", ...ledger, ...(modeNotPreserved > 0 ? { modeNotPreserved } : {}) }; } catch (err) { const touched = empty.filesChanged.length > 0 || empty.identicalSkipped.length > 0 || empty.refused.length > 0 || empty.failed.length > 0; return { outcome: touched ? "partial" : "failed", ...empty, error: { code: "restore_failed", message: thrownText(err) } }; } } async canRestore(scope, entryId) { return this.scopes.get(scope)?.boundaries.has(entryId) ?? false; } async diffStats(scope, entryId, env, root) { try { const sc = this.scopes.get(scope); const boundary = sc?.boundaries.get(entryId); if (sc === undefined || boundary === undefined) return null; const filesChanged = []; let insertions = 0; let deletions = 0; for (const key of [...sc.tracked.keys()].sort()) { const t = sc.tracked.get(key); let targetVersion = boundary.get(key); if (targetVersion === undefined) { if (!t.baseAvailable) continue; targetVersion = 1; } const rec = t.versions.find((v) => v.version === targetVersion); const resolved = resolveTrackKey(root, key, t.keyDesc); if (rec === undefined || !resolved.ok) continue; const targetBytes = rec.blobHash === null ? undefined : this.blobs.get(rec.blobHash); if (rec.blobHash !== null && targetBytes === undefined) continue; const delta = await previewFileDelta(env, resolved.abs, rec.blobHash, targetBytes, rec.mode); if (delta === undefined) continue; filesChanged.push(key); insertions += delta.insertions; deletions += delta.deletions; } return { filesChanged, insertions, deletions }; } catch { return null; } } async reap(scope, keepEntryIds) { const sc = this.scopes.get(scope); if (sc === undefined) return 0; const keep = new Set(keepEntryIds); let removed = 0; for (const id of [...sc.boundaries.keys()]) { if (!keep.has(id)) { sc.boundaries.delete(id); sc.reapedEntries.add(id); removed++; } } const referenced = new Map(); for (const mapping of sc.boundaries.values()) { for (const [key, v] of mapping) { let s = referenced.get(key); if (s === undefined) referenced.set(key, (s = new Set())); s.add(v); } } for (const [key, t] of sc.tracked) { const refs = referenced.get(key); t.versions = t.versions.filter((v) => v.version === 1 || (refs?.has(v.version) ?? false)); } const live = new Set(this.inFlightBlobs.keys()); for (const s of this.scopes.values()) { for (const t of s.tracked.values()) for (const v of t.versions) if (v.blobHash !== null) live.add(v.blobHash); } for (const h of [...this.blobs.keys()]) if (!live.has(h)) this.blobs.delete(h); return removed; } async adoptScope(fromScope, toScope) { try { if (fromScope === toScope) return { ok: true }; const from = this.scopes.get(fromScope); const to = this.scopes.get(toScope); if (to !== undefined && (to.tracked.size > 0 || to.boundaries.size > 0)) { return { ok: false, error: { code: "conflict", message: `adoptScope target "${toScope}" already has history of its own — adoption is legal only onto an EMPTY scope (the fork/resume boundary before the new session's first turn)` } }; } if (from === undefined || (from.tracked.size === 0 && from.boundaries.size === 0)) { if (from !== undefined && from.reapedEntries.size > 0) { const target = this.scope(toScope); for (const id of from.reapedEntries) target.reapedEntries.add(id); } return { ok: true }; } const copy = { tracked: new Map(), boundaries: new Map(), spentAttempts: new Set(), activeAttempts: new Set(), reapedEntries: new Set([...from.reapedEntries, ...(to?.reapedEntries ?? [])]) }; for (const [key, t] of from.tracked) copy.tracked.set(key, { baseAvailable: t.baseAvailable, keyDesc: { ...t.keyDesc }, ...(t.observedByLaterTouch === true ? { observedByLaterTouch: true } : {}), versions: t.versions.map((v) => ({ ...v })) }); for (const [id, mapping] of from.boundaries) copy.boundaries.set(id, new Map(mapping)); this.scopes.set(toScope, copy); return { ok: true }; } catch (err) { return { ok: false, error: { code: "adopt_failed", message: `adoptScope failed: ${thrownText(err)}` } }; } } async exportHistory(scope) { const sc = this.scopes.get(scope); if (sc === undefined || (sc.tracked.size === 0 && sc.boundaries.size === 0)) return null; return this.exportOf(sc); } exportOf(sc) { return { formatVersion: "file-history/1", tracked: [...sc.tracked.entries()] .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .map(([key, t]) => ({ relOrAbsPath: key, keyKind: t.keyDesc.kind, keyFamily: t.keyDesc.family, baseAvailable: t.baseAvailable, versions: t.versions.map((v) => ({ version: v.version, blobHash: v.blobHash, ...(v.mode !== undefined ? { mode: v.mode } : {}), ...(v.parentIdent !== undefined ? { parentIdent: v.parentIdent } : {}), backupTime: v.backupTime })), })), boundaries: [...sc.boundaries.entries()] .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .map(([entryId, mapping]) => ({ entryId, files: [...mapping.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([relOrAbsPath, version]) => ({ relOrAbsPath, version })) })), }; } async importHistory(scope, data, srcGetBlob) { try { const shaped = validateFileHistoryExport(data); if (!shaped.ok) return shaped; const existing = await this.exportHistory(scope); if (existing !== null) { if (fileHistoryExportDigest(existing) === fileHistoryExportDigest(data)) return { ok: true }; return { ok: false, error: { code: "conflict", message: `importHistory target "${scope}" already holds a DIFFERENT history — a first import requires an empty target and a retry is legal only for the identical envelope (atomic visibility prevents half-graphs; this law prevents publishing the wrong complete graph over locally-grown history)` } }; } const needed = new Set(); for (const t of data.tracked) for (const v of t.versions) if (v.blobHash !== null) needed.add(v.blobHash); for (const hash of needed) this.leaseBlob(hash); try { return await this.importStaged(scope, data, srcGetBlob, needed); } finally { for (const hash of needed) this.releaseBlob(hash); } } catch (err) { return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${thrownText(err)}` } }; } } async importStaged(scope, data, srcGetBlob, needed) { try { const staged = new Map(); for (const hash of needed) { if (this.blobs.has(hash)) continue; let bytes; try { bytes = await srcGetBlob(hash); } catch (err) { return { ok: false, error: { code: "unsupported_format", message: `source blob ${hash} fetch failed: ${thrownText(err)}` } }; } if (bytes === undefined) return { ok: false, error: { code: "not_found", message: `source blob ${hash} is missing — the envelope's graph cannot be completed, so nothing is published` } }; if (createHash("sha256").update(bytes).digest("hex") !== hash) { return { ok: false, error: { code: "unsupported_format", message: `source blob ${hash} fails content-address integrity — refused, nothing published` } }; } staged.set(hash, bytes); } const live = this.scopes.get(scope); if (live !== undefined && (live.tracked.size > 0 || live.boundaries.size > 0)) { const now = this.exportOf(live); if (fileHistoryExportDigest(now) === fileHistoryExportDigest(data)) return { ok: true }; return { ok: false, error: { code: "conflict", message: `importHistory target "${scope}" gained a DIFFERENT history while this import was fetching blobs — nothing was published` } }; } for (const [hash, bytes] of staged) this.blobs.set(hash, bytes); const built = { tracked: new Map(), boundaries: new Map(), spentAttempts: new Set(), activeAttempts: new Set(), reapedEntries: new Set(live?.reapedEntries ?? []) }; for (const t of data.tracked) { built.tracked.set(t.relOrAbsPath, { baseAvailable: t.baseAvailable, keyDesc: { kind: t.keyKind, family: t.keyFamily }, versions: t.versions.map((v) => ({ version: v.version, blobHash: v.blobHash, ...(v.mode !== undefined ? { mode: v.mode } : {}), ...(v.parentIdent !== undefined ? { parentIdent: v.parentIdent } : {}), backupTime: v.backupTime })), }); } for (const b of data.boundaries) built.boundaries.set(b.entryId, new Map(b.files.map((f) => [f.relOrAbsPath, f.version]))); this.scopes.set(scope, built); return { ok: true }; } catch (err) { return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${thrownText(err)}` } }; } } } export const __testables = { joinUnder, relUnder, stripTrailSep, trackKeyOf, resolveTrackKey, parentDirOf };