@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
@@ -0,0 +1,529 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, open, readFile, rename, unlink, type FileHandle } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { resolveSafeStoragePath } from "../storage-paths.js";
5
+ import type { MemoryFile } from "../types.js";
6
+ import { isErrnoCode } from "../utils/errno.js";
7
+ import { withHeldFileLock, type HeldFileLockController } from "../utils/serialize-mutations.js";
8
+ import { log } from "../logger.js";
9
+
10
+ type DeletionRevisionMetadata = {
11
+ version: 1;
12
+ deletions: Array<{ path: string; mtimeMs: number }>;
13
+ };
14
+
15
+ type InvalidationCommitMetadata = {
16
+ version: 1;
17
+ commits: Array<{ memoryId: string; fingerprint: string; committedAt: number }>;
18
+ };
19
+
20
+ type InvalidationCommit = { fingerprint: string; committedAt: number };
21
+
22
+ type ManagedStoragePathGuard = (filePath: string, method: string) => string;
23
+
24
+ export type DeletionRevisionStoreOptions = {
25
+ baseDir: string;
26
+ deletionRevisionMetadataPath: string;
27
+ invalidationCommitMetadataPath: string;
28
+ deletionRevisionLockPath: string;
29
+ assertManagedStoragePath: ManagedStoragePathGuard;
30
+ };
31
+
32
+ const DELETION_REVISION_MAX_MTIME_MS = 8_640_000_000_000_000;
33
+ const DELETION_REVISION_LOCK_STALE_MS = 60_000;
34
+ const DELETION_REVISION_LOCK_MAX_WAIT_MS = 120_000;
35
+ const INVALIDATION_COMMIT_MAX_TIMESTAMP_MS = 8_640_000_000_000_000;
36
+ const INVALIDATION_PROOF_QUARANTINE_MAX_IDS = 1024;
37
+
38
+ function canonicalizeInvalidationEvidence(value: unknown): unknown {
39
+ if (Array.isArray(value)) return value.map((item) => canonicalizeInvalidationEvidence(item));
40
+ if (value && typeof value === "object") {
41
+ const result: Record<string, unknown> = {};
42
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
43
+ result[key] = canonicalizeInvalidationEvidence((value as Record<string, unknown>)[key]);
44
+ }
45
+ return result;
46
+ }
47
+ return value;
48
+ }
49
+
50
+ export function invalidationCommitFingerprint(memory: Pick<MemoryFile, "content" | "frontmatter">): string {
51
+ const frontmatter = { ...memory.frontmatter };
52
+ delete frontmatter.accessCount;
53
+ delete frontmatter.lastAccessed;
54
+ return createHash("sha256")
55
+ .update(
56
+ JSON.stringify(
57
+ canonicalizeInvalidationEvidence({
58
+ content: memory.content,
59
+ frontmatter,
60
+ }),
61
+ ),
62
+ )
63
+ .digest("hex");
64
+ }
65
+
66
+ function isValidInvalidationCommitTimestamp(value: unknown): value is number {
67
+ return (
68
+ typeof value === "number" &&
69
+ Number.isFinite(value) &&
70
+ value >= 0 &&
71
+ value <= INVALIDATION_COMMIT_MAX_TIMESTAMP_MS
72
+ );
73
+ }
74
+
75
+ function isValidInvalidationCommitFingerprint(value: unknown): value is string {
76
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
77
+ }
78
+
79
+ function isValidMemoryId(value: unknown): value is string {
80
+ return typeof value === "string" && value.length > 0 && !value.includes("\0");
81
+ }
82
+
83
+ function isValidDeletionRevisionPath(value: unknown): value is string {
84
+ return (
85
+ typeof value === "string" &&
86
+ value.length > 0 &&
87
+ !value.includes("\\") &&
88
+ !value.includes("\0") &&
89
+ !path.posix.isAbsolute(value) &&
90
+ path.posix.normalize(value) === value &&
91
+ value !== "." &&
92
+ value !== ".." &&
93
+ !value.startsWith("../")
94
+ );
95
+ }
96
+
97
+ function deletionRevisionPathIdentity(value: string): string {
98
+ return value.normalize("NFC").toUpperCase().toLowerCase();
99
+ }
100
+
101
+ export class DeletionRevisionStore {
102
+ private readonly options: DeletionRevisionStoreOptions;
103
+ private readonly invalidationProofQuarantine = new Set<string>();
104
+ private invalidationProofQuarantineFull = false;
105
+ private invalidationProofQuarantineFullAt: number | null = null;
106
+
107
+ constructor(options: DeletionRevisionStoreOptions) {
108
+ this.options = options;
109
+ }
110
+ private quarantineInvalidationProof(memoryId: string): void {
111
+ if (this.invalidationProofQuarantineFull) {
112
+ const nextBoundary = Date.now() + 1;
113
+ this.invalidationProofQuarantineFullAt = Math.max(
114
+ this.invalidationProofQuarantineFullAt ?? 0,
115
+ nextBoundary,
116
+ );
117
+ return;
118
+ }
119
+ if (this.invalidationProofQuarantine.has(memoryId)) return;
120
+ if (this.invalidationProofQuarantine.size >= INVALIDATION_PROOF_QUARANTINE_MAX_IDS) {
121
+ this.invalidationProofQuarantineFull = true;
122
+ this.invalidationProofQuarantineFullAt = Date.now() + 1;
123
+ return;
124
+ }
125
+ this.invalidationProofQuarantine.add(memoryId);
126
+ }
127
+ private async resolveConfiguredStoragePath(targetPath: string): Promise<string> {
128
+ const baseDir = path.resolve(this.options.baseDir);
129
+ const relativePath = path.relative(baseDir, path.resolve(targetPath));
130
+ return resolveSafeStoragePath(baseDir, relativePath);
131
+ }
132
+
133
+ private async resolveManagedStoragePath(filePath: string, method: string): Promise<{
134
+ target: string;
135
+ relativePath: string;
136
+ }> {
137
+ const assertedPath = this.options.assertManagedStoragePath(filePath, method);
138
+ const target = await this.resolveConfiguredStoragePath(assertedPath);
139
+ const relativePath = path.relative(path.resolve(this.options.baseDir), target).split(path.sep).join("/");
140
+ return { target, relativePath };
141
+ }
142
+
143
+ private parseDeletionRevisionMetadata(raw: string): Map<string, number> {
144
+ let parsed: unknown;
145
+ try {
146
+ parsed = JSON.parse(raw);
147
+ } catch {
148
+ throw new Error("Deletion revision metadata is invalid.");
149
+ }
150
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
151
+ throw new Error("Deletion revision metadata is invalid.");
152
+ }
153
+ const root = parsed as Record<string, unknown>;
154
+ if (
155
+ Object.keys(root).sort().join(",") !== "deletions,version" ||
156
+ root.version !== 1 ||
157
+ !Array.isArray(root.deletions)
158
+ ) {
159
+ throw new Error("Deletion revision metadata is invalid.");
160
+ }
161
+ const revisions = new Map<string, number>();
162
+ const pathByIdentity = new Map<string, string>();
163
+ for (const rawEntry of root.deletions) {
164
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
165
+ throw new Error("Deletion revision metadata is invalid.");
166
+ }
167
+ const entry = rawEntry as Record<string, unknown>;
168
+ if (
169
+ Object.keys(entry).sort().join(",") !== "mtimeMs,path" ||
170
+ !isValidDeletionRevisionPath(entry.path) ||
171
+ typeof entry.mtimeMs !== "number" ||
172
+ !Number.isFinite(entry.mtimeMs) ||
173
+ entry.mtimeMs < 0 ||
174
+ entry.mtimeMs > DELETION_REVISION_MAX_MTIME_MS ||
175
+ revisions.has(entry.path)
176
+ ) {
177
+ throw new Error("Deletion revision metadata is invalid.");
178
+ }
179
+ const identity = deletionRevisionPathIdentity(entry.path);
180
+ if (pathByIdentity.has(identity)) {
181
+ throw new Error("Deletion revision metadata is invalid.");
182
+ }
183
+ pathByIdentity.set(identity, entry.path);
184
+ revisions.set(entry.path, entry.mtimeMs);
185
+ }
186
+ return new Map([...revisions.entries()].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));
187
+ }
188
+ private async readDeletionRevisionMetadata(): Promise<Map<string, number>> {
189
+ const metadataPath = await this.resolveConfiguredStoragePath(this.options.deletionRevisionMetadataPath);
190
+ let raw: string;
191
+ try {
192
+ raw = await readFile(metadataPath, "utf8");
193
+ } catch (error) {
194
+ if (isErrnoCode(error, "ENOENT")) return new Map();
195
+ throw new Error("Deletion revision metadata is unavailable.");
196
+ }
197
+ return this.parseDeletionRevisionMetadata(raw);
198
+ }
199
+
200
+ private async writeDeletionRevisionMetadata(
201
+ revisions: ReadonlyMap<string, number>,
202
+ lock: HeldFileLockController,
203
+ ): Promise<void> {
204
+ const metadataPath = await this.resolveConfiguredStoragePath(this.options.deletionRevisionMetadataPath);
205
+ const deletions = [...revisions.entries()]
206
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
207
+ .map(([entryPath, mtimeMs]) => ({ path: entryPath, mtimeMs }));
208
+ const metadata: DeletionRevisionMetadata = { version: 1, deletions };
209
+ const temporaryPath = await this.resolveConfiguredStoragePath(
210
+ `${metadataPath}.${process.pid}.${randomUUID()}.tmp`,
211
+ );
212
+ let handle: FileHandle | null = null;
213
+ try {
214
+ handle = await open(temporaryPath, "wx", 0o600);
215
+ await handle.writeFile(`${JSON.stringify(metadata)}\n`, "utf8");
216
+ await handle.sync();
217
+ await handle.close();
218
+ handle = null;
219
+ if (!(await lock.refresh())) {
220
+ throw new Error("Deletion revision metadata lock was lost.");
221
+ }
222
+ const renameTarget = await this.resolveConfiguredStoragePath(this.options.deletionRevisionMetadataPath);
223
+ await rename(temporaryPath, renameTarget);
224
+ } finally {
225
+ if (handle !== null) {
226
+ await handle.close().catch((error: unknown) => {
227
+ log.warn("failed to close deletion revision metadata temporary file", error);
228
+ });
229
+ }
230
+ const cleanupPath = await this.resolveConfiguredStoragePath(temporaryPath).catch((error: unknown) => {
231
+ log.warn("failed to resolve deletion revision metadata temporary file for cleanup", error);
232
+ return null;
233
+ });
234
+ if (cleanupPath !== null) {
235
+ await unlink(cleanupPath).catch((error: unknown) => {
236
+ if (!isErrnoCode(error, "ENOENT")) {
237
+ log.warn("failed to clean up deletion revision metadata temporary file", error);
238
+ }
239
+ });
240
+ }
241
+ }
242
+ }
243
+
244
+ private async withDeletionRevisionLock<T>(task: (lock: HeldFileLockController) => Promise<T>): Promise<T> {
245
+ const lockPath = await this.resolveConfiguredStoragePath(this.options.deletionRevisionLockPath);
246
+ return withHeldFileLock(
247
+ lockPath,
248
+ {
249
+ staleMs: DELETION_REVISION_LOCK_STALE_MS,
250
+ maxWaitMs: DELETION_REVISION_LOCK_MAX_WAIT_MS,
251
+ },
252
+ async (acquired, lock) => {
253
+ if (!acquired) throw new Error("Deletion revision metadata lock is unavailable.");
254
+ return task(lock);
255
+ },
256
+ );
257
+ }
258
+
259
+ async readDeletionRevisions(): Promise<ReadonlyMap<string, number>> {
260
+ return this.withDeletionRevisionLock(async () => this.readDeletionRevisionMetadata());
261
+ }
262
+
263
+ private parseInvalidationCommitMetadata(raw: string): Map<string, InvalidationCommit> {
264
+ let parsed: unknown;
265
+ try {
266
+ parsed = JSON.parse(raw);
267
+ } catch {
268
+ throw new Error("Invalidation commit metadata is invalid.");
269
+ }
270
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
271
+ throw new Error("Invalidation commit metadata is invalid.");
272
+ }
273
+ const root = parsed as Record<string, unknown>;
274
+ if (
275
+ Object.keys(root).sort().join(",") !== "commits,version" ||
276
+ root.version !== 1 ||
277
+ !Array.isArray(root.commits)
278
+ ) {
279
+ throw new Error("Invalidation commit metadata is invalid.");
280
+ }
281
+ const commits = new Map<string, InvalidationCommit>();
282
+ for (const rawEntry of root.commits) {
283
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
284
+ throw new Error("Invalidation commit metadata is invalid.");
285
+ }
286
+ const entry = rawEntry as Record<string, unknown>;
287
+ if (
288
+ Object.keys(entry).sort().join(",") !== "committedAt,fingerprint,memoryId" ||
289
+ !isValidMemoryId(entry.memoryId) ||
290
+ !isValidInvalidationCommitFingerprint(entry.fingerprint) ||
291
+ !isValidInvalidationCommitTimestamp(entry.committedAt) ||
292
+ commits.has(entry.memoryId)
293
+ ) {
294
+ throw new Error("Invalidation commit metadata is invalid.");
295
+ }
296
+ commits.set(entry.memoryId, {
297
+ fingerprint: entry.fingerprint,
298
+ committedAt: entry.committedAt,
299
+ });
300
+ }
301
+ return commits;
302
+ }
303
+
304
+ private async readInvalidationCommitMetadata(): Promise<Map<string, InvalidationCommit>> {
305
+ const metadataPath = await this.resolveConfiguredStoragePath(this.options.invalidationCommitMetadataPath);
306
+ let raw: string;
307
+ try {
308
+ raw = await readFile(metadataPath, "utf8");
309
+ } catch (error) {
310
+ if (isErrnoCode(error, "ENOENT")) return new Map();
311
+ throw new Error("Invalidation commit metadata is unavailable.");
312
+ }
313
+ return this.parseInvalidationCommitMetadata(raw);
314
+ }
315
+
316
+ private async writeInvalidationCommitMetadata(
317
+ commits: ReadonlyMap<string, InvalidationCommit>,
318
+ lock: HeldFileLockController,
319
+ ): Promise<void> {
320
+ const metadataPath = await this.resolveConfiguredStoragePath(this.options.invalidationCommitMetadataPath);
321
+ const entries = [...commits.entries()]
322
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
323
+ .map(([memoryId, value]) => ({
324
+ memoryId,
325
+ fingerprint: value.fingerprint,
326
+ committedAt: value.committedAt,
327
+ }));
328
+ const metadata: InvalidationCommitMetadata = { version: 1, commits: entries };
329
+ const temporaryPath = await this.resolveConfiguredStoragePath(
330
+ `${metadataPath}.${process.pid}.${randomUUID()}.tmp`,
331
+ );
332
+ const metadataDir = await this.resolveConfiguredStoragePath(path.dirname(metadataPath));
333
+ let handle: FileHandle | null = null;
334
+ let renamed = false;
335
+ try {
336
+ await mkdir(metadataDir, { recursive: true });
337
+ handle = await open(temporaryPath, "wx", 0o600);
338
+ await handle.writeFile(`${JSON.stringify(metadata)}\n`, "utf8");
339
+ await handle.sync();
340
+ await handle.close();
341
+ handle = null;
342
+ if (!(await lock.refresh())) {
343
+ throw new Error("Invalidation commit metadata lock was lost.");
344
+ }
345
+ const renameTarget = await this.resolveConfiguredStoragePath(this.options.invalidationCommitMetadataPath);
346
+ await rename(temporaryPath, renameTarget);
347
+ renamed = true;
348
+ } finally {
349
+ if (handle !== null) {
350
+ await handle.close().catch((error: unknown) => {
351
+ log.warn("failed to close invalidation commit metadata temporary file", error);
352
+ });
353
+ }
354
+ const cleanupPath = await this.resolveConfiguredStoragePath(temporaryPath).catch((error: unknown) => {
355
+ log.warn("failed to resolve invalidation commit metadata temporary file for cleanup", error);
356
+ return null;
357
+ });
358
+ if (cleanupPath !== null) {
359
+ await unlink(cleanupPath).catch((error: unknown) => {
360
+ if (!isErrnoCode(error, "ENOENT")) {
361
+ log.warn(
362
+ `failed to clean up invalidation commit metadata temporary file after ${renamed ? "commit" : "failure"}`,
363
+ error,
364
+ );
365
+ }
366
+ });
367
+ }
368
+ }
369
+ }
370
+
371
+ async hasCommittedInvalidation(memory: Pick<MemoryFile, "content" | "frontmatter">): Promise<boolean> {
372
+ const memoryId = memory.frontmatter.id;
373
+ const fingerprint = invalidationCommitFingerprint(memory);
374
+ return this.withDeletionRevisionLock(async () => {
375
+ const entry = (await this.readInvalidationCommitMetadata()).get(memoryId);
376
+ if (entry?.fingerprint !== fingerprint) return false;
377
+ if (this.invalidationProofQuarantine.has(memoryId)) return false;
378
+ if (!this.invalidationProofQuarantineFull) return true;
379
+ const fullAt = this.invalidationProofQuarantineFullAt;
380
+ return fullAt !== null && entry.committedAt >= fullAt;
381
+ });
382
+ }
383
+
384
+ async recordCommittedInvalidation(memory: MemoryFile): Promise<void> {
385
+ const fingerprint = invalidationCommitFingerprint(memory);
386
+ await this.withDeletionRevisionLock(async (lock) => {
387
+ const fullAt = this.invalidationProofQuarantineFullAt;
388
+ const committedAt = fullAt === null ? Date.now() : Math.max(Date.now(), fullAt + 1);
389
+ const commits = await this.readInvalidationCommitMetadata();
390
+ commits.set(memory.frontmatter.id, {
391
+ fingerprint,
392
+ committedAt,
393
+ });
394
+ await this.writeInvalidationCommitMetadata(commits, lock);
395
+ this.invalidationProofQuarantine.delete(memory.frontmatter.id);
396
+ });
397
+ }
398
+
399
+ async clearCommittedInvalidation(memory: Pick<MemoryFile, "content" | "frontmatter">): Promise<void> {
400
+ const memoryId = memory.frontmatter.id;
401
+ this.quarantineInvalidationProof(memoryId);
402
+ const fingerprint = invalidationCommitFingerprint(memory);
403
+ let cleared = false;
404
+ try {
405
+ await this.withDeletionRevisionLock(async (lock) => {
406
+ const commits = await this.readInvalidationCommitMetadata();
407
+ const current = commits.get(memoryId);
408
+ if (current?.fingerprint !== fingerprint) return;
409
+ commits.delete(memoryId);
410
+ await this.writeInvalidationCommitMetadata(commits, lock);
411
+ });
412
+ cleared = true;
413
+ } finally {
414
+ if (cleared) this.invalidationProofQuarantine.delete(memoryId);
415
+ }
416
+ }
417
+
418
+ async recordReplicatedDeletionRevision(filePath: string, mtimeMs: number): Promise<void> {
419
+ const { relativePath } = await this.resolveManagedStoragePath(
420
+ filePath,
421
+ "storage.recordReplicatedDeletionRevision",
422
+ );
423
+ if (
424
+ typeof mtimeMs !== "number" ||
425
+ !Number.isFinite(mtimeMs) ||
426
+ mtimeMs < 0 ||
427
+ mtimeMs > DELETION_REVISION_MAX_MTIME_MS
428
+ ) {
429
+ throw new Error("Deletion revision timestamp is invalid.");
430
+ }
431
+ if (!isValidDeletionRevisionPath(relativePath)) {
432
+ throw new Error("Deletion revision path is invalid.");
433
+ }
434
+ await this.withDeletionRevisionLock(async (lock) => {
435
+ const revisions = await this.readDeletionRevisionMetadata();
436
+ const identity = deletionRevisionPathIdentity(relativePath);
437
+ let existingPath: string | undefined;
438
+ for (const candidatePath of revisions.keys()) {
439
+ if (deletionRevisionPathIdentity(candidatePath) === identity) {
440
+ existingPath = candidatePath;
441
+ break;
442
+ }
443
+ }
444
+ const existingMtimeMs = existingPath === undefined ? undefined : revisions.get(existingPath);
445
+ if (existingMtimeMs !== undefined && existingMtimeMs >= mtimeMs) return;
446
+ const updated = new Map(revisions);
447
+ if (existingPath !== undefined) updated.delete(existingPath);
448
+ updated.set(relativePath, mtimeMs);
449
+ await this.writeDeletionRevisionMetadata(updated, lock);
450
+ });
451
+ }
452
+
453
+ async writeManagedStorageFile(filePath: string, write: () => Promise<void>): Promise<void> {
454
+ const { relativePath } = await this.resolveManagedStoragePath(filePath, "storage.writeManagedStorageFile");
455
+ if (!isValidDeletionRevisionPath(relativePath)) {
456
+ throw new Error("Deletion revision path is invalid.");
457
+ }
458
+ await this.withDeletionRevisionLock(async (lock) => {
459
+ const before = await this.readDeletionRevisionMetadata();
460
+ const identity = deletionRevisionPathIdentity(relativePath);
461
+ const existingPath = [...before.keys()].find(
462
+ (candidatePath) => deletionRevisionPathIdentity(candidatePath) === identity,
463
+ );
464
+ await write();
465
+ if (existingPath === undefined) return;
466
+ const updated = new Map(before);
467
+ updated.delete(existingPath);
468
+ await this.writeDeletionRevisionMetadata(updated, lock);
469
+ });
470
+ }
471
+
472
+ async deleteManagedStorageFile(filePath: string, deletionMtimeMs?: number | null): Promise<boolean> {
473
+ const { target, relativePath } = await this.resolveManagedStoragePath(
474
+ filePath,
475
+ "storage.deleteManagedStorageFile",
476
+ );
477
+ if (
478
+ deletionMtimeMs !== undefined &&
479
+ deletionMtimeMs !== null &&
480
+ (typeof deletionMtimeMs !== "number" ||
481
+ !Number.isFinite(deletionMtimeMs) ||
482
+ deletionMtimeMs < 0 ||
483
+ deletionMtimeMs > DELETION_REVISION_MAX_MTIME_MS)
484
+ ) {
485
+ throw new Error("Deletion revision timestamp is invalid.");
486
+ }
487
+ return this.withDeletionRevisionLock(async (lock) => {
488
+ const safeTarget = await this.resolveConfiguredStoragePath(target);
489
+ try {
490
+ await lstat(safeTarget);
491
+ } catch (error) {
492
+ if (isErrnoCode(error, "ENOENT")) return false;
493
+ throw error;
494
+ }
495
+ if (!isValidDeletionRevisionPath(relativePath)) {
496
+ throw new Error("Deletion revision path is invalid.");
497
+ }
498
+ const revision = deletionMtimeMs === null ? undefined : (deletionMtimeMs ?? Date.now());
499
+ const before = await this.readDeletionRevisionMetadata();
500
+ const identity = deletionRevisionPathIdentity(relativePath);
501
+ let existingPath: string | undefined;
502
+ for (const candidatePath of before.keys()) {
503
+ if (deletionRevisionPathIdentity(candidatePath) === identity) {
504
+ existingPath = candidatePath;
505
+ break;
506
+ }
507
+ }
508
+ const existing = existingPath === undefined ? undefined : before.get(existingPath);
509
+ const changed =
510
+ (existingPath !== undefined && existingPath !== relativePath) ||
511
+ (revision === undefined ? existing !== undefined : existing !== revision);
512
+ if (changed) {
513
+ const updated = new Map(before);
514
+ if (existingPath !== undefined) updated.delete(existingPath);
515
+ if (revision !== undefined) updated.set(relativePath, revision);
516
+ await this.writeDeletionRevisionMetadata(updated, lock);
517
+ }
518
+ try {
519
+ const unlinkTarget = await this.resolveConfiguredStoragePath(target);
520
+ await unlink(unlinkTarget);
521
+ return true;
522
+ } catch (error) {
523
+ if (changed) await this.writeDeletionRevisionMetadata(before, lock);
524
+ if (isErrnoCode(error, "ENOENT")) return false;
525
+ throw error;
526
+ }
527
+ });
528
+ }
529
+ }
@@ -0,0 +1,41 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import type { Dirent } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import type { MemoryFile } from "../types.js";
6
+ import { isErrnoCode } from "../utils/errno.js";
7
+
8
+ export interface SupersessionAuditLookup {
9
+ correctionsDir: string;
10
+ readMemoryByPath(filePath: string): Promise<MemoryFile | null>;
11
+ }
12
+
13
+ export async function hasSupersessionAudit(
14
+ lookup: SupersessionAuditLookup,
15
+ oldMemoryId: string,
16
+ newMemoryId: string,
17
+ auditBody: string,
18
+ ): Promise<boolean> {
19
+ let entries: Dirent[];
20
+ try {
21
+ entries = await readdir(lookup.correctionsDir, { withFileTypes: true });
22
+ } catch (error) {
23
+ if (isErrnoCode(error, "ENOENT")) return false;
24
+ throw error;
25
+ }
26
+
27
+ for (const entry of entries) {
28
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
29
+ const memory = await lookup.readMemoryByPath(path.join(lookup.correctionsDir, entry.name));
30
+ if (
31
+ memory?.frontmatter.category === "correction" &&
32
+ (memory.frontmatter.sourceMemoryId === oldMemoryId ||
33
+ (memory.frontmatter.sourceMemoryId === undefined && memory.frontmatter.lineage?.includes(oldMemoryId))) &&
34
+ memory.frontmatter.lineage?.includes(newMemoryId) &&
35
+ memory.content === auditBody
36
+ ) {
37
+ return true;
38
+ }
39
+ }
40
+ return false;
41
+ }