@sema-agent/core 5.45.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.
- package/CHANGELOG.md +56 -0
- package/dist/agents/subagent.js +1 -1
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/hooks.d.ts +9 -2
- package/dist/core/hooks.js +6 -5
- package/dist/core/memory-engine/content-origin.d.ts +3 -1
- package/dist/core/memory-engine/engine.d.ts +50 -3
- package/dist/core/memory-engine/engine.js +194 -32
- package/dist/core/memory-engine/export-bundle.d.ts +10 -1
- package/dist/core/memory-engine/export-bundle.js +21 -0
- package/dist/core/memory-engine/file-backend.d.ts +33 -4
- package/dist/core/memory-engine/file-backend.js +165 -39
- package/dist/core/memory-engine/frontmatter.d.ts +42 -1
- package/dist/core/memory-engine/frontmatter.js +141 -1
- package/dist/core/memory-engine/header-hints.d.ts +17 -0
- package/dist/core/memory-engine/header-hints.js +6 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +25 -2
- package/dist/core/memory-engine/layout.js +25 -12
- package/dist/core/memory-engine/memory-backend-contract.js +65 -0
- package/dist/core/memory-engine/sync-client.d.ts +1 -1
- package/dist/core/memory-engine/sync-client.js +33 -1
- package/dist/core/memory-engine/tools.d.ts +7 -0
- package/dist/core/memory-engine/tools.js +3 -0
- package/dist/core/memory-engine/types.d.ts +75 -1
- package/dist/core/memory-engine/types.js +1 -1
- package/dist/core/reminder-mint.d.ts +70 -0
- package/dist/core/reminder-mint.js +25 -0
- package/dist/core/runner/git-status-frame.d.ts +3 -14
- package/dist/core/runner/git-status-frame.js +39 -14
- package/dist/core/runner/prepare-config-doors.d.ts +4 -0
- package/dist/core/runner/prepare-config-doors.js +15 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
- package/dist/core/runner/prepare-hands-readface.js +26 -0
- package/dist/core/runner/prepare-memory.d.ts +11 -0
- package/dist/core/runner/prepare-memory.js +12 -10
- package/dist/core/runner/prepare-task.d.ts +22 -1
- package/dist/core/runner/prepare-task.js +48 -13
- package/dist/core/runner/runtask.js +62 -55
- package/dist/core/side-query.d.ts +11 -1
- package/dist/core/side-query.js +3 -0
- package/dist/core/types.d.ts +47 -7
- package/dist/engine/harness/types.d.ts +46 -1
- package/dist/engine/harness/types.js +11 -0
- package/dist/engine/session/import-validate.js +6 -1
- package/dist/engine/session/session.d.ts +20 -0
- package/dist/engine/session/session.js +26 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/orchestration/run-workflow-tool.d.ts +16 -0
- package/dist/orchestration/run-workflow-tool.js +23 -3
- package/dist/orchestration/workflow-governance.d.ts +8 -1
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/types.d.ts +6 -0
- package/dist/prompts/default.d.ts +13 -1
- package/dist/prompts/default.js +5 -1
- package/dist/tools/fs/fs-bash.d.ts +4 -0
- package/dist/tools/fs/fs-bash.js +1 -1
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +8 -7
- package/dist/tools/fs/fs-shared.d.ts +10 -4
- package/dist/tools/fs/fs-shared.js +6 -3
- package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
- package/dist/tools/fs/gh-rate-limit.js +3 -2
- package/dist/tools/fs/index.d.ts +10 -2
- package/dist/tools/fs/index.js +2 -1
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
1114
|
-
if (
|
|
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
|
-
|
|
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
|
|
1703
|
+
if (committedFm === undefined)
|
|
1632
1704
|
return undefined;
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
|
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;
|