@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.
Files changed (234) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -5
  2. package/dist/access-admin-ops-surface.js +16 -16
  3. package/dist/access-authorization-probe.d.ts +6 -5
  4. package/dist/access-authorization-probe.js +17 -17
  5. package/dist/access-boundary.d.ts +6 -5
  6. package/dist/access-boundary.js +16 -16
  7. package/dist/access-cli.js +39 -39
  8. package/dist/access-extraction-force-flush.d.ts +6 -5
  9. package/dist/access-extraction-force-flush.js +16 -16
  10. package/dist/access-http-lcm-compaction.d.ts +6 -5
  11. package/dist/access-http-lifecycle-flush.d.ts +6 -5
  12. package/dist/access-http-offline-stream.d.ts +6 -5
  13. package/dist/access-http-query.js +16 -16
  14. package/dist/access-http.d.ts +6 -5
  15. package/dist/access-http.js +22 -22
  16. package/dist/access-identity-continuity-surface.d.ts +5 -4
  17. package/dist/access-identity-continuity-surface.js +16 -16
  18. package/dist/access-lcm-surface.d.ts +6 -5
  19. package/dist/access-lcm-surface.js +16 -16
  20. package/dist/access-mcp.d.ts +6 -5
  21. package/dist/access-mcp.js +20 -20
  22. package/dist/access-namespace-preflight.js +16 -16
  23. package/dist/access-observe-write-surface.d.ts +6 -5
  24. package/dist/access-observe-write-surface.js +16 -16
  25. package/dist/access-offline-manifest.d.ts +5 -4
  26. package/dist/access-operations-batch.js +17 -17
  27. package/dist/access-operations.d.ts +9 -8
  28. package/dist/access-operations.js +19 -19
  29. package/dist/access-recall-concurrency.d.ts +6 -5
  30. package/dist/access-recall-concurrency.js +16 -16
  31. package/dist/access-recall-response.d.ts +6 -5
  32. package/dist/access-recall-response.js +16 -16
  33. package/dist/access-recall-surface.d.ts +6 -5
  34. package/dist/access-recall-surface.js +16 -16
  35. package/dist/access-schema.d.ts +88 -88
  36. package/dist/{access-service-BY0KJ44c.d.ts → access-service-Cbk3bNbD.d.ts} +3 -3
  37. package/dist/access-service-helpers.d.ts +6 -5
  38. package/dist/access-service.d.ts +6 -5
  39. package/dist/access-service.js +16 -16
  40. package/dist/access-surface-catalog.d.ts +6 -5
  41. package/dist/access-wearables-meetings-surface.d.ts +2 -2
  42. package/dist/bootstrap.d.ts +5 -4
  43. package/dist/briefing.d.ts +1 -1
  44. package/dist/briefing.js +7 -7
  45. package/dist/buffer.d.ts +1 -1
  46. package/dist/{capsule-merge-XMFBXVDP.js → capsule-merge-HCJJMLPS.js} +4 -4
  47. package/dist/causal-consolidation.js +8 -8
  48. package/dist/{chunk-PWN32TAG.js → chunk-2ASAS3CQ.js} +2 -2
  49. package/dist/{chunk-WQM326GS.js → chunk-2LY6GIIE.js} +2 -2
  50. package/dist/{chunk-JL43BVDE.js → chunk-2OIQDTDC.js} +2 -2
  51. package/dist/{chunk-33SV3E43.js → chunk-3OY7KXHM.js} +3 -3
  52. package/dist/{chunk-AIUBBHSF.js → chunk-5UZ6UCOZ.js} +3 -3
  53. package/dist/chunk-5WITETWU.js +591 -0
  54. package/dist/chunk-5WITETWU.js.map +1 -0
  55. package/dist/{chunk-UT5BV5OG.js → chunk-7HXZJAID.js} +2 -2
  56. package/dist/{chunk-M7BXDKPR.js → chunk-7M6NYQWB.js} +3 -3
  57. package/dist/{chunk-TPTECFWR.js → chunk-B6LZ4IBH.js} +3 -3
  58. package/dist/{chunk-J5LLX6IL.js → chunk-BF6SXNQJ.js} +10 -10
  59. package/dist/{chunk-4PC4PRJE.js → chunk-BKIFTXHR.js} +2 -2
  60. package/dist/{chunk-VVG2BOKA.js → chunk-BMEYMKWX.js} +2 -2
  61. package/dist/{chunk-DCZBSE4F.js → chunk-D5YOZ66N.js} +18 -18
  62. package/dist/{chunk-BHXYAVUM.js → chunk-DLG5V6ND.js} +2 -2
  63. package/dist/{chunk-52UKFTUK.js → chunk-F4EU6VUC.js} +2 -2
  64. package/dist/{chunk-M4A44LUA.js → chunk-GSTIBXBW.js} +2 -2
  65. package/dist/{chunk-W5XVQFLM.js → chunk-GXFSQ6SD.js} +2 -2
  66. package/dist/{chunk-XADV5IZ3.js → chunk-HDIUYMSB.js} +5 -5
  67. package/dist/{chunk-FXTYIA46.js → chunk-J272WKX2.js} +2 -2
  68. package/dist/{chunk-ENTCVTQR.js → chunk-JJRVPWFY.js} +11 -1
  69. package/dist/chunk-JJRVPWFY.js.map +1 -0
  70. package/dist/{chunk-JYL3BVVZ.js → chunk-JSEJH2YQ.js} +2 -2
  71. package/dist/{chunk-5UDMFE3Q.js → chunk-MLXZD4WM.js} +2 -2
  72. package/dist/{chunk-PUSLMZBQ.js → chunk-NOEMGE56.js} +2 -2
  73. package/dist/{chunk-C5QLI33R.js → chunk-QINJITDS.js} +2 -2
  74. package/dist/{chunk-45BRYON6.js → chunk-QPOHMO6M.js} +2 -2
  75. package/dist/{chunk-ZQOCRIGA.js → chunk-QYN5PXDZ.js} +3 -3
  76. package/dist/{chunk-PA45XCUD.js → chunk-RANKJI5T.js} +2 -2
  77. package/dist/{chunk-ZHY5TMJG.js → chunk-RMHMUUBW.js} +2392 -628
  78. package/dist/chunk-RMHMUUBW.js.map +1 -0
  79. package/dist/{chunk-37ZO6LLJ.js → chunk-RYLEDKQ2.js} +2 -2
  80. package/dist/{chunk-CV6BBVYV.js → chunk-TEMSWHQD.js} +2 -2
  81. package/dist/{chunk-QGR6OVGT.js → chunk-VFSAWRMZ.js} +2 -2
  82. package/dist/{chunk-NLK2VZYZ.js → chunk-VXCNQYTC.js} +2 -2
  83. package/dist/{chunk-AZ2QBGYB.js → chunk-W7Y5JDVB.js} +1 -1
  84. package/dist/{chunk-VBHPOWBJ.js → chunk-WD5B5AIE.js} +1051 -591
  85. package/dist/chunk-WD5B5AIE.js.map +1 -0
  86. package/dist/{chunk-OYESWJ43.js → chunk-WLRD2TXF.js} +15 -10
  87. package/dist/chunk-WLRD2TXF.js.map +1 -0
  88. package/dist/{chunk-4F6X3RXW.js → chunk-X57M2Q4I.js} +2 -2
  89. package/dist/{chunk-6MTSFX7A.js → chunk-X7XEDOSL.js} +8 -8
  90. package/dist/{chunk-UEVLYVNW.js → chunk-YXKVLYXU.js} +2 -2
  91. package/dist/{chunk-OQAL6SIR.js → chunk-ZMSZVISA.js} +2 -2
  92. package/dist/{cli-CnOkH9hA.d.ts → cli-DMAFI0qt.d.ts} +3 -3
  93. package/dist/cli.d.ts +7 -6
  94. package/dist/cli.js +33 -33
  95. package/dist/compounding/engine.d.ts +1 -1
  96. package/dist/compounding/engine.js +7 -7
  97. package/dist/connectors/codex-materialize-runner.js +7 -7
  98. package/dist/connectors/index.js +7 -7
  99. package/dist/consolidation-provenance-check.d.ts +1 -1
  100. package/dist/consolidation-provenance-check.js +4 -4
  101. package/dist/consolidation-undo.d.ts +1 -1
  102. package/dist/consolidation-undo.js +3 -3
  103. package/dist/contradiction/index.d.ts +1 -1
  104. package/dist/convergence-refresh.d.ts +1 -1
  105. package/dist/convergence-refresh.js +9 -9
  106. package/dist/corpus-watermark.js +9 -9
  107. package/dist/dependency-propagation-delivery-D63r24pH.d.ts +41 -0
  108. package/dist/entity-retrieval.d.ts +1 -1
  109. package/dist/entity-retrieval.js +7 -7
  110. package/dist/explicit-capture.d.ts +5 -4
  111. package/dist/explicit-capture.js +4 -4
  112. package/dist/external-wiki-access.d.ts +6 -5
  113. package/dist/external-wiki-access.js +17 -17
  114. package/dist/external-wiki-mcp-tools.d.ts +6 -5
  115. package/dist/extraction.js +7 -7
  116. package/dist/{forget-UJDJHG6R.js → forget-HDS4N6CG.js} +3 -3
  117. package/dist/harmonic-retrieval.js +1 -1
  118. package/dist/index.d.ts +687 -686
  119. package/dist/index.js +39 -39
  120. package/dist/maintenance/memory-governance.js +7 -7
  121. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
  122. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  123. package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
  124. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  125. package/dist/{maintenance-CKwxpdRx.d.ts → maintenance-kBII2qUI.d.ts} +3 -2
  126. package/dist/mcp-memory-inspector-app.d.ts +6 -5
  127. package/dist/memory-worth-outcomes.d.ts +1 -1
  128. package/dist/namespaces/migrate.d.ts +1 -1
  129. package/dist/namespaces/migrate.js +8 -8
  130. package/dist/namespaces/storage.d.ts +1 -1
  131. package/dist/namespaces/storage.js +7 -7
  132. package/dist/operator-doctor-corpus.js +10 -10
  133. package/dist/operator-toolkit.d.ts +1 -1
  134. package/dist/operator-toolkit.js +15 -15
  135. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  136. package/dist/orchestration/maintenance.d.ts +2 -2
  137. package/dist/orchestration/maintenance.js +12 -12
  138. package/dist/{orchestrator-BshGam-o.d.ts → orchestrator-DHy5LtTy.d.ts} +20 -35
  139. package/dist/orchestrator.d.ts +5 -4
  140. package/dist/orchestrator.js +39 -39
  141. package/dist/page-versioning.js +3 -3
  142. package/dist/schemas.d.ts +138 -138
  143. package/dist/semantic-consolidation.js +8 -8
  144. package/dist/semantic-rule-promotion.js +7 -7
  145. package/dist/semantic-rule-verifier.js +7 -7
  146. package/dist/{service-BIgVM7fL.d.ts → service-PT2_z7xm.d.ts} +1 -1
  147. package/dist/shared-context/manager.d.ts +8 -8
  148. package/dist/source-agent-qualifier.js +7 -7
  149. package/dist/{storage-CVfGWqCI.d.ts → storage-BmVudhk4.d.ts} +46 -27
  150. package/dist/storage.d.ts +1 -1
  151. package/dist/storage.js +6 -6
  152. package/dist/temporal-supersession.d.ts +51 -26
  153. package/dist/temporal-supersession.js +3 -1
  154. package/dist/tier-migration.d.ts +1 -1
  155. package/dist/transfer/capsule-import.js +4 -4
  156. package/dist/transfer/types.d.ts +66 -66
  157. package/dist/verified-recall.js +7 -7
  158. package/package.json +2 -2
  159. package/src/lifecycle/tombstones.ts +30 -0
  160. package/src/orchestration/consolidation-run.ts +324 -106
  161. package/src/orchestration/contradiction-linking-coordinator.ts +237 -64
  162. package/src/orchestration/dependency-propagation-delivery.test.ts +2353 -0
  163. package/src/orchestration/dependency-propagation-delivery.ts +1143 -0
  164. package/src/orchestration/dependency-propagation-queue-state.ts +220 -0
  165. package/src/orchestration/dependency-propagation-recovery-replay.ts +237 -0
  166. package/src/orchestration/dependency-propagation-replay-reconciliation.ts +73 -0
  167. package/src/orchestration/dependency-propagation-triggers.test.ts +1069 -11
  168. package/src/orchestration/dependency-propagation.test.ts +82 -31
  169. package/src/orchestration/dependency-propagation.ts +55 -17
  170. package/src/orchestration/extraction-persist-deps.ts +2 -2
  171. package/src/orchestration/extraction-persist.ts +14 -15
  172. package/src/orchestration/extraction-queue-coordinator.ts +23 -1
  173. package/src/orchestration/extraction-shutdown.ts +29 -0
  174. package/src/orchestration/maintenance.ts +13 -7
  175. package/src/orchestration/turn-ingestion.ts +5 -1
  176. package/src/orchestrator-background-writes.test.ts +265 -1
  177. package/src/orchestrator-extraction-queue.test.ts +39 -0
  178. package/src/orchestrator.ts +59 -58
  179. package/src/storage/committed-invalidation.ts +34 -0
  180. package/src/storage/deletion-revision-store.ts +529 -0
  181. package/src/storage/supersession-audit.ts +41 -0
  182. package/src/storage/supersession-side-effects.ts +52 -25
  183. package/src/storage-contract/failure-semantics.test.ts +12 -0
  184. package/src/storage-contract/round-trip.test.ts +184 -2
  185. package/src/storage-deletion-revisions.test.ts +213 -1
  186. package/src/storage-frontmatter-cas.test.ts +141 -0
  187. package/src/storage-memory-lifecycle-ledger.test.ts +372 -0
  188. package/src/storage-replay.test.ts +149 -0
  189. package/src/storage.ts +345 -358
  190. package/src/temporal-supersession.test.ts +431 -2
  191. package/src/temporal-supersession.ts +567 -107
  192. package/src/testing/subjects/dependency-propagation.test.ts +179 -38
  193. package/dist/chunk-ENTCVTQR.js.map +0 -1
  194. package/dist/chunk-EWMTOVMY.js +0 -484
  195. package/dist/chunk-EWMTOVMY.js.map +0 -1
  196. package/dist/chunk-OYESWJ43.js.map +0 -1
  197. package/dist/chunk-VBHPOWBJ.js.map +0 -1
  198. package/dist/chunk-ZHY5TMJG.js.map +0 -1
  199. /package/dist/{capsule-merge-XMFBXVDP.js.map → capsule-merge-HCJJMLPS.js.map} +0 -0
  200. /package/dist/{chunk-PWN32TAG.js.map → chunk-2ASAS3CQ.js.map} +0 -0
  201. /package/dist/{chunk-WQM326GS.js.map → chunk-2LY6GIIE.js.map} +0 -0
  202. /package/dist/{chunk-JL43BVDE.js.map → chunk-2OIQDTDC.js.map} +0 -0
  203. /package/dist/{chunk-33SV3E43.js.map → chunk-3OY7KXHM.js.map} +0 -0
  204. /package/dist/{chunk-AIUBBHSF.js.map → chunk-5UZ6UCOZ.js.map} +0 -0
  205. /package/dist/{chunk-UT5BV5OG.js.map → chunk-7HXZJAID.js.map} +0 -0
  206. /package/dist/{chunk-M7BXDKPR.js.map → chunk-7M6NYQWB.js.map} +0 -0
  207. /package/dist/{chunk-TPTECFWR.js.map → chunk-B6LZ4IBH.js.map} +0 -0
  208. /package/dist/{chunk-J5LLX6IL.js.map → chunk-BF6SXNQJ.js.map} +0 -0
  209. /package/dist/{chunk-4PC4PRJE.js.map → chunk-BKIFTXHR.js.map} +0 -0
  210. /package/dist/{chunk-VVG2BOKA.js.map → chunk-BMEYMKWX.js.map} +0 -0
  211. /package/dist/{chunk-DCZBSE4F.js.map → chunk-D5YOZ66N.js.map} +0 -0
  212. /package/dist/{chunk-BHXYAVUM.js.map → chunk-DLG5V6ND.js.map} +0 -0
  213. /package/dist/{chunk-52UKFTUK.js.map → chunk-F4EU6VUC.js.map} +0 -0
  214. /package/dist/{chunk-M4A44LUA.js.map → chunk-GSTIBXBW.js.map} +0 -0
  215. /package/dist/{chunk-W5XVQFLM.js.map → chunk-GXFSQ6SD.js.map} +0 -0
  216. /package/dist/{chunk-XADV5IZ3.js.map → chunk-HDIUYMSB.js.map} +0 -0
  217. /package/dist/{chunk-FXTYIA46.js.map → chunk-J272WKX2.js.map} +0 -0
  218. /package/dist/{chunk-JYL3BVVZ.js.map → chunk-JSEJH2YQ.js.map} +0 -0
  219. /package/dist/{chunk-5UDMFE3Q.js.map → chunk-MLXZD4WM.js.map} +0 -0
  220. /package/dist/{chunk-PUSLMZBQ.js.map → chunk-NOEMGE56.js.map} +0 -0
  221. /package/dist/{chunk-C5QLI33R.js.map → chunk-QINJITDS.js.map} +0 -0
  222. /package/dist/{chunk-45BRYON6.js.map → chunk-QPOHMO6M.js.map} +0 -0
  223. /package/dist/{chunk-ZQOCRIGA.js.map → chunk-QYN5PXDZ.js.map} +0 -0
  224. /package/dist/{chunk-PA45XCUD.js.map → chunk-RANKJI5T.js.map} +0 -0
  225. /package/dist/{chunk-37ZO6LLJ.js.map → chunk-RYLEDKQ2.js.map} +0 -0
  226. /package/dist/{chunk-CV6BBVYV.js.map → chunk-TEMSWHQD.js.map} +0 -0
  227. /package/dist/{chunk-QGR6OVGT.js.map → chunk-VFSAWRMZ.js.map} +0 -0
  228. /package/dist/{chunk-NLK2VZYZ.js.map → chunk-VXCNQYTC.js.map} +0 -0
  229. /package/dist/{chunk-AZ2QBGYB.js.map → chunk-W7Y5JDVB.js.map} +0 -0
  230. /package/dist/{chunk-4F6X3RXW.js.map → chunk-X57M2Q4I.js.map} +0 -0
  231. /package/dist/{chunk-6MTSFX7A.js.map → chunk-X7XEDOSL.js.map} +0 -0
  232. /package/dist/{chunk-UEVLYVNW.js.map → chunk-YXKVLYXU.js.map} +0 -0
  233. /package/dist/{chunk-OQAL6SIR.js.map → chunk-ZMSZVISA.js.map} +0 -0
  234. /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
- function makeStorage(initial: MemoryFile[]): TriggerStorage {
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(path: string): Promise<MemoryFile | null> {
87
- return [...memories.values()].find((memory) => memory.path === path) ?? null;
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<void> {},
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(id: string, replacementId: string, reason: string): Promise<boolean> {
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.status = "superseded";
114
- current.frontmatter.supersededBy = replacementId;
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 invalidateMemory(id: string): Promise<boolean> {
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 { memories, supersedeCalls, frontmatterCalls, invalidateCalls, storage };
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: propagationConfig(),
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
+ });