@sema-agent/core 5.38.0 → 5.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +182 -10
  2. package/dist/agents/send-message-tool.d.ts +8 -0
  3. package/dist/agents/send-message-tool.js +8 -0
  4. package/dist/agents/teacher.js +9 -3
  5. package/dist/agents/verify.js +9 -3
  6. package/dist/core/checkpoint-store.d.ts +12 -0
  7. package/dist/core/governance-codes.js +2 -0
  8. package/dist/core/hooks.d.ts +23 -0
  9. package/dist/core/hooks.js +53 -4
  10. package/dist/core/mailbox-store.d.ts +39 -0
  11. package/dist/core/mailbox-store.js +9 -0
  12. package/dist/core/memory-engine/engine.d.ts +27 -0
  13. package/dist/core/memory-engine/engine.js +103 -1
  14. package/dist/core/memory-engine/export-bundle.d.ts +192 -0
  15. package/dist/core/memory-engine/export-bundle.js +306 -0
  16. package/dist/core/memory-engine/file-backend.d.ts +178 -1
  17. package/dist/core/memory-engine/file-backend.js +637 -6
  18. package/dist/core/memory-engine/index.d.ts +2 -1
  19. package/dist/core/memory-engine/index.js +1 -0
  20. package/dist/core/memory-engine/layout.d.ts +89 -1
  21. package/dist/core/memory-engine/layout.js +131 -1
  22. package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
  23. package/dist/core/memory-engine/memory-backend-contract.js +52 -0
  24. package/dist/core/memory-engine/tools.js +8 -1
  25. package/dist/core/permission-rule-consent.d.ts +27 -4
  26. package/dist/core/permission-rule-consent.js +41 -4
  27. package/dist/core/permission-rule-model.d.ts +7 -1
  28. package/dist/core/runner/prepare-task.js +24 -3
  29. package/dist/core/runner/runtask.js +5 -0
  30. package/dist/core/runner/synthetic-tools.js +3 -1
  31. package/dist/core/runner/tool-disclosure.js +2 -1
  32. package/dist/core/sensitive-path-policy.js +3 -3
  33. package/dist/core/store-contracts/mailbox-store-contract.d.ts +29 -1
  34. package/dist/core/store-contracts/mailbox-store-contract.js +78 -0
  35. package/dist/core/types.d.ts +21 -0
  36. package/dist/core/write-protect.d.ts +73 -0
  37. package/dist/core/write-protect.js +195 -0
  38. package/dist/index.d.ts +4 -3
  39. package/dist/index.js +4 -3
  40. package/dist/orchestration/governance-baseline-validity.d.ts +44 -0
  41. package/dist/orchestration/governance-baseline-validity.js +55 -0
  42. package/dist/orchestration/run-workflow-tool.js +33 -8
  43. package/dist/orchestration/workflow-script-runner.js +9 -4
  44. package/dist/tools/fs/read-deny.d.ts +15 -5
  45. package/dist/tools/fs/read-deny.js +33 -12
  46. package/dist/tools/fs/safety.d.ts +5 -2
  47. package/dist/tools/fs/safety.js +5 -3
  48. package/dist/tools/fs/search.d.ts +33 -0
  49. package/dist/tools/fs/search.js +72 -0
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +21 -1
@@ -1,11 +1,12 @@
1
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, openSync, fsyncSync, closeSync } from "node:fs";
1
+ import { existsSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, openSync, fsyncSync, closeSync } from "node:fs";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { dirname, join, relative } from "node:path";
4
4
  import { jaccardDistance, termSet } from "../memory-vector.js";
5
5
  import { MAX_MEMORY_BYTES } from "../memory.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { computeEntryRev, entryFromFile, isValidEntryId, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
- import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, claimRootScope, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, isContainedIn, lineageContributionsOfSession, readLineageRecord, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, writeAllSync, } from "./layout.js";
8
+ import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, claimRootScope, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, stageImportLatch, writeAllSync, } from "./layout.js";
9
+ import { screenInboundEntries } from "./data-plane.js";
9
10
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
10
11
  export const MEMORY_INDEX_FILENAME = "MEMORY.md";
11
12
  export const DEFAULT_MAX_ENTRY_DEPTH = 3;
@@ -159,6 +160,7 @@ const TXN_LOCK_STALE_MS = 30_000;
159
160
  const TXN_LOCK_WAIT_MS = 15_000;
160
161
  const TXN_LOCK_STEAL_GRACE_MS = 250;
161
162
  const SHADOW_DIR = "shadow";
163
+ const STORE_IDENTITY_FILE = "store-identity.json";
162
164
  function rowsRev(rows, id) {
163
165
  return rows[id]?.rev;
164
166
  }
@@ -331,9 +333,18 @@ function transferEventInvalid(raw) {
331
333
  return "non-string/empty 'origin'";
332
334
  if (e.srcEv !== undefined && (typeof e.srcEv !== "string" || e.srcEv.length === 0))
333
335
  return "non-string/empty 'srcEv'";
336
+ const redactedKeys = new Set();
334
337
  if (e.redacted !== undefined) {
335
- if (e.redacted !== true)
336
- return "'redacted', when present, must be literal true";
338
+ const list = Array.isArray(e.redacted) ? e.redacted : undefined;
339
+ if (list === undefined || list.length === 0)
340
+ return "'redacted', when present, must be a non-empty array naming the removed keys";
341
+ for (const k of list) {
342
+ if (k !== "from" && k !== "to")
343
+ return "'redacted' may only name 'from'/'to'";
344
+ if (redactedKeys.has(k))
345
+ return "'redacted' names a key twice";
346
+ redactedKeys.add(k);
347
+ }
337
348
  if (e.origin === undefined)
338
349
  return "'redacted' requires the import pair (origin/srcEv) — a local row is never redacted";
339
350
  }
@@ -349,12 +360,25 @@ function transferEventInvalid(raw) {
349
360
  if (e.channel === "adopted-move" || e.channel === "applyPatches-move" || e.channel === "migration-bind") {
350
361
  if (typeof e.id !== "string" || e.id.length === 0)
351
362
  return "row transfer missing 'id'";
352
- if (bindingInvalid(e.to))
363
+ if (e.to !== undefined && redactedKeys.has("to"))
364
+ return "'to' is present but named in 'redacted'";
365
+ if (e.from !== undefined && redactedKeys.has("from"))
366
+ return "'from' is present but named in 'redacted'";
367
+ if (e.to === undefined) {
368
+ if (!redactedKeys.has("to"))
369
+ return "row transfer missing/malformed 'to'";
370
+ }
371
+ else if (bindingInvalid(e.to)) {
353
372
  return "row transfer missing/malformed 'to'";
373
+ }
354
374
  if (e.channel === "migration-bind") {
355
375
  if (e.from !== undefined && bindingInvalid(e.from))
356
376
  return "malformed 'from'";
357
377
  }
378
+ else if (e.from === undefined) {
379
+ if (!redactedKeys.has("from"))
380
+ return `'${e.channel}' requires a well-formed 'from'`;
381
+ }
358
382
  else if (bindingInvalid(e.from)) {
359
383
  return `'${e.channel}' requires a well-formed 'from'`;
360
384
  }
@@ -516,6 +540,7 @@ export class FileMemoryEngineBackend {
516
540
  unboundRowsKnown = false;
517
541
  adoptionNoticeKeys = new Set();
518
542
  transfersAppendFault;
543
+ exportFaceProbe;
519
544
  deletedIdsDigest;
520
545
  inboundFindings = [];
521
546
  batchScan;
@@ -2704,7 +2729,7 @@ export class FileMemoryEngineBackend {
2704
2729
  `(or the ledger kept advancing) across every try — the committed account cannot be snapshotted without tearing. ` +
2705
2730
  `A journal that never clears is a crashed transaction: the next backend construction or locked write recovers it.`);
2706
2731
  }
2707
- resolveCommittedContent(id, rev, binding, final) {
2732
+ resolveCommittedContent(id, rev, binding, final, lockHeld = false) {
2708
2733
  if (binding === undefined)
2709
2734
  return { state: "unavailable", reason: "carrier-missing" };
2710
2735
  if (!isValidEntryId(id))
@@ -2726,6 +2751,8 @@ export class FileMemoryEngineBackend {
2726
2751
  if (projEntry.rev === rev)
2727
2752
  return { state: "present", entry: projEntry };
2728
2753
  }
2754
+ if (lockHeld)
2755
+ return { state: "unavailable", reason: "carrier-missing" };
2729
2756
  if (!final)
2730
2757
  throw new AuditObservationUnstableError(`shadow bytes for ${id} do not carry the committed rev — possible adoption in flight`);
2731
2758
  if (this.txnInFlight()) {
@@ -2884,6 +2911,610 @@ export class FileMemoryEngineBackend {
2884
2911
  }
2885
2912
  return out;
2886
2913
  }
2914
+ static EXPORT_INCOMPLETE = "memory.export_incomplete";
2915
+ storeIdentityLocked() {
2916
+ const path = join(this.controlPlaneRoot, STORE_IDENTITY_FILE);
2917
+ const parse = (raw) => {
2918
+ let parsed;
2919
+ try {
2920
+ parsed = JSON.parse(raw);
2921
+ }
2922
+ catch {
2923
+ throw new ControlPlaneCorruptError(`store identity is unparseable: ${path}`);
2924
+ }
2925
+ const rec = parsed;
2926
+ if (!rec || typeof rec !== "object" || rec.v !== 1 || typeof rec.storeId !== "string" || rec.storeId.length === 0) {
2927
+ throw new ControlPlaneCorruptError(`store identity has the wrong shape: ${path}`);
2928
+ }
2929
+ return rec.storeId;
2930
+ };
2931
+ const raw = readControlFileOrAbsent(path, "store identity");
2932
+ if (raw !== undefined)
2933
+ return parse(raw);
2934
+ const storeId = randomUUID();
2935
+ let fd;
2936
+ try {
2937
+ fd = openSync(path, "wx", 0o600);
2938
+ }
2939
+ catch (err) {
2940
+ if (err.code === "EEXIST") {
2941
+ const again = readControlFileOrAbsent(path, "store identity");
2942
+ if (again === undefined)
2943
+ throw new ControlPlaneCorruptError(`store identity vanished between its EEXIST and the re-read: ${path}`);
2944
+ return parse(again);
2945
+ }
2946
+ throw err;
2947
+ }
2948
+ try {
2949
+ writeAllSync(fd, `${JSON.stringify({ v: 1, storeId }, null, 2)}\n`);
2950
+ fsyncSync(fd);
2951
+ }
2952
+ finally {
2953
+ closeSync(fd);
2954
+ }
2955
+ return storeId;
2956
+ }
2957
+ exportGovernanceFingerprint() {
2958
+ const h = createHash("sha256");
2959
+ const feed = (label, text) => {
2960
+ h.update(JSON.stringify([label, text]));
2961
+ };
2962
+ for (const file of [LINEAGE_FILE, CHALLENGES_FILE]) {
2963
+ feed(file, readControlFileOrAbsent(join(this.controlPlaneRoot, file), `${file} (export fence)`) ?? "<absent>");
2964
+ let journalRaw;
2965
+ try {
2966
+ journalRaw = readControlFileOrAbsent(join(this.controlPlaneRoot, `${file}.journal`), `${file} journal (export fence)`);
2967
+ }
2968
+ catch (err) {
2969
+ throw erasureCodedError(`memory export refused: the ${file} journal could not be read (${err instanceof Error ? err.message : String(err)}) — a journal read failure is not absence, and exporting over it could seal stale governance under an honest hash`, FileMemoryEngineBackend.EXPORT_INCOMPLETE);
2970
+ }
2971
+ feed(`${file}.journal`, journalRaw ?? "<absent>");
2972
+ }
2973
+ const dir = join(this.controlPlaneRoot, SESSION_POLLUTION_DIR);
2974
+ let names = [];
2975
+ try {
2976
+ names = readdirSync(dir).sort();
2977
+ }
2978
+ catch (err) {
2979
+ if (err.code !== "ENOENT") {
2980
+ throw new ControlPlaneCorruptError(`session pollution markers could not be enumerated for export (${err.code ?? "io error"}) at ${dir} — fail-closed`, { cause: err });
2981
+ }
2982
+ }
2983
+ for (const name of names) {
2984
+ let text;
2985
+ try {
2986
+ text = readFileSync(join(dir, name), "utf8");
2987
+ }
2988
+ catch (err) {
2989
+ text = `<unreadable:${err.code ?? "io error"}>`;
2990
+ }
2991
+ feed(`pollution/${name}`, text);
2992
+ }
2993
+ return h.digest("hex");
2994
+ }
2995
+ readExportChainRowsPure() {
2996
+ const path = join(this.controlPlaneRoot, TRANSFERS_FILE);
2997
+ const raw = readControlFileOrAbsent(path, "transfer evidence log");
2998
+ const out = [];
2999
+ if (raw === undefined)
3000
+ return out;
3001
+ const byEv = new Map();
3002
+ const lines = raw.split("\n");
3003
+ for (let i = 0; i < lines.length; i++) {
3004
+ const line = lines[i];
3005
+ if (line === undefined || line === "")
3006
+ continue;
3007
+ let parsed;
3008
+ try {
3009
+ parsed = JSON.parse(line);
3010
+ }
3011
+ catch {
3012
+ if (lines.slice(i + 1).every((l) => l === "")) {
3013
+ throw erasureCodedError(`memory export refused: the transfer evidence log has a torn tail (line ${i + 1}) — the export reads it purely (the file is untouched); run any locked mutation entry (a write, an adopting read) to heal the chain, then export again`, FileMemoryEngineBackend.EXPORT_INCOMPLETE);
3014
+ }
3015
+ throw new ControlPlaneCorruptError(`transfer evidence log line ${i + 1} is unparseable (not a torn tail — fail-closed): ${path}`);
3016
+ }
3017
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3018
+ throw new ControlPlaneCorruptError(`transfer evidence log line ${i + 1} is not an object (fail-closed): ${path}`);
3019
+ }
3020
+ const e = parsed;
3021
+ if (typeof e.ev !== "string" || e.ev.length === 0 || typeof e.channel !== "string" || e.channel.length === 0 || typeof e.at !== "number" || !Number.isInteger(e.at)) {
3022
+ throw new ControlPlaneCorruptError(`transfer evidence log line ${i + 1} is missing its base keys (ev/channel/at) — fail-closed: ${path}`);
3023
+ }
3024
+ if (!KNOWN_TRANSFER_CHANNELS.has(e.channel)) {
3025
+ throw erasureCodedError(`memory export refused: the evidence chain carries channel ${JSON.stringify(e.channel)} this exporter does not know (line ${i + 1}) — an unknown row can neither be tenant-sliced nor silently dropped; export with the binary that wrote it (or newer)`, FileMemoryEngineBackend.EXPORT_INCOMPLETE);
3026
+ }
3027
+ const invalid = transferEventInvalid(parsed);
3028
+ if (invalid !== undefined)
3029
+ throw new ControlPlaneCorruptError(`transfer evidence log line ${i + 1} is invalid (${invalid}): ${path}`);
3030
+ const canonical = canonicalJsonStringify(e);
3031
+ const prior = byEv.get(e.ev);
3032
+ if (prior !== undefined) {
3033
+ if (prior !== canonical) {
3034
+ throw erasureCodedError(`memory export refused: the evidence chain carries event id ${JSON.stringify(e.ev)} twice with DIFFERENT payloads (line ${i + 1}) — a spliced chain cannot be exported`, FileMemoryEngineBackend.EXPORT_INCOMPLETE);
3035
+ }
3036
+ continue;
3037
+ }
3038
+ byEv.set(e.ev, canonical);
3039
+ out.push(e);
3040
+ }
3041
+ return out;
3042
+ }
3043
+ sliceCustodyForScopes(rows, scopeSet, fullStore, storeId) {
3044
+ const custody = [];
3045
+ let unsliceable = 0;
3046
+ for (const e of rows) {
3047
+ const channel = e.channel;
3048
+ if (channel === "migration" || channel === "erasure-anchor")
3049
+ continue;
3050
+ if (channel === "erasure-request") {
3051
+ if (scopeSet.has(e.scope))
3052
+ custody.push(e);
3053
+ continue;
3054
+ }
3055
+ if (channel === "delete") {
3056
+ const from = e.from;
3057
+ if (from !== undefined && typeof from.scope === "string") {
3058
+ if (scopeSet.has(from.scope))
3059
+ custody.push(e);
3060
+ }
3061
+ else if (fullStore) {
3062
+ custody.push(e);
3063
+ }
3064
+ else {
3065
+ unsliceable++;
3066
+ }
3067
+ continue;
3068
+ }
3069
+ const from = e.from;
3070
+ const to = e.to;
3071
+ const fromIn = from !== undefined && typeof from.scope === "string" && scopeSet.has(from.scope);
3072
+ const toIn = to !== undefined && typeof to.scope === "string" && scopeSet.has(to.scope);
3073
+ if (!fromIn && !toIn)
3074
+ continue;
3075
+ if ((from === undefined || fromIn) && (to === undefined || toIn)) {
3076
+ custody.push(e);
3077
+ continue;
3078
+ }
3079
+ const redacted = { ...e };
3080
+ const named = new Set(Array.isArray(e.redacted) ? e.redacted : []);
3081
+ if (from !== undefined && !fromIn) {
3082
+ delete redacted.from;
3083
+ named.add("from");
3084
+ }
3085
+ if (to !== undefined && !toIn) {
3086
+ delete redacted.to;
3087
+ named.add("to");
3088
+ }
3089
+ redacted.redacted = [...named].sort();
3090
+ if (redacted.origin === undefined) {
3091
+ redacted.origin = storeId();
3092
+ redacted.srcEv = e.ev;
3093
+ }
3094
+ custody.push(redacted);
3095
+ }
3096
+ return { custody, unsliceable };
3097
+ }
3098
+ enumerateQuarantineFor(ids) {
3099
+ const qDir = join(this.controlPlaneRoot, QUARANTINE_DIR);
3100
+ let names = [];
3101
+ try {
3102
+ names = readdirSync(qDir);
3103
+ }
3104
+ catch (err) {
3105
+ if (err.code !== "ENOENT") {
3106
+ throw new ControlPlaneCorruptError(`export residual enumeration failed: the quarantine dir could not be read (${err.code ?? "io error"}) at ${qDir} — a bundle must not silently claim an empty residual set`);
3107
+ }
3108
+ }
3109
+ const quarantined = [];
3110
+ let quarantineOpaque = 0;
3111
+ for (const name of names.sort()) {
3112
+ const text = readSafe(join(qDir, name));
3113
+ if (text === undefined) {
3114
+ quarantineOpaque++;
3115
+ continue;
3116
+ }
3117
+ const qid = parseEntryFile(text).id;
3118
+ if (qid === undefined) {
3119
+ quarantineOpaque++;
3120
+ continue;
3121
+ }
3122
+ if (ids.has(qid))
3123
+ quarantined.push(name);
3124
+ }
3125
+ return { quarantined, quarantineOpaque };
3126
+ }
3127
+ async exportSnapshotOf(scopes, timings) {
3128
+ const scopeSet = new Set(scopes);
3129
+ const lock = await this.acquireTxnLock(timings);
3130
+ try {
3131
+ this.assertTxnLockOwnership(lock.token);
3132
+ this.ledger = undefined;
3133
+ this.recoverJournal();
3134
+ const rows = this.loadLedger();
3135
+ if (this.persistedSchemaVersion !== "v2") {
3136
+ throw erasureCodedError("memory export refused: the committed-rev ledger is still schema v1 (pre-binding) — the store migrates itself at its next write/read entry or checkControlPlane; an export never triggers the migration", FileMemoryEngineBackend.EXPORT_INCOMPLETE);
3137
+ }
3138
+ if (this.chainDegradedMarkerPresent()) {
3139
+ throw erasureCodedError("memory export refused: the custody chain carries the durable degradation marker (transfers.chain-degraded.json) — evidence rows were lost to an out-of-band tear and a bundle cannot testify to a complete deletion history; resolve the marker deliberately before exporting", FileMemoryEngineBackend.EXPORT_INCOMPLETE);
3140
+ }
3141
+ const storeId = this.storeIdentityLocked();
3142
+ const registered = registeredScopes(this.controlPlaneRoot);
3143
+ const fullStore = Object.keys(registered).every((s) => scopeSet.has(s));
3144
+ for (let round = 0; round < 3; round++) {
3145
+ const before = this.exportGovernanceFingerprint();
3146
+ const lineageRec = readLineageRecord(this.controlPlaneRoot);
3147
+ const challenges = readChallengeEvents(this.controlPlaneRoot);
3148
+ const polluted = listSessionPollution(this.controlPlaneRoot);
3149
+ const chainRows = this.readExportChainRowsPure();
3150
+ this.exportFaceProbe?.(round);
3151
+ const after = this.exportGovernanceFingerprint();
3152
+ if (before !== after)
3153
+ continue;
3154
+ const latched = new Set();
3155
+ for (const txn of Object.values(lineageRec.pending))
3156
+ for (const row of txn.rows)
3157
+ latched.add(row.entryId);
3158
+ const entries = [];
3159
+ const contentUnavailable = [];
3160
+ const boundIdsInScopes = new Set();
3161
+ for (const [id, row] of rowsEntries(rows)) {
3162
+ const binding = rowsBinding(rows, id);
3163
+ if (binding === undefined || !scopeSet.has(binding.scope))
3164
+ continue;
3165
+ boundIdsInScopes.add(id);
3166
+ if (latched.has(id))
3167
+ continue;
3168
+ const content = this.resolveCommittedContent(id, row.rev, binding, true, true);
3169
+ if (content.state === "present")
3170
+ entries.push(content.entry);
3171
+ else
3172
+ contentUnavailable.push({ id, scope: binding.scope, slug: binding.slug, rev: row.rev, reason: content.reason });
3173
+ }
3174
+ const sliced = this.sliceCustodyForScopes(chainRows, scopeSet, fullStore, () => storeId);
3175
+ const residual = this.enumerateQuarantineFor(boundIdsInScopes);
3176
+ const lineage = [];
3177
+ for (const [entryId, sessions] of Object.entries(lineageRec.committed)) {
3178
+ for (const [sessionId, c] of Object.entries(sessions))
3179
+ lineage.push({ entryId, sessionId, lastRev: c.lastRev, lastAt: c.lastAt });
3180
+ }
3181
+ const pollutedSessions = Object.entries(polluted).map(([sessionId, r]) => ({ sessionId, at: r.at, reason: r.reason }));
3182
+ try {
3183
+ this.assertTxnLockOwnership(lock.token);
3184
+ }
3185
+ catch (err) {
3186
+ throw erasureCodedError(`memory export refused: the transaction lock was lost during assembly (a stalled hold past the stale line was stolen from) — the read set can no longer be proven consistent; retry the export (${err instanceof Error ? err.message : String(err)})`, FileMemoryEngineBackend.EXPORT_INCOMPLETE);
3187
+ }
3188
+ return {
3189
+ storeId,
3190
+ fullStore,
3191
+ entries,
3192
+ contentUnavailable,
3193
+ unbound: unboundRowIds(rows).sort(),
3194
+ pendingLatch: [...latched].sort(),
3195
+ custody: sliced.custody,
3196
+ unsliceableCustody: sliced.unsliceable,
3197
+ challenges,
3198
+ lineage,
3199
+ pollutedSessions,
3200
+ quarantined: residual.quarantined,
3201
+ quarantineOpaque: residual.quarantineOpaque,
3202
+ };
3203
+ }
3204
+ throw erasureCodedError("memory export refused: the governance faces (challenge/lineage/pollution) kept changing across three consistency rounds — the store is too hot to snapshot without tearing; retry when its writers settle", FileMemoryEngineBackend.EXPORT_INCOMPLETE);
3205
+ }
3206
+ finally {
3207
+ lock.release();
3208
+ }
3209
+ }
3210
+ async governanceExport(scopes, timings) {
3211
+ const scopeSet = new Set(scopes);
3212
+ const lock = await this.acquireTxnLock(timings);
3213
+ try {
3214
+ this.assertTxnLockOwnership(lock.token);
3215
+ this.ledger = undefined;
3216
+ this.recoverJournal();
3217
+ const rows = this.loadLedger();
3218
+ if (this.persistedSchemaVersion !== "v2")
3219
+ return { custody: [], unbound: [], complete: false, unsliceableCustody: 0, reason: "the committed-rev ledger is still schema v1 (migrate first)" };
3220
+ if (this.chainDegradedMarkerPresent()) {
3221
+ return { custody: [], unbound: [], complete: false, unsliceableCustody: 0, reason: "the custody chain carries the durable degradation marker (transfers.chain-degraded.json) — evidence rows were lost to an out-of-band tear" };
3222
+ }
3223
+ const registered = registeredScopes(this.controlPlaneRoot);
3224
+ const fullStore = Object.keys(registered).every((s) => scopeSet.has(s));
3225
+ try {
3226
+ const chainRows = this.readExportChainRowsPure();
3227
+ const sliced = this.sliceCustodyForScopes(chainRows, scopeSet, fullStore, () => this.storeIdentityLocked());
3228
+ this.assertTxnLockOwnership(lock.token);
3229
+ return { custody: sliced.custody, unbound: unboundRowIds(rows).sort(), complete: true, unsliceableCustody: sliced.unsliceable };
3230
+ }
3231
+ catch (err) {
3232
+ if (err.code === FileMemoryEngineBackend.EXPORT_INCOMPLETE || err instanceof ControlPlaneCorruptError) {
3233
+ if (err instanceof ControlPlaneCorruptError && !err.message.includes("ownership"))
3234
+ throw err;
3235
+ return { custody: [], unbound: [], complete: false, unsliceableCustody: 0, reason: err instanceof Error ? err.message : String(err) };
3236
+ }
3237
+ throw err;
3238
+ }
3239
+ }
3240
+ finally {
3241
+ lock.release();
3242
+ }
3243
+ }
3244
+ async custodyImport(rows, opts, timings) {
3245
+ if (typeof opts?.bundleHash !== "string" || opts.bundleHash.length === 0 || typeof opts.sourceStoreId !== "string" || opts.sourceStoreId.length === 0) {
3246
+ throw erasureCodedError("custodyImport: bundleHash and sourceStoreId are required (the remint namespace and the origin stamp)", "config.memory_import_request");
3247
+ }
3248
+ const lock = await this.acquireTxnLock(timings);
3249
+ try {
3250
+ this.assertTxnLockOwnership(lock.token);
3251
+ this.recoverJournal();
3252
+ return this.custodyImportLocked(rows, opts);
3253
+ }
3254
+ finally {
3255
+ lock.release();
3256
+ }
3257
+ }
3258
+ custodyImportLocked(rows, opts) {
3259
+ const withheld = [];
3260
+ const toAppend = [];
3261
+ for (const row of rows) {
3262
+ const channel = typeof row.channel === "string" ? row.channel : "<malformed>";
3263
+ const srcEv = typeof row.srcEv === "string" ? row.srcEv : typeof row.ev === "string" ? row.ev : "<malformed>";
3264
+ if (typeof row.ev !== "string" || row.ev.length === 0 || typeof row.channel !== "string" || row.channel.length === 0 || typeof row.at !== "number" || !Number.isInteger(row.at)) {
3265
+ withheld.push({ srcEv, channel });
3266
+ continue;
3267
+ }
3268
+ if (!KNOWN_TRANSFER_CHANNELS.has(row.channel)) {
3269
+ withheld.push({ srcEv, channel });
3270
+ continue;
3271
+ }
3272
+ if (transferEventInvalid(row) !== undefined) {
3273
+ withheld.push({ srcEv, channel });
3274
+ continue;
3275
+ }
3276
+ const reminted = {
3277
+ ...row,
3278
+ ev: `imp:${opts.bundleHash}:${row.ev}`,
3279
+ origin: row.origin ?? opts.sourceStoreId,
3280
+ srcEv: row.srcEv ?? row.ev,
3281
+ };
3282
+ if (transferEventInvalid(reminted) !== undefined) {
3283
+ withheld.push({ srcEv, channel });
3284
+ continue;
3285
+ }
3286
+ toAppend.push(reminted);
3287
+ }
3288
+ this.appendTransfers(toAppend);
3289
+ return { appended: toAppend.length, withheld };
3290
+ }
3291
+ appendImportChallengesVerified(events) {
3292
+ if (events.length === 0)
3293
+ return;
3294
+ const { assignments } = appendChallengeEvents(this.controlPlaneRoot, events, this.now);
3295
+ const replayed = assignments.filter((a) => a.replayed);
3296
+ if (replayed.length === 0)
3297
+ return;
3298
+ const byEventId = new Map();
3299
+ for (const e of readChallengeEvents(this.controlPlaneRoot))
3300
+ if (!byEventId.has(e.eventId))
3301
+ byEventId.set(e.eventId, e);
3302
+ for (const a of replayed) {
3303
+ const want = events.find((e) => e.eventId === a.eventId);
3304
+ const got = byEventId.get(a.eventId);
3305
+ if (want === undefined || got === undefined || got.kind !== "challenge" || got.entryId !== want.entryId || got.reason !== want.reason || got.challengedRev !== want.challengedRev) {
3306
+ throw new ControlPlaneCorruptError(`bundle import aborted: challenge event ${JSON.stringify(a.eventId)} replayed onto an existing ledger event with a DIFFERENT payload (kind/entryId/reason/challengedRev) — key idempotency would silently swallow the imported challenge; the import latch stays until a clean re-import converges`);
3307
+ }
3308
+ }
3309
+ }
3310
+ async importBundleCommit(plan, timings) {
3311
+ const bundleHash = typeof plan?.bundleHash === "string" ? plan.bundleHash : "";
3312
+ const sourceStoreId = typeof plan?.sourceStoreId === "string" ? plan.sourceStoreId : "";
3313
+ if (!/^[0-9a-f]{64}$/.test(bundleHash) || sourceStoreId.length === 0) {
3314
+ throw erasureCodedError("importBundleCommit: plan.bundleHash must be the 64-hex bundle digest and sourceStoreId a non-empty string", "config.memory_import_request");
3315
+ }
3316
+ const lock = await this.acquireTxnLock(timings);
3317
+ try {
3318
+ this.assertTxnLockOwnership(lock.token);
3319
+ this.ledger = undefined;
3320
+ this.recoverJournal();
3321
+ this.migrateLockedIfNeeded(lock.token);
3322
+ const ctl = this.controlPlaneRoot;
3323
+ const txnId = importLatchTxnId(bundleHash);
3324
+ const receiptPath = join(ctl, IMPORT_RECEIPTS_DIR, `${bundleHash}.json`);
3325
+ const receiptRaw = readControlFileOrAbsent(receiptPath, "bundle import completion receipt");
3326
+ if (receiptRaw !== undefined) {
3327
+ let report;
3328
+ try {
3329
+ const parsed = JSON.parse(receiptRaw);
3330
+ if (parsed === null ||
3331
+ typeof parsed !== "object" ||
3332
+ !("v" in parsed) ||
3333
+ parsed.v !== 1 ||
3334
+ !("bundleHash" in parsed) ||
3335
+ parsed.bundleHash !== bundleHash ||
3336
+ !("report" in parsed)) {
3337
+ throw new Error("wrong envelope");
3338
+ }
3339
+ const rep = parsed.report;
3340
+ if (!rep ||
3341
+ typeof rep !== "object" ||
3342
+ rep.v !== 1 ||
3343
+ rep.bundleHash !== bundleHash ||
3344
+ typeof rep.sourceStoreId !== "string" ||
3345
+ !Array.isArray(rep.landed) ||
3346
+ !Array.isArray(rep.alreadyPresent) ||
3347
+ !Array.isArray(rep.refusedErased) ||
3348
+ !Array.isArray(rep.refusedUntrusted) ||
3349
+ !Array.isArray(rep.conflicts) ||
3350
+ !Array.isArray(rep.governanceWithheld) ||
3351
+ !Array.isArray(rep.erasedAtSource) ||
3352
+ !Array.isArray(rep.pollutionDivergence) ||
3353
+ !Array.isArray(rep.referentialOrphans) ||
3354
+ !Array.isArray(rep.lineageDivergence) ||
3355
+ !Array.isArray(rep.custodyWithheld) ||
3356
+ typeof rep.custodyAppended !== "number") {
3357
+ throw new Error("wrong report shape");
3358
+ }
3359
+ report = rep;
3360
+ }
3361
+ catch {
3362
+ throw new ControlPlaneCorruptError(`bundle import completion receipt is unreadable/malformed at ${receiptPath} — it cannot prove completion and cannot be re-minted; repair or remove it deliberately (fail-closed)`);
3363
+ }
3364
+ releaseImportLatch(ctl, txnId);
3365
+ return report;
3366
+ }
3367
+ const digest = this.deletedIdsDigestLocked();
3368
+ if (digest.state === "damaged" || this.chainDegradedMarkerPresent()) {
3369
+ throw erasureCodedError("memory bundle import refused: the destination evidence chain is damaged or durably marked degraded — deletion history cannot be proven, so the anti-resurrection judgment cannot run; heal/rebuild the chain (locked mutation entry / explicit rebuild), then import again (nothing was landed, no latch was set)", "memory.import_rejected");
3370
+ }
3371
+ stageImportLatch(ctl, bundleHash, plan.entries.map((e) => ({ entryId: e.id, rev: e.rev })), this.now);
3372
+ const rows = this.loadLedger();
3373
+ const rowPresent = (id) => Object.prototype.hasOwnProperty.call(rows, id);
3374
+ const report = {
3375
+ v: 1,
3376
+ bundleHash,
3377
+ sourceStoreId,
3378
+ landed: [],
3379
+ alreadyPresent: [],
3380
+ refusedErased: [],
3381
+ refusedUntrusted: [],
3382
+ conflicts: [],
3383
+ governanceWithheld: [],
3384
+ erasedAtSource: [],
3385
+ pollutionDivergence: [],
3386
+ referentialOrphans: [],
3387
+ lineageDivergence: [],
3388
+ custodyWithheld: [],
3389
+ custodyAppended: 0,
3390
+ };
3391
+ const entryById = new Map(plan.entries.map((e) => [e.id, e]));
3392
+ const toLand = [];
3393
+ for (const e of plan.entries) {
3394
+ const present = rowPresent(e.id);
3395
+ if (!present && digest.ids.has(e.id)) {
3396
+ report.refusedErased.push(e.id);
3397
+ continue;
3398
+ }
3399
+ if (present && rowsRev(rows, e.id) === e.rev && rowsBinding(rows, e.id)?.scope === e.scope) {
3400
+ report.alreadyPresent.push(e.id);
3401
+ continue;
3402
+ }
3403
+ if (!present && e.frontmatter.provenance?.kind === "repo_file" && e.frontmatter.trust === undefined) {
3404
+ report.refusedUntrusted.push(e.id);
3405
+ continue;
3406
+ }
3407
+ toLand.push(e);
3408
+ }
3409
+ const erasedAtSource = new Set();
3410
+ for (const c of plan.custody) {
3411
+ if (c.channel === "delete" && typeof c.id === "string" && rowPresent(c.id))
3412
+ erasedAtSource.add(c.id);
3413
+ }
3414
+ report.erasedAtSource = [...erasedAtSource].sort();
3415
+ const screened = new Set();
3416
+ for (const finding of screenInboundEntries(toLand, { perFileBytes: MAX_MEMORY_BYTES })) {
3417
+ screened.add(finding.id);
3418
+ report.conflicts.push({ id: finding.id, reason: `inbound screen: ${finding.findings.map((f) => `${f.code} (${f.reason})`).join("; ")}` });
3419
+ }
3420
+ const patches = toLand.filter((e) => !screened.has(e.id)).map((e) => ({ op: "add", id: e.id, entry: e, guard: "absent" }));
3421
+ const patchReport = patches.length > 0 ? await this.applyPatchesLocked(patches, lock.token) : { applied: [], conflicts: [] };
3422
+ for (const c of patchReport.conflicts)
3423
+ report.conflicts.push({ id: c.id, reason: c.reason });
3424
+ const landedIds = new Set();
3425
+ for (const a of patchReport.applied) {
3426
+ if (a.op !== "add")
3427
+ continue;
3428
+ landedIds.add(a.id);
3429
+ const src = entryById.get(a.id);
3430
+ report.landed.push({ id: a.id, scope: src?.scope ?? "", slug: a.slug ?? src?.slug ?? "" });
3431
+ }
3432
+ const custodyOutcome = this.custodyImportLocked(plan.custody, { bundleHash, sourceStoreId });
3433
+ report.custodyAppended = custodyOutcome.appended;
3434
+ report.custodyWithheld = custodyOutcome.withheld;
3435
+ const eligible = new Set([...landedIds, ...report.alreadyPresent]);
3436
+ const destRows = this.loadLedger();
3437
+ const destKnown = (id) => Object.prototype.hasOwnProperty.call(destRows, id);
3438
+ const withheldIds = new Set();
3439
+ const orphanIds = new Set();
3440
+ const lineageToApply = [];
3441
+ for (const row of plan.lineage) {
3442
+ if (eligible.has(row.entryId))
3443
+ lineageToApply.push(row);
3444
+ else if (destKnown(row.entryId) || entryById.has(row.entryId))
3445
+ withheldIds.add(row.entryId);
3446
+ else
3447
+ orphanIds.add(row.entryId);
3448
+ }
3449
+ const challengesToApply = [];
3450
+ for (const c of plan.challenges) {
3451
+ if (eligible.has(c.entryId))
3452
+ challengesToApply.push(c);
3453
+ else if (destKnown(c.entryId) || entryById.has(c.entryId))
3454
+ withheldIds.add(c.entryId);
3455
+ else
3456
+ orphanIds.add(c.entryId);
3457
+ }
3458
+ report.governanceWithheld = [...withheldIds].sort();
3459
+ report.referentialOrphans = [...orphanIds].sort();
3460
+ report.lineageDivergence = importLineageCommitted(ctl, lineageToApply);
3461
+ this.appendImportChallengesVerified(challengesToApply.map((c) => ({
3462
+ eventId: `imp:${bundleHash}:${c.eventId}`,
3463
+ entryId: c.entryId,
3464
+ reason: c.reason,
3465
+ ...(c.challengedRev !== undefined ? { challengedRev: c.challengedRev } : {}),
3466
+ })));
3467
+ for (const p of [...plan.pollutedSessions].sort((a, b) => (a.sessionId < b.sessionId ? -1 : 1))) {
3468
+ const contributions = lineageContributionsOfSession(ctl, p.sessionId);
3469
+ this.appendImportChallengesVerified(contributions.map((c) => ({
3470
+ eventId: `imp-sweep:${bundleHash}:pollution:${p.sessionId}:${p.at}:${c.entryId}`,
3471
+ entryId: c.entryId,
3472
+ reason: `challenged: the contributing session was marked polluted`,
3473
+ challengedRev: c.lastRev,
3474
+ })));
3475
+ const outcome = importSessionPollution(ctl, p.sessionId, { at: p.at, reason: p.reason });
3476
+ if (outcome === "divergent")
3477
+ report.pollutionDivergence.push({ sessionId: p.sessionId, kept: "destination" });
3478
+ }
3479
+ ensureDirExists(join(ctl, IMPORT_RECEIPTS_DIR));
3480
+ if (readControlFileOrAbsent(receiptPath, "bundle import completion receipt") !== undefined) {
3481
+ throw new ControlPlaneCorruptError(`bundle import completion receipt appeared mid-import at ${receiptPath} — refusing to overwrite it (fail-closed)`);
3482
+ }
3483
+ const staged = join(ctl, IMPORT_RECEIPTS_DIR, `.stage-${bundleHash}-${randomUUID().slice(0, 8)}`);
3484
+ const fd = openSync(staged, "wx", 0o600);
3485
+ try {
3486
+ writeAllSync(fd, `${JSON.stringify({ v: 1, bundleHash, at: this.now(), report }, null, 2)}\n`);
3487
+ fsyncSync(fd);
3488
+ }
3489
+ finally {
3490
+ closeSync(fd);
3491
+ }
3492
+ try {
3493
+ linkSync(staged, receiptPath);
3494
+ rmSync(staged, { force: true });
3495
+ }
3496
+ catch (err) {
3497
+ try {
3498
+ rmSync(staged, { force: true });
3499
+ }
3500
+ catch {
3501
+ }
3502
+ if (err.code === "EEXIST") {
3503
+ throw new ControlPlaneCorruptError(`bundle import completion receipt appeared mid-import at ${receiptPath} — refusing to overwrite it (fail-closed)`);
3504
+ }
3505
+ throw err;
3506
+ }
3507
+ releaseImportLatch(ctl, txnId);
3508
+ return report;
3509
+ }
3510
+ catch (err) {
3511
+ this.ledger = undefined;
3512
+ throw err;
3513
+ }
3514
+ finally {
3515
+ lock.release();
3516
+ }
3517
+ }
2887
3518
  async getConsolidationCursor(scope) {
2888
3519
  const cursors = this.readCursors();
2889
3520
  return cursors[scope];