@remnic/core 9.51.0 → 9.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -1,10 +1,15 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
- import { findDependents, propagateInvalidation } from "./dependency-propagation.js";
4
+ import {
5
+ findDependents,
6
+ isDependencyPropagationEnabled,
7
+ propagateInvalidation,
8
+ } from "./dependency-propagation.js";
5
9
  import type { ExtractionEngine } from "../extraction.js";
6
10
  import type { MemoryFile, MemoryLinkType, PluginConfig } from "../types.js";
7
11
  import type { StorageManager } from "../storage.js";
12
+ import { invalidationCommitFingerprint } from "../storage/deletion-revision-store.js";
8
13
 
9
14
  type Verdict = {
10
15
  memoryId: string;
@@ -25,8 +30,17 @@ type Fixture = {
25
30
  extraction: ExtractionEngine;
26
31
  calls: {
27
32
  revalidate: RevalidationCall[];
28
- supersede: Array<{ id: string; replacementId: string; reason: string }>;
29
- frontmatter: Array<{ id: string; patch: Record<string, unknown> }>;
33
+ supersede: Array<{
34
+ id: string;
35
+ replacementId: string;
36
+ reason: string;
37
+ metadata?: Record<string, unknown>;
38
+ options: {
39
+ requireActive?: boolean;
40
+ acceptExactReplay?: boolean;
41
+ expectedSnapshot?: Pick<MemoryFile, "content" | "frontmatter"> & Partial<Pick<MemoryFile, "path">>;
42
+ };
43
+ }>;
30
44
  };
31
45
  };
32
46
 
@@ -77,36 +91,63 @@ function config(overrides: Record<string, unknown> = {}): PluginConfig {
77
91
  } as unknown as PluginConfig;
78
92
  }
79
93
 
94
+ test("dependency propagation enable gate accepts only normalized true values and a positive limit", () => {
95
+ assert.equal(isDependencyPropagationEnabled(config()), true);
96
+ assert.equal(isDependencyPropagationEnabled(config({ dependencyPropagation: { enabled: "yes" } })), true);
97
+ assert.equal(isDependencyPropagationEnabled(config({ dependencyPropagation: { enabled: "disabled" } })), false);
98
+ assert.equal(isDependencyPropagationEnabled(config({ dependencyPropagation: { maxDependents: 0 } })), false);
99
+ });
100
+
80
101
  function fixture(
81
102
  initial: MemoryFile[],
82
103
  verdicts: Verdict[] | Error | ((signal?: AbortSignal) => Promise<{ verdicts: Verdict[] }>),
83
104
  ): Fixture {
84
105
  const memories = new Map(initial.map((item) => [item.frontmatter.id, item]));
85
- const calls: Fixture["calls"] = { revalidate: [], supersede: [], frontmatter: [] };
86
-
106
+ const calls: Fixture["calls"] = { revalidate: [], supersede: [] };
87
107
  const storage = {
108
+
88
109
  async readAllMemories(): Promise<MemoryFile[]> {
89
- return [...memories.values()];
110
+ return structuredClone([...memories.values()]);
90
111
  },
91
112
  async getMemoryById(id: string): Promise<MemoryFile | null> {
92
113
  return memories.get(id) ?? null;
93
114
  },
94
- async supersedeMemory(id: string, replacementId: string, reason: string): Promise<boolean> {
95
- calls.supersede.push({ id, replacementId, reason });
115
+ async supersedeMemory(
116
+ id: string,
117
+ replacementId: string,
118
+ reason: string,
119
+ metadata?: Record<string, unknown>,
120
+ options: {
121
+ requireActive?: boolean;
122
+ acceptExactReplay?: boolean;
123
+ expectedSnapshot?: Pick<MemoryFile, "content" | "frontmatter"> & Partial<Pick<MemoryFile, "path">>;
124
+ } = {},
125
+ ): Promise<boolean> {
126
+ calls.supersede.push({ id, replacementId, reason, metadata, options });
96
127
  const current = memories.get(id);
97
128
  if (!current) return false;
98
- current.frontmatter.status = "superseded";
99
- current.frontmatter.supersededBy = replacementId;
100
- return true;
101
- },
102
- async writeMemoryFrontmatter(
103
- current: MemoryFile,
104
- patch: Record<string, unknown>,
105
- ): Promise<boolean> {
106
- calls.frontmatter.push({ id: current.frontmatter.id, patch });
107
- const stored = memories.get(current.frontmatter.id);
108
- if (!stored) return false;
109
- Object.assign(stored.frontmatter, patch);
129
+ if (
130
+ options.expectedSnapshot &&
131
+ invalidationCommitFingerprint(current) !== invalidationCommitFingerprint(options.expectedSnapshot)
132
+ ) {
133
+ return false;
134
+ }
135
+ const exactReplay =
136
+ current.frontmatter.status === "superseded" &&
137
+ current.frontmatter.supersededBy === replacementId &&
138
+ current.frontmatter.supersessionCause === metadata?.supersessionCause &&
139
+ current.frontmatter.invalidatedBy === metadata?.invalidatedBy;
140
+ if (exactReplay) return options.acceptExactReplay === true;
141
+ if (
142
+ options.requireActive === true &&
143
+ (current.frontmatter.status ?? "active") !== "active"
144
+ ) {
145
+ return false;
146
+ }
147
+ Object.assign(current.frontmatter, metadata, {
148
+ status: "superseded",
149
+ supersededBy: replacementId,
150
+ });
110
151
  return true;
111
152
  },
112
153
  } as unknown as StorageManager;
@@ -293,6 +334,7 @@ test("invalidated verdict supersedes the dependent and persists dependency front
293
334
  const old = memory("old", { links: [{ targetId: "dep", linkType: "supports" }] });
294
335
  const dependent = memory("dep");
295
336
  const fixtureValue = fixture([old, dependent], [{ memoryId: "dep", verdict: "invalidated", reason: "claim lost support" }]);
337
+ const expectedSnapshot = structuredClone(dependent);
296
338
 
297
339
  const result = await propagateInvalidation(
298
340
  deps(fixtureValue),
@@ -317,18 +359,32 @@ test("invalidated verdict supersedes the dependent and persists dependency front
317
359
  id: "dep",
318
360
  replacementId: "replacement",
319
361
  reason: "dependency_propagation:contradiction",
320
- },
321
- ]);
322
- assert.deepEqual(fixtureValue.calls.frontmatter, [
323
- {
324
- id: "dep",
325
- patch: { supersessionCause: "dependency", invalidatedBy: "old" },
362
+ metadata: { supersessionCause: "dependency", invalidatedBy: "old" },
363
+ options: { requireActive: true, acceptExactReplay: true, expectedSnapshot },
326
364
  },
327
365
  ]);
328
366
  assert.equal(fixtureValue.memories.get("dep")?.frontmatter.status, "superseded");
329
367
  assert.equal(fixtureValue.memories.get("dep")?.frontmatter.supersessionCause, "dependency");
330
368
  assert.equal(fixtureValue.memories.get("dep")?.frontmatter.invalidatedBy, "old");
331
369
  });
370
+ test("supersede fixture rejects a stale expected snapshot after concurrent mutation", async () => {
371
+ const old = memory("old", { links: [{ targetId: "dep", linkType: "supports" }] });
372
+ const dependent = memory("dep");
373
+ const fixtureValue = fixture([old, dependent], []);
374
+ const expectedSnapshot = structuredClone(dependent);
375
+ dependent.content = "concurrent update";
376
+
377
+ const superseded = await fixtureValue.storage.supersedeMemory(
378
+ "dep",
379
+ "replacement",
380
+ "dependency_propagation:contradiction",
381
+ { supersessionCause: "dependency", invalidatedBy: "old" },
382
+ { requireActive: true, expectedSnapshot },
383
+ );
384
+
385
+ assert.equal(superseded, false);
386
+ assert.equal(fixtureValue.memories.get("dep")?.frontmatter.status, "active");
387
+ });
332
388
 
333
389
  test("still_valid and uncertain verdicts do not write", async () => {
334
390
  const old = memory("old", {
@@ -356,7 +412,6 @@ test("still_valid and uncertain verdicts do not write", async () => {
356
412
  assert.equal(result.stillValid, 1);
357
413
  assert.equal(result.uncertain, 1);
358
414
  assert.equal(fixtureValue.calls.supersede.length, 0);
359
- assert.equal(fixtureValue.calls.frontmatter.length, 0);
360
415
  });
361
416
 
362
417
  test("unknown, missing, and garbage verdicts default to uncertain and drop unknown ids", async () => {
@@ -412,7 +467,6 @@ test("LLM errors skip the event and perform zero writes", async () => {
412
467
  assert.equal(typeof result.durationMs, "number");
413
468
  assert.equal(result.skipped, "llm_error");
414
469
  assert.equal(fixtureValue.calls.supersede.length, 0);
415
- assert.equal(fixtureValue.calls.frontmatter.length, 0);
416
470
  });
417
471
 
418
472
  test("LLM timeout aborts the batch and performs zero writes", async () => {
@@ -439,7 +493,6 @@ test("LLM timeout aborts the batch and performs zero writes", async () => {
439
493
  assert.equal(typeof result.durationMs, "number");
440
494
  assert.equal(result.skipped, "timeout");
441
495
  assert.equal(fixtureValue.calls.supersede.length, 0);
442
- assert.equal(fixtureValue.calls.frontmatter.length, 0);
443
496
  });
444
497
 
445
498
  test("a completion that ignores AbortSignal returns at the shared deadline", async (t) => {
@@ -470,7 +523,6 @@ test("a completion that ignores AbortSignal returns at the shared deadline", asy
470
523
  assert.equal(result.route, "fast-completion");
471
524
  assert.equal(result.durationMs, 20);
472
525
  assert.equal(fixtureValue.calls.supersede.length, 0);
473
- assert.equal(fixtureValue.calls.frontmatter.length, 0);
474
526
  } finally {
475
527
  t.mock.timers.reset();
476
528
  }
@@ -515,7 +567,6 @@ test("dryRun computes invalidation verdicts without writing", async () => {
515
567
  assert.equal(result.invalidated, 1);
516
568
  assert.equal(fixtureValue.calls.revalidate.length, 1);
517
569
  assert.equal(fixtureValue.calls.supersede.length, 0);
518
- assert.equal(fixtureValue.calls.frontmatter.length, 0);
519
570
  assert.equal(fixtureValue.memories.get("dep")?.frontmatter.status, "active");
520
571
  });
521
572
 
@@ -6,7 +6,6 @@
6
6
  */
7
7
  import type { ExtractionEngine } from "../extraction.js";
8
8
  import { log } from "../logger.js";
9
- import type { StorageManager } from "../index.js";
10
9
  import type {
11
10
  DependencyPropagationConfig,
12
11
  MemoryFile,
@@ -15,9 +14,14 @@ import type {
15
14
  PluginConfig,
16
15
  } from "../types.js";
17
16
 
17
+ export function isDependencyPropagationEnabled(config: Pick<PluginConfig, "dependencyPropagation">): boolean {
18
+ const parsed = propagationConfig(config);
19
+ return isEnabled(parsed.enabled) && parsed.maxDependents > 0;
20
+ }
21
+
18
22
  export interface PropagationEvent {
19
23
  /** Snapshot captured before the primary supersession or deletion. */
20
- oldMemory: { content: string; frontmatter: MemoryFrontmatter };
24
+ oldMemory: { content: string; frontmatter: MemoryFrontmatter; path?: string };
21
25
  replacementId: string | null;
22
26
  replacementContent: string | null;
23
27
  cause:
@@ -26,6 +30,11 @@ export interface PropagationEvent {
26
30
  | "consolidation_invalidate"
27
31
  | "consolidation_merge";
28
32
  namespaceScope: string;
33
+ temporalMutation?: {
34
+ supersededAt: string;
35
+ invalidAt?: string;
36
+ matchedKeys: string[];
37
+ };
29
38
  }
30
39
 
31
40
  export interface PropagationResult {
@@ -38,7 +47,27 @@ export interface PropagationResult {
38
47
  durationMs: number;
39
48
  }
40
49
 
41
- function propagationConfig(config: PluginConfig): DependencyPropagationConfig {
50
+ export type DependencyPropagationWriteFence = <T>(
51
+ write: () => Promise<T>,
52
+ ) => Promise<T | undefined>;
53
+
54
+ export interface DependencyPropagationStorage {
55
+ readAllMemories(): Promise<MemoryFile[]>;
56
+ getMemoryById(id: string): Promise<MemoryFile | null>;
57
+ supersedeMemory(
58
+ id: string,
59
+ replacementId: string,
60
+ reason: string,
61
+ metadata?: Pick<MemoryFrontmatter, "supersessionCause" | "invalidatedBy">,
62
+ options?: {
63
+ requireActive?: boolean;
64
+ acceptExactReplay?: boolean;
65
+ expectedSnapshot?: Pick<MemoryFile, "content" | "frontmatter"> & Partial<Pick<MemoryFile, "path">>;
66
+ },
67
+ ): Promise<boolean>;
68
+ }
69
+
70
+ function propagationConfig(config: Pick<PluginConfig, "dependencyPropagation">): DependencyPropagationConfig {
42
71
  return config.dependencyPropagation;
43
72
  }
44
73
 
@@ -142,7 +171,12 @@ function isTimeoutError(error: unknown, signal: AbortSignal): boolean {
142
171
  * constructs another storage or scans a different namespace.
143
172
  */
144
173
  export async function propagateInvalidation(
145
- deps: { storage: StorageManager; extraction: ExtractionEngine; config: PluginConfig },
174
+ deps: {
175
+ storage: DependencyPropagationStorage;
176
+ extraction: ExtractionEngine;
177
+ config: PluginConfig;
178
+ writeFence?: DependencyPropagationWriteFence;
179
+ },
146
180
  event: PropagationEvent,
147
181
  ): Promise<PropagationResult> {
148
182
  const startedAt = Date.now();
@@ -255,19 +289,23 @@ export async function propagateInvalidation(
255
289
  }
256
290
  try {
257
291
  const replacementId = event.replacementId ?? event.oldMemory.frontmatter.id;
258
- const superseded = await deps.storage.supersedeMemory(
259
- dependent.frontmatter.id,
260
- replacementId,
261
- `dependency_propagation:${event.cause}`,
262
- );
263
- if (!superseded) continue;
264
- const current = await deps.storage.getMemoryById(dependent.frontmatter.id);
265
- if (current) {
266
- await deps.storage.writeMemoryFrontmatter(current, {
267
- supersessionCause: "dependency",
268
- invalidatedBy: event.oldMemory.frontmatter.id,
269
- });
270
- }
292
+ const write = () =>
293
+ deps.storage.supersedeMemory(
294
+ dependent.frontmatter.id,
295
+ replacementId,
296
+ `dependency_propagation:${event.cause}`,
297
+ {
298
+ supersessionCause: "dependency",
299
+ invalidatedBy: event.oldMemory.frontmatter.id,
300
+ },
301
+ {
302
+ requireActive: true,
303
+ acceptExactReplay: true,
304
+ expectedSnapshot: dependent,
305
+ },
306
+ );
307
+ const superseded = deps.writeFence ? await deps.writeFence(write) : await write();
308
+ if (superseded !== true) continue;
271
309
  invalidated += 1;
272
310
  } catch (error) {
273
311
  log.warn(`dependency propagation write failed for ${dependent.frontmatter.id}: ${error}`);
@@ -12,12 +12,12 @@ import type { RouteRule, RoutingEngineOptions } from "../routing/engine.js";
12
12
  import type { SearchBackend } from "../search/port.js";
13
13
  import type { ThreadingManager } from "../threading.js";
14
14
  import type { MemoryFile, MemoryLink, PluginConfig } from "../types.js";
15
-
15
+ import type { DependencyPropagationDeliveryPort } from "./dependency-propagation-delivery.js";
16
16
  export interface ExtractionPersistDeps {
17
17
  config: PluginConfig;
18
18
  getStorageRouter: () => NamespaceStorageRouter;
19
19
  getThreading: () => ThreadingManager;
20
- getExtraction: () => ExtractionEngine;
20
+ getDependencyPropagationDelivery: () => DependencyPropagationDeliveryPort;
21
21
  getLocalLlm: () => LocalLlmClient;
22
22
  getQmd: () => SearchBackend;
23
23
  getJudgeVerdictCache: () => Map<string, JudgeVerdict>;
@@ -84,6 +84,7 @@ import { decideSemanticDedup, type SemanticDedupDecision, type SemanticDedupHit
84
84
  import { selectRouteRule, type RouteRule, type RoutingEngineOptions } from "../routing/engine.js";
85
85
  import { ThreadingManager } from "../threading.js";
86
86
  import { NamespaceStorageRouter } from "../namespaces/storage.js";
87
+ import type { ResolvedScopeProfilePlan } from "../namespaces/scope-profiles.js";
87
88
  import type { SearchBackend } from "../search/port.js";
88
89
  import { inferIntentFromText } from "../intent.js";
89
90
  import type {
@@ -103,7 +104,8 @@ import {
103
104
  shouldPromoteToShared,
104
105
  } from "./extraction-persist-promotion.js";
105
106
  import type { ExtractionPersistDeps } from "./extraction-persist-deps.js";
106
- import type { ResolvedScopeProfilePlan } from "../namespaces/scope-profiles.js";
107
+ import type { DependencyPropagationDeliveryPort } from "./dependency-propagation-delivery.js";
108
+ import { isDependencyPropagationEnabled } from "./dependency-propagation.js";
107
109
  import {
108
110
  buildMemoryPathById,
109
111
  appendMemoryToGraphContext,
@@ -114,8 +116,6 @@ import { persistConstructedHarmonicRecords } from "./harmonic-construction-persi
114
116
  import { ExtractionAnchorSnapshot } from "./extraction-anchor-snapshot.js";
115
117
 
116
118
 
117
-
118
-
119
119
  export class ExtractionPersistCoordinator {
120
120
  constructor(
121
121
  private readonly deps: ExtractionPersistDeps,
@@ -123,6 +123,10 @@ export class ExtractionPersistCoordinator {
123
123
  private get config(): PluginConfig {
124
124
  return this.deps.config;
125
125
  }
126
+ private get dependencyPropagationDelivery(): DependencyPropagationDeliveryPort | undefined {
127
+ if (!isDependencyPropagationEnabled(this.config)) return undefined;
128
+ return this.deps.getDependencyPropagationDelivery();
129
+ }
126
130
 
127
131
  async persistExtraction(
128
132
  result: ExtractionResult,
@@ -500,8 +504,7 @@ export class ExtractionPersistCoordinator {
500
504
  structuredAttributes: { ...targetSurvivingAttrs },
501
505
  createdAt: supersessionOrderingAt(options.validAt),
502
506
  enabled: !(options.eventTimeSource === "extracted" && !options.validAt),
503
- extraction: this.deps.getExtraction(),
504
- config: this.config,
507
+ dependencyPropagationDelivery: this.dependencyPropagationDelivery,
505
508
  namespaceScope: this.deps.storageDirNamespace(targetStorage.dir),
506
509
  });
507
510
  } catch (profileSupersessionErr) {
@@ -800,10 +803,9 @@ export class ExtractionPersistCoordinator {
800
803
  entityRef: sharedPromotionEnvelope.entityRef,
801
804
  structuredAttributes: { ...sharedSurvivingAttrs },
802
805
  createdAt: supersessionOrderingAt(options.validAt),
803
- enabled: !(options.eventTimeSource === "extracted" && !options.validAt),
804
806
  useCallerTimestamp: true,
805
- extraction: this.deps.getExtraction(),
806
- config: this.config,
807
+ enabled: !(options.eventTimeSource === "extracted" && !options.validAt),
808
+ dependencyPropagationDelivery: this.dependencyPropagationDelivery,
807
809
  namespaceScope: this.deps.storageDirNamespace(sharedStorage.dir),
808
810
  });
809
811
  // Catalog touch (issue #1499 — codex P2 NElSf): this dedup branch
@@ -927,8 +929,7 @@ export class ExtractionPersistCoordinator {
927
929
  structuredAttributes: { ...sharedSurvivingAttrs },
928
930
  createdAt: supersessionOrderingAt(options.validAt),
929
931
  enabled: !(options.eventTimeSource === "extracted" && !options.validAt),
930
- extraction: this.deps.getExtraction(),
931
- config: this.config,
932
+ dependencyPropagationDelivery: this.dependencyPropagationDelivery,
932
933
  namespaceScope: this.deps.storageDirNamespace(sharedStorage.dir),
933
934
  });
934
935
  } catch (sharedSupersessionErr) {
@@ -2358,14 +2359,13 @@ export class ExtractionPersistCoordinator {
2358
2359
  structuredAttributes: parentWriteEnvelope.rawStructuredAttributes
2359
2360
  ? { ...parentWriteEnvelope.rawStructuredAttributes }
2360
2361
  : undefined,
2361
- createdAt: supersessionOrderingAt(biTemporal?.validFrom ?? sourceContext?.validAt),
2362
2362
  // #1578 r3: an extracted end-only bound (validFrom absent) is
2363
2363
  // historical, not a new authoritative state — never let it
2364
2364
  // supersede a later active fact (codex P1 on :15534).
2365
+ createdAt: supersessionOrderingAt(biTemporal?.validFrom ?? sourceContext?.validAt),
2365
2366
  enabled: lifecycleCaps.temporalSupersession &&
2366
2367
  !(biTemporal && !biTemporal.validFrom),
2367
- extraction: this.deps.getExtraction(),
2368
- config: this.config,
2368
+ dependencyPropagationDelivery: this.dependencyPropagationDelivery,
2369
2369
  namespaceScope: this.deps.storageDirNamespace(targetStorage.dir),
2370
2370
  });
2371
2371
  for (const supersededId of temporalSupersession.supersededIds) {
@@ -2646,8 +2646,7 @@ export class ExtractionPersistCoordinator {
2646
2646
  createdAt: supersessionOrderingAt(biTemporal?.validFrom ?? sourceContext?.validAt),
2647
2647
  enabled: lifecycleCaps.temporalSupersession &&
2648
2648
  !(biTemporal && !biTemporal.validFrom),
2649
- extraction: this.deps.getExtraction(),
2650
- config: this.config,
2649
+ dependencyPropagationDelivery: this.dependencyPropagationDelivery,
2651
2650
  namespaceScope: this.deps.storageDirNamespace(targetStorage.dir),
2652
2651
  });
2653
2652
  for (const supersededId of temporalSupersession.supersededIds) {
@@ -39,6 +39,7 @@ export class ExtractionQueueCoordinator {
39
39
  private readonly queue: Array<() => Promise<void>> = [];
40
40
  /** Whether the serial drain loop is currently running. */
41
41
  private processing = false;
42
+ private accepting = true;
42
43
 
43
44
  /** Current queue depth (idle-wait + test seam). */
44
45
  get length(): number {
@@ -55,7 +56,8 @@ export class ExtractionQueueCoordinator {
55
56
  * the production entry point — the orchestrator's `queueBufferedExtraction`
56
57
  * builds each task closure and hands it here.
57
58
  */
58
- enqueue(task: () => Promise<void>): void {
59
+ enqueue(task: () => Promise<void>): boolean {
60
+ if (!this.accepting) return false;
59
61
  this.queue.push(task);
60
62
  if (!this.processing) {
61
63
  this.processing = true;
@@ -64,6 +66,26 @@ export class ExtractionQueueCoordinator {
64
66
  this.processing = false;
65
67
  });
66
68
  }
69
+ return true;
70
+ }
71
+
72
+ /** Stop new producers from entering the queue. */
73
+ stopAccepting(): void {
74
+ this.accepting = false;
75
+ }
76
+
77
+ /** Resume producer acceptance after an orchestrator re-initializes. */
78
+ resumeAccepting(): void {
79
+ this.accepting = true;
80
+ }
81
+
82
+ /** Stop new producers and wait for all accepted work to finish. */
83
+ async pauseAndDrain(timeoutMs: number = 60_000): Promise<boolean> {
84
+ this.stopAccepting();
85
+ if (await this.waitForIdle(timeoutMs)) return true;
86
+ log.warn("extraction queue drain exceeded timeout; waiting for accepted work to settle");
87
+ await this.waitForIdle(Number.POSITIVE_INFINITY);
88
+ return true;
67
89
  }
68
90
 
69
91
  /**
@@ -0,0 +1,29 @@
1
+ import { log } from "../logger.js";
2
+
3
+ export interface ExtractionDrainPort {
4
+ pauseAndDrain(): Promise<boolean>;
5
+ }
6
+
7
+ export interface DependencyPropagationShutdownPort {
8
+ shutdown(): Promise<void>;
9
+ }
10
+
11
+ export async function drainExtractionAndShutdownDependencyPropagation(
12
+ extractionQueue: ExtractionDrainPort,
13
+ dependencyPropagationDelivery: DependencyPropagationShutdownPort | undefined,
14
+ ): Promise<unknown | undefined> {
15
+ let extractionDrainError: unknown;
16
+ try {
17
+ if (!await extractionQueue.pauseAndDrain()) {
18
+ extractionDrainError = new Error("orchestrator.destroy: extraction queue did not drain before teardown");
19
+ }
20
+ } catch (error) {
21
+ extractionDrainError = error;
22
+ }
23
+ try {
24
+ await dependencyPropagationDelivery?.shutdown();
25
+ } catch (error) {
26
+ log.warn(`orchestrator.destroy: dependency propagation shutdown failed: ${error}`);
27
+ }
28
+ return extractionDrainError;
29
+ }
@@ -126,7 +126,8 @@ export class MaintenanceScheduler {
126
126
  // ── Consolidation scheduling state ──
127
127
  private nonZeroExtractionsSinceConsolidation = 0;
128
128
  private lastConsolidationRunAtMs = 0;
129
- private consolidationInFlight = false;
129
+ private consolidationRunPromise: Promise<unknown> | null = null;
130
+ private disposed = false;
130
131
 
131
132
  // ── QMD maintenance scheduling state ──
132
133
  private qmdMaintenanceTimer: NodeJS.Timeout | null = null;
@@ -442,15 +443,17 @@ export class MaintenanceScheduler {
442
443
  ) {
443
444
  return;
444
445
  }
445
- if (this.consolidationInFlight) return;
446
+ if (this.disposed || this.consolidationRunPromise) return;
446
447
 
447
- this.consolidationInFlight = true;
448
448
  this.lastConsolidationRunAtMs = now;
449
449
  this.nonZeroExtractionsSinceConsolidation = 0;
450
- runConsolidation()
451
- .catch((err) => log.error("background consolidation failed", err))
450
+ this.consolidationRunPromise = Promise.resolve()
451
+ .then(runConsolidation)
452
+ .catch((err) => {
453
+ log.error("background consolidation failed", err);
454
+ })
452
455
  .finally(() => {
453
- this.consolidationInFlight = false;
456
+ this.consolidationRunPromise = null;
454
457
  });
455
458
  }
456
459
 
@@ -458,7 +461,7 @@ export class MaintenanceScheduler {
458
461
  * the orchestrator's waitForConsolidationIdle can poll idle state without
459
462
  * owning the cadence state (issue #1526 PR1). */
460
463
  isConsolidationInFlight(): boolean {
461
- return this.consolidationInFlight;
464
+ return this.consolidationRunPromise !== null;
462
465
  }
463
466
 
464
467
  /** Whether a debounced QMD maintenance pass is armed and waiting for the
@@ -1174,6 +1177,8 @@ export class MaintenanceScheduler {
1174
1177
  * teardown. Async: awaits the scheduler's abort+drain before resolving.
1175
1178
  */
1176
1179
  async dispose(): Promise<void> {
1180
+ this.disposed = true;
1181
+ const consolidationDrain = this.consolidationRunPromise;
1177
1182
  // The registrar latches teardown first (so a draining tick's onRun cannot
1178
1183
  // re-arm QMD maintenance) and aborts+drains the sync. Capture its drain and
1179
1184
  // await it last so the rest of teardown runs while the aborted tick unwinds.
@@ -1190,5 +1195,6 @@ export class MaintenanceScheduler {
1190
1195
  this.qmdMaintenanceTimer = null;
1191
1196
  }
1192
1197
  this.qmdMaintenancePending = false;
1198
+ if (consolidationDrain) await consolidationDrain;
1193
1199
  }
1194
1200
  }
@@ -727,7 +727,7 @@ export class TurnIngestionCoordinator {
727
727
  scheduleQueueWaitTimeout();
728
728
  }
729
729
 
730
- this.deps.extractionQueueCoordinator.enqueue(async () => {
730
+ const accepted = this.deps.extractionQueueCoordinator.enqueue(async () => {
731
731
  if (settled) return;
732
732
  if (
733
733
  typeof extractionDeadlineMs === "number" &&
@@ -761,6 +761,10 @@ export class TurnIngestionCoordinator {
761
761
  }
762
762
  }
763
763
  });
764
+ if (!accepted) {
765
+ settleTask(new Error("extraction queue is stopped"));
766
+ return;
767
+ }
764
768
 
765
769
  log.debug(`queued extraction from ${reason}`);
766
770
  }