@remnic/core 9.51.1 → 9.52.1
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/dist/access-admin-ops-surface.d.ts +6 -5
- package/dist/access-admin-ops-surface.js +16 -16
- package/dist/access-authorization-probe.d.ts +6 -5
- package/dist/access-authorization-probe.js +17 -17
- package/dist/access-boundary.d.ts +6 -5
- package/dist/access-boundary.js +16 -16
- package/dist/access-cli.js +39 -39
- package/dist/access-extraction-force-flush.d.ts +6 -5
- package/dist/access-extraction-force-flush.js +16 -16
- package/dist/access-http-lcm-compaction.d.ts +6 -5
- package/dist/access-http-lifecycle-flush.d.ts +6 -5
- package/dist/access-http-offline-stream.d.ts +6 -5
- package/dist/access-http-query.js +16 -16
- package/dist/access-http.d.ts +6 -5
- package/dist/access-http.js +22 -22
- package/dist/access-identity-continuity-surface.d.ts +5 -4
- package/dist/access-identity-continuity-surface.js +16 -16
- package/dist/access-lcm-surface.d.ts +6 -5
- package/dist/access-lcm-surface.js +16 -16
- package/dist/access-mcp.d.ts +6 -5
- package/dist/access-mcp.js +20 -20
- package/dist/access-namespace-preflight.js +16 -16
- package/dist/access-observe-write-surface.d.ts +6 -5
- package/dist/access-observe-write-surface.js +16 -16
- package/dist/access-offline-manifest.d.ts +5 -4
- package/dist/access-operations-batch.js +17 -17
- package/dist/access-operations.d.ts +9 -8
- package/dist/access-operations.js +19 -19
- package/dist/access-recall-concurrency.d.ts +6 -5
- package/dist/access-recall-concurrency.js +16 -16
- package/dist/access-recall-response.d.ts +6 -5
- package/dist/access-recall-response.js +16 -16
- package/dist/access-recall-surface.d.ts +6 -5
- package/dist/access-recall-surface.js +16 -16
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-BY0KJ44c.d.ts → access-service-Cbk3bNbD.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +6 -5
- package/dist/access-service.d.ts +6 -5
- package/dist/access-service.js +16 -16
- package/dist/access-surface-catalog.d.ts +6 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/bootstrap.d.ts +5 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -7
- package/dist/buffer.d.ts +1 -1
- package/dist/{capsule-merge-XMFBXVDP.js → capsule-merge-HCJJMLPS.js} +4 -4
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-PWN32TAG.js → chunk-2ASAS3CQ.js} +2 -2
- package/dist/{chunk-WQM326GS.js → chunk-2LY6GIIE.js} +2 -2
- package/dist/{chunk-JL43BVDE.js → chunk-2OIQDTDC.js} +2 -2
- package/dist/{chunk-33SV3E43.js → chunk-3OY7KXHM.js} +3 -3
- package/dist/{chunk-AIUBBHSF.js → chunk-5UZ6UCOZ.js} +3 -3
- package/dist/chunk-5WITETWU.js +591 -0
- package/dist/chunk-5WITETWU.js.map +1 -0
- package/dist/{chunk-UT5BV5OG.js → chunk-7HXZJAID.js} +2 -2
- package/dist/{chunk-M7BXDKPR.js → chunk-7M6NYQWB.js} +3 -3
- package/dist/{chunk-TPTECFWR.js → chunk-B6LZ4IBH.js} +3 -3
- package/dist/{chunk-J5LLX6IL.js → chunk-BF6SXNQJ.js} +10 -10
- package/dist/{chunk-4PC4PRJE.js → chunk-BKIFTXHR.js} +2 -2
- package/dist/{chunk-VVG2BOKA.js → chunk-BMEYMKWX.js} +2 -2
- package/dist/{chunk-DCZBSE4F.js → chunk-D5YOZ66N.js} +18 -18
- package/dist/{chunk-BHXYAVUM.js → chunk-DLG5V6ND.js} +2 -2
- package/dist/{chunk-52UKFTUK.js → chunk-F4EU6VUC.js} +2 -2
- package/dist/{chunk-M4A44LUA.js → chunk-GSTIBXBW.js} +2 -2
- package/dist/{chunk-W5XVQFLM.js → chunk-GXFSQ6SD.js} +2 -2
- package/dist/{chunk-XADV5IZ3.js → chunk-HDIUYMSB.js} +5 -5
- package/dist/{chunk-FXTYIA46.js → chunk-J272WKX2.js} +2 -2
- package/dist/{chunk-ENTCVTQR.js → chunk-JJRVPWFY.js} +11 -1
- package/dist/chunk-JJRVPWFY.js.map +1 -0
- package/dist/{chunk-JYL3BVVZ.js → chunk-JSEJH2YQ.js} +2 -2
- package/dist/{chunk-5UDMFE3Q.js → chunk-MLXZD4WM.js} +2 -2
- package/dist/{chunk-PUSLMZBQ.js → chunk-NOEMGE56.js} +2 -2
- package/dist/{chunk-C5QLI33R.js → chunk-QINJITDS.js} +2 -2
- package/dist/{chunk-45BRYON6.js → chunk-QPOHMO6M.js} +2 -2
- package/dist/{chunk-ZQOCRIGA.js → chunk-QYN5PXDZ.js} +3 -3
- package/dist/{chunk-PA45XCUD.js → chunk-RANKJI5T.js} +2 -2
- package/dist/{chunk-ZHY5TMJG.js → chunk-RMHMUUBW.js} +2392 -628
- package/dist/chunk-RMHMUUBW.js.map +1 -0
- package/dist/{chunk-37ZO6LLJ.js → chunk-RYLEDKQ2.js} +2 -2
- package/dist/{chunk-CV6BBVYV.js → chunk-TEMSWHQD.js} +2 -2
- package/dist/{chunk-QGR6OVGT.js → chunk-VFSAWRMZ.js} +2 -2
- package/dist/{chunk-NLK2VZYZ.js → chunk-VXCNQYTC.js} +2 -2
- package/dist/{chunk-AZ2QBGYB.js → chunk-W7Y5JDVB.js} +1 -1
- package/dist/{chunk-VBHPOWBJ.js → chunk-WD5B5AIE.js} +1051 -591
- package/dist/chunk-WD5B5AIE.js.map +1 -0
- package/dist/{chunk-OYESWJ43.js → chunk-WLRD2TXF.js} +15 -10
- package/dist/chunk-WLRD2TXF.js.map +1 -0
- package/dist/{chunk-4F6X3RXW.js → chunk-X57M2Q4I.js} +2 -2
- package/dist/{chunk-6MTSFX7A.js → chunk-X7XEDOSL.js} +8 -8
- package/dist/{chunk-UEVLYVNW.js → chunk-YXKVLYXU.js} +2 -2
- package/dist/{chunk-OQAL6SIR.js → chunk-ZMSZVISA.js} +2 -2
- package/dist/{cli-CnOkH9hA.d.ts → cli-DMAFI0qt.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +33 -33
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -7
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/index.js +7 -7
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/consolidation-undo.js +3 -3
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/convergence-refresh.d.ts +1 -1
- package/dist/convergence-refresh.js +9 -9
- package/dist/corpus-watermark.js +9 -9
- package/dist/dependency-propagation-delivery-D63r24pH.d.ts +41 -0
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +7 -7
- package/dist/explicit-capture.d.ts +5 -4
- package/dist/explicit-capture.js +4 -4
- package/dist/external-wiki-access.d.ts +6 -5
- package/dist/external-wiki-access.js +17 -17
- package/dist/external-wiki-mcp-tools.d.ts +6 -5
- package/dist/extraction.js +7 -7
- package/dist/{forget-UJDJHG6R.js → forget-HDS4N6CG.js} +3 -3
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.d.ts +687 -686
- package/dist/index.js +39 -39
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- package/dist/{maintenance-CKwxpdRx.d.ts → maintenance-kBII2qUI.d.ts} +3 -2
- package/dist/mcp-memory-inspector-app.d.ts +6 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +7 -7
- package/dist/operator-doctor-corpus.js +10 -10
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +15 -15
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +12 -12
- package/dist/{orchestrator-BshGam-o.d.ts → orchestrator-DHy5LtTy.d.ts} +20 -35
- package/dist/orchestrator.d.ts +5 -4
- package/dist/orchestrator.js +39 -39
- package/dist/page-versioning.js +3 -3
- package/dist/schemas.d.ts +138 -138
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +7 -7
- package/dist/{service-BIgVM7fL.d.ts → service-PT2_z7xm.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +7 -7
- package/dist/{storage-CVfGWqCI.d.ts → storage-BmVudhk4.d.ts} +46 -27
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +6 -6
- package/dist/temporal-supersession.d.ts +51 -26
- package/dist/temporal-supersession.js +3 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/capsule-import.js +4 -4
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +7 -7
- package/package.json +2 -2
- package/src/lifecycle/tombstones.ts +30 -0
- package/src/orchestration/consolidation-run.ts +324 -106
- package/src/orchestration/contradiction-linking-coordinator.ts +237 -64
- package/src/orchestration/dependency-propagation-delivery.test.ts +2353 -0
- package/src/orchestration/dependency-propagation-delivery.ts +1143 -0
- package/src/orchestration/dependency-propagation-queue-state.ts +220 -0
- package/src/orchestration/dependency-propagation-recovery-replay.ts +237 -0
- package/src/orchestration/dependency-propagation-replay-reconciliation.ts +73 -0
- package/src/orchestration/dependency-propagation-triggers.test.ts +1069 -11
- package/src/orchestration/dependency-propagation.test.ts +82 -31
- package/src/orchestration/dependency-propagation.ts +55 -17
- package/src/orchestration/extraction-persist-deps.ts +2 -2
- package/src/orchestration/extraction-persist.ts +14 -15
- package/src/orchestration/extraction-queue-coordinator.ts +23 -1
- package/src/orchestration/extraction-shutdown.ts +29 -0
- package/src/orchestration/maintenance.ts +13 -7
- package/src/orchestration/turn-ingestion.ts +5 -1
- package/src/orchestrator-background-writes.test.ts +265 -1
- package/src/orchestrator-extraction-queue.test.ts +39 -0
- package/src/orchestrator.ts +59 -58
- package/src/storage/committed-invalidation.ts +34 -0
- package/src/storage/deletion-revision-store.ts +529 -0
- package/src/storage/supersession-audit.ts +41 -0
- package/src/storage/supersession-side-effects.ts +52 -25
- package/src/storage-contract/failure-semantics.test.ts +12 -0
- package/src/storage-contract/round-trip.test.ts +184 -2
- package/src/storage-deletion-revisions.test.ts +213 -1
- package/src/storage-frontmatter-cas.test.ts +141 -0
- package/src/storage-memory-lifecycle-ledger.test.ts +372 -0
- package/src/storage-replay.test.ts +149 -0
- package/src/storage.ts +345 -358
- package/src/temporal-supersession.test.ts +431 -2
- package/src/temporal-supersession.ts +567 -107
- package/src/testing/subjects/dependency-propagation.test.ts +179 -38
- package/dist/chunk-ENTCVTQR.js.map +0 -1
- package/dist/chunk-EWMTOVMY.js +0 -484
- package/dist/chunk-EWMTOVMY.js.map +0 -1
- package/dist/chunk-OYESWJ43.js.map +0 -1
- package/dist/chunk-VBHPOWBJ.js.map +0 -1
- package/dist/chunk-ZHY5TMJG.js.map +0 -1
- /package/dist/{capsule-merge-XMFBXVDP.js.map → capsule-merge-HCJJMLPS.js.map} +0 -0
- /package/dist/{chunk-PWN32TAG.js.map → chunk-2ASAS3CQ.js.map} +0 -0
- /package/dist/{chunk-WQM326GS.js.map → chunk-2LY6GIIE.js.map} +0 -0
- /package/dist/{chunk-JL43BVDE.js.map → chunk-2OIQDTDC.js.map} +0 -0
- /package/dist/{chunk-33SV3E43.js.map → chunk-3OY7KXHM.js.map} +0 -0
- /package/dist/{chunk-AIUBBHSF.js.map → chunk-5UZ6UCOZ.js.map} +0 -0
- /package/dist/{chunk-UT5BV5OG.js.map → chunk-7HXZJAID.js.map} +0 -0
- /package/dist/{chunk-M7BXDKPR.js.map → chunk-7M6NYQWB.js.map} +0 -0
- /package/dist/{chunk-TPTECFWR.js.map → chunk-B6LZ4IBH.js.map} +0 -0
- /package/dist/{chunk-J5LLX6IL.js.map → chunk-BF6SXNQJ.js.map} +0 -0
- /package/dist/{chunk-4PC4PRJE.js.map → chunk-BKIFTXHR.js.map} +0 -0
- /package/dist/{chunk-VVG2BOKA.js.map → chunk-BMEYMKWX.js.map} +0 -0
- /package/dist/{chunk-DCZBSE4F.js.map → chunk-D5YOZ66N.js.map} +0 -0
- /package/dist/{chunk-BHXYAVUM.js.map → chunk-DLG5V6ND.js.map} +0 -0
- /package/dist/{chunk-52UKFTUK.js.map → chunk-F4EU6VUC.js.map} +0 -0
- /package/dist/{chunk-M4A44LUA.js.map → chunk-GSTIBXBW.js.map} +0 -0
- /package/dist/{chunk-W5XVQFLM.js.map → chunk-GXFSQ6SD.js.map} +0 -0
- /package/dist/{chunk-XADV5IZ3.js.map → chunk-HDIUYMSB.js.map} +0 -0
- /package/dist/{chunk-FXTYIA46.js.map → chunk-J272WKX2.js.map} +0 -0
- /package/dist/{chunk-JYL3BVVZ.js.map → chunk-JSEJH2YQ.js.map} +0 -0
- /package/dist/{chunk-5UDMFE3Q.js.map → chunk-MLXZD4WM.js.map} +0 -0
- /package/dist/{chunk-PUSLMZBQ.js.map → chunk-NOEMGE56.js.map} +0 -0
- /package/dist/{chunk-C5QLI33R.js.map → chunk-QINJITDS.js.map} +0 -0
- /package/dist/{chunk-45BRYON6.js.map → chunk-QPOHMO6M.js.map} +0 -0
- /package/dist/{chunk-ZQOCRIGA.js.map → chunk-QYN5PXDZ.js.map} +0 -0
- /package/dist/{chunk-PA45XCUD.js.map → chunk-RANKJI5T.js.map} +0 -0
- /package/dist/{chunk-37ZO6LLJ.js.map → chunk-RYLEDKQ2.js.map} +0 -0
- /package/dist/{chunk-CV6BBVYV.js.map → chunk-TEMSWHQD.js.map} +0 -0
- /package/dist/{chunk-QGR6OVGT.js.map → chunk-VFSAWRMZ.js.map} +0 -0
- /package/dist/{chunk-NLK2VZYZ.js.map → chunk-VXCNQYTC.js.map} +0 -0
- /package/dist/{chunk-AZ2QBGYB.js.map → chunk-W7Y5JDVB.js.map} +0 -0
- /package/dist/{chunk-4F6X3RXW.js.map → chunk-X57M2Q4I.js.map} +0 -0
- /package/dist/{chunk-6MTSFX7A.js.map → chunk-X7XEDOSL.js.map} +0 -0
- /package/dist/{chunk-UEVLYVNW.js.map → chunk-YXKVLYXU.js.map} +0 -0
- /package/dist/{chunk-OQAL6SIR.js.map → chunk-ZMSZVISA.js.map} +0 -0
- /package/dist/{forget-UJDJHG6R.js.map → forget-HDS4N6CG.js.map} +0 -0
|
@@ -6,6 +6,8 @@ import { ConsolidationRunCoordinator } from "./consolidation-run.js";
|
|
|
6
6
|
import type { ExtractionEngine } from "../extraction.js";
|
|
7
7
|
import type { MemoryFile, PluginConfig } from "../types.js";
|
|
8
8
|
import type { StorageManager } from "../storage.js";
|
|
9
|
+
import { propagateInvalidation, type PropagationEvent } from "./dependency-propagation.js";
|
|
10
|
+
import type { DependencyPropagationPreparationToken } from "./dependency-propagation-delivery.js";
|
|
9
11
|
|
|
10
12
|
const NOW = "2026-08-08T00:00:00.000Z";
|
|
11
13
|
|
|
@@ -55,26 +57,145 @@ function propagationConfig(): PluginConfig {
|
|
|
55
57
|
},
|
|
56
58
|
} as unknown as PluginConfig;
|
|
57
59
|
}
|
|
60
|
+
type TombstoneInput = Parameters<StorageManager["appendTombstone"]>[0];
|
|
61
|
+
|
|
58
62
|
|
|
59
63
|
type TriggerStorage = {
|
|
60
64
|
memories: Map<string, MemoryFile>;
|
|
61
65
|
supersedeCalls: Array<{ id: string; replacementId: string; reason: string }>;
|
|
66
|
+
tombstoneInputs: TombstoneInput[];
|
|
67
|
+
tombstoneIds: Array<string | null>;
|
|
62
68
|
frontmatterCalls: Array<{ id: string; patch: Record<string, unknown> }>;
|
|
63
69
|
invalidateCalls: string[];
|
|
70
|
+
order: string[];
|
|
64
71
|
storage: StorageManager;
|
|
65
72
|
};
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
type SyntheticImmediateDelivery = {
|
|
75
|
+
prepare(event: PropagationEvent): Promise<DependencyPropagationPreparationToken | null>;
|
|
76
|
+
afterMutation(token: DependencyPropagationPreparationToken | null, event: PropagationEvent): Promise<void>;
|
|
77
|
+
cancel(token: DependencyPropagationPreparationToken | null): Promise<void>;
|
|
78
|
+
deferPrepared(token: DependencyPropagationPreparationToken | null): Promise<void>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type SyntheticImmediateDeliveryOptions = {
|
|
82
|
+
prepareReturnsNull?: boolean;
|
|
83
|
+
requireExactReplacement?: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function makeSyntheticImmediateDelivery(
|
|
87
|
+
storage: StorageManager,
|
|
88
|
+
extraction: ExtractionEngine,
|
|
89
|
+
config: PluginConfig,
|
|
90
|
+
order: string[],
|
|
91
|
+
options: SyntheticImmediateDeliveryOptions = {},
|
|
92
|
+
): SyntheticImmediateDelivery {
|
|
93
|
+
const prepared = new Map<string, PropagationEvent>();
|
|
94
|
+
let nextJob = 0;
|
|
95
|
+
return {
|
|
96
|
+
async prepare(event) {
|
|
97
|
+
order.push(`prepare:${event.cause}`);
|
|
98
|
+
if (options.prepareReturnsNull) return null;
|
|
99
|
+
const jobId = `synthetic-job-${++nextJob}`;
|
|
100
|
+
prepared.set(jobId, event);
|
|
101
|
+
return { jobId, revision: 0, ownsPreparedJob: true, reservationId: `reservation-${jobId}` };
|
|
102
|
+
},
|
|
103
|
+
async afterMutation(token, event) {
|
|
104
|
+
if (token !== null) assert.ok(prepared.has(token.jobId));
|
|
105
|
+
if (
|
|
106
|
+
options.requireExactReplacement &&
|
|
107
|
+
event.replacementId !== null &&
|
|
108
|
+
event.replacementContent !== null
|
|
109
|
+
) {
|
|
110
|
+
const replacement = await storage.getMemoryById(event.replacementId);
|
|
111
|
+
if (!replacement || replacement.content !== event.replacementContent) return;
|
|
112
|
+
}
|
|
113
|
+
order.push(`afterMutation:${event.cause}`);
|
|
114
|
+
order.push(`ready:${event.cause}`);
|
|
115
|
+
await propagateInvalidation({ storage, extraction, config }, event);
|
|
116
|
+
},
|
|
117
|
+
async cancel(token) {
|
|
118
|
+
if (token === null) {
|
|
119
|
+
order.push("cancel:null");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const event = prepared.get(token.jobId);
|
|
123
|
+
assert.ok(event);
|
|
124
|
+
order.push(`cancel:${event.cause}`);
|
|
125
|
+
prepared.delete(token.jobId);
|
|
126
|
+
},
|
|
127
|
+
async deferPrepared(token) {
|
|
128
|
+
const event = token === null ? undefined : prepared.get(token.jobId);
|
|
129
|
+
order.push(`defer:${event?.cause ?? "null"}`);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertReadyAfterMutation(
|
|
135
|
+
order: string[],
|
|
136
|
+
cause: PropagationEvent["cause"],
|
|
137
|
+
primaryMutation: string,
|
|
138
|
+
): void {
|
|
139
|
+
const prepare = order.indexOf(`prepare:${cause}`);
|
|
140
|
+
const mutation = order.indexOf(primaryMutation);
|
|
141
|
+
const afterMutation = order.indexOf(`afterMutation:${cause}`);
|
|
142
|
+
const ready = order.indexOf(`ready:${cause}`);
|
|
143
|
+
assert.ok(prepare >= 0);
|
|
144
|
+
assert.ok(mutation >= 0);
|
|
145
|
+
assert.ok(afterMutation >= 0);
|
|
146
|
+
assert.ok(ready >= 0);
|
|
147
|
+
assert.ok(prepare < mutation);
|
|
148
|
+
assert.ok(mutation < afterMutation);
|
|
149
|
+
assert.ok(afterMutation < ready);
|
|
150
|
+
}
|
|
151
|
+
function makeStorage(
|
|
152
|
+
initial: MemoryFile[],
|
|
153
|
+
options: {
|
|
154
|
+
failSupersedeIds?: string[];
|
|
155
|
+
partialSupersedeIds?: string[];
|
|
156
|
+
failStateReadAfterSupersedeIds?: string[];
|
|
157
|
+
failPrePrimaryReadIds?: string[];
|
|
158
|
+
failInvalidateIds?: string[];
|
|
159
|
+
throwAfterUpdateIds?: string[];
|
|
160
|
+
partialFrontmatterIds?: string[];
|
|
161
|
+
mutateBeforeInvalidateIds?: string[];
|
|
162
|
+
touchAccessBeforeInvalidateIds?: string[];
|
|
163
|
+
} = {},
|
|
164
|
+
): TriggerStorage {
|
|
68
165
|
const memories = new Map(initial.map((item) => [item.frontmatter.id, item]));
|
|
166
|
+
const tombstoneInputs: TombstoneInput[] = [];
|
|
167
|
+
const tombstoneIds: Array<string | null> = [];
|
|
69
168
|
const supersedeCalls: TriggerStorage["supersedeCalls"] = [];
|
|
70
169
|
const frontmatterCalls: TriggerStorage["frontmatterCalls"] = [];
|
|
71
170
|
const invalidateCalls: string[] = [];
|
|
171
|
+
const failSupersedeIds = new Set(options.failSupersedeIds ?? []);
|
|
172
|
+
const partialSupersedeIds = new Set(options.partialSupersedeIds ?? []);
|
|
173
|
+
const failStateReadAfterSupersedeIds = new Set(options.failStateReadAfterSupersedeIds ?? []);
|
|
174
|
+
const failPrePrimaryReadIds = new Set(options.failPrePrimaryReadIds ?? []);
|
|
175
|
+
const failInvalidateIds = new Set(options.failInvalidateIds ?? []);
|
|
176
|
+
const throwAfterUpdateIds = new Set(options.throwAfterUpdateIds ?? []);
|
|
177
|
+
const partialFrontmatterIds = new Set(options.partialFrontmatterIds ?? []);
|
|
178
|
+
const mutateBeforeInvalidateIds = new Set(options.mutateBeforeInvalidateIds ?? []);
|
|
179
|
+
const touchAccessBeforeInvalidateIds = new Set(options.touchAccessBeforeInvalidateIds ?? []);
|
|
180
|
+
const order: string[] = [];
|
|
72
181
|
const storage = {
|
|
73
182
|
memories,
|
|
74
183
|
supersedeCalls,
|
|
184
|
+
tombstoneInputs,
|
|
185
|
+
tombstoneIds,
|
|
75
186
|
frontmatterCalls,
|
|
76
187
|
invalidateCalls,
|
|
188
|
+
order,
|
|
77
189
|
async getMemoryById(id: string): Promise<MemoryFile | null> {
|
|
190
|
+
if (
|
|
191
|
+
failPrePrimaryReadIds.has(id) &&
|
|
192
|
+
order.some((entry) => entry.startsWith("prepare:consolidation_"))
|
|
193
|
+
) {
|
|
194
|
+
throw new Error("synthetic pre-primary read failure");
|
|
195
|
+
}
|
|
196
|
+
if (failStateReadAfterSupersedeIds.has(id) && order.includes(`supersede:${id}`)) {
|
|
197
|
+
throw new Error("synthetic post-mutation read failure");
|
|
198
|
+
}
|
|
78
199
|
return memories.get(id) ?? null;
|
|
79
200
|
},
|
|
80
201
|
async readAllMemories(): Promise<MemoryFile[]> {
|
|
@@ -83,13 +204,32 @@ function makeStorage(initial: MemoryFile[]): TriggerStorage {
|
|
|
83
204
|
async readAllColdMemories(): Promise<MemoryFile[]> {
|
|
84
205
|
return [];
|
|
85
206
|
},
|
|
86
|
-
async readMemoryByPath(
|
|
87
|
-
return [...memories.values()].find((memory) => memory.path ===
|
|
207
|
+
async readMemoryByPath(memoryPath: string): Promise<MemoryFile | null> {
|
|
208
|
+
return [...memories.values()].find((memory) => memory.path === memoryPath) ?? null;
|
|
209
|
+
},
|
|
210
|
+
async getMemoryTimeline(): Promise<[]> {
|
|
211
|
+
return [];
|
|
88
212
|
},
|
|
89
213
|
async readProfile(): Promise<string> {
|
|
90
214
|
return "";
|
|
91
215
|
},
|
|
92
|
-
async appendTombstone(): Promise<
|
|
216
|
+
async appendTombstone(input: TombstoneInput): Promise<string | null> {
|
|
217
|
+
const recordedInput = structuredClone(input);
|
|
218
|
+
const returnedId = `synthetic-tombstone-${input.sourceMemoryId}`;
|
|
219
|
+
tombstoneInputs.push(recordedInput);
|
|
220
|
+
tombstoneIds.push(returnedId);
|
|
221
|
+
return returnedId;
|
|
222
|
+
},
|
|
223
|
+
async hasExactTombstone(input: TombstoneInput): Promise<boolean> {
|
|
224
|
+
return tombstoneInputs.some((existing) =>
|
|
225
|
+
existing.sourceMemoryId === input.sourceMemoryId &&
|
|
226
|
+
existing.contentHash === input.contentHash &&
|
|
227
|
+
existing.entityRef === input.entityRef &&
|
|
228
|
+
existing.supersessionKey === input.supersessionKey &&
|
|
229
|
+
existing.createdAt === input.createdAt &&
|
|
230
|
+
existing.operationKey === input.operationKey
|
|
231
|
+
);
|
|
232
|
+
},
|
|
93
233
|
async mergeFragmentedEntities(): Promise<number> {
|
|
94
234
|
return 0;
|
|
95
235
|
},
|
|
@@ -106,29 +246,100 @@ function makeStorage(initial: MemoryFile[]): TriggerStorage {
|
|
|
106
246
|
return {};
|
|
107
247
|
},
|
|
108
248
|
async saveMeta(): Promise<void> {},
|
|
109
|
-
async supersedeMemory(
|
|
249
|
+
async supersedeMemory(
|
|
250
|
+
id: string,
|
|
251
|
+
replacementId: string,
|
|
252
|
+
reason: string,
|
|
253
|
+
metadata?: Record<string, unknown>,
|
|
254
|
+
): Promise<boolean> {
|
|
255
|
+
order.push(`supersede:${id}`);
|
|
110
256
|
supersedeCalls.push({ id, replacementId, reason });
|
|
111
257
|
const current = memories.get(id);
|
|
112
|
-
if (!current) return false;
|
|
113
|
-
current.frontmatter
|
|
114
|
-
|
|
258
|
+
if (!current || failSupersedeIds.has(id)) return false;
|
|
259
|
+
Object.assign(current.frontmatter, metadata, {
|
|
260
|
+
status: "superseded",
|
|
261
|
+
supersededBy: replacementId,
|
|
262
|
+
});
|
|
263
|
+
if (partialSupersedeIds.has(id)) return false;
|
|
115
264
|
return true;
|
|
116
265
|
},
|
|
117
266
|
async writeMemoryFrontmatter(current: MemoryFile, patch: Record<string, unknown>): Promise<boolean> {
|
|
267
|
+
order.push(`frontmatter:${current.frontmatter.id}`);
|
|
118
268
|
frontmatterCalls.push({ id: current.frontmatter.id, patch });
|
|
119
269
|
const stored = memories.get(current.frontmatter.id);
|
|
120
270
|
if (!stored) return false;
|
|
121
271
|
Object.assign(stored.frontmatter, patch);
|
|
272
|
+
if (partialFrontmatterIds.has(current.frontmatter.id)) return false;
|
|
122
273
|
return true;
|
|
123
274
|
},
|
|
124
|
-
async
|
|
275
|
+
async writeMemoryFrontmatterIfUnchanged(
|
|
276
|
+
current: MemoryFile,
|
|
277
|
+
patch: Record<string, unknown>,
|
|
278
|
+
): Promise<boolean> {
|
|
279
|
+
return storage.writeMemoryFrontmatter(current, patch);
|
|
280
|
+
},
|
|
281
|
+
async invalidateMemory(
|
|
282
|
+
id: string,
|
|
283
|
+
expectedSnapshot?: Pick<MemoryFile, "content" | "frontmatter">,
|
|
284
|
+
): Promise<boolean> {
|
|
285
|
+
order.push(`invalidate:${id}`);
|
|
125
286
|
invalidateCalls.push(id);
|
|
287
|
+
if (failInvalidateIds.has(id)) return false;
|
|
288
|
+
const current = memories.get(id);
|
|
289
|
+
if (!current) return false;
|
|
290
|
+
if (mutateBeforeInvalidateIds.has(id)) {
|
|
291
|
+
current.content = "concurrent source update";
|
|
292
|
+
mutateBeforeInvalidateIds.delete(id);
|
|
293
|
+
}
|
|
294
|
+
if (touchAccessBeforeInvalidateIds.has(id)) {
|
|
295
|
+
current.frontmatter.accessCount = 4;
|
|
296
|
+
current.frontmatter.lastAccessed = "2026-08-10T00:00:00.000Z";
|
|
297
|
+
touchAccessBeforeInvalidateIds.delete(id);
|
|
298
|
+
}
|
|
299
|
+
if (
|
|
300
|
+
expectedSnapshot &&
|
|
301
|
+
(current.content !== expectedSnapshot.content ||
|
|
302
|
+
JSON.stringify({ ...current.frontmatter, accessCount: undefined, lastAccessed: undefined }) !==
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
...expectedSnapshot.frontmatter,
|
|
305
|
+
accessCount: undefined,
|
|
306
|
+
lastAccessed: undefined,
|
|
307
|
+
}))
|
|
308
|
+
) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
126
311
|
return memories.delete(id);
|
|
127
312
|
},
|
|
313
|
+
async updateMemory(
|
|
314
|
+
id: string,
|
|
315
|
+
newContent: string,
|
|
316
|
+
patch?: { supersedes?: string; lineage?: string[] },
|
|
317
|
+
): Promise<boolean> {
|
|
318
|
+
order.push(`update:${id}`);
|
|
319
|
+
const current = memories.get(id);
|
|
320
|
+
if (!current) return false;
|
|
321
|
+
current.content = newContent;
|
|
322
|
+
if (patch?.supersedes !== undefined) current.frontmatter.supersedes = patch.supersedes;
|
|
323
|
+
if (patch?.lineage !== undefined) current.frontmatter.lineage = patch.lineage;
|
|
324
|
+
if (throwAfterUpdateIds.has(id)) {
|
|
325
|
+
throw new Error("synthetic post-write update failure");
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
},
|
|
128
329
|
} as unknown as StorageManager;
|
|
129
|
-
return {
|
|
330
|
+
return {
|
|
331
|
+
memories,
|
|
332
|
+
supersedeCalls,
|
|
333
|
+
tombstoneInputs,
|
|
334
|
+
tombstoneIds,
|
|
335
|
+
frontmatterCalls,
|
|
336
|
+
invalidateCalls,
|
|
337
|
+
order,
|
|
338
|
+
storage,
|
|
339
|
+
};
|
|
130
340
|
}
|
|
131
341
|
|
|
342
|
+
|
|
132
343
|
function makeRevalidationExtraction(
|
|
133
344
|
revalidateCalls: Array<unknown>,
|
|
134
345
|
): ExtractionEngine {
|
|
@@ -151,6 +362,81 @@ function makeRevalidationExtraction(
|
|
|
151
362
|
} as unknown as ExtractionEngine;
|
|
152
363
|
}
|
|
153
364
|
|
|
365
|
+
function makeContradictionCoordinator(
|
|
366
|
+
fixture: TriggerStorage,
|
|
367
|
+
extraction: ExtractionEngine,
|
|
368
|
+
config: PluginConfig,
|
|
369
|
+
delivery: SyntheticImmediateDelivery,
|
|
370
|
+
): ContradictionLinkingCoordinator {
|
|
371
|
+
return new ContradictionLinkingCoordinator({
|
|
372
|
+
getConfig: () => config,
|
|
373
|
+
isSearchAvailable: () => false,
|
|
374
|
+
searchAcrossNamespaces: async () => [],
|
|
375
|
+
extractMemoryIdsFromResults: () => [],
|
|
376
|
+
namespaceFromPath: () => "namespace-a",
|
|
377
|
+
storageForNamespace: async () => fixture.storage,
|
|
378
|
+
getExtraction: () => extraction,
|
|
379
|
+
storageDirNamespace: () => "namespace-a",
|
|
380
|
+
getDependencyPropagationDelivery: () => delivery,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function makeConsolidationCoordinator(
|
|
385
|
+
fixture: TriggerStorage,
|
|
386
|
+
extraction: ExtractionEngine,
|
|
387
|
+
config: PluginConfig,
|
|
388
|
+
delivery: SyntheticImmediateDelivery,
|
|
389
|
+
): ConsolidationRunCoordinator {
|
|
390
|
+
return new ConsolidationRunCoordinator({
|
|
391
|
+
config,
|
|
392
|
+
getStorage: () => fixture.storage,
|
|
393
|
+
getStorageRouter: () => ({ recordWrite: async () => {} }) as never,
|
|
394
|
+
getExtraction: () => extraction,
|
|
395
|
+
storageDirNamespace: () => "namespace-a",
|
|
396
|
+
getDependencyPropagationDelivery: () => delivery,
|
|
397
|
+
embeddingFallback: { removeFromIndex: async () => {} } as never,
|
|
398
|
+
tmtBuilder: { maybeRebuildNodes: async () => {} } as never,
|
|
399
|
+
consolidationObservers: new Set(),
|
|
400
|
+
getAccessTrackingBuffer: () => new Map(),
|
|
401
|
+
lifecyclePolicyCoordinator: {
|
|
402
|
+
runFactArchival: async () => 0,
|
|
403
|
+
runSummarization: async () => {},
|
|
404
|
+
runTopicExtraction: async () => {},
|
|
405
|
+
} as never,
|
|
406
|
+
compressionGuidelineCoordinator: {
|
|
407
|
+
runCompressionGuidelineLearningPass: async () => {},
|
|
408
|
+
} as never,
|
|
409
|
+
semanticConsolidationCoordinator: {} as never,
|
|
410
|
+
entitySynthesisCoordinator: {} as never,
|
|
411
|
+
recallSectionCoordinator: {
|
|
412
|
+
getRecallSectionEntry: () => undefined,
|
|
413
|
+
} as never,
|
|
414
|
+
tierMigrationCoordinator: {
|
|
415
|
+
runCycle: async () => ({ migrated: 0 }),
|
|
416
|
+
} as never,
|
|
417
|
+
flushAccessTracking: async () => {},
|
|
418
|
+
indexPersistedMemory: async () => {},
|
|
419
|
+
autoConsolidateIdentity: async () => {},
|
|
420
|
+
fastChatCompletion: async () => null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function makeConsolidationExtraction(
|
|
425
|
+
revalidateCalls: Array<unknown>,
|
|
426
|
+
item: Record<string, unknown>,
|
|
427
|
+
): ExtractionEngine {
|
|
428
|
+
return {
|
|
429
|
+
...makeRevalidationExtraction(revalidateCalls),
|
|
430
|
+
async consolidate() {
|
|
431
|
+
return {
|
|
432
|
+
items: [item],
|
|
433
|
+
profileUpdates: [],
|
|
434
|
+
entityUpdates: [],
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
} as unknown as ExtractionEngine;
|
|
438
|
+
}
|
|
439
|
+
|
|
154
440
|
test("contradiction resolve propagates after superseding the captured old memory", async () => {
|
|
155
441
|
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
156
442
|
const replacement = makeMemory("replacement", { content: "new claim" });
|
|
@@ -159,6 +445,12 @@ test("contradiction resolve propagates after superseding the captured old memory
|
|
|
159
445
|
const revalidateCalls: unknown[] = [];
|
|
160
446
|
const extraction = makeRevalidationExtraction(revalidateCalls);
|
|
161
447
|
const config = propagationConfig();
|
|
448
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
449
|
+
fixture.storage,
|
|
450
|
+
extraction,
|
|
451
|
+
config,
|
|
452
|
+
fixture.order,
|
|
453
|
+
);
|
|
162
454
|
const coordinator = new ContradictionLinkingCoordinator({
|
|
163
455
|
getConfig: () => config,
|
|
164
456
|
isSearchAvailable: () => false,
|
|
@@ -167,6 +459,8 @@ test("contradiction resolve propagates after superseding the captured old memory
|
|
|
167
459
|
namespaceFromPath: () => "namespace-a",
|
|
168
460
|
storageForNamespace: async () => fixture.storage,
|
|
169
461
|
getExtraction: () => extraction,
|
|
462
|
+
storageDirNamespace: () => "namespace-a",
|
|
463
|
+
getDependencyPropagationDelivery: () => dependencyPropagationDelivery,
|
|
170
464
|
});
|
|
171
465
|
|
|
172
466
|
await coordinator.applyDeferredContradictionResolve(
|
|
@@ -189,6 +483,82 @@ test("contradiction resolve propagates after superseding the captured old memory
|
|
|
189
483
|
assert.equal(fixture.memories.get("dependent")?.frontmatter.supersessionCause, "dependency");
|
|
190
484
|
assert.equal(fixture.memories.get("dependent")?.frontmatter.invalidatedBy, "old");
|
|
191
485
|
assert.equal(revalidateCalls.length, 1);
|
|
486
|
+
assertReadyAfterMutation(fixture.order, "contradiction", "supersede:old");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("contradiction resolve skips propagation when replacement content is missing", async () => {
|
|
490
|
+
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
491
|
+
const dependent = makeMemory("dependent");
|
|
492
|
+
const fixture = makeStorage([old, dependent]);
|
|
493
|
+
const extraction = makeRevalidationExtraction([]);
|
|
494
|
+
const config = propagationConfig();
|
|
495
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
496
|
+
fixture.storage,
|
|
497
|
+
extraction,
|
|
498
|
+
config,
|
|
499
|
+
fixture.order,
|
|
500
|
+
);
|
|
501
|
+
const coordinator = makeContradictionCoordinator(fixture, extraction, config, delivery);
|
|
502
|
+
|
|
503
|
+
await coordinator.applyDeferredContradictionResolve(
|
|
504
|
+
{
|
|
505
|
+
supersededId: "old",
|
|
506
|
+
reason: "replacement unavailable",
|
|
507
|
+
supersededPath: old.path,
|
|
508
|
+
supersededCreated: old.frontmatter.created,
|
|
509
|
+
supersededTags: [],
|
|
510
|
+
},
|
|
511
|
+
fixture.storage,
|
|
512
|
+
"replacement",
|
|
513
|
+
false,
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
assert.deepEqual(fixture.supersedeCalls, [
|
|
517
|
+
{ id: "old", replacementId: "replacement", reason: "replacement unavailable" },
|
|
518
|
+
]);
|
|
519
|
+
assert.equal(fixture.order.some((entry) => entry === "prepare:contradiction"), false);
|
|
520
|
+
assert.equal(fixture.memories.get("dependent")?.frontmatter.status, "active");
|
|
521
|
+
});
|
|
522
|
+
test("contradiction resolve skips dependent retirement when replacement content changes", async () => {
|
|
523
|
+
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
524
|
+
const replacement = makeMemory("replacement", { content: "captured claim" });
|
|
525
|
+
const dependent = makeMemory("dependent");
|
|
526
|
+
const fixture = makeStorage([old, replacement, dependent]);
|
|
527
|
+
const extraction = makeRevalidationExtraction([]);
|
|
528
|
+
const config = propagationConfig();
|
|
529
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
530
|
+
fixture.storage,
|
|
531
|
+
extraction,
|
|
532
|
+
config,
|
|
533
|
+
fixture.order,
|
|
534
|
+
{ requireExactReplacement: true },
|
|
535
|
+
);
|
|
536
|
+
const originalSupersede = fixture.storage.supersedeMemory.bind(fixture.storage);
|
|
537
|
+
fixture.storage.supersedeMemory = async (...args) => {
|
|
538
|
+
const committed = await originalSupersede(...args);
|
|
539
|
+
replacement.content = "changed claim";
|
|
540
|
+
return committed;
|
|
541
|
+
};
|
|
542
|
+
const coordinator = makeContradictionCoordinator(fixture, extraction, config, delivery);
|
|
543
|
+
|
|
544
|
+
await coordinator.applyDeferredContradictionResolve(
|
|
545
|
+
{
|
|
546
|
+
supersededId: "old",
|
|
547
|
+
reason: "replacement changed",
|
|
548
|
+
supersededPath: old.path,
|
|
549
|
+
supersededCreated: old.frontmatter.created,
|
|
550
|
+
supersededTags: [],
|
|
551
|
+
},
|
|
552
|
+
fixture.storage,
|
|
553
|
+
"replacement",
|
|
554
|
+
false,
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
assert.deepEqual(fixture.supersedeCalls, [
|
|
558
|
+
{ id: "old", replacementId: "replacement", reason: "replacement changed" },
|
|
559
|
+
]);
|
|
560
|
+
assert.ok(fixture.order.includes("prepare:contradiction"));
|
|
561
|
+
assert.equal(fixture.memories.get("dependent")?.frontmatter.status, "active");
|
|
192
562
|
});
|
|
193
563
|
|
|
194
564
|
test("temporal supersession propagates to a dependent after the old fact is retired", async () => {
|
|
@@ -207,6 +577,13 @@ test("temporal supersession propagates to a dependent after the old fact is reti
|
|
|
207
577
|
const fixture = makeStorage([old, replacement, dependent]);
|
|
208
578
|
const revalidateCalls: unknown[] = [];
|
|
209
579
|
const extraction = makeRevalidationExtraction(revalidateCalls);
|
|
580
|
+
const config = propagationConfig();
|
|
581
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
582
|
+
fixture.storage,
|
|
583
|
+
extraction,
|
|
584
|
+
config,
|
|
585
|
+
fixture.order,
|
|
586
|
+
);
|
|
210
587
|
const temporalArgs = {
|
|
211
588
|
storage: fixture.storage,
|
|
212
589
|
newMemoryId: "replacement",
|
|
@@ -215,8 +592,9 @@ test("temporal supersession propagates to a dependent after the old fact is reti
|
|
|
215
592
|
createdAt: "2026-08-09T00:00:00.000Z",
|
|
216
593
|
enabled: true,
|
|
217
594
|
extraction,
|
|
218
|
-
config
|
|
595
|
+
config,
|
|
219
596
|
namespaceScope: "namespace-a",
|
|
597
|
+
dependencyPropagationDelivery,
|
|
220
598
|
};
|
|
221
599
|
|
|
222
600
|
const result = await (applyTemporalSupersession as unknown as (args: typeof temporalArgs) => Promise<{
|
|
@@ -226,6 +604,78 @@ test("temporal supersession propagates to a dependent after the old fact is reti
|
|
|
226
604
|
assert.deepEqual(result.supersededIds, ["old"]);
|
|
227
605
|
assert.equal(fixture.memories.get("dependent")?.frontmatter.invalidatedBy, "old");
|
|
228
606
|
assert.equal(revalidateCalls.length, 1);
|
|
607
|
+
assertReadyAfterMutation(fixture.order, "temporal_supersession", "frontmatter:old");
|
|
608
|
+
assert.equal(fixture.tombstoneInputs.length, 1);
|
|
609
|
+
assert.deepEqual(fixture.tombstoneIds, ["synthetic-tombstone-old"]);
|
|
610
|
+
const tombstone = fixture.tombstoneInputs[0];
|
|
611
|
+
assert.equal(tombstone.reason, "supersession");
|
|
612
|
+
assert.equal(tombstone.createdBy, "supersession");
|
|
613
|
+
assert.equal(tombstone.sourceMemoryId, "old");
|
|
614
|
+
assert.equal(tombstone.rawContent, old.content);
|
|
615
|
+
assert.equal(tombstone.entityRef, "project-a");
|
|
616
|
+
assert.equal(tombstone.supersessionKey, "project-a::city");
|
|
617
|
+
assert.equal(tombstone.createdAt, "2026-08-09T00:00:00.000Z");
|
|
618
|
+
assert.match(tombstone.operationKey ?? "", /^[a-f0-9]{64}:tombstone:project-a::city$/);
|
|
619
|
+
|
|
620
|
+
const replay = await (applyTemporalSupersession as unknown as (args: typeof temporalArgs) => Promise<{
|
|
621
|
+
supersededIds: string[];
|
|
622
|
+
}>)(temporalArgs);
|
|
623
|
+
assert.deepEqual(replay.supersededIds, []);
|
|
624
|
+
assert.equal(fixture.tombstoneInputs.length, 1);
|
|
625
|
+
});
|
|
626
|
+
test("temporal supersession defers prepared recovery after a committed false mutation", async () => {
|
|
627
|
+
const old = makeMemory("old", {
|
|
628
|
+
entityRef: "project-a",
|
|
629
|
+
structuredAttributes: { city: "Austin" },
|
|
630
|
+
links: [{ targetId: "dependent", linkType: "supports" }],
|
|
631
|
+
});
|
|
632
|
+
const replacement = makeMemory("replacement", {
|
|
633
|
+
entityRef: "project-a",
|
|
634
|
+
structuredAttributes: { city: "Boston" },
|
|
635
|
+
content: "project-a moved to Boston",
|
|
636
|
+
});
|
|
637
|
+
replacement.frontmatter.created = "2026-08-09T00:00:00.000Z";
|
|
638
|
+
const dependent = makeMemory("dependent");
|
|
639
|
+
const fixture = makeStorage([old, replacement, dependent], { partialFrontmatterIds: ["old"] });
|
|
640
|
+
const extraction = makeRevalidationExtraction([]);
|
|
641
|
+
const config = propagationConfig();
|
|
642
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
643
|
+
fixture.storage,
|
|
644
|
+
extraction,
|
|
645
|
+
config,
|
|
646
|
+
fixture.order,
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const result = await (applyTemporalSupersession as unknown as (args: {
|
|
650
|
+
storage: StorageManager;
|
|
651
|
+
newMemoryId: string;
|
|
652
|
+
entityRef: string;
|
|
653
|
+
structuredAttributes: Record<string, string>;
|
|
654
|
+
createdAt: string;
|
|
655
|
+
enabled: boolean;
|
|
656
|
+
extraction: ExtractionEngine;
|
|
657
|
+
config: PluginConfig;
|
|
658
|
+
namespaceScope: string;
|
|
659
|
+
dependencyPropagationDelivery: SyntheticImmediateDelivery;
|
|
660
|
+
}) => Promise<{ supersededIds: string[] }>)({
|
|
661
|
+
storage: fixture.storage,
|
|
662
|
+
newMemoryId: "replacement",
|
|
663
|
+
entityRef: "project-a",
|
|
664
|
+
structuredAttributes: { city: "Boston" },
|
|
665
|
+
createdAt: "2026-08-09T00:00:00.000Z",
|
|
666
|
+
enabled: true,
|
|
667
|
+
extraction,
|
|
668
|
+
config,
|
|
669
|
+
namespaceScope: "namespace-a",
|
|
670
|
+
dependencyPropagationDelivery,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
assert.deepEqual(result.supersededIds, []);
|
|
674
|
+
assert.equal(fixture.memories.get("old")?.frontmatter.status, "superseded");
|
|
675
|
+
assert.ok(fixture.order.includes("defer:temporal_supersession"));
|
|
676
|
+
assert.equal(fixture.order.includes("cancel:temporal_supersession"), false);
|
|
677
|
+
assert.equal(fixture.tombstoneInputs.length, 0);
|
|
678
|
+
assert.deepEqual(fixture.tombstoneIds, []);
|
|
229
679
|
});
|
|
230
680
|
|
|
231
681
|
test("consolidation INVALIDATE captures links before deletion with query-aware indexing disabled", async () => {
|
|
@@ -237,6 +687,7 @@ test("consolidation INVALIDATE captures links before deletion with query-aware i
|
|
|
237
687
|
const extraction = {
|
|
238
688
|
...makeRevalidationExtraction(revalidateCalls),
|
|
239
689
|
async consolidate() {
|
|
690
|
+
doomed.content = "current source snapshot";
|
|
240
691
|
return {
|
|
241
692
|
items: [{ existingId: "doomed", action: "INVALIDATE", reason: "duplicate" }],
|
|
242
693
|
profileUpdates: [],
|
|
@@ -245,11 +696,18 @@ test("consolidation INVALIDATE captures links before deletion with query-aware i
|
|
|
245
696
|
},
|
|
246
697
|
} as unknown as ExtractionEngine;
|
|
247
698
|
const config = propagationConfig();
|
|
699
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
700
|
+
fixture.storage,
|
|
701
|
+
extraction,
|
|
702
|
+
config,
|
|
703
|
+
fixture.order,
|
|
704
|
+
);
|
|
248
705
|
const coordinator = new ConsolidationRunCoordinator({
|
|
249
706
|
config,
|
|
250
707
|
getStorage: () => fixture.storage,
|
|
251
708
|
getStorageRouter: () => ({ recordWrite: async () => {} }) as never,
|
|
252
709
|
getExtraction: () => extraction,
|
|
710
|
+
storageDirNamespace: () => "namespace-a",
|
|
253
711
|
embeddingFallback: { removeFromIndex: async () => {} } as never,
|
|
254
712
|
tmtBuilder: { maybeRebuildNodes: async () => {} } as never,
|
|
255
713
|
consolidationObservers: new Set(),
|
|
@@ -274,6 +732,7 @@ test("consolidation INVALIDATE captures links before deletion with query-aware i
|
|
|
274
732
|
indexPersistedMemory: async () => {},
|
|
275
733
|
autoConsolidateIdentity: async () => {},
|
|
276
734
|
fastChatCompletion: async () => null,
|
|
735
|
+
getDependencyPropagationDelivery: () => dependencyPropagationDelivery,
|
|
277
736
|
});
|
|
278
737
|
|
|
279
738
|
await coordinator.run();
|
|
@@ -283,4 +742,603 @@ test("consolidation INVALIDATE captures links before deletion with query-aware i
|
|
|
283
742
|
assert.equal(fixture.memories.get("dependent")?.frontmatter.status, "superseded");
|
|
284
743
|
assert.equal(fixture.memories.get("dependent")?.frontmatter.invalidatedBy, "doomed");
|
|
285
744
|
assert.equal(revalidateCalls.length, 1);
|
|
745
|
+
assert.equal((revalidateCalls[0] as { superseded: { content: string } }).superseded.content, "current source snapshot");
|
|
746
|
+
assertReadyAfterMutation(fixture.order, "consolidation_invalidate", "invalidate:doomed");
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("consolidation INVALIDATE does not delete after a concurrent source update", async () => {
|
|
750
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
751
|
+
const dependent = makeMemory("dependent");
|
|
752
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
753
|
+
const fixture = makeStorage([doomed, dependent, ...fillers], {
|
|
754
|
+
mutateBeforeInvalidateIds: ["doomed"],
|
|
755
|
+
});
|
|
756
|
+
const revalidateCalls: unknown[] = [];
|
|
757
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
758
|
+
existingId: "doomed",
|
|
759
|
+
action: "INVALIDATE",
|
|
760
|
+
reason: "duplicate",
|
|
761
|
+
});
|
|
762
|
+
const config = propagationConfig();
|
|
763
|
+
const delivery = makeSyntheticImmediateDelivery(fixture.storage, extraction, config, fixture.order);
|
|
764
|
+
const coordinator = makeConsolidationCoordinator(fixture, extraction, config, delivery);
|
|
765
|
+
|
|
766
|
+
await coordinator.run();
|
|
767
|
+
|
|
768
|
+
assert.equal(fixture.memories.has("doomed"), true);
|
|
769
|
+
assert.equal(fixture.memories.get("doomed")?.content, "concurrent source update");
|
|
770
|
+
assert.equal(revalidateCalls.length, 0);
|
|
771
|
+
assert.equal(fixture.order.includes("afterMutation:consolidation_invalidate"), false);
|
|
772
|
+
});
|
|
773
|
+
test("consolidation INVALIDATE preserves mutation semantics when propagation is disabled", async () => {
|
|
774
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
775
|
+
const dependent = makeMemory("dependent");
|
|
776
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
777
|
+
const fixture = makeStorage([doomed, dependent, ...fillers], {
|
|
778
|
+
mutateBeforeInvalidateIds: ["doomed"],
|
|
779
|
+
});
|
|
780
|
+
const revalidateCalls: unknown[] = [];
|
|
781
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
782
|
+
existingId: "doomed",
|
|
783
|
+
action: "INVALIDATE",
|
|
784
|
+
reason: "duplicate",
|
|
785
|
+
});
|
|
786
|
+
const config = propagationConfig();
|
|
787
|
+
config.dependencyPropagation.enabled = false;
|
|
788
|
+
const delivery = makeSyntheticImmediateDelivery(fixture.storage, extraction, config, fixture.order);
|
|
789
|
+
const coordinator = makeConsolidationCoordinator(fixture, extraction, config, delivery);
|
|
790
|
+
|
|
791
|
+
await coordinator.run();
|
|
792
|
+
|
|
793
|
+
assert.equal(fixture.memories.has("doomed"), false);
|
|
794
|
+
assert.equal(fixture.invalidateCalls[0], "doomed");
|
|
795
|
+
assert.equal(revalidateCalls.length, 0);
|
|
796
|
+
assert.equal(fixture.order.some((entry) => entry.startsWith("prepare:")), false);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
test("consolidation INVALIDATE deletes after an access-only source update", async () => {
|
|
801
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
802
|
+
const dependent = makeMemory("dependent");
|
|
803
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
804
|
+
const fixture = makeStorage([doomed, dependent, ...fillers], {
|
|
805
|
+
touchAccessBeforeInvalidateIds: ["doomed"],
|
|
806
|
+
});
|
|
807
|
+
const revalidateCalls: unknown[] = [];
|
|
808
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
809
|
+
existingId: "doomed",
|
|
810
|
+
action: "INVALIDATE",
|
|
811
|
+
reason: "duplicate",
|
|
812
|
+
});
|
|
813
|
+
const config = propagationConfig();
|
|
814
|
+
const delivery = makeSyntheticImmediateDelivery(fixture.storage, extraction, config, fixture.order);
|
|
815
|
+
const coordinator = makeConsolidationCoordinator(fixture, extraction, config, delivery);
|
|
816
|
+
|
|
817
|
+
await coordinator.run();
|
|
818
|
+
|
|
819
|
+
assert.equal(fixture.memories.has("doomed"), false);
|
|
820
|
+
assert.equal(revalidateCalls.length, 1);
|
|
821
|
+
assertReadyAfterMutation(fixture.order, "consolidation_invalidate", "invalidate:doomed");
|
|
286
822
|
});
|
|
823
|
+
|
|
824
|
+
test("consolidation MERGE prepares before update and invalidation, then marks ready", async () => {
|
|
825
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
826
|
+
const replacement = makeMemory("replacement", { content: "merged claim" });
|
|
827
|
+
const dependent = makeMemory("dependent");
|
|
828
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
829
|
+
const fixture = makeStorage([doomed, replacement, dependent, ...fillers]);
|
|
830
|
+
const revalidateCalls: unknown[] = [];
|
|
831
|
+
const extraction = {
|
|
832
|
+
...makeRevalidationExtraction(revalidateCalls),
|
|
833
|
+
async consolidate() {
|
|
834
|
+
return {
|
|
835
|
+
items: [{
|
|
836
|
+
existingId: "replacement",
|
|
837
|
+
action: "MERGE",
|
|
838
|
+
mergeWith: "doomed",
|
|
839
|
+
updatedContent: "merged claim",
|
|
840
|
+
reason: "duplicate",
|
|
841
|
+
}],
|
|
842
|
+
profileUpdates: [],
|
|
843
|
+
entityUpdates: [],
|
|
844
|
+
};
|
|
845
|
+
},
|
|
846
|
+
} as unknown as ExtractionEngine;
|
|
847
|
+
const config = propagationConfig();
|
|
848
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
849
|
+
fixture.storage,
|
|
850
|
+
extraction,
|
|
851
|
+
config,
|
|
852
|
+
fixture.order,
|
|
853
|
+
);
|
|
854
|
+
const coordinator = new ConsolidationRunCoordinator({
|
|
855
|
+
config,
|
|
856
|
+
getStorage: () => fixture.storage,
|
|
857
|
+
getStorageRouter: () => ({ recordWrite: async () => {} }) as never,
|
|
858
|
+
getExtraction: () => extraction,
|
|
859
|
+
storageDirNamespace: () => "namespace-a",
|
|
860
|
+
getDependencyPropagationDelivery: () => dependencyPropagationDelivery,
|
|
861
|
+
embeddingFallback: { removeFromIndex: async () => {} } as never,
|
|
862
|
+
tmtBuilder: { maybeRebuildNodes: async () => {} } as never,
|
|
863
|
+
consolidationObservers: new Set(),
|
|
864
|
+
getAccessTrackingBuffer: () => new Map(),
|
|
865
|
+
lifecyclePolicyCoordinator: {
|
|
866
|
+
runFactArchival: async () => 0,
|
|
867
|
+
runSummarization: async () => {},
|
|
868
|
+
runTopicExtraction: async () => {},
|
|
869
|
+
} as never,
|
|
870
|
+
compressionGuidelineCoordinator: {
|
|
871
|
+
runCompressionGuidelineLearningPass: async () => {},
|
|
872
|
+
} as never,
|
|
873
|
+
semanticConsolidationCoordinator: {} as never,
|
|
874
|
+
entitySynthesisCoordinator: {} as never,
|
|
875
|
+
recallSectionCoordinator: {
|
|
876
|
+
getRecallSectionEntry: () => undefined,
|
|
877
|
+
} as never,
|
|
878
|
+
tierMigrationCoordinator: {
|
|
879
|
+
runCycle: async () => ({ migrated: 0 }),
|
|
880
|
+
} as never,
|
|
881
|
+
flushAccessTracking: async () => {},
|
|
882
|
+
indexPersistedMemory: async () => {},
|
|
883
|
+
autoConsolidateIdentity: async () => {},
|
|
884
|
+
fastChatCompletion: async () => null,
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
await coordinator.run();
|
|
888
|
+
|
|
889
|
+
assert.equal(fixture.memories.has("doomed"), false);
|
|
890
|
+
assert.equal(fixture.memories.get("replacement")?.content, "merged claim");
|
|
891
|
+
assert.equal(fixture.memories.get("dependent")?.frontmatter.invalidatedBy, "doomed");
|
|
892
|
+
assert.equal(revalidateCalls.length, 1);
|
|
893
|
+
assertReadyAfterMutation(fixture.order, "consolidation_merge", "invalidate:doomed");
|
|
894
|
+
assert.ok(fixture.order.indexOf("prepare:consolidation_merge") < fixture.order.indexOf("update:replacement"));
|
|
895
|
+
assert.ok(fixture.order.indexOf("update:replacement") < fixture.order.indexOf("invalidate:doomed"));
|
|
896
|
+
});
|
|
897
|
+
test("consolidation MERGE preserves mutation semantics when propagation is disabled", async () => {
|
|
898
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
899
|
+
const replacement = makeMemory("replacement", { content: "original replacement" });
|
|
900
|
+
const dependent = makeMemory("dependent");
|
|
901
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
902
|
+
const fixture = makeStorage([doomed, replacement, dependent, ...fillers], {
|
|
903
|
+
mutateBeforeInvalidateIds: ["doomed"],
|
|
904
|
+
});
|
|
905
|
+
const revalidateCalls: unknown[] = [];
|
|
906
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
907
|
+
existingId: "replacement",
|
|
908
|
+
action: "MERGE",
|
|
909
|
+
mergeWith: "doomed",
|
|
910
|
+
updatedContent: "merged claim",
|
|
911
|
+
reason: "duplicate",
|
|
912
|
+
});
|
|
913
|
+
const config = propagationConfig();
|
|
914
|
+
config.dependencyPropagation.enabled = false;
|
|
915
|
+
const delivery = makeSyntheticImmediateDelivery(fixture.storage, extraction, config, fixture.order);
|
|
916
|
+
const coordinator = makeConsolidationCoordinator(fixture, extraction, config, delivery);
|
|
917
|
+
|
|
918
|
+
await coordinator.run();
|
|
919
|
+
|
|
920
|
+
assert.equal(fixture.memories.has("doomed"), false);
|
|
921
|
+
assert.equal(fixture.memories.get("replacement")?.content, "merged claim");
|
|
922
|
+
assert.equal(revalidateCalls.length, 0);
|
|
923
|
+
assert.equal(fixture.order.some((entry) => entry.startsWith("prepare:")), false);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
test("consolidation MERGE cancels propagation when source invalidation did not commit", async () => {
|
|
928
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
929
|
+
const replacement = makeMemory("replacement", { content: "merged claim" });
|
|
930
|
+
const dependent = makeMemory("dependent");
|
|
931
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
932
|
+
const fixture = makeStorage([doomed, replacement, dependent, ...fillers], {
|
|
933
|
+
failInvalidateIds: ["doomed"],
|
|
934
|
+
});
|
|
935
|
+
const revalidateCalls: unknown[] = [];
|
|
936
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
937
|
+
existingId: "replacement",
|
|
938
|
+
action: "MERGE",
|
|
939
|
+
mergeWith: "doomed",
|
|
940
|
+
updatedContent: "merged claim",
|
|
941
|
+
reason: "duplicate",
|
|
942
|
+
});
|
|
943
|
+
const config = propagationConfig();
|
|
944
|
+
const delivery = makeSyntheticImmediateDelivery(fixture.storage, extraction, config, fixture.order);
|
|
945
|
+
const coordinator = makeConsolidationCoordinator(fixture, extraction, config, delivery);
|
|
946
|
+
|
|
947
|
+
await coordinator.run();
|
|
948
|
+
|
|
949
|
+
assert.equal(fixture.memories.get("replacement")?.content, "merged claim");
|
|
950
|
+
assert.equal(fixture.memories.has("doomed"), true);
|
|
951
|
+
assert.equal(revalidateCalls.length, 0);
|
|
952
|
+
assert.equal(fixture.order.includes("cancel:consolidation_merge"), true);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("contradiction resolve retains propagation after partial supersession failure", async () => {
|
|
956
|
+
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
957
|
+
const replacement = makeMemory("replacement", { content: "new claim" });
|
|
958
|
+
const dependent = makeMemory("dependent");
|
|
959
|
+
const fixture = makeStorage([old, replacement, dependent], { partialSupersedeIds: ["old"] });
|
|
960
|
+
const revalidateCalls: unknown[] = [];
|
|
961
|
+
const extraction = makeRevalidationExtraction(revalidateCalls);
|
|
962
|
+
const config = propagationConfig();
|
|
963
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
964
|
+
fixture.storage,
|
|
965
|
+
extraction,
|
|
966
|
+
config,
|
|
967
|
+
fixture.order,
|
|
968
|
+
);
|
|
969
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
970
|
+
getConfig: () => config,
|
|
971
|
+
isSearchAvailable: () => false,
|
|
972
|
+
searchAcrossNamespaces: async () => [],
|
|
973
|
+
extractMemoryIdsFromResults: () => [],
|
|
974
|
+
namespaceFromPath: () => "namespace-a",
|
|
975
|
+
storageForNamespace: async () => fixture.storage,
|
|
976
|
+
getExtraction: () => extraction,
|
|
977
|
+
storageDirNamespace: () => "namespace-a",
|
|
978
|
+
getDependencyPropagationDelivery: () => dependencyPropagationDelivery,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
await coordinator.applyDeferredContradictionResolve(
|
|
982
|
+
{
|
|
983
|
+
supersededId: "old",
|
|
984
|
+
reason: "newer contradiction",
|
|
985
|
+
supersededPath: old.path,
|
|
986
|
+
supersededCreated: old.frontmatter.created,
|
|
987
|
+
supersededTags: [],
|
|
988
|
+
},
|
|
989
|
+
fixture.storage,
|
|
990
|
+
"replacement",
|
|
991
|
+
false,
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
assert.equal(fixture.memories.get("old")?.frontmatter.status, "superseded");
|
|
995
|
+
assert.equal(revalidateCalls.length, 1);
|
|
996
|
+
assert.equal(fixture.order.includes("cancel:contradiction"), false);
|
|
997
|
+
assertReadyAfterMutation(fixture.order, "contradiction", "supersede:old");
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("contradiction resolve retains prepared propagation when commit state cannot be read", async () => {
|
|
1001
|
+
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1002
|
+
const replacement = makeMemory("replacement", { content: "new claim" });
|
|
1003
|
+
const dependent = makeMemory("dependent");
|
|
1004
|
+
const fixture = makeStorage(
|
|
1005
|
+
[old, replacement, dependent],
|
|
1006
|
+
{
|
|
1007
|
+
failSupersedeIds: ["old"],
|
|
1008
|
+
failStateReadAfterSupersedeIds: ["old"],
|
|
1009
|
+
},
|
|
1010
|
+
);
|
|
1011
|
+
const revalidateCalls: unknown[] = [];
|
|
1012
|
+
const extraction = makeRevalidationExtraction(revalidateCalls);
|
|
1013
|
+
const config = propagationConfig();
|
|
1014
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
1015
|
+
fixture.storage,
|
|
1016
|
+
extraction,
|
|
1017
|
+
config,
|
|
1018
|
+
fixture.order,
|
|
1019
|
+
);
|
|
1020
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1021
|
+
getConfig: () => config,
|
|
1022
|
+
isSearchAvailable: () => false,
|
|
1023
|
+
searchAcrossNamespaces: async () => [],
|
|
1024
|
+
extractMemoryIdsFromResults: () => [],
|
|
1025
|
+
namespaceFromPath: () => "namespace-a",
|
|
1026
|
+
storageForNamespace: async () => fixture.storage,
|
|
1027
|
+
getExtraction: () => extraction,
|
|
1028
|
+
storageDirNamespace: () => "namespace-a",
|
|
1029
|
+
getDependencyPropagationDelivery: () => dependencyPropagationDelivery,
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
await coordinator.applyDeferredContradictionResolve(
|
|
1033
|
+
{
|
|
1034
|
+
supersededId: "old",
|
|
1035
|
+
reason: "newer contradiction",
|
|
1036
|
+
supersededPath: old.path,
|
|
1037
|
+
supersededCreated: old.frontmatter.created,
|
|
1038
|
+
supersededTags: [],
|
|
1039
|
+
},
|
|
1040
|
+
fixture.storage,
|
|
1041
|
+
"replacement",
|
|
1042
|
+
false,
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
assert.deepEqual(fixture.order, ["prepare:contradiction", "supersede:old", "defer:contradiction"]);
|
|
1046
|
+
assert.equal(revalidateCalls.length, 0);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test("contradiction resolve cancels prepared propagation when primary supersession fails", async () => {
|
|
1050
|
+
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1051
|
+
const replacement = makeMemory("replacement", { content: "new claim" });
|
|
1052
|
+
const dependent = makeMemory("dependent");
|
|
1053
|
+
const fixture = makeStorage([old, replacement, dependent], { failSupersedeIds: ["old"] });
|
|
1054
|
+
const revalidateCalls: unknown[] = [];
|
|
1055
|
+
const extraction = makeRevalidationExtraction(revalidateCalls);
|
|
1056
|
+
const config = propagationConfig();
|
|
1057
|
+
const dependencyPropagationDelivery = makeSyntheticImmediateDelivery(
|
|
1058
|
+
fixture.storage,
|
|
1059
|
+
extraction,
|
|
1060
|
+
config,
|
|
1061
|
+
fixture.order,
|
|
1062
|
+
);
|
|
1063
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1064
|
+
getConfig: () => config,
|
|
1065
|
+
isSearchAvailable: () => false,
|
|
1066
|
+
searchAcrossNamespaces: async () => [],
|
|
1067
|
+
extractMemoryIdsFromResults: () => [],
|
|
1068
|
+
namespaceFromPath: () => "namespace-a",
|
|
1069
|
+
storageForNamespace: async () => fixture.storage,
|
|
1070
|
+
getExtraction: () => extraction,
|
|
1071
|
+
storageDirNamespace: () => "namespace-a",
|
|
1072
|
+
getDependencyPropagationDelivery: () => dependencyPropagationDelivery,
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
await coordinator.applyDeferredContradictionResolve(
|
|
1076
|
+
{
|
|
1077
|
+
supersededId: "old",
|
|
1078
|
+
reason: "newer contradiction",
|
|
1079
|
+
supersededPath: old.path,
|
|
1080
|
+
|
|
1081
|
+
supersededCreated: old.frontmatter.created,
|
|
1082
|
+
supersededTags: [],
|
|
1083
|
+
},
|
|
1084
|
+
fixture.storage,
|
|
1085
|
+
"replacement",
|
|
1086
|
+
false,
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
assert.deepEqual(fixture.order, [
|
|
1090
|
+
"prepare:contradiction",
|
|
1091
|
+
"supersede:old",
|
|
1092
|
+
"cancel:contradiction",
|
|
1093
|
+
]);
|
|
1094
|
+
assert.equal(revalidateCalls.length, 0);
|
|
1095
|
+
assert.equal(fixture.order.some((entry) => entry.startsWith("afterMutation:")), false);
|
|
1096
|
+
});
|
|
1097
|
+
test("contradiction resolve propagates after a null preparation token", async () => {
|
|
1098
|
+
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1099
|
+
const replacement = makeMemory("replacement", { content: "new claim" });
|
|
1100
|
+
const dependent = makeMemory("dependent");
|
|
1101
|
+
const fixture = makeStorage([old, replacement, dependent]);
|
|
1102
|
+
const revalidateCalls: unknown[] = [];
|
|
1103
|
+
const extraction = makeRevalidationExtraction(revalidateCalls);
|
|
1104
|
+
const config = propagationConfig();
|
|
1105
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
1106
|
+
fixture.storage,
|
|
1107
|
+
extraction,
|
|
1108
|
+
config,
|
|
1109
|
+
fixture.order,
|
|
1110
|
+
{ prepareReturnsNull: true },
|
|
1111
|
+
);
|
|
1112
|
+
const coordinator = makeContradictionCoordinator(fixture, extraction, config, delivery);
|
|
1113
|
+
|
|
1114
|
+
await coordinator.applyDeferredContradictionResolve(
|
|
1115
|
+
{
|
|
1116
|
+
supersededId: "old",
|
|
1117
|
+
reason: "newer contradiction",
|
|
1118
|
+
supersededPath: old.path,
|
|
1119
|
+
supersededCreated: old.frontmatter.created,
|
|
1120
|
+
supersededTags: [],
|
|
1121
|
+
},
|
|
1122
|
+
fixture.storage,
|
|
1123
|
+
"replacement",
|
|
1124
|
+
false,
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
assertReadyAfterMutation(fixture.order, "contradiction", "supersede:old");
|
|
1128
|
+
assert.equal(fixture.order.filter((entry) => entry === "afterMutation:contradiction").length, 1);
|
|
1129
|
+
assert.equal(revalidateCalls.length, 1);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
test("consolidation INVALIDATE propagates after a null preparation token", async () => {
|
|
1133
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1134
|
+
const dependent = makeMemory("dependent");
|
|
1135
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
1136
|
+
const fixture = makeStorage([doomed, dependent, ...fillers]);
|
|
1137
|
+
const revalidateCalls: unknown[] = [];
|
|
1138
|
+
const config = propagationConfig();
|
|
1139
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
1140
|
+
existingId: "doomed",
|
|
1141
|
+
action: "INVALIDATE",
|
|
1142
|
+
reason: "duplicate",
|
|
1143
|
+
});
|
|
1144
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
1145
|
+
fixture.storage,
|
|
1146
|
+
extraction,
|
|
1147
|
+
config,
|
|
1148
|
+
fixture.order,
|
|
1149
|
+
{ prepareReturnsNull: true },
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
await makeConsolidationCoordinator(fixture, extraction, config, delivery).run();
|
|
1153
|
+
|
|
1154
|
+
assertReadyAfterMutation(fixture.order, "consolidation_invalidate", "invalidate:doomed");
|
|
1155
|
+
assert.equal(fixture.order.filter((entry) => entry === "afterMutation:consolidation_invalidate").length, 1);
|
|
1156
|
+
assert.equal(revalidateCalls.length, 1);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
test("consolidation MERGE propagates after a null preparation token", async () => {
|
|
1160
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1161
|
+
const replacement = makeMemory("replacement", { content: "merged claim" });
|
|
1162
|
+
const dependent = makeMemory("dependent");
|
|
1163
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
1164
|
+
const fixture = makeStorage([doomed, replacement, dependent, ...fillers]);
|
|
1165
|
+
const revalidateCalls: unknown[] = [];
|
|
1166
|
+
const config = propagationConfig();
|
|
1167
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
1168
|
+
existingId: "replacement",
|
|
1169
|
+
action: "MERGE",
|
|
1170
|
+
mergeWith: "doomed",
|
|
1171
|
+
updatedContent: "merged claim",
|
|
1172
|
+
reason: "duplicate",
|
|
1173
|
+
});
|
|
1174
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
1175
|
+
fixture.storage,
|
|
1176
|
+
extraction,
|
|
1177
|
+
config,
|
|
1178
|
+
fixture.order,
|
|
1179
|
+
{ prepareReturnsNull: true },
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
await makeConsolidationCoordinator(fixture, extraction, config, delivery).run();
|
|
1183
|
+
|
|
1184
|
+
assertReadyAfterMutation(fixture.order, "consolidation_merge", "invalidate:doomed");
|
|
1185
|
+
assert.equal(fixture.order.filter((entry) => entry === "afterMutation:consolidation_merge").length, 1);
|
|
1186
|
+
assert.equal(revalidateCalls.length, 1);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
test("disabled propagation skips preparation and afterMutation", async () => {
|
|
1190
|
+
for (const disabledBy of ["enabled", "maxDependents"] as const) {
|
|
1191
|
+
const old = makeMemory("old", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1192
|
+
const replacement = makeMemory("replacement", { content: "new claim" });
|
|
1193
|
+
const dependent = makeMemory("dependent");
|
|
1194
|
+
const fixture = makeStorage([old, replacement, dependent]);
|
|
1195
|
+
const revalidateCalls: unknown[] = [];
|
|
1196
|
+
const extraction = makeRevalidationExtraction(revalidateCalls);
|
|
1197
|
+
const config = propagationConfig();
|
|
1198
|
+
if (disabledBy === "enabled") {
|
|
1199
|
+
config.dependencyPropagation.enabled = false;
|
|
1200
|
+
} else {
|
|
1201
|
+
config.dependencyPropagation.maxDependents = 0;
|
|
1202
|
+
}
|
|
1203
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
1204
|
+
fixture.storage,
|
|
1205
|
+
extraction,
|
|
1206
|
+
config,
|
|
1207
|
+
fixture.order,
|
|
1208
|
+
);
|
|
1209
|
+
const coordinator = makeContradictionCoordinator(fixture, extraction, config, delivery);
|
|
1210
|
+
|
|
1211
|
+
await coordinator.applyDeferredContradictionResolve(
|
|
1212
|
+
{
|
|
1213
|
+
supersededId: "old",
|
|
1214
|
+
reason: "newer contradiction",
|
|
1215
|
+
supersededPath: old.path,
|
|
1216
|
+
supersededCreated: old.frontmatter.created,
|
|
1217
|
+
supersededTags: [],
|
|
1218
|
+
},
|
|
1219
|
+
fixture.storage,
|
|
1220
|
+
"replacement",
|
|
1221
|
+
false,
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
assert.equal(fixture.order.some((entry) => entry.startsWith("prepare:")), false);
|
|
1225
|
+
assert.equal(fixture.order.some((entry) => entry.startsWith("afterMutation:")), false);
|
|
1226
|
+
assert.equal(revalidateCalls.length, 0);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
test("consolidation cancels or defers prepared propagation when source invalidation fails", async () => {
|
|
1232
|
+
for (const action of ["INVALIDATE", "MERGE"] as const) {
|
|
1233
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1234
|
+
const replacement = makeMemory("replacement", { content: "merged claim" });
|
|
1235
|
+
const dependent = makeMemory("dependent");
|
|
1236
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
1237
|
+
const fixture = makeStorage(
|
|
1238
|
+
[doomed, replacement, dependent, ...fillers],
|
|
1239
|
+
{ failInvalidateIds: ["doomed"] },
|
|
1240
|
+
);
|
|
1241
|
+
const revalidateCalls: unknown[] = [];
|
|
1242
|
+
const config = propagationConfig();
|
|
1243
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, action === "INVALIDATE"
|
|
1244
|
+
? {
|
|
1245
|
+
existingId: "doomed",
|
|
1246
|
+
action,
|
|
1247
|
+
reason: "duplicate",
|
|
1248
|
+
}
|
|
1249
|
+
: {
|
|
1250
|
+
existingId: "replacement",
|
|
1251
|
+
action,
|
|
1252
|
+
mergeWith: "doomed",
|
|
1253
|
+
updatedContent: "merged claim",
|
|
1254
|
+
reason: "duplicate",
|
|
1255
|
+
});
|
|
1256
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
1257
|
+
fixture.storage,
|
|
1258
|
+
extraction,
|
|
1259
|
+
config,
|
|
1260
|
+
fixture.order,
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
await makeConsolidationCoordinator(fixture, extraction, config, delivery).run();
|
|
1264
|
+
|
|
1265
|
+
const cause = action === "INVALIDATE"
|
|
1266
|
+
? "consolidation_invalidate"
|
|
1267
|
+
: "consolidation_merge";
|
|
1268
|
+
assert.ok(fixture.order.includes(`prepare:${cause}`));
|
|
1269
|
+
assert.ok(fixture.order.includes("invalidate:doomed"));
|
|
1270
|
+
const terminalAction = "cancel";
|
|
1271
|
+
assert.ok(fixture.order.includes(`${terminalAction}:${cause}`));
|
|
1272
|
+
assert.equal(fixture.order.some((entry) => entry.startsWith("afterMutation:")), false);
|
|
1273
|
+
assert.equal(revalidateCalls.length, 0);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
test("consolidation cancels a prepared propagation when the pre-primary source read throws", async () => {
|
|
1277
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1278
|
+
const dependent = makeMemory("dependent");
|
|
1279
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
1280
|
+
const fixture = makeStorage([doomed, dependent, ...fillers], {
|
|
1281
|
+
failPrePrimaryReadIds: ["doomed"],
|
|
1282
|
+
});
|
|
1283
|
+
const revalidateCalls: unknown[] = [];
|
|
1284
|
+
const config = propagationConfig();
|
|
1285
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
1286
|
+
existingId: "doomed",
|
|
1287
|
+
action: "INVALIDATE",
|
|
1288
|
+
reason: "duplicate",
|
|
1289
|
+
});
|
|
1290
|
+
const delivery = makeSyntheticImmediateDelivery(fixture.storage, extraction, config, fixture.order);
|
|
1291
|
+
|
|
1292
|
+
await assert.rejects(
|
|
1293
|
+
makeConsolidationCoordinator(fixture, extraction, config, delivery).run(),
|
|
1294
|
+
/synthetic pre-primary read failure/,
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
assert.deepEqual(fixture.order, [
|
|
1298
|
+
"prepare:consolidation_invalidate",
|
|
1299
|
+
"cancel:consolidation_invalidate",
|
|
1300
|
+
]);
|
|
1301
|
+
assert.equal(fixture.memories.has("doomed"), true);
|
|
1302
|
+
assert.equal(revalidateCalls.length, 0);
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
test("consolidation defers merge propagation when the survivor write throws after commit", async () => {
|
|
1306
|
+
const doomed = makeMemory("doomed", { links: [{ targetId: "dependent", linkType: "supports" }] });
|
|
1307
|
+
const replacement = makeMemory("replacement", { content: "old replacement claim" });
|
|
1308
|
+
const dependent = makeMemory("dependent");
|
|
1309
|
+
const fillers = ["filler-a", "filler-b", "filler-c"].map((id) => makeMemory(id));
|
|
1310
|
+
const fixture = makeStorage([doomed, replacement, dependent, ...fillers], {
|
|
1311
|
+
throwAfterUpdateIds: ["replacement"],
|
|
1312
|
+
});
|
|
1313
|
+
const revalidateCalls: unknown[] = [];
|
|
1314
|
+
const config = propagationConfig();
|
|
1315
|
+
const extraction = makeConsolidationExtraction(revalidateCalls, {
|
|
1316
|
+
existingId: "replacement",
|
|
1317
|
+
action: "MERGE",
|
|
1318
|
+
mergeWith: "doomed",
|
|
1319
|
+
updatedContent: "merged claim",
|
|
1320
|
+
reason: "duplicate",
|
|
1321
|
+
});
|
|
1322
|
+
const delivery = makeSyntheticImmediateDelivery(
|
|
1323
|
+
fixture.storage,
|
|
1324
|
+
extraction,
|
|
1325
|
+
config,
|
|
1326
|
+
fixture.order,
|
|
1327
|
+
);
|
|
1328
|
+
|
|
1329
|
+
await assert.rejects(
|
|
1330
|
+
makeConsolidationCoordinator(fixture, extraction, config, delivery).run(),
|
|
1331
|
+
/synthetic post-write update failure/,
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
assert.equal(fixture.memories.get("replacement")?.content, "merged claim");
|
|
1335
|
+
assert.equal(fixture.memories.get("dependent")?.frontmatter.invalidatedBy, undefined);
|
|
1336
|
+
assert.deepEqual(fixture.order.slice(0, 3), [
|
|
1337
|
+
"prepare:consolidation_merge",
|
|
1338
|
+
"update:replacement",
|
|
1339
|
+
"defer:consolidation_merge",
|
|
1340
|
+
]);
|
|
1341
|
+
assert.equal(fixture.order.includes("afterMutation:consolidation_merge"), false);
|
|
1342
|
+
assert.equal(fixture.order.includes("invalidate:doomed"), false);
|
|
1343
|
+
assert.equal(revalidateCalls.length, 0);
|
|
1344
|
+
});
|