@sema-agent/core 5.44.0 → 5.46.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 (79) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/dist/agents/subagent.d.ts +3 -1
  3. package/dist/agents/subagent.js +1 -1
  4. package/dist/brain/reasoning.d.ts +50 -4
  5. package/dist/brain/reasoning.js +28 -7
  6. package/dist/brain/request-params.d.ts +0 -12
  7. package/dist/brain/request-params.js +1 -1
  8. package/dist/core/checkpoint-store.d.ts +12 -0
  9. package/dist/core/governance-codes.js +1 -0
  10. package/dist/core/hooks.d.ts +9 -2
  11. package/dist/core/hooks.js +6 -5
  12. package/dist/core/memory-engine/content-origin.d.ts +9 -4
  13. package/dist/core/memory-engine/delegation-provenance.d.ts +12 -7
  14. package/dist/core/memory-engine/engine.d.ts +50 -3
  15. package/dist/core/memory-engine/engine.js +194 -32
  16. package/dist/core/memory-engine/export-bundle.d.ts +10 -1
  17. package/dist/core/memory-engine/export-bundle.js +21 -0
  18. package/dist/core/memory-engine/file-backend.d.ts +33 -4
  19. package/dist/core/memory-engine/file-backend.js +165 -39
  20. package/dist/core/memory-engine/frontmatter.d.ts +42 -1
  21. package/dist/core/memory-engine/frontmatter.js +141 -1
  22. package/dist/core/memory-engine/header-hints.d.ts +17 -0
  23. package/dist/core/memory-engine/header-hints.js +6 -0
  24. package/dist/core/memory-engine/index.d.ts +4 -3
  25. package/dist/core/memory-engine/index.js +3 -2
  26. package/dist/core/memory-engine/layout.d.ts +25 -2
  27. package/dist/core/memory-engine/layout.js +25 -12
  28. package/dist/core/memory-engine/memory-backend-contract.js +65 -0
  29. package/dist/core/memory-engine/sync-client.d.ts +1 -1
  30. package/dist/core/memory-engine/sync-client.js +33 -1
  31. package/dist/core/memory-engine/tools.d.ts +7 -0
  32. package/dist/core/memory-engine/tools.js +3 -0
  33. package/dist/core/memory-engine/types.d.ts +75 -1
  34. package/dist/core/memory-engine/types.js +1 -1
  35. package/dist/core/reminder-mint.d.ts +70 -0
  36. package/dist/core/reminder-mint.js +25 -0
  37. package/dist/core/runner/assemble-result.d.ts +5 -0
  38. package/dist/core/runner/assemble-result.js +1 -1
  39. package/dist/core/runner/git-status-frame.d.ts +3 -14
  40. package/dist/core/runner/git-status-frame.js +39 -14
  41. package/dist/core/runner/prepare-config-doors.d.ts +10 -0
  42. package/dist/core/runner/prepare-config-doors.js +32 -0
  43. package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
  44. package/dist/core/runner/prepare-hands-readface.js +26 -0
  45. package/dist/core/runner/prepare-memory.d.ts +11 -0
  46. package/dist/core/runner/prepare-memory.js +12 -10
  47. package/dist/core/runner/prepare-task.d.ts +25 -2
  48. package/dist/core/runner/prepare-task.js +71 -14
  49. package/dist/core/runner/runtask.js +82 -72
  50. package/dist/core/side-query.d.ts +11 -1
  51. package/dist/core/side-query.js +3 -0
  52. package/dist/core/trace.d.ts +17 -2
  53. package/dist/core/types.d.ts +119 -8
  54. package/dist/engine/harness/types.d.ts +46 -1
  55. package/dist/engine/harness/types.js +11 -0
  56. package/dist/engine/session/import-validate.js +6 -1
  57. package/dist/engine/session/session.d.ts +20 -0
  58. package/dist/engine/session/session.js +26 -1
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/orchestration/run-workflow-tool.d.ts +16 -0
  62. package/dist/orchestration/run-workflow-tool.js +23 -3
  63. package/dist/orchestration/workflow-governance.d.ts +8 -1
  64. package/dist/prompt-assembly/epoch.js +2 -0
  65. package/dist/prompt-assembly/types.d.ts +6 -0
  66. package/dist/prompts/default.d.ts +13 -1
  67. package/dist/prompts/default.js +5 -1
  68. package/dist/tools/fs/fs-bash.d.ts +4 -0
  69. package/dist/tools/fs/fs-bash.js +1 -1
  70. package/dist/tools/fs/fs-read.d.ts +1 -1
  71. package/dist/tools/fs/fs-read.js +8 -7
  72. package/dist/tools/fs/fs-shared.d.ts +10 -4
  73. package/dist/tools/fs/fs-shared.js +6 -3
  74. package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
  75. package/dist/tools/fs/gh-rate-limit.js +3 -2
  76. package/dist/tools/fs/index.d.ts +10 -2
  77. package/dist/tools/fs/index.js +2 -1
  78. package/package.json +1 -1
  79. package/test/export-surface.snapshot.json +12 -1
@@ -4,7 +4,7 @@ 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
- import { computeEntryRev, entryFromFile, isValidEntryId, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
7
+ import { ambiguousOriginRepresentation, committedOriginOf, computeEntryRev, entryFromFile, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
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
9
  import { screenInboundEntries } from "./data-plane.js";
10
10
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
@@ -540,6 +540,8 @@ export class FileMemoryEngineBackend {
540
540
  unboundRowsKnown = false;
541
541
  adoptionNoticeKeys = new Set();
542
542
  transfersAppendFault;
543
+ recoveryCommitProbe;
544
+ chainHealProbe;
543
545
  exportFaceProbe;
544
546
  deletedIdsDigest;
545
547
  inboundFindings = [];
@@ -735,11 +737,18 @@ export class FileMemoryEngineBackend {
735
737
  const tryMkdir = () => {
736
738
  try {
737
739
  mkdirSync(this.controlPlaneRoot, { recursive: true });
740
+ }
741
+ catch (err) {
742
+ throw new ControlPlaneCorruptError(`memory control-plane root could not be created (${err.code ?? "io error"}) at ${this.controlPlaneRoot} — a lock-acquisition fault is not contention; deferring recovery/migration on it would be silent (fail-closed)`, { cause: err });
743
+ }
744
+ try {
738
745
  mkdirSync(lockDir);
739
746
  return true;
740
747
  }
741
- catch {
742
- return false;
748
+ catch (err) {
749
+ if (err.code === "EEXIST")
750
+ return false;
751
+ throw new ControlPlaneCorruptError(`memory txn lock could not be created (${err.code ?? "io error"}) at ${lockDir} — a lock-acquisition fault is not contention; deferring recovery/migration on it would be silent (fail-closed)`, { cause: err });
743
752
  }
744
753
  };
745
754
  let acquired = tryMkdir();
@@ -748,7 +757,22 @@ export class FileMemoryEngineBackend {
748
757
  try {
749
758
  mtimeMs = statSync(lockDir).mtimeMs;
750
759
  }
751
- catch {
760
+ catch (err) {
761
+ if (err.code !== "ENOENT") {
762
+ throw new ControlPlaneCorruptError(`memory txn lock state could not be probed (${err.code ?? "io error"}) at ${lockDir} — a probe failure is not contention (fail-closed)`, { cause: err });
763
+ }
764
+ let occupant;
765
+ try {
766
+ occupant = lstatSync(lockDir);
767
+ }
768
+ catch (lerr) {
769
+ if (lerr.code !== "ENOENT") {
770
+ throw new ControlPlaneCorruptError(`memory txn lock re-probe failed (${lerr.code ?? "io error"}) at ${lockDir} — a probe failure is not contention (fail-closed)`, { cause: lerr });
771
+ }
772
+ }
773
+ if (occupant !== undefined && !occupant.isDirectory()) {
774
+ throw new ControlPlaneCorruptError(`memory txn lock path is occupied by an invalid node (a dangling symlink?) at ${lockDir} — not contention and not a release race; remove the occupant (fail-closed)`, { cause: err });
775
+ }
752
776
  mtimeMs = undefined;
753
777
  }
754
778
  if (mtimeMs !== undefined && this.now() - mtimeMs > TXN_LOCK_STALE_MS) {
@@ -757,7 +781,10 @@ export class FileMemoryEngineBackend {
757
781
  statSync(ownerPath);
758
782
  tokenPresent = true;
759
783
  }
760
- catch {
784
+ catch (err) {
785
+ if (err.code !== "ENOENT") {
786
+ throw new ControlPlaneCorruptError(`memory txn lock owner token could not be probed (${err.code ?? "io error"}) at ${ownerPath} — a probe failure is not a mid-acquisition (fail-closed)`, { cause: err });
787
+ }
761
788
  }
762
789
  if (tokenPresent) {
763
790
  const tomb = join(this.controlPlaneRoot, `txn.lock.stale-${randomUUID()}`);
@@ -766,7 +793,10 @@ export class FileMemoryEngineBackend {
766
793
  renameSync(lockDir, tomb);
767
794
  stolen = true;
768
795
  }
769
- catch {
796
+ catch (err) {
797
+ if (err.code !== "ENOENT") {
798
+ throw new ControlPlaneCorruptError(`memory txn lock stale-steal rename failed (${err.code ?? "io error"}) at ${lockDir} — a rename fault is not a lost race (fail-closed)`, { cause: err });
799
+ }
770
800
  }
771
801
  if (stolen) {
772
802
  rmSync(tomb, { recursive: true, force: true });
@@ -819,7 +849,7 @@ export class FileMemoryEngineBackend {
819
849
  if (lock === undefined)
820
850
  return;
821
851
  try {
822
- this.recoverJournal();
852
+ this.recoverJournal({ token: lock.token });
823
853
  this.migrateLockedIfNeeded(lock.token);
824
854
  }
825
855
  finally {
@@ -892,8 +922,10 @@ export class FileMemoryEngineBackend {
892
922
  fd = openSync(path, "wx", 0o600);
893
923
  }
894
924
  catch (err) {
895
- if (err.code === "EEXIST")
925
+ if (err.code === "EEXIST") {
926
+ this.fsyncControlPlaneDir();
896
927
  return;
928
+ }
897
929
  throw new ControlPlaneCorruptError(`evidence-chain degradation marker could not be created at ${path} (${err.code ?? "io error"}) — heal aborted, the chain is unchanged (fail-closed)`);
898
930
  }
899
931
  try {
@@ -903,11 +935,32 @@ export class FileMemoryEngineBackend {
903
935
  finally {
904
936
  closeSync(fd);
905
937
  }
938
+ this.fsyncControlPlaneDir();
906
939
  if (readControlFileOrAbsent(path, "evidence-chain degradation marker") === undefined) {
907
940
  throw new ControlPlaneCorruptError(`evidence-chain degradation marker vanished after its write at ${path} — heal aborted, the chain is unchanged (fail-closed)`);
908
941
  }
909
942
  }
910
- readTransferChain() {
943
+ fsyncControlPlaneDir() {
944
+ if (process.platform === "win32")
945
+ return;
946
+ let dfd;
947
+ try {
948
+ dfd = openSync(this.controlPlaneRoot, "r");
949
+ }
950
+ catch (err) {
951
+ throw new ControlPlaneCorruptError(`evidence-chain degradation marker's directory could not be opened for fsync (${err.code ?? "io error"}) at ${this.controlPlaneRoot} — heal aborted, the chain is unchanged (fail-closed: an unproven marker must not authorize a truncation)`, { cause: err });
952
+ }
953
+ try {
954
+ fsyncSync(dfd);
955
+ }
956
+ catch (err) {
957
+ throw new ControlPlaneCorruptError(`evidence-chain degradation marker's directory entry could not be fsynced (${err.code ?? "io error"}) at ${this.controlPlaneRoot} — heal aborted, the chain is unchanged (fail-closed: an unproven marker must not authorize a truncation)`, { cause: err });
958
+ }
959
+ finally {
960
+ closeSync(dfd);
961
+ }
962
+ }
963
+ readTransferChain(fenceToken) {
911
964
  const path = join(this.controlPlaneRoot, TRANSFERS_FILE);
912
965
  const raw = readControlFileOrAbsent(path, "transfer evidence log");
913
966
  const out = [];
@@ -940,6 +993,8 @@ export class FileMemoryEngineBackend {
940
993
  }
941
994
  }
942
995
  const outOfBand = !journalHasPendingEvidence;
996
+ if (fenceToken !== undefined)
997
+ this.assertTxnLockOwnership(fenceToken);
943
998
  let fragmentPath;
944
999
  try {
945
1000
  ensureDirExists(join(this.controlPlaneRoot, QUARANTINE_DIR));
@@ -951,6 +1006,9 @@ export class FileMemoryEngineBackend {
951
1006
  }
952
1007
  if (outOfBand)
953
1008
  this.mintChainDegradedMarker(fragmentPath);
1009
+ this.chainHealProbe?.();
1010
+ if (fenceToken !== undefined)
1011
+ this.assertTxnLockOwnership(fenceToken);
954
1012
  try {
955
1013
  atomicWriteFileSync(path, lines.slice(0, i).join("\n") + (i > 0 ? "\n" : ""));
956
1014
  }
@@ -1110,21 +1168,22 @@ export class FileMemoryEngineBackend {
1110
1168
  closeSync(fd);
1111
1169
  }
1112
1170
  }
1113
- recoverJournal(opts = {}) {
1114
- if (opts.unlocked) {
1171
+ recoverJournal(fence) {
1172
+ if (fence.token === undefined) {
1115
1173
  if (this.txnInFlight())
1116
1174
  return;
1117
1175
  const lock = this.tryAcquireTxnLockSync();
1118
1176
  if (lock === undefined)
1119
1177
  return;
1120
1178
  try {
1121
- this.recoverJournal();
1179
+ this.recoverJournal({ token: lock.token });
1122
1180
  }
1123
1181
  finally {
1124
1182
  lock.release();
1125
1183
  }
1126
1184
  return;
1127
1185
  }
1186
+ const fenceToken = fence.token;
1128
1187
  const jp = join(this.controlPlaneRoot, JOURNAL_FILE);
1129
1188
  const raw = readControlFileOrAbsent(jp, "transaction journal");
1130
1189
  if (raw !== undefined) {
@@ -1205,7 +1264,7 @@ export class FileMemoryEngineBackend {
1205
1264
  if (hasV1)
1206
1265
  throw new ControlPlaneCorruptError(`transaction journal carries transfers with a v1 snapshot (impossible form): ${jp}`);
1207
1266
  if (journalEvs.size > 0) {
1208
- for (const row of this.readTransferChain()) {
1267
+ for (const row of this.readTransferChain(fenceToken)) {
1209
1268
  const j = journalEvs.get(row.ev);
1210
1269
  if (j !== undefined && j !== row.canonical) {
1211
1270
  throw new ControlPlaneCorruptError(`transaction journal event id ${JSON.stringify(row.ev)} collides with a DIFFERENT payload already on the evidence chain (fail-closed before redo): ${jp}`);
@@ -1242,7 +1301,7 @@ export class FileMemoryEngineBackend {
1242
1301
  }
1243
1302
  }
1244
1303
  if (e.req !== undefined && !anchorRows.some((a) => a.req === e.req)) {
1245
- chainAnchors ??= this.readTransferChain()
1304
+ chainAnchors ??= this.readTransferChain(fenceToken)
1246
1305
  .map((r) => r.parsed)
1247
1306
  .filter((p) => p.channel === "erasure-anchor" && p.origin === undefined);
1248
1307
  const matching = chainAnchors.filter((p) => p.req === e.req);
@@ -1254,7 +1313,7 @@ export class FileMemoryEngineBackend {
1254
1313
  }
1255
1314
  const chainAnchorEvsByReq = new Map();
1256
1315
  if (anchorRows.length > 0) {
1257
- for (const p of (chainAnchors ??= this.readTransferChain()
1316
+ for (const p of (chainAnchors ??= this.readTransferChain(fenceToken)
1258
1317
  .map((r) => r.parsed)
1259
1318
  .filter((p) => p.channel === "erasure-anchor" && p.origin === undefined))) {
1260
1319
  const set = chainAnchorEvsByReq.get(p.req) ?? new Set();
@@ -1332,6 +1391,7 @@ export class FileMemoryEngineBackend {
1332
1391
  }
1333
1392
  }
1334
1393
  }
1394
+ this.assertTxnLockOwnership(fenceToken);
1335
1395
  for (const op of journal.ops) {
1336
1396
  try {
1337
1397
  if (op.kind === "write" && op.content !== undefined) {
@@ -1354,6 +1414,8 @@ export class FileMemoryEngineBackend {
1354
1414
  throw new ControlPlaneCorruptError(`transaction journal replay failed on op kind=${op.kind} target=${JSON.stringify(op.target)} (${code ?? (err instanceof Error ? err.message : String(err))}) — the journal is kept at ${jp}; an I/O failure retries at the next construction, a persistent fault needs the explicit control-plane rebuild path`);
1355
1415
  }
1356
1416
  }
1417
+ this.recoveryCommitProbe?.();
1418
+ this.assertTxnLockOwnership(fenceToken);
1357
1419
  if (hasV2) {
1358
1420
  this.ledger = snapshotRows;
1359
1421
  this.envelopeExtras = snapshotExtras;
@@ -1366,8 +1428,15 @@ export class FileMemoryEngineBackend {
1366
1428
  this.persistedSchemaVersion = "v1";
1367
1429
  this.persistLedgerV1(snapshotRows);
1368
1430
  }
1369
- rmSync(jp, { force: true });
1431
+ if (typeof journal.txn === "string") {
1432
+ this.closeJournalIfOurs(journal.txn);
1433
+ }
1434
+ else {
1435
+ this.assertTxnLockOwnership(fenceToken);
1436
+ rmSync(jp, { force: true });
1437
+ }
1370
1438
  }
1439
+ this.assertTxnLockOwnership(fenceToken);
1371
1440
  this.sweepStagedFiles();
1372
1441
  }
1373
1442
  txnInFlight() {
@@ -1452,6 +1521,9 @@ export class FileMemoryEngineBackend {
1452
1521
  if (first !== undefined) {
1453
1522
  return { path: relPath, code: first.code, reason: `inbound memory change blocked: ${first.reason} (quarantined)` };
1454
1523
  }
1524
+ if (ambiguousOriginRepresentation(parseEntryFile(text).frontmatter)) {
1525
+ return { path: relPath, code: "invalid", reason: `inbound memory change blocked: conflicting or duplicated origin representations (ambiguous external-origin marker; quarantined)` };
1526
+ }
1455
1527
  return undefined;
1456
1528
  }
1457
1529
  readScope(scope, sync, probeOnly, lockToken) {
@@ -1628,17 +1700,28 @@ export class FileMemoryEngineBackend {
1628
1700
  whitewashInboundFinding(relPath, entry, containment) {
1629
1701
  const committedShadow = this.readCommittedShadow(entry.id);
1630
1702
  const committedFm = committedShadow !== undefined ? parseEntryFile(committedShadow).frontmatter : undefined;
1631
- if (committedFm?.provenance?.kind !== "repo_file")
1703
+ if (committedFm === undefined)
1632
1704
  return undefined;
1633
- const stripsProvenance = entry.frontmatter.provenance === undefined;
1634
- const stripsTrust = committedFm.trust !== undefined && entry.frontmatter.trust === undefined;
1635
- if (!stripsProvenance && !stripsTrust)
1636
- return undefined;
1637
- return {
1638
- path: relPath,
1639
- code: "invalid",
1640
- reason: `inbound memory change blocked: repo_file provenance whitewash refused — the on-disk change drops ${stripsProvenance ? "provenance" : "trust"} from a repo-ingested entry ${containment === "restored" ? "(quarantined; committed content restored)" : "(quarantined)"}`,
1641
- };
1705
+ if (committedFm.provenance?.kind === "repo_file") {
1706
+ const stripsProvenance = entry.frontmatter.provenance === undefined;
1707
+ const stripsTrust = committedFm.trust !== undefined && entry.frontmatter.trust === undefined;
1708
+ if (stripsProvenance || stripsTrust) {
1709
+ return {
1710
+ path: relPath,
1711
+ code: "invalid",
1712
+ reason: `inbound memory change blocked: repo_file provenance whitewash refused — the on-disk change drops ${stripsProvenance ? "provenance" : "trust"} from a repo-ingested entry ${containment === "restored" ? "(quarantined; committed content restored)" : "(quarantined)"}`,
1713
+ };
1714
+ }
1715
+ }
1716
+ const committedOrigin = committedOriginOf(committedFm);
1717
+ if (committedOrigin !== undefined && !originEquals(committedOrigin, committedOriginOf(entry.frontmatter))) {
1718
+ return {
1719
+ path: relPath,
1720
+ code: "invalid",
1721
+ reason: `inbound memory change blocked: external-origin marker whitewash refused — the on-disk change strips or rewrites the origin marker of a marked entry ${containment === "restored" ? "(quarantined; committed content restored)" : "(quarantined)"}`,
1722
+ };
1723
+ }
1724
+ return undefined;
1642
1725
  }
1643
1726
  adoptMoveCandidateLocked(rows, entry, absPath, slug, scope, relPath, text, committedRev, from, moveEvents, moveAnnouncements, adoptedExternal) {
1644
1727
  const census = this.censusProjectionsLocked();
@@ -1685,7 +1768,7 @@ export class FileMemoryEngineBackend {
1685
1768
  try {
1686
1769
  this.assertTxnLockOwnership(lock.token);
1687
1770
  this.ledger = undefined;
1688
- this.recoverJournal();
1771
+ this.recoverJournal({ token: lock.token });
1689
1772
  this.migrateLockedIfNeeded(lock.token);
1690
1773
  return this.readScope(scope, true, false, lock.token);
1691
1774
  }
@@ -2177,7 +2260,7 @@ export class FileMemoryEngineBackend {
2177
2260
  async applyPatchesLocked(patches, lockToken) {
2178
2261
  this.assertTxnLockOwnership(lockToken);
2179
2262
  this.ledger = undefined;
2180
- this.recoverJournal();
2263
+ this.recoverJournal({ token: lockToken });
2181
2264
  this.migrateLockedIfNeeded(lockToken);
2182
2265
  const rows = cloneRows(this.loadLedger());
2183
2266
  const report = { applied: [], conflicts: [] };
@@ -2291,6 +2374,14 @@ export class FileMemoryEngineBackend {
2291
2374
  report.conflicts.push({ op: patch.op, id: patch.id, reason: `invalid_slug_shape (slug ${JSON.stringify(patch.entry.slug)} is empty, absolute, or path-escaping — a projection must stay inside its scope dir)` });
2292
2375
  return;
2293
2376
  }
2377
+ if (patch.op !== "delete" && patch.entry !== undefined && ambiguousOriginRepresentation(patch.entry.frontmatter)) {
2378
+ report.conflicts.push({
2379
+ op: patch.op,
2380
+ id: patch.id,
2381
+ reason: `external-origin marker representation conflict: the entry carries conflicting or duplicated origin representations (malformed patch refused)`,
2382
+ });
2383
+ return;
2384
+ }
2294
2385
  if (patch.op === "add") {
2295
2386
  const entry = patch.entry;
2296
2387
  if (!entry) {
@@ -2299,6 +2390,19 @@ export class FileMemoryEngineBackend {
2299
2390
  }
2300
2391
  const locatedAnywhere = this.locateById(patch.id);
2301
2392
  const existingAnywhere = locatedAnywhere !== undefined && plannedDeletes.has(patch.id) ? undefined : locatedAnywhere;
2393
+ if (locatedAnywhere !== undefined) {
2394
+ const shadowText = this.readCommittedShadow(patch.id);
2395
+ const committedFm = shadowText !== undefined ? parseEntryFile(shadowText).frontmatter : locatedAnywhere.entry.frontmatter;
2396
+ const committedOrigin = committedOriginOf(committedFm);
2397
+ if (committedOrigin !== undefined && !originEquals(committedOrigin, committedOriginOf(entry.frontmatter))) {
2398
+ report.conflicts.push({
2399
+ op: "add",
2400
+ id: patch.id,
2401
+ reason: `external-origin marker whitewash refused: the add strips or rewrites the origin marker of a marked entry (malformed patch refused)`,
2402
+ });
2403
+ return;
2404
+ }
2405
+ }
2302
2406
  if (patch.guard === "absent") {
2303
2407
  if (existingAnywhere) {
2304
2408
  const currentRev = rowsRev(rows, patch.id) ?? existingAnywhere.entry.rev;
@@ -2376,6 +2480,15 @@ export class FileMemoryEngineBackend {
2376
2480
  return;
2377
2481
  }
2378
2482
  }
2483
+ const committedOrigin = committedOriginOf(committedFm);
2484
+ if (committedOrigin !== undefined && !originEquals(committedOrigin, committedOriginOf(patch.entry.frontmatter))) {
2485
+ report.conflicts.push({
2486
+ op: "update",
2487
+ id: patch.id,
2488
+ reason: `external-origin marker whitewash refused: the update strips or rewrites the origin marker of a marked entry (malformed patch refused)`,
2489
+ });
2490
+ return;
2491
+ }
2379
2492
  }
2380
2493
  const currentRev = rowsRev(rows, patch.id) ?? found.entry.rev;
2381
2494
  if (patch.baseRev !== undefined && patch.baseRev !== currentRev) {
@@ -2447,10 +2560,10 @@ export class FileMemoryEngineBackend {
2447
2560
  try {
2448
2561
  this.assertTxnLockOwnership(lock.token);
2449
2562
  this.ledger = undefined;
2450
- this.recoverJournal();
2563
+ this.recoverJournal({ token: lock.token });
2451
2564
  this.migrateLockedIfNeeded(lock.token);
2452
2565
  const rows = cloneRows(this.loadLedger());
2453
- const chain = this.readTransferChain();
2566
+ const chain = this.readTransferChain(lock.token);
2454
2567
  const degraded = this.chainDegradedMarkerPresent();
2455
2568
  const custodyState = degraded ? "damaged" : "complete";
2456
2569
  const selectHash = erasureSelectHash(select);
@@ -3141,7 +3254,7 @@ export class FileMemoryEngineBackend {
3141
3254
  try {
3142
3255
  this.assertTxnLockOwnership(lock.token);
3143
3256
  this.ledger = undefined;
3144
- this.recoverJournal();
3257
+ this.recoverJournal({ token: lock.token });
3145
3258
  const rows = this.loadLedger();
3146
3259
  if (this.persistedSchemaVersion !== "v2") {
3147
3260
  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);
@@ -3187,9 +3300,9 @@ export class FileMemoryEngineBackend {
3187
3300
  const lineage = [];
3188
3301
  for (const [entryId, sessions] of Object.entries(lineageRec.committed)) {
3189
3302
  for (const [sessionId, c] of Object.entries(sessions))
3190
- lineage.push({ entryId, sessionId, lastRev: c.lastRev, lastAt: c.lastAt });
3303
+ lineage.push({ entryId, sessionId, lastRev: c.lastRev, lastAt: c.lastAt, ...(c.marked === true ? { marked: true } : {}) });
3191
3304
  }
3192
- const pollutedSessions = Object.entries(polluted).map(([sessionId, r]) => ({ sessionId, at: r.at, reason: r.reason }));
3305
+ const pollutedSessions = Object.entries(polluted).map(([sessionId, r]) => ({ sessionId, at: r.at, reason: r.reason, ...(r.cause !== undefined ? { cause: r.cause } : {}) }));
3193
3306
  try {
3194
3307
  this.assertTxnLockOwnership(lock.token);
3195
3308
  }
@@ -3224,7 +3337,7 @@ export class FileMemoryEngineBackend {
3224
3337
  try {
3225
3338
  this.assertTxnLockOwnership(lock.token);
3226
3339
  this.ledger = undefined;
3227
- this.recoverJournal();
3340
+ this.recoverJournal({ token: lock.token });
3228
3341
  const rows = this.loadLedger();
3229
3342
  if (this.persistedSchemaVersion !== "v2")
3230
3343
  return { custody: [], unbound: [], complete: false, unsliceableCustody: 0, reason: "the committed-rev ledger is still schema v1 (migrate first)" };
@@ -3259,7 +3372,7 @@ export class FileMemoryEngineBackend {
3259
3372
  const lock = await this.acquireTxnLock(timings);
3260
3373
  try {
3261
3374
  this.assertTxnLockOwnership(lock.token);
3262
- this.recoverJournal();
3375
+ this.recoverJournal({ token: lock.token });
3263
3376
  return this.custodyImportLocked(rows, opts);
3264
3377
  }
3265
3378
  finally {
@@ -3328,7 +3441,7 @@ export class FileMemoryEngineBackend {
3328
3441
  try {
3329
3442
  this.assertTxnLockOwnership(lock.token);
3330
3443
  this.ledger = undefined;
3331
- this.recoverJournal();
3444
+ this.recoverJournal({ token: lock.token });
3332
3445
  this.migrateLockedIfNeeded(lock.token);
3333
3446
  const ctl = this.controlPlaneRoot;
3334
3447
  const txnId = importLatchTxnId(bundleHash);
@@ -3450,8 +3563,21 @@ export class FileMemoryEngineBackend {
3450
3563
  const orphanIds = new Set();
3451
3564
  const lineageToApply = [];
3452
3565
  for (const row of plan.lineage) {
3453
- if (eligible.has(row.entryId))
3566
+ if (eligible.has(row.entryId)) {
3567
+ if (row.marked === true) {
3568
+ const shadow = this.readCommittedShadow(row.entryId);
3569
+ let committedFm = shadow !== undefined ? parseEntryFile(shadow).frontmatter : undefined;
3570
+ if (committedFm === undefined)
3571
+ committedFm = this.locateById(row.entryId)?.entry.frontmatter;
3572
+ if (committedFm !== undefined && committedOriginOf(committedFm) === undefined) {
3573
+ const { marked: _dropped, ...unmarked } = row;
3574
+ lineageToApply.push(unmarked);
3575
+ report.conflicts.push({ id: row.entryId, reason: `lineage row claims marked:true but the committed content carries no origin marker — the bit was dropped (the pollution sweep judges the contribution)` });
3576
+ continue;
3577
+ }
3578
+ }
3454
3579
  lineageToApply.push(row);
3580
+ }
3455
3581
  else if (destKnown(row.entryId) || entryById.has(row.entryId))
3456
3582
  withheldIds.add(row.entryId);
3457
3583
  else
@@ -3476,14 +3602,14 @@ export class FileMemoryEngineBackend {
3476
3602
  ...(c.challengedRev !== undefined ? { challengedRev: c.challengedRev } : {}),
3477
3603
  })));
3478
3604
  for (const p of [...plan.pollutedSessions].sort((a, b) => (a.sessionId < b.sessionId ? -1 : 1))) {
3479
- const contributions = lineageContributionsOfSession(ctl, p.sessionId);
3605
+ const contributions = lineageContributionsOfSession(ctl, p.sessionId).filter((c) => c.marked !== true);
3480
3606
  this.appendImportChallengesVerified(contributions.map((c) => ({
3481
3607
  eventId: `imp-sweep:${bundleHash}:pollution:${p.sessionId}:${p.at}:${c.entryId}`,
3482
3608
  entryId: c.entryId,
3483
3609
  reason: `challenged: the contributing session was marked polluted`,
3484
3610
  challengedRev: c.lastRev,
3485
3611
  })));
3486
- const outcome = importSessionPollution(ctl, p.sessionId, { at: p.at, reason: p.reason });
3612
+ const outcome = importSessionPollution(ctl, p.sessionId, { at: p.at, reason: p.reason, ...(p.cause !== undefined ? { cause: p.cause } : {}) });
3487
3613
  if (outcome === "divergent")
3488
3614
  report.pollutionDivergence.push({ sessionId: p.sessionId, kept: "destination" });
3489
3615
  }
@@ -1,4 +1,4 @@
1
- import type { MemoryEntry, MemoryEntryFrontmatter } from "./types.js";
1
+ import { type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryOrigin } from "./types.js";
2
2
  /** A parsed entry file: frontmatter (may be absent — CC allows bare files; harvest mints them) + body. */
3
3
  export interface ParsedEntryFile {
4
4
  /** The hidden immutable id (`id:` frontmatter line), when present. */
@@ -24,5 +24,46 @@ export declare function serializeEntryFile(entry: Pick<MemoryEntry, "id" | "fron
24
24
  * of incidental serialization formatting, or every materialize would see a phantom diff.
25
25
  */
26
26
  export declare function computeEntryRev(entry: Pick<MemoryEntry, "id" | "frontmatter" | "body">): string;
27
+ /** True ⇔ `extra` carries origin-form bytes (an invalid/foreign origin block preserved verbatim,
28
+ * or an origin block a pre-336 vintage folded into `extra`). */
29
+ export declare function hasOriginFormExtra(fm: Pick<MemoryEntryFrontmatter, "extra">): boolean;
30
+ export declare function committedOriginOf(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra">): MemoryEntryOrigin | undefined;
31
+ /** Deep equality over the origin marker (every member, absence included) — the immutability law's
32
+ * comparator: a downgraded cause or a rewritten `at` is as refused as a strip. */
33
+ export declare function originEquals(a: MemoryEntryOrigin | undefined, b: MemoryEntryOrigin | undefined): boolean;
34
+ /**
35
+ * design/336 (adversarial rounds 1+2) — the AMBIGUOUS origin-representation predicate, ONE law for
36
+ * every inbound boundary (backend patch law, sync wire, bundle validation, out-of-band adoption):
37
+ * a frontmatter whose marker has MORE THAN ONE carrier is refused unless it is the one legal
38
+ * legacy shape — a typed origin beside EXACTLY ONE extra-form carrier that normalizes EQUAL.
39
+ * Covered vehicles: typed A + disagreeing extra block B (round 1 — the typed member satisfies the
40
+ * immutability comparator while B rides into storage and a merging reparse answers B); extra-only
41
+ * duplicate blocks (round 2 — cross-block field combination plus the parser's block reordering
42
+ * across a serialize cycle rewrite members with no typed origin at all); typed + multiple extra
43
+ * carriers. Single-carrier shapes and the equal legacy carriage stay legal, so carry-forward of
44
+ * old committed forms never bricks. The model-writable face never reaches this predicate — the
45
+ * harvest strips every origin form and re-judges (§2.3-1).
46
+ *
47
+ * ⚠ KNOWN OPEN (adversarial round 3 — ESCALATED under the three-same-root-rounds rule, no further
48
+ * spelling patch here): the predicate's accepted set is not SERIALIZATION-CLOSED. A scalar
49
+ * `origin: …` sub-line INSIDE an accepted block is not counted as a carrier, yet a serialize →
50
+ * reparse cycle migrates the block to the typed field and drops that sub-line into `extra` — the
51
+ * semantic rev moves, so the committed account diverges from the reparsed bytes (downstream: CAS
52
+ * refusal / quarantine, the FAIL-CLOSED direction — the marker VALUE itself is preserved, no
53
+ * member is rewritten). The registered deep fix is a serialization-closed carrier tokenization
54
+ * with round-trip property tests (carrier count, normalized origin, ambiguity verdict and rev all
55
+ * invariant across serialize/parse at every boundary) — a slice-2 admission item, not a patch.
56
+ */
57
+ export declare function ambiguousOriginRepresentation(fm: Pick<MemoryEntryFrontmatter, "origin" | "extra">): boolean;
58
+ /**
59
+ * design/336 §2.3-1 — the harvest-side STRIP of model-written origin bytes (typed field AND
60
+ * origin-form `extra` lines, a block header's indented continuation included). The origin seat is
61
+ * engine-authored in every mode: whatever a file on the model-writable plane says about its own
62
+ * origin is not evidence — the engine strips it and mints (or carries forward) its own judgment.
63
+ * Same family as the invalid-id "dropped, fresh one minted" exception to the round-trip law.
64
+ * MUTATES the given (already-copied) frontmatter; returns true ⇔ anything was stripped (the
65
+ * caller discloses via a report warning).
66
+ */
67
+ export declare function stripModelWrittenOrigin(fm: MemoryEntryFrontmatter): boolean;
27
68
  /** Parse a file's text directly into a full MemoryEntry (id must already be present/decided). */
28
69
  export declare function entryFromFile(text: string, id: string, slug: string, scope: string): MemoryEntry;