@fgv/ts-agent-memory 5.1.0-47 → 5.1.0-49

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 (376) hide show
  1. package/dist/packlets/ingest/orchestrator.js +11 -1
  2. package/dist/packlets/ingest/orchestrator.js.map +1 -1
  3. package/dist/packlets/observe/memoryObservationStore.js +6 -0
  4. package/dist/packlets/observe/memoryObservationStore.js.map +1 -1
  5. package/dist/packlets/observe/observer.js.map +1 -1
  6. package/dist/packlets/retrieve/linkTraversalRetriever.js +1 -1
  7. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  8. package/dist/packlets/retrieve/recencyRetriever.js +1 -1
  9. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -1
  10. package/dist/packlets/retrieve/retriever.js +11 -5
  11. package/dist/packlets/retrieve/retriever.js.map +1 -1
  12. package/dist/packlets/retrieve/structuredFilterRetriever.js +27 -5
  13. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  14. package/dist/packlets/retrieve/tagRetriever.js +1 -1
  15. package/dist/packlets/retrieve/tagRetriever.js.map +1 -1
  16. package/dist/packlets/retrieve/temporalRetrievers.js +1 -1
  17. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -1
  18. package/dist/packlets/store/fileTreeMemoryStore.js +152 -132
  19. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  20. package/dist/packlets/store/vectorMaintenance.js +251 -0
  21. package/dist/packlets/store/vectorMaintenance.js.map +1 -0
  22. package/dist/packlets/types/envelope.js.map +1 -1
  23. package/dist/packlets/vector/inMemoryCosineIndex.js +81 -18
  24. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  25. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js +7 -0
  26. package/dist/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  27. package/dist/packlets/vector/vectorIndex.js.map +1 -1
  28. package/dist/ts-agent-memory.d.ts +431 -79
  29. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -1
  30. package/lib/packlets/ingest/orchestrator.js +11 -1
  31. package/lib/packlets/ingest/orchestrator.js.map +1 -1
  32. package/lib/packlets/observe/memoryObservationStore.d.ts.map +1 -1
  33. package/lib/packlets/observe/memoryObservationStore.js +6 -0
  34. package/lib/packlets/observe/memoryObservationStore.js.map +1 -1
  35. package/lib/packlets/observe/observer.d.ts +61 -0
  36. package/lib/packlets/observe/observer.d.ts.map +1 -1
  37. package/lib/packlets/observe/observer.js.map +1 -1
  38. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +1 -1
  39. package/lib/packlets/retrieve/linkTraversalRetriever.js +1 -1
  40. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -1
  41. package/lib/packlets/retrieve/recencyRetriever.d.ts +1 -1
  42. package/lib/packlets/retrieve/recencyRetriever.js +1 -1
  43. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -1
  44. package/lib/packlets/retrieve/retriever.d.ts +26 -7
  45. package/lib/packlets/retrieve/retriever.d.ts.map +1 -1
  46. package/lib/packlets/retrieve/retriever.js +11 -5
  47. package/lib/packlets/retrieve/retriever.js.map +1 -1
  48. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +26 -4
  49. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -1
  50. package/lib/packlets/retrieve/structuredFilterRetriever.js +27 -5
  51. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -1
  52. package/lib/packlets/retrieve/tagRetriever.d.ts +1 -1
  53. package/lib/packlets/retrieve/tagRetriever.js +1 -1
  54. package/lib/packlets/retrieve/tagRetriever.js.map +1 -1
  55. package/lib/packlets/retrieve/temporalRetrievers.js +1 -1
  56. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -1
  57. package/lib/packlets/store/fileTreeMemoryStore.d.ts +135 -59
  58. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  59. package/lib/packlets/store/fileTreeMemoryStore.js +151 -131
  60. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  61. package/lib/packlets/store/vectorMaintenance.d.ts +149 -0
  62. package/lib/packlets/store/vectorMaintenance.d.ts.map +1 -0
  63. package/lib/packlets/store/vectorMaintenance.js +255 -0
  64. package/lib/packlets/store/vectorMaintenance.js.map +1 -0
  65. package/lib/packlets/types/envelope.d.ts +11 -0
  66. package/lib/packlets/types/envelope.d.ts.map +1 -1
  67. package/lib/packlets/types/envelope.js.map +1 -1
  68. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +30 -7
  69. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -1
  70. package/lib/packlets/vector/inMemoryCosineIndex.js +80 -17
  71. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -1
  72. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts +7 -0
  73. package/lib/packlets/vector/inMemoryFragmentCosineIndex.d.ts.map +1 -1
  74. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js +7 -0
  75. package/lib/packlets/vector/inMemoryFragmentCosineIndex.js.map +1 -1
  76. package/lib/packlets/vector/vectorIndex.d.ts +129 -1
  77. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -1
  78. package/lib/packlets/vector/vectorIndex.js.map +1 -1
  79. package/package.json +17 -7
  80. package/.rush/temp/5667e36f1c0cc503a5c20a3ebff76e7b9a702d41.tar.log +0 -468
  81. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +0 -9
  82. package/.rush/temp/operation/build/all.log +0 -9
  83. package/.rush/temp/operation/build/log-chunks.jsonl +0 -9
  84. package/.rush/temp/operation/build/state.json +0 -3
  85. package/.rush/temp/shrinkwrap-deps.json +0 -689
  86. package/config/api-extractor.json +0 -343
  87. package/config/jest.config.json +0 -14
  88. package/config/rig.json +0 -4
  89. package/dist/test/unit/converters/antagonistRoundTrip.test.js +0 -95
  90. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +0 -1
  91. package/dist/test/unit/converters/bodyConverterRegistry.test.js +0 -72
  92. package/dist/test/unit/converters/bodyConverterRegistry.test.js.map +0 -1
  93. package/dist/test/unit/converters/envelopeConverter.test.js +0 -313
  94. package/dist/test/unit/converters/envelopeConverter.test.js.map +0 -1
  95. package/dist/test/unit/index/memoryIndex.test.js +0 -214
  96. package/dist/test/unit/index/memoryIndex.test.js.map +0 -1
  97. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +0 -277
  98. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +0 -1
  99. package/dist/test/unit/ingest/cycleGuard.test.js +0 -81
  100. package/dist/test/unit/ingest/cycleGuard.test.js.map +0 -1
  101. package/dist/test/unit/ingest/dedupScope.test.js +0 -434
  102. package/dist/test/unit/ingest/dedupScope.test.js.map +0 -1
  103. package/dist/test/unit/ingest/orchestrator.test.js +0 -1058
  104. package/dist/test/unit/ingest/orchestrator.test.js.map +0 -1
  105. package/dist/test/unit/observe/memoryObservationStore.test.js +0 -118
  106. package/dist/test/unit/observe/memoryObservationStore.test.js.map +0 -1
  107. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js +0 -116
  108. package/dist/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +0 -1
  109. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +0 -257
  110. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +0 -1
  111. package/dist/test/unit/retrieve/retrievers.test.js +0 -768
  112. package/dist/test/unit/retrieve/retrievers.test.js.map +0 -1
  113. package/dist/test/unit/retrieve/temporalRetrievers.test.js +0 -182
  114. package/dist/test/unit/retrieve/temporalRetrievers.test.js.map +0 -1
  115. package/dist/test/unit/store/antagonistTemporalBoundary.test.js +0 -120
  116. package/dist/test/unit/store/antagonistTemporalBoundary.test.js.map +0 -1
  117. package/dist/test/unit/store/embedOnWrite.test.js +0 -317
  118. package/dist/test/unit/store/embedOnWrite.test.js.map +0 -1
  119. package/dist/test/unit/store/fileTreeMemoryStore.test.js +0 -778
  120. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +0 -1
  121. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js +0 -255
  122. package/dist/test/unit/store/fragmentEmbedOnWrite.test.js.map +0 -1
  123. package/dist/test/unit/store/indexInjection.test.js +0 -432
  124. package/dist/test/unit/store/indexInjection.test.js.map +0 -1
  125. package/dist/test/unit/store/lenientOpen.test.js +0 -248
  126. package/dist/test/unit/store/lenientOpen.test.js.map +0 -1
  127. package/dist/test/unit/store/listScoped.test.js +0 -109
  128. package/dist/test/unit/store/listScoped.test.js.map +0 -1
  129. package/dist/test/unit/store/observations.test.js +0 -239
  130. package/dist/test/unit/store/observations.test.js.map +0 -1
  131. package/dist/test/unit/store/rankAxis.test.js +0 -254
  132. package/dist/test/unit/store/rankAxis.test.js.map +0 -1
  133. package/dist/test/unit/store/scopeEncoding.test.js +0 -24
  134. package/dist/test/unit/store/scopeEncoding.test.js.map +0 -1
  135. package/dist/test/unit/store/temporalStore.test.js +0 -398
  136. package/dist/test/unit/store/temporalStore.test.js.map +0 -1
  137. package/dist/test/unit/tools/memoryTools.test.js +0 -842
  138. package/dist/test/unit/tools/memoryTools.test.js.map +0 -1
  139. package/dist/test/unit/types/identityCodec.test.js +0 -187
  140. package/dist/test/unit/types/identityCodec.test.js.map +0 -1
  141. package/dist/test/unit/types/ids.test.js +0 -84
  142. package/dist/test/unit/types/ids.test.js.map +0 -1
  143. package/dist/test/unit/types/temporalCodec.test.js +0 -203
  144. package/dist/test/unit/types/temporalCodec.test.js.map +0 -1
  145. package/dist/test/unit/types/temporalPolicy.test.js +0 -83
  146. package/dist/test/unit/types/temporalPolicy.test.js.map +0 -1
  147. package/dist/test/unit/types/writePolicy.test.js +0 -331
  148. package/dist/test/unit/types/writePolicy.test.js.map +0 -1
  149. package/dist/test/unit/vector/fragmentConverters.test.js +0 -58
  150. package/dist/test/unit/vector/fragmentConverters.test.js.map +0 -1
  151. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +0 -252
  152. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +0 -1
  153. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js +0 -362
  154. package/dist/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +0 -1
  155. package/dist/test/unit/vector/vectorIndex.test.js +0 -51
  156. package/dist/test/unit/vector/vectorIndex.test.js.map +0 -1
  157. package/eslint.config.js +0 -15
  158. package/etc/ts-agent-memory.api.md +0 -940
  159. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts +0 -10
  160. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts.map +0 -1
  161. package/lib/test/unit/converters/antagonistRoundTrip.test.js +0 -97
  162. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +0 -1
  163. package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts +0 -2
  164. package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts.map +0 -1
  165. package/lib/test/unit/converters/bodyConverterRegistry.test.js +0 -74
  166. package/lib/test/unit/converters/bodyConverterRegistry.test.js.map +0 -1
  167. package/lib/test/unit/converters/envelopeConverter.test.d.ts +0 -2
  168. package/lib/test/unit/converters/envelopeConverter.test.d.ts.map +0 -1
  169. package/lib/test/unit/converters/envelopeConverter.test.js +0 -315
  170. package/lib/test/unit/converters/envelopeConverter.test.js.map +0 -1
  171. package/lib/test/unit/index/memoryIndex.test.d.ts +0 -2
  172. package/lib/test/unit/index/memoryIndex.test.d.ts.map +0 -1
  173. package/lib/test/unit/index/memoryIndex.test.js +0 -216
  174. package/lib/test/unit/index/memoryIndex.test.js.map +0 -1
  175. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts +0 -9
  176. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts.map +0 -1
  177. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +0 -279
  178. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +0 -1
  179. package/lib/test/unit/ingest/cycleGuard.test.d.ts +0 -2
  180. package/lib/test/unit/ingest/cycleGuard.test.d.ts.map +0 -1
  181. package/lib/test/unit/ingest/cycleGuard.test.js +0 -83
  182. package/lib/test/unit/ingest/cycleGuard.test.js.map +0 -1
  183. package/lib/test/unit/ingest/dedupScope.test.d.ts +0 -2
  184. package/lib/test/unit/ingest/dedupScope.test.d.ts.map +0 -1
  185. package/lib/test/unit/ingest/dedupScope.test.js +0 -436
  186. package/lib/test/unit/ingest/dedupScope.test.js.map +0 -1
  187. package/lib/test/unit/ingest/orchestrator.test.d.ts +0 -2
  188. package/lib/test/unit/ingest/orchestrator.test.d.ts.map +0 -1
  189. package/lib/test/unit/ingest/orchestrator.test.js +0 -1060
  190. package/lib/test/unit/ingest/orchestrator.test.js.map +0 -1
  191. package/lib/test/unit/observe/memoryObservationStore.test.d.ts +0 -2
  192. package/lib/test/unit/observe/memoryObservationStore.test.d.ts.map +0 -1
  193. package/lib/test/unit/observe/memoryObservationStore.test.js +0 -120
  194. package/lib/test/unit/observe/memoryObservationStore.test.js.map +0 -1
  195. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts +0 -2
  196. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.d.ts.map +0 -1
  197. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js +0 -118
  198. package/lib/test/unit/retrieve/fragmentSemanticRetriever.test.js.map +0 -1
  199. package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts +0 -2
  200. package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts.map +0 -1
  201. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +0 -259
  202. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +0 -1
  203. package/lib/test/unit/retrieve/retrievers.test.d.ts +0 -2
  204. package/lib/test/unit/retrieve/retrievers.test.d.ts.map +0 -1
  205. package/lib/test/unit/retrieve/retrievers.test.js +0 -770
  206. package/lib/test/unit/retrieve/retrievers.test.js.map +0 -1
  207. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts +0 -2
  208. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts.map +0 -1
  209. package/lib/test/unit/retrieve/temporalRetrievers.test.js +0 -184
  210. package/lib/test/unit/retrieve/temporalRetrievers.test.js.map +0 -1
  211. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts +0 -9
  212. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts.map +0 -1
  213. package/lib/test/unit/store/antagonistTemporalBoundary.test.js +0 -122
  214. package/lib/test/unit/store/antagonistTemporalBoundary.test.js.map +0 -1
  215. package/lib/test/unit/store/embedOnWrite.test.d.ts +0 -2
  216. package/lib/test/unit/store/embedOnWrite.test.d.ts.map +0 -1
  217. package/lib/test/unit/store/embedOnWrite.test.js +0 -319
  218. package/lib/test/unit/store/embedOnWrite.test.js.map +0 -1
  219. package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts +0 -2
  220. package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts.map +0 -1
  221. package/lib/test/unit/store/fileTreeMemoryStore.test.js +0 -780
  222. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +0 -1
  223. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts +0 -2
  224. package/lib/test/unit/store/fragmentEmbedOnWrite.test.d.ts.map +0 -1
  225. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js +0 -257
  226. package/lib/test/unit/store/fragmentEmbedOnWrite.test.js.map +0 -1
  227. package/lib/test/unit/store/indexInjection.test.d.ts +0 -2
  228. package/lib/test/unit/store/indexInjection.test.d.ts.map +0 -1
  229. package/lib/test/unit/store/indexInjection.test.js +0 -434
  230. package/lib/test/unit/store/indexInjection.test.js.map +0 -1
  231. package/lib/test/unit/store/lenientOpen.test.d.ts +0 -2
  232. package/lib/test/unit/store/lenientOpen.test.d.ts.map +0 -1
  233. package/lib/test/unit/store/lenientOpen.test.js +0 -250
  234. package/lib/test/unit/store/lenientOpen.test.js.map +0 -1
  235. package/lib/test/unit/store/listScoped.test.d.ts +0 -2
  236. package/lib/test/unit/store/listScoped.test.d.ts.map +0 -1
  237. package/lib/test/unit/store/listScoped.test.js +0 -111
  238. package/lib/test/unit/store/listScoped.test.js.map +0 -1
  239. package/lib/test/unit/store/observations.test.d.ts +0 -2
  240. package/lib/test/unit/store/observations.test.d.ts.map +0 -1
  241. package/lib/test/unit/store/observations.test.js +0 -241
  242. package/lib/test/unit/store/observations.test.js.map +0 -1
  243. package/lib/test/unit/store/rankAxis.test.d.ts +0 -2
  244. package/lib/test/unit/store/rankAxis.test.d.ts.map +0 -1
  245. package/lib/test/unit/store/rankAxis.test.js +0 -256
  246. package/lib/test/unit/store/rankAxis.test.js.map +0 -1
  247. package/lib/test/unit/store/scopeEncoding.test.d.ts +0 -2
  248. package/lib/test/unit/store/scopeEncoding.test.d.ts.map +0 -1
  249. package/lib/test/unit/store/scopeEncoding.test.js +0 -26
  250. package/lib/test/unit/store/scopeEncoding.test.js.map +0 -1
  251. package/lib/test/unit/store/temporalStore.test.d.ts +0 -2
  252. package/lib/test/unit/store/temporalStore.test.d.ts.map +0 -1
  253. package/lib/test/unit/store/temporalStore.test.js +0 -400
  254. package/lib/test/unit/store/temporalStore.test.js.map +0 -1
  255. package/lib/test/unit/tools/memoryTools.test.d.ts +0 -2
  256. package/lib/test/unit/tools/memoryTools.test.d.ts.map +0 -1
  257. package/lib/test/unit/tools/memoryTools.test.js +0 -844
  258. package/lib/test/unit/tools/memoryTools.test.js.map +0 -1
  259. package/lib/test/unit/types/identityCodec.test.d.ts +0 -2
  260. package/lib/test/unit/types/identityCodec.test.d.ts.map +0 -1
  261. package/lib/test/unit/types/identityCodec.test.js +0 -189
  262. package/lib/test/unit/types/identityCodec.test.js.map +0 -1
  263. package/lib/test/unit/types/ids.test.d.ts +0 -2
  264. package/lib/test/unit/types/ids.test.d.ts.map +0 -1
  265. package/lib/test/unit/types/ids.test.js +0 -86
  266. package/lib/test/unit/types/ids.test.js.map +0 -1
  267. package/lib/test/unit/types/temporalCodec.test.d.ts +0 -2
  268. package/lib/test/unit/types/temporalCodec.test.d.ts.map +0 -1
  269. package/lib/test/unit/types/temporalCodec.test.js +0 -205
  270. package/lib/test/unit/types/temporalCodec.test.js.map +0 -1
  271. package/lib/test/unit/types/temporalPolicy.test.d.ts +0 -2
  272. package/lib/test/unit/types/temporalPolicy.test.d.ts.map +0 -1
  273. package/lib/test/unit/types/temporalPolicy.test.js +0 -85
  274. package/lib/test/unit/types/temporalPolicy.test.js.map +0 -1
  275. package/lib/test/unit/types/writePolicy.test.d.ts +0 -2
  276. package/lib/test/unit/types/writePolicy.test.d.ts.map +0 -1
  277. package/lib/test/unit/types/writePolicy.test.js +0 -333
  278. package/lib/test/unit/types/writePolicy.test.js.map +0 -1
  279. package/lib/test/unit/vector/fragmentConverters.test.d.ts +0 -2
  280. package/lib/test/unit/vector/fragmentConverters.test.d.ts.map +0 -1
  281. package/lib/test/unit/vector/fragmentConverters.test.js +0 -60
  282. package/lib/test/unit/vector/fragmentConverters.test.js.map +0 -1
  283. package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts +0 -2
  284. package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts.map +0 -1
  285. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +0 -254
  286. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +0 -1
  287. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts +0 -2
  288. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.d.ts.map +0 -1
  289. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js +0 -364
  290. package/lib/test/unit/vector/inMemoryFragmentCosineIndex.test.js.map +0 -1
  291. package/lib/test/unit/vector/vectorIndex.test.d.ts +0 -2
  292. package/lib/test/unit/vector/vectorIndex.test.d.ts.map +0 -1
  293. package/lib/test/unit/vector/vectorIndex.test.js +0 -53
  294. package/lib/test/unit/vector/vectorIndex.test.js.map +0 -1
  295. package/rush-logs/ts-agent-memory.build.cache.log +0 -3
  296. package/rush-logs/ts-agent-memory.build.log +0 -9
  297. package/src/index.ts +0 -14
  298. package/src/packlets/converters/bodyConverterRegistry.ts +0 -105
  299. package/src/packlets/converters/envelopeConverter.ts +0 -233
  300. package/src/packlets/converters/index.ts +0 -7
  301. package/src/packlets/index/index.ts +0 -6
  302. package/src/packlets/index/memoryIndex.ts +0 -332
  303. package/src/packlets/ingest/cycleGuard.ts +0 -153
  304. package/src/packlets/ingest/hostStages.ts +0 -111
  305. package/src/packlets/ingest/index.ts +0 -9
  306. package/src/packlets/ingest/model.ts +0 -195
  307. package/src/packlets/ingest/orchestrator.ts +0 -968
  308. package/src/packlets/observe/index.ts +0 -7
  309. package/src/packlets/observe/memoryObservationStore.ts +0 -153
  310. package/src/packlets/observe/observer.ts +0 -119
  311. package/src/packlets/retrieve/fragmentSemanticRetriever.ts +0 -138
  312. package/src/packlets/retrieve/hybridRetriever.ts +0 -194
  313. package/src/packlets/retrieve/index.ts +0 -14
  314. package/src/packlets/retrieve/linkTraversalRetriever.ts +0 -158
  315. package/src/packlets/retrieve/recencyRetriever.ts +0 -54
  316. package/src/packlets/retrieve/retriever.ts +0 -291
  317. package/src/packlets/retrieve/semanticRetriever.ts +0 -152
  318. package/src/packlets/retrieve/structuredFilterRetriever.ts +0 -58
  319. package/src/packlets/retrieve/tagRetriever.ts +0 -58
  320. package/src/packlets/retrieve/temporalRetrievers.ts +0 -210
  321. package/src/packlets/store/fileTreeMemoryStore.ts +0 -1872
  322. package/src/packlets/store/index.ts +0 -7
  323. package/src/packlets/store/scopeEncoding.ts +0 -36
  324. package/src/packlets/tools/index.ts +0 -6
  325. package/src/packlets/tools/memoryTools.ts +0 -706
  326. package/src/packlets/types/envelope.ts +0 -196
  327. package/src/packlets/types/filenameSafety.ts +0 -57
  328. package/src/packlets/types/identityCodec.ts +0 -447
  329. package/src/packlets/types/ids.ts +0 -124
  330. package/src/packlets/types/index.ts +0 -11
  331. package/src/packlets/types/temporal.ts +0 -96
  332. package/src/packlets/types/writePolicy.ts +0 -612
  333. package/src/packlets/vector/fragmentConverters.ts +0 -55
  334. package/src/packlets/vector/inMemoryCosineIndex.ts +0 -196
  335. package/src/packlets/vector/inMemoryFragmentCosineIndex.ts +0 -289
  336. package/src/packlets/vector/index.ts +0 -9
  337. package/src/packlets/vector/vectorIndex.ts +0 -273
  338. package/src/test/unit/converters/antagonistRoundTrip.test.ts +0 -110
  339. package/src/test/unit/converters/bodyConverterRegistry.test.ts +0 -89
  340. package/src/test/unit/converters/envelopeConverter.test.ts +0 -418
  341. package/src/test/unit/index/memoryIndex.test.ts +0 -272
  342. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +0 -367
  343. package/src/test/unit/ingest/cycleGuard.test.ts +0 -110
  344. package/src/test/unit/ingest/dedupScope.test.ts +0 -542
  345. package/src/test/unit/ingest/orchestrator.test.ts +0 -1355
  346. package/src/test/unit/observe/memoryObservationStore.test.ts +0 -158
  347. package/src/test/unit/retrieve/fragmentSemanticRetriever.test.ts +0 -163
  348. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +0 -329
  349. package/src/test/unit/retrieve/retrievers.test.ts +0 -1018
  350. package/src/test/unit/retrieve/temporalRetrievers.test.ts +0 -226
  351. package/src/test/unit/store/antagonistTemporalBoundary.test.ts +0 -158
  352. package/src/test/unit/store/embedOnWrite.test.ts +0 -418
  353. package/src/test/unit/store/fileTreeMemoryStore.test.ts +0 -1041
  354. package/src/test/unit/store/fragmentEmbedOnWrite.test.ts +0 -349
  355. package/src/test/unit/store/indexInjection.test.ts +0 -550
  356. package/src/test/unit/store/lenientOpen.test.ts +0 -292
  357. package/src/test/unit/store/listScoped.test.ts +0 -138
  358. package/src/test/unit/store/observations.test.ts +0 -290
  359. package/src/test/unit/store/rankAxis.test.ts +0 -349
  360. package/src/test/unit/store/scopeEncoding.test.ts +0 -37
  361. package/src/test/unit/store/temporalStore.test.ts +0 -469
  362. package/src/test/unit/tools/memoryTools.test.ts +0 -1121
  363. package/src/test/unit/types/identityCodec.test.ts +0 -266
  364. package/src/test/unit/types/ids.test.ts +0 -94
  365. package/src/test/unit/types/temporalCodec.test.ts +0 -259
  366. package/src/test/unit/types/temporalPolicy.test.ts +0 -124
  367. package/src/test/unit/types/writePolicy.test.ts +0 -446
  368. package/src/test/unit/vector/fragmentConverters.test.ts +0 -80
  369. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +0 -318
  370. package/src/test/unit/vector/inMemoryFragmentCosineIndex.test.ts +0 -477
  371. package/src/test/unit/vector/vectorIndex.test.ts +0 -64
  372. package/temp/build/lint/_eslint-5eVG3S6w.json +0 -306
  373. package/temp/build/typescript/ts_8nwakTlr.json +0 -1
  374. package/temp/ts-agent-memory.api.json +0 -21030
  375. package/temp/ts-agent-memory.api.md +0 -940
  376. package/tsconfig.json +0 -8
@@ -1,1872 +0,0 @@
1
- /*
2
- * Copyright (c) 2026 Erik Fortune
3
- * SPDX-License-Identifier: MIT
4
- */
5
-
6
- import { Hash, Logging, Result, fail, mapResults, mapSuccess, succeed } from '@fgv/ts-utils';
7
- import { FileTree } from '@fgv/ts-json-base';
8
- import {
9
- AdmissionDecision,
10
- DEFAULT_DEDUP_SCOPE,
11
- DedupScope,
12
- EntityId,
13
- IEdgeTarget,
14
- IIdentityCodec,
15
- IMemoryEnvelope,
16
- IMemoryRecord,
17
- IProvenance,
18
- ITemporalIdentityCodec,
19
- IWritePolicy,
20
- Kind,
21
- KnowledgeLwwPolicy,
22
- MemoryId,
23
- MemoryScopeKey,
24
- RankProjector,
25
- Tag,
26
- isTemporalIdentityCodec,
27
- isTemporalRecord,
28
- isVersionCurrent,
29
- selectCurrentVersion,
30
- selectVersionAsOf
31
- } from '../types';
32
- import { IBodyConverterRegistry as IRegistry, parseMemoryFile, serializeMemoryFile } from '../converters';
33
- import { IIndexedMemoryRecord, IMemoryIndex, MemoryIndex } from '../index';
34
- import {
35
- IMemoryObservationRecord,
36
- IMemoryObserver,
37
- MemoryObservationOutcome,
38
- MemoryObservationPhase
39
- } from '../observe';
40
- import {
41
- FragmentEmbedder,
42
- IEmbeddedFragment,
43
- IFragmentVectorIndex,
44
- IMemoryRecordSource,
45
- IScopedMemoryRecord,
46
- IVectorIndex,
47
- MemoryEmbedder
48
- } from '../vector';
49
- import { defaultMemoryScopeEncoding } from './scopeEncoding';
50
-
51
- /** The on-disk extension for a memory record file. */
52
- const MEMORY_FILE_EXTENSION: string = '.md';
53
-
54
- /**
55
- * Filter for {@link IMemoryStore.list}. All present fields are ANDed together.
56
- * @public
57
- */
58
- export interface IMemoryStoreListFilter {
59
- /** Restrict to records in this scope. */
60
- readonly scope?: MemoryScopeKey;
61
- /** Restrict to records of this kind. */
62
- readonly kind?: Kind;
63
- /** Restrict to records carrying this tag (exact match). */
64
- readonly tag?: Tag;
65
- /**
66
- * For temporal (versioned) kinds: collapse each entity to the single version
67
- * valid at this epoch ms. Non-temporal records are timeless and pass through
68
- * unchanged. Absent = no temporal projection (every version is returned).
69
- */
70
- readonly asOf?: number;
71
- }
72
-
73
- /**
74
- * Policy for how {@link FileTreeMemoryStore.create}'s initial vault walk reacts
75
- * to a record that fails to parse or validate.
76
- *
77
- * - `'fail'` (the default) — one unreadable record fails the whole open. The
78
- * walk collapses per-record results with `mapResults`, so any single failure
79
- * aborts `create()`. This is the historical behavior, preserved byte-for-byte.
80
- * - `'skip'` — an unreadable record is quarantined (not indexed) rather than
81
- * failing the open. Every record that DOES parse loads normally; each skip is
82
- * logged at `warn` and surfaced structurally on
83
- * {@link FileTreeMemoryStore.skippedRecords}. The offending file is never
84
- * deleted or mutated, so a later open (after the body converter is fixed)
85
- * re-indexes it. A vault holds every kind in one store, so a required-field
86
- * migration on one kind must not make every other record unreadable.
87
- * @public
88
- */
89
- export type MemoryRecordErrorMode = 'skip' | 'fail';
90
-
91
- /**
92
- * A record that {@link FileTreeMemoryStore.create} could not load and
93
- * quarantined (only produced in {@link MemoryRecordErrorMode | `'skip'` mode}).
94
- * The `path` identifies WHICH record was skipped so a host can repair it.
95
- * @public
96
- */
97
- export interface ISkippedRecord {
98
- /** The record file's path within the vault (`<scope>/<filename>.md`). */
99
- readonly path: string;
100
- /** The scope the record lives under (its parent directory path). */
101
- readonly scope: MemoryScopeKey;
102
- /** The parse/validation failure message (includes the record path). */
103
- readonly error: string;
104
- }
105
-
106
- /**
107
- * The writable, FileTree-backed, content-hash-deduped memory store.
108
- * @public
109
- */
110
- export interface IMemoryStore {
111
- /**
112
- * Keyed read by entity id. Resolves `entityId` to a storage address via the
113
- * registered {@link IIdentityCodec} for `kind`. Returns `undefined` when no
114
- * record exists. For a versioned (temporal) kind this returns the current
115
- * version, resolved from the derived in-memory index (not re-read/re-verified
116
- * from disk per call — the index is kept in sync with every write).
117
- */
118
- get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
119
-
120
- /**
121
- * Direct read by `(scope, MemoryId)`. Returns `undefined` when not found.
122
- */
123
- getById(scope: MemoryScopeKey, id: MemoryId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
124
-
125
- /**
126
- * List records, filtered in-memory over the derived index.
127
- */
128
- list(filter?: IMemoryStoreListFilter): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
129
-
130
- /**
131
- * List EVERY record in the vault, each paired with its scope-qualified
132
- * `(scope, id)` address — the projection {@link IMemoryRecordSource} requires.
133
- * Unlike {@link IMemoryStore.list | list}, it takes no filter (whole-vault) and
134
- * returns {@link IScopedMemoryRecord}s so a re-index keys each entry on the same
135
- * scoped target the incremental embed-on-write path uses. Two records that share
136
- * a filename stem across scopes appear as distinct entries.
137
- */
138
- listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>>;
139
-
140
- /**
141
- * Adapt this store to the {@link IMemoryRecordSource} seam so it can drive
142
- * {@link IVectorIndex} rebuilds (e.g. `InMemoryCosineIndex.rebuild`). The
143
- * returned source's `list()` delegates to {@link IMemoryStore.listScoped}. The
144
- * store cannot implement {@link IMemoryRecordSource} directly because its
145
- * `list(filter?)` returns bare records (the ergonomic query surface) while the
146
- * seam's `list()` returns scope-qualified records.
147
- */
148
- asRecordSource(): IMemoryRecordSource;
149
-
150
- /**
151
- * The EFFECTIVE {@link DedupScope} for `kind` — the granularity at which a
152
- * write for this kind deduplicates against the existing vault.
153
- *
154
- * @remarks
155
- * This is a **read accessor over the store's already-injected write policies**,
156
- * and it is the single place any caller — the store's own write path included —
157
- * asks what a kind's dedup granularity is. It resolves the full chain the store
158
- * applies on write: the kind's registered {@link IWritePolicy}, falling back to
159
- * the store's default policy, then that policy's
160
- * {@link IWritePolicy.dedupScope | dedupScope}, falling back to
161
- * {@link DEFAULT_DEDUP_SCOPE}. Note the store's default policy is a
162
- * {@link KnowledgeLwwPolicy}, which declares `'content'` — so a kind with NO
163
- * registered policy resolves to `'content'`, not to `DEFAULT_DEDUP_SCOPE`.
164
- *
165
- * It exists so a caller that must agree with the store about dedup granularity
166
- * — notably the ingest orchestrator's stage-4 layer-1 exact match — can read the
167
- * declaration through this seam instead of being handed a second copy of the
168
- * policy map. A second declaration site is precisely the defect this accessor
169
- * was added to remove.
170
- *
171
- * Deliberately synchronous, total, and NOT `Result`-returning: it reads
172
- * constructor-injected configuration, touches no I/O, and cannot fail (every
173
- * link in the fallback chain has a total default). It exposes only the scope,
174
- * never the {@link IWritePolicy} itself, so it can never become a back door for
175
- * invoking admission or merge logic out of band.
176
- */
177
- dedupScopeFor(kind: Kind): DedupScope;
178
-
179
- /**
180
- * Write a record. Validates the body, computes a content hash, deduplicates
181
- * (scope-wide, before policy), applies the kind's {@link IWritePolicy}, stamps
182
- * transaction-time metadata (`created` / `updated` / `seq` / `contentHash`),
183
- * writes the file, and patches the index. Returns the written record — or the
184
- * existing record unchanged on a dedup no-op.
185
- */
186
- put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>>;
187
-
188
- /**
189
- * Delete a record by `(kind, entityId)`. Non-temporal kinds physically delete
190
- * the file and return the deleted record's {@link MemoryId}. Temporal
191
- * (versioned) kinds SOFT-delete: the current version is invalidated
192
- * (`invalid_at` set), history is retained, and the invalidated version's
193
- * {@link MemoryId} is returned.
194
- */
195
- delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>>;
196
- }
197
-
198
- /**
199
- * Parameters for {@link FileTreeMemoryStore.create}.
200
- * @public
201
- */
202
- export interface IFileTreeMemoryStoreCreateParams {
203
- /** Root directory under which scope-encoded sub-trees live. Must be mutable. */
204
- readonly root: FileTree.IMutableFileTreeDirectoryItem;
205
- /** Per-kind body converter registry. Gates every body on write and read. */
206
- readonly registry: IRegistry;
207
- /** Per-kind write policies. Kinds without an entry use a default LWW policy. */
208
- readonly writePolicies?: ReadonlyMap<Kind, IWritePolicy>;
209
- /** Per-kind identity codecs. */
210
- readonly codecs?: ReadonlyMap<Kind, IIdentityCodec>;
211
- /**
212
- * Optional per-kind host projector map. When a kind has an entry, the store
213
- * runs the projector on the fully-resolved (post-merge) record on every put
214
- * AND every update — in the same pass that recomputes `contentHash` — and
215
- * stamps the numeric result into {@link IMemoryEnvelope.rank}, which the index's
216
- * rank view and `orderBy: 'rank'` retrieval sort by (descending, absent last).
217
- * Absent for a kind → that kind's records carry no `rank`. Purely additive and
218
- * zero-overhead when unwired: an absent map leaves every write byte-identical.
219
- * The projector is a host callback — a throw is logged at `warn` and the record
220
- * is stamped with no `rank`, never failing the write (mirrors the store's other
221
- * guard-host-callbacks conventions).
222
- */
223
- readonly rankProjectors?: ReadonlyMap<Kind, RankProjector>;
224
- /** Default codec for kinds without an explicit entry. */
225
- readonly defaultCodec?: IIdentityCodec;
226
- /**
227
- * Optional derived-index implementation. Defaults to a fresh {@link MemoryIndex} —
228
- * omitting this parameter is byte-identical to the store's behavior before the
229
- * parameter existed. When supplied, the store uses it for EVERY index operation
230
- * it performs and never holds a second index, so an injected index is the store's
231
- * only view of its own records:
232
- *
233
- * - `rebuild` — once, from the initial vault walk in `create()`.
234
- * - `patch` — on every persisted write, delete, version invalidation, and
235
- * cap-cull eviction.
236
- * - `entries` — behind {@link IMemoryStore.list | list} /
237
- * {@link IMemoryStore.listScoped | listScoped}, the keyed temporal reads, the
238
- * write path's content-hash dedup and write-policy admission cohort, AND the
239
- * temporal (versioned) write and delete paths, which resolve an entity's
240
- * version history entirely from the index.
241
- *
242
- * That last group is the one to weigh before injecting anything other than a
243
- * pass-through decorator: an index that filters, reorders, or otherwise reshapes
244
- * `entries()` changes WRITE semantics, not just what reads return. Concretely,
245
- * on a versioned kind the store derives an entity's whole version history from
246
- * `entries()` filtered by scope, and that derivation decides which version a
247
- * `put` treats as current (so what it dedups against and what it merges its
248
- * patch over), which prior versions it stamps `invalid_at` on, what the
249
- * admission cohort is, and which versions a `delete` tombstones. An index that
250
- * hides a version makes it invisible to all of those — the FileTree still holds
251
- * it, but the store will not supersede, invalidate, or tombstone it. On flat
252
- * kinds the same reshaping changes what dedups and what a cap-cull policy
253
- * evicts. A faithful delegating decorator — the intended use below — has no such
254
- * effect. Note the store's keyed reads ({@link IMemoryStore.get | get} on a flat
255
- * kind, and {@link IMemoryStore.getById | getById}) go to the FileTree, not the
256
- * index; the FileTree remains the source of truth and the index stays a derived
257
- * view.
258
- *
259
- * @remarks
260
- * **This is an instrumentation seam, NOT a resident-memory fix.** The intended
261
- * use is wrapping the shipped {@link MemoryIndex} in a decorator that counts and
262
- * times the calls the store makes — resident bytes by kind, open cost against
263
- * vault size, where the curve actually bends — so a decision about a partial-read
264
- * redesign can be driven by measurements instead of estimates.
265
- *
266
- * It does NOT lower the store's resident-memory ceiling, and injecting a
267
- * "persisted" or "lazy" index will not change that. {@link IMemoryIndex}'s read
268
- * surface returns whole records by construction: `entries()` yields
269
- * {@link IIndexedMemoryRecord}s and `byKind` / `byTag` / `byRecency` / `byRank`
270
- * yield `IMemoryRecord<unknown>` — `{ envelope, body }` pairs with the body
271
- * materialized. Any implementation satisfying the current contract must therefore
272
- * be able to produce every body on demand. An injected index changes WHERE records
273
- * come from; it does not change WHETHER bodies are held. Lowering the ceiling
274
- * requires a partial-read (id-or-envelope-only) redesign of `IMemoryIndex` itself,
275
- * which is separate, breaking, design-first work and is deliberately not part of
276
- * this seam.
277
- */
278
- readonly index?: IMemoryIndex;
279
- /** Scope encoding. Defaults to {@link defaultMemoryScopeEncoding}. */
280
- readonly scopeEncoding?: (scope: MemoryScopeKey) => Result<string>;
281
- /**
282
- * Transaction-time clock. Defaults to `Date.now`. Injectable so tests can
283
- * make `created` / `updated` deterministic. Also stamps observation
284
- * `timestamp`s.
285
- */
286
- readonly clock?: () => number;
287
- /**
288
- * Optional observers fired once per public `get` / `put` / `delete` call.
289
- * Purely additive — when absent, no observation records are produced and the
290
- * store behaves exactly as it did without this parameter. Observer errors
291
- * never affect the store operation (swallowed, logged to {@link
292
- * IFileTreeMemoryStoreCreateParams.logger | logger} at `warn`).
293
- */
294
- readonly observers?: ReadonlyArray<IMemoryObserver>;
295
- /**
296
- * Diagnostic logger for swallowed observer failures. Defaults to a
297
- * `Logging.NoOpLogger`.
298
- */
299
- readonly logger?: Logging.ILogger;
300
- /**
301
- * Optional vector index for semantic recall. Wired together with
302
- * {@link IFileTreeMemoryStoreCreateParams.embed | embed}: when both are present
303
- * the store embeds each written record and maintains the index on
304
- * `put` / `delete` / cap-cull eviction. Absent (or `embed` absent) → no
305
- * embedding work happens and the store behaves exactly as it does without this
306
- * parameter (the additive, zero-overhead-when-unwired default — mirrors the
307
- * observer hook).
308
- */
309
- readonly vectorIndex?: IVectorIndex;
310
- /**
311
- * Optional embedder applied to each record on write, wired together with
312
- * {@link IFileTreeMemoryStoreCreateParams.vectorIndex | vectorIndex}. The
313
- * consumer supplies it (e.g. `callProviderEmbedding` or in-process
314
- * transformers); the store never calls an embedding provider directly, so the
315
- * core stays embedder-agnostic. Embedding/index maintenance is **best-effort**:
316
- * a failed (or throwing) `embed` / `add` / `remove` is logged at `warn` via
317
- * {@link IFileTreeMemoryStoreCreateParams.logger | logger} and the record
318
- * operation still succeeds — the vector index is a derived view that a later
319
- * `rebuild` reconciles, so a vector failure never fails an authoritative write.
320
- */
321
- readonly embed?: MemoryEmbedder;
322
- /**
323
- * Optional fragment-granular vector index for sub-document semantic search.
324
- * Wired together with
325
- * {@link IFileTreeMemoryStoreCreateParams.fragmentEmbedder | fragmentEmbedder}:
326
- * when both are present the store chunks + embeds each written record and
327
- * maintains the fragment index on `put` / `delete` / cap-cull eviction — the
328
- * "discovery" half of a search-then-read contract, queried through a
329
- * {@link FragmentSemanticRetriever}. Independent of the record-granular
330
- * {@link IFileTreeMemoryStoreCreateParams.vectorIndex | vectorIndex} pair: a
331
- * store may wire record vectors, fragment vectors, both, or neither. Absent (or
332
- * `fragmentEmbedder` absent) → no fragment work happens and the store behaves
333
- * byte-identically (the additive, zero-overhead-when-unwired default).
334
- */
335
- readonly fragmentIndex?: IFragmentVectorIndex;
336
- /**
337
- * Optional fragment embedder applied to each record on write, wired together
338
- * with {@link IFileTreeMemoryStoreCreateParams.fragmentIndex | fragmentIndex}.
339
- * The consumer owns the chunking policy (window size, overlap) and the embedding
340
- * call; the store stays chunking- and embedder-agnostic. Fragment index
341
- * maintenance is **best-effort**, exactly like the record-vector path: a failed
342
- * (or throwing) `fragmentEmbedder` / `addFragments` / `remove` is logged at
343
- * `warn` and the record operation still succeeds — the fragment index is a
344
- * derived view a later `rebuild` reconciles.
345
- */
346
- readonly fragmentEmbedder?: FragmentEmbedder;
347
- /**
348
- * How the initial vault walk reacts to a record that fails to parse or
349
- * validate. Defaults to `'fail'` — one bad record fails the whole open, the
350
- * historical behavior, preserved byte-for-byte. Set `'skip'` to quarantine
351
- * unreadable records instead: valid records still load, each skip is logged
352
- * at `warn` and surfaced on {@link FileTreeMemoryStore.skippedRecords}, and
353
- * the offending file is left untouched for a later (post-fix) re-index. See
354
- * {@link MemoryRecordErrorMode}.
355
- */
356
- readonly onRecordError?: MemoryRecordErrorMode;
357
- }
358
-
359
- /**
360
- * The internal outcome of a locked `put`: the written (or existing, on a
361
- * dedup no-op) record plus the {@link MemoryId}s evicted by a `cull-oldest`
362
- * admission decision. The eviction file-deletes and index patches happen
363
- * inside the write-lock; the `evicted` ids are surfaced so the public `put`
364
- * can fire one `'delete'` observation per evicted record AFTER the lock
365
- * releases — consistent with the post-op firing of the other observation
366
- * hooks, and avoiding observer re-entrancy into the still-held write-lock.
367
- */
368
- interface IPutOutcome {
369
- readonly record: IMemoryRecord<unknown>;
370
- readonly evicted: ReadonlyArray<MemoryId>;
371
- }
372
-
373
- interface IInternalParams {
374
- readonly root: FileTree.IMutableFileTreeDirectoryItem;
375
- readonly registry: IRegistry;
376
- readonly writePolicies: ReadonlyMap<Kind, IWritePolicy>;
377
- readonly codecs: ReadonlyMap<Kind, IIdentityCodec>;
378
- readonly rankProjectors: ReadonlyMap<Kind, RankProjector>;
379
- readonly defaultCodec?: IIdentityCodec;
380
- readonly defaultPolicy: IWritePolicy;
381
- readonly scopeEncoding: (scope: MemoryScopeKey) => Result<string>;
382
- readonly clock: () => number;
383
- readonly index: IMemoryIndex;
384
- readonly observers: ReadonlyArray<IMemoryObserver>;
385
- readonly logger: Logging.ILogger;
386
- readonly vectorIndex?: IVectorIndex;
387
- readonly embed?: MemoryEmbedder;
388
- readonly fragmentIndex?: IFragmentVectorIndex;
389
- readonly fragmentEmbedder?: FragmentEmbedder;
390
- }
391
-
392
- /**
393
- * Flat-layout, FileTree-backed {@link IMemoryStore}. The FileTree is the source
394
- * of truth; the {@link IMemoryIndex} is a derived in-memory view patched on
395
- * every write. Concurrent writes are serialized through a per-instance async
396
- * write-lock so the index and the on-disk files never interleave.
397
- *
398
- * @remarks
399
- * Bodies are string (markdown). Both layouts are supported: flat
400
- * (one-file-per-entity, non-versioned) and versioned (subtree-per-entity,
401
- * invalidate-don't-delete), dispatched per kind by the codec's `isVersioned`
402
- * flag. A kind is flat with zero behavioral impact unless it opts into a
403
- * {@link ITemporalIdentityCodec}.
404
- * @public
405
- */
406
- export class FileTreeMemoryStore implements IMemoryStore {
407
- private readonly _root: FileTree.IMutableFileTreeDirectoryItem;
408
- private readonly _registry: IRegistry;
409
- private readonly _writePolicies: ReadonlyMap<Kind, IWritePolicy>;
410
- private readonly _codecs: ReadonlyMap<Kind, IIdentityCodec>;
411
- private readonly _rankProjectors: ReadonlyMap<Kind, RankProjector>;
412
- private readonly _defaultCodec: IIdentityCodec | undefined;
413
- private readonly _defaultPolicy: IWritePolicy;
414
- private readonly _scopeEncoding: (scope: MemoryScopeKey) => Result<string>;
415
- private readonly _clock: () => number;
416
- private readonly _index: IMemoryIndex;
417
- private readonly _hasher: Hash.Crc32Normalizer;
418
- private readonly _observers: ReadonlyArray<IMemoryObserver>;
419
- private readonly _logger: Logging.ILogger;
420
- private readonly _vectorIndex: IVectorIndex | undefined;
421
- private readonly _embed: MemoryEmbedder | undefined;
422
- private readonly _fragmentIndex: IFragmentVectorIndex | undefined;
423
- private readonly _fragmentEmbedder: FragmentEmbedder | undefined;
424
- /**
425
- * Records the initial walk could not load. Populated during `create()` in
426
- * {@link MemoryRecordErrorMode | `'skip'` mode}; empty otherwise.
427
- */
428
- private readonly _skippedRecords: ISkippedRecord[];
429
-
430
- /** Monotonic write counter; incremented inside the write-lock on each put. */
431
- private _seq: number;
432
- /**
433
- * Monotonic observation-sequence counter. A distinct authority from `_seq`
434
- * (the envelope write counter): it numbers the audit stream so a single
435
- * {@link MemoryObservationStore} fed by this store sees strictly increasing
436
- * `seq`, satisfying the ring buffer's cursor contract.
437
- */
438
- private _observationSeq: number;
439
- /** Tail of the write-lock promise chain that serializes mutating ops. */
440
- private _writeTail: Promise<unknown>;
441
-
442
- /**
443
- * The record-level mutable-field vocabulary: maps a declared mutable field
444
- * name to its canonical location on a record. Used to project an incoming
445
- * record into a merge-patch on update. B1 ships the knowledge-LWW surface
446
- * (body + envelope metadata); body-internal mutable fields (Phase-C cap-cull)
447
- * extend this map.
448
- */
449
- private static readonly _mutableFieldAccessors: ReadonlyMap<
450
- string,
451
- (record: IMemoryRecord<unknown>) => unknown
452
- > = new Map<string, (record: IMemoryRecord<unknown>) => unknown>([
453
- ['body', (r) => r.body],
454
- ['tags', (r) => r.envelope.tags],
455
- ['links', (r) => r.envelope.links],
456
- ['provenance', (r) => r.envelope.provenance],
457
- ['embeddingRef', (r) => r.envelope.embeddingRef]
458
- ]);
459
-
460
- private constructor(params: IInternalParams) {
461
- this._root = params.root;
462
- this._registry = params.registry;
463
- this._writePolicies = params.writePolicies;
464
- this._codecs = params.codecs;
465
- this._rankProjectors = params.rankProjectors;
466
- this._defaultCodec = params.defaultCodec;
467
- this._defaultPolicy = params.defaultPolicy;
468
- this._scopeEncoding = params.scopeEncoding;
469
- this._clock = params.clock;
470
- this._index = params.index;
471
- this._hasher = new Hash.Crc32Normalizer();
472
- this._observers = params.observers;
473
- this._logger = params.logger;
474
- this._vectorIndex = params.vectorIndex;
475
- this._embed = params.embed;
476
- this._fragmentIndex = params.fragmentIndex;
477
- this._fragmentEmbedder = params.fragmentEmbedder;
478
- this._skippedRecords = [];
479
- this._seq = 0;
480
- this._observationSeq = 0;
481
- this._writeTail = Promise.resolve();
482
- }
483
-
484
- /**
485
- * Records the initial vault walk could not parse or validate and quarantined
486
- * (not indexed). Non-empty only when the store was opened with
487
- * {@link MemoryRecordErrorMode | `onRecordError: 'skip'`} AND at least one
488
- * record failed to load. Each entry identifies the offending file so a host
489
- * can repair it; the file itself is never deleted or mutated, so a later open
490
- * (after the body converter is fixed) re-indexes it.
491
- */
492
- public get skippedRecords(): ReadonlyArray<ISkippedRecord> {
493
- return this._skippedRecords;
494
- }
495
-
496
- /**
497
- * Family-convention factory. Resolves the derived index (the caller's
498
- * {@link IFileTreeMemoryStoreCreateParams.index | index} when supplied, a fresh
499
- * {@link MemoryIndex} otherwise) and a default LWW policy, then performs an
500
- * initial FileTree walk so an existing vault is indexed (and the `seq` counter
501
- * resumes past the highest persisted `seq`).
502
- */
503
- public static create(params: IFileTreeMemoryStoreCreateParams): Result<FileTreeMemoryStore> {
504
- return KnowledgeLwwPolicy.create().onSuccess((defaultPolicy) =>
505
- FileTreeMemoryStore._resolveIndex(params.index).onSuccess((index) => {
506
- const store: FileTreeMemoryStore = new FileTreeMemoryStore({
507
- root: params.root,
508
- registry: params.registry,
509
- writePolicies: params.writePolicies ?? new Map<Kind, IWritePolicy>(),
510
- codecs: params.codecs ?? new Map<Kind, IIdentityCodec>(),
511
- rankProjectors: params.rankProjectors ?? new Map<Kind, RankProjector>(),
512
- defaultCodec: params.defaultCodec,
513
- defaultPolicy,
514
- scopeEncoding: params.scopeEncoding ?? defaultMemoryScopeEncoding,
515
- clock: params.clock ?? Date.now,
516
- index,
517
- observers: params.observers ?? [],
518
- logger: params.logger ?? new Logging.NoOpLogger(),
519
- vectorIndex: params.vectorIndex,
520
- embed: params.embed,
521
- fragmentIndex: params.fragmentIndex,
522
- fragmentEmbedder: params.fragmentEmbedder
523
- });
524
- return store._initialIndex(params.onRecordError ?? 'fail').onSuccess(() => succeed(store));
525
- })
526
- );
527
- }
528
-
529
- /**
530
- * Resolve the derived index for a `create()`: the caller's injected
531
- * {@link IMemoryIndex} verbatim, or a fresh {@link MemoryIndex} when none was
532
- * supplied (the default that keeps an omitting caller byte-identical).
533
- */
534
- private static _resolveIndex(index: IMemoryIndex | undefined): Result<IMemoryIndex> {
535
- // Nullish rather than strictly-undefined, matching how every sibling optional
536
- // param in `create()` handles absence (`params.codecs ?? new Map()`, and so on).
537
- // A JS caller — or a TS caller arriving through an `unknown` escape hatch —
538
- // passing `null` otherwise gets `null` installed as the store's index and fails
539
- // later inside `entries()` with a message that names neither the param nor the
540
- // cause.
541
- return index ? succeed(index) : MemoryIndex.create();
542
- }
543
-
544
- /** {@inheritDoc IMemoryStore.get} */
545
- public async get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>> {
546
- const result: Result<IMemoryRecord<unknown> | undefined> = this._codecFor(kind).onSuccess((codec) =>
547
- codec.encode(entityId).onSuccess((addr) => {
548
- if (addr.isVersioned) {
549
- if (!isTemporalIdentityCodec(codec)) {
550
- return fail(
551
- `memory get '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`
552
- );
553
- }
554
- // Versioned keyed read resolves the CURRENT version (highest-seq
555
- // version whose `invalid_at` is null/absent) from the entity subtree,
556
- // read off the derived index. `asOf` resolution is via `list({ asOf })`
557
- // and the temporal retrievers.
558
- return succeed(this._readVersionedCurrent(addr.scope));
559
- }
560
- return this._readRecord(addr.scope, addr.idStem);
561
- })
562
- );
563
- await this._fireObservation('read', kind, entityId, {
564
- outcome: result.isSuccess() ? 'success' : 'failure',
565
- id: result.isSuccess() ? result.value?.envelope.id : undefined,
566
- error: result.isFailure() ? result.message : undefined
567
- });
568
- return result;
569
- }
570
-
571
- /** {@inheritDoc IMemoryStore.getById} */
572
- public async getById(
573
- scope: MemoryScopeKey,
574
- id: MemoryId
575
- ): Promise<Result<IMemoryRecord<unknown> | undefined>> {
576
- return this._readRecord(scope, id);
577
- }
578
-
579
- /** {@inheritDoc IMemoryStore.list} */
580
- public async list(filter?: IMemoryStoreListFilter): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {
581
- const matches: IMemoryRecord<unknown>[] = this._index
582
- .entries()
583
- .filter((entry) => {
584
- if (filter?.scope !== undefined && entry.scope !== filter.scope) {
585
- return false;
586
- }
587
- if (filter?.kind !== undefined && entry.record.envelope.kind !== filter.kind) {
588
- return false;
589
- }
590
- if (filter?.tag !== undefined && !entry.record.envelope.tags.includes(filter.tag)) {
591
- return false;
592
- }
593
- return true;
594
- })
595
- .map((entry) => entry.record);
596
- if (filter?.asOf === undefined) {
597
- // No temporal projection requested: byte-identical to the pre-temporal
598
- // behavior (the flat-path guarantee — every version is returned).
599
- return succeed(matches);
600
- }
601
- return succeed(FileTreeMemoryStore._projectAsOf(matches, filter.asOf));
602
- }
603
-
604
- /** {@inheritDoc IMemoryStore.listScoped} */
605
- public async listScoped(): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> {
606
- // The derived index already carries each record's scope
607
- // ({@link IIndexedMemoryRecord.scope}), so the scoped projection is a direct
608
- // map — the record's `(scope, id)` is exactly the address the vector index
609
- // keys on. No filter/temporal projection: the seam re-embeds the whole vault.
610
- return succeed(
611
- this._index.entries().map((entry) => ({
612
- target: { scope: entry.scope, id: entry.record.envelope.id },
613
- record: entry.record
614
- }))
615
- );
616
- }
617
-
618
- /** {@inheritDoc IMemoryStore.asRecordSource} */
619
- public asRecordSource(): IMemoryRecordSource {
620
- return { list: (): Promise<Result<ReadonlyArray<IScopedMemoryRecord>>> => this.listScoped() };
621
- }
622
-
623
- /**
624
- * Collapse temporal records to the single version valid at `asOf` per entity;
625
- * non-temporal records are timeless and pass through unchanged (valid-time
626
- * `asOf` applies only to versioned kinds; transaction-time / full bi-temporal
627
- * filtering is deferred — OQ-9). An entity with no version valid at `asOf`
628
- * contributes nothing.
629
- */
630
- private static _projectAsOf(
631
- records: ReadonlyArray<IMemoryRecord<unknown>>,
632
- asOf: number
633
- ): ReadonlyArray<IMemoryRecord<unknown>> {
634
- const passthrough: IMemoryRecord<unknown>[] = [];
635
- const groups: Map<string, IMemoryRecord<unknown>[]> = new Map<string, IMemoryRecord<unknown>[]>();
636
- for (const record of records) {
637
- if (!isTemporalRecord(record)) {
638
- passthrough.push(record);
639
- continue;
640
- }
641
- const key: string = `${record.envelope.kind}\0${record.envelope.entityId}`;
642
- const existing: IMemoryRecord<unknown>[] | undefined = groups.get(key);
643
- if (existing === undefined) {
644
- groups.set(key, [record]);
645
- } else {
646
- existing.push(record);
647
- }
648
- }
649
- const result: IMemoryRecord<unknown>[] = [...passthrough];
650
- for (const versions of groups.values()) {
651
- const valid: IMemoryRecord<unknown> | undefined = selectVersionAsOf(versions, asOf);
652
- if (valid !== undefined) {
653
- result.push(valid);
654
- }
655
- }
656
- return result;
657
- }
658
-
659
- /** {@inheritDoc IMemoryStore.put} */
660
- public async put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>> {
661
- const result: Result<IPutOutcome> = await this._enqueue(() => this._putLocked(record));
662
- await this._fireObservation('write', record.envelope.kind, record.envelope.entityId, {
663
- outcome: result.isSuccess() ? 'success' : 'failure',
664
- id: result.isSuccess() ? result.value.record.envelope.id : record.envelope.id,
665
- provenance: record.envelope.provenance,
666
- error: result.isFailure() ? result.message : undefined
667
- });
668
- // Fire one `'delete'` observation per record evicted by cap-cull. The
669
- // evicted records are always in the same `(scope, kind)` cohort as the
670
- // incoming record, so resolving the scope from the incoming `entityId`
671
- // yields the evicted record's scope; the observation records the evicted
672
- // record's own `id`. A no-op / first-write / reject path yields no
673
- // evictions, so this loop is empty there.
674
- if (result.isSuccess()) {
675
- for (const evictedId of result.value.evicted) {
676
- await this._fireObservation('delete', record.envelope.kind, record.envelope.entityId, {
677
- outcome: 'success',
678
- id: evictedId
679
- });
680
- }
681
- }
682
- return result.onSuccess((outcome) => succeed(outcome.record));
683
- }
684
-
685
- /** {@inheritDoc IMemoryStore.delete} */
686
- public async delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>> {
687
- const result: Result<MemoryId> = await this._enqueue(() => this._deleteLocked(kind, entityId));
688
- await this._fireObservation('delete', kind, entityId, {
689
- outcome: result.isSuccess() ? 'success' : 'failure',
690
- id: result.isSuccess() ? result.value : undefined,
691
- error: result.isFailure() ? result.message : undefined
692
- });
693
- return result;
694
- }
695
-
696
- /**
697
- * Serialize a mutating task behind the write-lock. Tasks run in submission
698
- * order; a failed task does not break the chain for subsequent ones.
699
- */
700
- private _enqueue<T>(task: () => Promise<Result<T>>): Promise<Result<T>> {
701
- const result: Promise<Result<T>> = this._writeTail.then(task);
702
- this._writeTail = result.then(
703
- () => undefined,
704
- () => undefined
705
- );
706
- return result;
707
- }
708
-
709
- /**
710
- * Build and fan out one observation record for a completed op. A no-op when no
711
- * observers are wired (the additive-default path pays nothing). The store is
712
- * the seq authority — it mints `seq` / `timestamp` so every observer sees the
713
- * same record. The scope is resolved best-effort via the codec.
714
- */
715
- private async _fireObservation(
716
- phase: MemoryObservationPhase,
717
- kind: Kind,
718
- entityId: EntityId,
719
- details: {
720
- readonly outcome: MemoryObservationOutcome;
721
- readonly id?: MemoryId;
722
- readonly provenance?: IProvenance;
723
- readonly error?: string;
724
- }
725
- ): Promise<void> {
726
- if (this._observers.length === 0) {
727
- return;
728
- }
729
- const record: IMemoryObservationRecord = {
730
- seq: ++this._observationSeq,
731
- timestamp: this._clock(),
732
- phase,
733
- scope: this._scopeBestEffort(kind, entityId),
734
- id: details.id,
735
- kind,
736
- outcome: details.outcome,
737
- error: details.error,
738
- provenance: details.provenance
739
- };
740
- const awaited: Promise<void>[] = [];
741
- for (const observer of this._observers) {
742
- if (observer.fireAndForget === true) {
743
- // Intentionally not awaited: a fire-and-forget observer must not extend
744
- // the store op's latency. `_safeObserve` swallows internally; the
745
- // `.catch` keeps the detached promise from being flagged as floating.
746
- this._safeObserve(observer, record).catch(() => undefined);
747
- } else {
748
- awaited.push(this._safeObserve(observer, record));
749
- }
750
- }
751
- await Promise.all(awaited);
752
- }
753
-
754
- /** Invoke one observer, swallowing any failure or throw (logged at `warn`). */
755
- private async _safeObserve(observer: IMemoryObserver, record: IMemoryObservationRecord): Promise<void> {
756
- try {
757
- const observed: Result<unknown> = await observer.observe(record);
758
- if (observed.isFailure()) {
759
- this._warnSwallowed(`memory observer failed (swallowed): ${observed.message}`);
760
- }
761
- } catch (error) {
762
- this._warnSwallowed(`memory observer threw (swallowed): ${String(error)}`);
763
- }
764
- }
765
-
766
- /**
767
- * Log a swallowed-issue warning (observer failure or best-effort vector
768
- * maintenance), tolerating a logger that itself throws — diagnostic logging
769
- * must never make a store op reject.
770
- */
771
- private _warnSwallowed(message: string): void {
772
- try {
773
- this._logger.warn(message);
774
- } catch {
775
- // Diagnostic logging must not affect the store operation.
776
- }
777
- }
778
-
779
- /** Resolve a scope for an observation, best-effort (undefined when unresolvable). */
780
- private _scopeBestEffort(kind: Kind, entityId: EntityId): MemoryScopeKey | undefined {
781
- return this._codecFor(kind)
782
- .onSuccess((codec) => codec.encode(entityId))
783
- .onSuccess((addr) => succeed(addr.scope))
784
- .orDefault();
785
- }
786
-
787
- private async _putLocked(record: IMemoryRecord<unknown>): Promise<Result<IPutOutcome>> {
788
- const envelope: IMemoryEnvelope = record.envelope;
789
- if (typeof record.body !== 'string') {
790
- return fail(
791
- `memory put '${envelope.id}': only string (markdown) bodies are supported (got ${typeof record.body})`
792
- );
793
- }
794
- const body: string = record.body;
795
- // Resolve the body converter + codec, then dispatch on layout. The flat
796
- // (non-versioned) path keeps its exact dedup/policy ordering; the versioned
797
- // path is a wholly separate branch so the flat path is behaviorally
798
- // unchanged (the consumer adoption guarantee).
799
- return this._registry
800
- .convert(envelope.kind, body)
801
- .withErrorFormat((msg) => `memory put '${envelope.id}': invalid body: ${msg}`)
802
- .onSuccess(() => this._codecFor(envelope.kind))
803
- .thenOnSuccess((codec) =>
804
- codec.encode(envelope.entityId).thenOnSuccess((addr) => {
805
- if (addr.isVersioned) {
806
- if (!isTemporalIdentityCodec(codec)) {
807
- return Promise.resolve(
808
- fail<IPutOutcome>(
809
- `memory put '${envelope.entityId}': codec for versioned kind '${envelope.kind}' does not implement the temporal codec interface`
810
- )
811
- );
812
- }
813
- return this._putVersioned(record, body, codec, addr.scope);
814
- }
815
- if (envelope.id !== addr.idStem) {
816
- return Promise.resolve(
817
- fail<IPutOutcome>(
818
- `memory put: envelope id '${envelope.id}' does not match codec-derived stem '${addr.idStem}'`
819
- )
820
- );
821
- }
822
- return this._contentHash(envelope.kind, body, envelope.links).thenOnSuccess((hash) =>
823
- this._writeResolved(record, body, addr.scope, addr.idStem, hash)
824
- );
825
- })
826
- );
827
- }
828
-
829
- /**
830
- * Run dedup → policy → stamp → embed → write for a resolved address and content
831
- * hash. Async because the embed-on-write hook (when wired) does a network call
832
- * or in-process inference; the whole chain runs inside the write-lock so the
833
- * vector index, the on-disk file, and the derived index never interleave.
834
- */
835
- private async _writeResolved(
836
- record: IMemoryRecord<unknown>,
837
- body: string,
838
- scope: MemoryScopeKey,
839
- idStem: string,
840
- hash: string
841
- ): Promise<Result<IPutOutcome>> {
842
- const policy: IWritePolicy = this._policyFor(record.envelope.kind);
843
- // Read through the public accessor so the write path and every external
844
- // caller (the ingest orchestrator's layer-1) resolve dedup granularity from
845
- // ONE place — there is no second declaration site.
846
- const dedupScope: DedupScope = this.dedupScopeFor(record.envelope.kind);
847
- // Content-hash dedup runs BEFORE policy. Its granularity is the kind's
848
- // `dedupScope`:
849
- // - 'content': an identical { kind, body, links } triple ANYWHERE in the
850
- // scope (even under a different id) is a no-op (knowledge family).
851
- // - 'entity' (default): only an identical re-put of the SAME id is a no-op;
852
- // two distinct entities with identical content never collapse (experience
853
- // families). The same-id check is folded into the `_readRecord` below.
854
- // (Tags / provenance are metadata and are NOT part of the hash; see
855
- // design-lock §2.5.)
856
- if (dedupScope === 'content') {
857
- // Cross-id content collapse: an identical { kind, body, links } triple under
858
- // a DIFFERENT id is a no-op (knowledge family). A same-id match is excluded
859
- // here and falls through to the LWW path below so a metadata-only revision
860
- // (tags / provenance — outside the content hash but inside the policy's
861
- // mutableFields) actually applies. Content-dedup must never shadow LWW for
862
- // the same entity.
863
- const duplicate: IMemoryRecord<unknown> | undefined = this._findByContentHash(
864
- scope,
865
- hash,
866
- record.envelope.id
867
- );
868
- if (duplicate !== undefined) {
869
- return succeed({ record: duplicate, evicted: [] });
870
- }
871
- }
872
- return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
873
- // Same-id re-put is a no-op ONLY when the content hash matches AND the
874
- // mutable metadata is also unchanged. The content hash covers
875
- // { kind, body, links }; a matching hash with revised tags/provenance is a
876
- // real update that must reach applyUpdate, not be swallowed as a duplicate
877
- // (both dedup scopes).
878
- if (
879
- existing !== undefined &&
880
- existing.envelope.contentHash === hash &&
881
- this._isMutableMetadataUnchanged(existing, record)
882
- ) {
883
- return Promise.resolve(succeed({ record: existing, evicted: [] }));
884
- }
885
- // The admission cohort is the set of records the policy's cap applies to:
886
- // every record in this scope of this kind EXCEPT the target id. On a first
887
- // write the post-write count is `cohort.length + 1`; on an update the prior
888
- // same-id record is excluded so the count is still `cohort.length + 1`
889
- // (a replace, not a grow). The same-id `existing` record is threaded
890
- // separately into `_buildRecord` for the merge-patch. Knowledge LWW ignores
891
- // this argument, so its behavior is unchanged by the wider cohort.
892
- const cohort: ReadonlyArray<IMemoryRecord<unknown>> = this._admissionCohort(
893
- scope,
894
- record.envelope.kind,
895
- idStem
896
- );
897
- return policy
898
- .admit(record, cohort)
899
- .thenOnSuccess((decision) =>
900
- this._admitWrite(record, body, scope, idStem, hash, policy, existing, decision)
901
- );
902
- });
903
- }
904
-
905
- /**
906
- * Build → embed → persist → evict for an admitted write.
907
- *
908
- * The durable record store is authoritative; the vector index is a **derived,
909
- * rebuildable** view, so vector maintenance is **best-effort** — a failed embed
910
- * or `add` is logged and the durable write still succeeds (the index can be
911
- * rebuilt via {@link InMemoryCosineIndex.rebuild}). Only genuine record-store
912
- * failures (body/codec/policy, persist, file eviction) fail the `put`.
913
- *
914
- * Ordering: the embed + `add` run immediately before the single `_persist` so
915
- * the index-returned `embeddingRef` lands in one durable write (a post-persist
916
- * stamp would need a second write whose failure path is effectively untestable).
917
- * Build and persist the replacement BEFORE evicting the cull-oldest cohort, so a
918
- * later eviction failure never loses data with nothing written in its place.
919
- */
920
- private async _admitWrite(
921
- record: IMemoryRecord<unknown>,
922
- body: string,
923
- scope: MemoryScopeKey,
924
- idStem: string,
925
- hash: string,
926
- policy: IWritePolicy,
927
- existing: IMemoryRecord<unknown> | undefined,
928
- decision: AdmissionDecision
929
- ): Promise<Result<IPutOutcome>> {
930
- if (decision.decision === 'reject') {
931
- return fail(`memory put: rejected by policy: ${decision.reason}`);
932
- }
933
- return this._buildRecord(record, body, existing, policy, hash)
934
- .onSuccess((built) => succeed(this._stampRank(built)))
935
- .thenOnSuccess((built) => this._embedOnWrite(built, scope))
936
- .thenOnSuccess((built) => this._embedFragmentsOnWrite(built, scope))
937
- .onSuccess((embeddedBuilt) => this._persist(embeddedBuilt, scope, idStem))
938
- .thenOnSuccess(async (persisted) => {
939
- // Everything after the authoritative `_persist` commit is best-effort and
940
- // never turns a committed write into a `Failure`:
941
- // - a cull-oldest eviction that fails is logged (the per-(scope,kind)
942
- // cap may be transiently exceeded; the next write's admission restores
943
- // it), and only the successfully-evicted ids flow onward;
944
- // - vector pruning of the evicted cohort is likewise best-effort.
945
- const evicted: ReadonlyArray<MemoryId> = this._applyEvictions(decision, scope);
946
- await this._removeEvictedVectors(evicted, scope);
947
- return succeed({ record: persisted, evicted });
948
- });
949
- }
950
-
951
- /**
952
- * Best-effort embed-on-write. When a vector index AND an embedder are wired,
953
- * embeds the built record, `add`s the vector (replace semantics handle a same-id
954
- * re-embed — no explicit remove), and stamps the returned `embeddingRef`. A
955
- * failure (returned `fail` OR a thrown/rejected hook) is logged and the
956
- * unembedded record is returned unchanged — the put still persists, and the
957
- * derived index is reconciled by a later `rebuild`. A pass-through no-op when
958
- * unwired (byte-identical record).
959
- *
960
- * Always succeeds (`Result` is the chain's shape, never a vector-induced
961
- * failure).
962
- */
963
- private async _embedOnWrite(
964
- built: IMemoryRecord<string>,
965
- scope: MemoryScopeKey
966
- ): Promise<Result<IMemoryRecord<string>>> {
967
- if (this._vectorIndex === undefined || this._embed === undefined) {
968
- return succeed(built);
969
- }
970
- const vectorIndex: IVectorIndex = this._vectorIndex;
971
- const embed: MemoryEmbedder = this._embed;
972
- const target: IEdgeTarget = { scope, id: built.envelope.id };
973
- const embedded: Result<Float32Array> = await this._tryVectorOp(
974
- () => embed(built),
975
- `embedding '${built.envelope.id}'`
976
- );
977
- if (embedded.isFailure()) {
978
- return succeed(built);
979
- }
980
- const added: Result<string> = await this._tryVectorOp(
981
- () => vectorIndex.add(target, embedded.value),
982
- `vector add for '${built.envelope.id}'`
983
- );
984
- if (added.isFailure()) {
985
- return succeed(built);
986
- }
987
- return succeed({ envelope: { ...built.envelope, embeddingRef: added.value }, body: built.body });
988
- }
989
-
990
- /**
991
- * Best-effort fragment-embed-on-write. When a fragment index AND a fragment
992
- * embedder are wired, chunks + embeds the built record and replaces its
993
- * fragments in the index (`addFragments` is whole-record-replace, so a re-authored
994
- * document never leaves stale fragments behind — no explicit remove needed). A
995
- * failure (returned `fail` OR a thrown/rejected hook) is logged and the record is
996
- * returned unchanged — the put still persists, and the fragment index is a derived
997
- * view a later `rebuild` reconciles. Unlike {@link FileTreeMemoryStore._embedOnWrite}
998
- * it stamps nothing on the record (fragments have no per-record `embeddingRef`
999
- * analog). A pass-through no-op when unwired (byte-identical record).
1000
- */
1001
- private async _embedFragmentsOnWrite(
1002
- built: IMemoryRecord<string>,
1003
- scope: MemoryScopeKey
1004
- ): Promise<Result<IMemoryRecord<string>>> {
1005
- if (this._fragmentIndex === undefined || this._fragmentEmbedder === undefined) {
1006
- return succeed(built);
1007
- }
1008
- const fragmentIndex: IFragmentVectorIndex = this._fragmentIndex;
1009
- const fragmentEmbedder: FragmentEmbedder = this._fragmentEmbedder;
1010
- const target: IEdgeTarget = { scope, id: built.envelope.id };
1011
- const embedded: Result<ReadonlyArray<IEmbeddedFragment>> = await this._tryVectorOp(
1012
- () => fragmentEmbedder(built),
1013
- `fragment embedding '${built.envelope.id}'`
1014
- );
1015
- if (embedded.isFailure()) {
1016
- return succeed(built);
1017
- }
1018
- await this._tryVectorOp(
1019
- () => fragmentIndex.addFragments(target, embedded.value),
1020
- `fragment add for '${built.envelope.id}'`
1021
- );
1022
- return succeed(built);
1023
- }
1024
-
1025
- /**
1026
- * Best-effort fragment removal. A no-op unless the full fragment lifecycle is
1027
- * wired (both an index AND an embedder), so an unwired store does no fragment
1028
- * work and behaves byte-identically. Failures are logged, never surfaced — a
1029
- * committed delete/eviction must not fail because a derived fragment index could
1030
- * not be pruned.
1031
- */
1032
- private async _removeFragmentsBestEffort(target: IEdgeTarget): Promise<void> {
1033
- if (this._fragmentIndex === undefined || this._fragmentEmbedder === undefined) {
1034
- return;
1035
- }
1036
- const fragmentIndex: IFragmentVectorIndex = this._fragmentIndex;
1037
- await this._tryVectorOp(() => fragmentIndex.remove(target), `fragment removal for '${target.id}'`);
1038
- }
1039
-
1040
- /**
1041
- * Evict the records named by a `cull-oldest` decision, best-effort. Runs only
1042
- * after the authoritative `_persist`, so a failed eviction is logged (never
1043
- * fatal) and the cap self-corrects on the next admission. Returns the ids that
1044
- * were actually evicted (so observations / vector pruning cover only those).
1045
- * `accept` / `reject` decisions evict nothing.
1046
- */
1047
- private _applyEvictions(decision: AdmissionDecision, scope: MemoryScopeKey): ReadonlyArray<MemoryId> {
1048
- if (decision.decision !== 'cull-oldest') {
1049
- return [];
1050
- }
1051
- const evicted: MemoryId[] = [];
1052
- for (const id of decision.evict) {
1053
- const result: Result<MemoryId> = this._evict(scope, id);
1054
- if (result.isSuccess()) {
1055
- evicted.push(result.value);
1056
- } else {
1057
- this._warnSwallowed(
1058
- `memory put: best-effort eviction of '${id}' failed (cap may be transiently exceeded; restored on the next write): ${result.message}`
1059
- );
1060
- }
1061
- }
1062
- return evicted;
1063
- }
1064
-
1065
- /**
1066
- * Best-effort vector removal for each evicted record (never fails the put).
1067
- * Every evicted record is in the same `scope` as the incoming write (the
1068
- * cull-oldest cohort is the incoming record's `(scope, kind)` cohort), so that
1069
- * scope qualifies each removal target.
1070
- */
1071
- private async _removeEvictedVectors(
1072
- evicted: ReadonlyArray<MemoryId>,
1073
- scope: MemoryScopeKey
1074
- ): Promise<void> {
1075
- for (const id of evicted) {
1076
- await this._removeVectorBestEffort({ scope, id });
1077
- await this._removeFragmentsBestEffort({ scope, id });
1078
- }
1079
- }
1080
-
1081
- /**
1082
- * Run a consumer-supplied vector hook, normalizing a thrown/rejected hook into a
1083
- * `Failure` and logging any failure at `warn`. Best-effort: the caller proceeds
1084
- * regardless, since the index is rebuildable.
1085
- */
1086
- private async _tryVectorOp<T>(op: () => Promise<Result<T>>, label: string): Promise<Result<T>> {
1087
- let result: Result<T>;
1088
- try {
1089
- result = await op();
1090
- } catch (err) {
1091
- result = fail(`${label} threw: ${String(err)}`);
1092
- }
1093
- if (result.isFailure()) {
1094
- this._warnSwallowed(
1095
- `memory: ${label} failed (best-effort; derived index left for rebuild): ${result.message}`
1096
- );
1097
- }
1098
- return result;
1099
- }
1100
-
1101
- /**
1102
- * Best-effort vector removal. A no-op unless the full vector lifecycle is wired
1103
- * (both an index AND an embedder), so an unwired store does no vector work and
1104
- * behaves byte-identically. Failures are logged, never surfaced — a committed
1105
- * delete/eviction must not fail because a derived index could not be pruned.
1106
- */
1107
- private async _removeVectorBestEffort(target: IEdgeTarget): Promise<void> {
1108
- if (this._vectorIndex === undefined || this._embed === undefined) {
1109
- return;
1110
- }
1111
- const vectorIndex: IVectorIndex = this._vectorIndex;
1112
- await this._tryVectorOp(() => vectorIndex.remove(target), `vector removal for '${target.id}'`);
1113
- }
1114
-
1115
- /**
1116
- * Build the record to persist. On a first write the incoming envelope is the
1117
- * base (final content equals the incoming content, so the dedup `hash` is
1118
- * reused). On an update the incoming record's mutable fields are projected into
1119
- * a merge-patch and the policy's `applyUpdate` merges them over the existing
1120
- * record (preserving `created`); the persisted body and `contentHash` are then
1121
- * taken from the policy's actual output, so a body-transforming policy is never
1122
- * bypassed and the stored hash always matches the stored `{ kind, body, links }`.
1123
- * The store stamps the transaction-time metadata it owns
1124
- * (`created` / `updated` / `seq` / `contentHash`).
1125
- */
1126
- private _buildRecord(
1127
- incoming: IMemoryRecord<unknown>,
1128
- body: string,
1129
- existing: IMemoryRecord<unknown> | undefined,
1130
- policy: IWritePolicy,
1131
- hash: string
1132
- ): Result<IMemoryRecord<string>> {
1133
- const now: number = this._clock();
1134
- const seq: number = ++this._seq;
1135
- if (existing === undefined) {
1136
- return succeed({
1137
- envelope: { ...incoming.envelope, created: now, updated: now, seq, contentHash: hash },
1138
- body
1139
- });
1140
- }
1141
- const patch: Record<string, unknown> = this._projectMutablePatch(incoming, policy.mutableFields);
1142
- return policy
1143
- .applyUpdate(existing, patch)
1144
- .withErrorFormat((msg) => `memory put '${incoming.envelope.id}': update failed: ${msg}`)
1145
- .onSuccess((updated) => {
1146
- if (typeof updated.body !== 'string') {
1147
- return fail(
1148
- `memory put '${incoming.envelope.id}': policy returned a non-string body (${typeof updated.body})`
1149
- );
1150
- }
1151
- const finalBody: string = updated.body;
1152
- return this._contentHash(updated.envelope.kind, finalBody, updated.envelope.links).onSuccess(
1153
- (finalHash) =>
1154
- succeed({
1155
- envelope: {
1156
- ...updated.envelope,
1157
- created: existing.envelope.created,
1158
- updated: now,
1159
- seq,
1160
- contentHash: finalHash
1161
- },
1162
- body: finalBody
1163
- })
1164
- );
1165
- });
1166
- }
1167
-
1168
- /** Serialize and write a fully-stamped record, then patch the index. */
1169
- private _persist(
1170
- record: IMemoryRecord<string>,
1171
- scope: MemoryScopeKey,
1172
- idStem: string
1173
- ): Result<IMemoryRecord<unknown>> {
1174
- return serializeMemoryFile(record.envelope, record.body)
1175
- .onSuccess((raw) => this._writeFile(scope, idStem, raw))
1176
- .onSuccess(() => this._index.patch('put', { scope, record }))
1177
- .onSuccess(() => succeed(record));
1178
- }
1179
-
1180
- private async _deleteLocked(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>> {
1181
- return this._codecFor(kind).thenOnSuccess((codec) =>
1182
- codec.encode(entityId).thenOnSuccess((addr) => {
1183
- if (addr.isVersioned) {
1184
- if (!isTemporalIdentityCodec(codec)) {
1185
- return Promise.resolve(
1186
- fail<MemoryId>(
1187
- `memory delete '${entityId}': codec for versioned kind '${kind}' does not implement the temporal codec interface`
1188
- )
1189
- );
1190
- }
1191
- return this._deleteVersioned(entityId, addr.scope);
1192
- }
1193
- return this._deleteFlat(entityId, addr.scope, addr.idStem);
1194
- })
1195
- );
1196
- }
1197
-
1198
- /**
1199
- * Flat (non-versioned) delete: physically remove the record file + index
1200
- * entry, then prune the vector best-effort. Structurally unchanged from the
1201
- * pre-temporal delete path.
1202
- */
1203
- private async _deleteFlat(
1204
- entityId: EntityId,
1205
- scope: MemoryScopeKey,
1206
- idStem: string
1207
- ): Promise<Result<MemoryId>> {
1208
- return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
1209
- if (existing === undefined) {
1210
- return Promise.resolve(fail<MemoryId>(`memory delete '${entityId}': no record found`));
1211
- }
1212
- // Delete the record file + index entry (authoritative), then prune the
1213
- // vector best-effort: a committed delete must not fail because the
1214
- // derived index could not be pruned.
1215
- return this._deleteFile(scope, idStem)
1216
- .onSuccess(() => this._index.patch('delete', { scope, record: existing }))
1217
- .thenOnSuccess(async () => {
1218
- await this._removeVectorBestEffort({ scope, id: existing.envelope.id });
1219
- await this._removeFragmentsBestEffort({ scope, id: existing.envelope.id });
1220
- return succeed(existing.envelope.id);
1221
- });
1222
- });
1223
- }
1224
-
1225
- /**
1226
- * Resolve the current version of a temporal entity from the derived index: the
1227
- * highest-`seq` version under the entity subtree `scope` whose `invalid_at` is
1228
- * null/absent. `undefined` when the entity has no current version (never
1229
- * written, or fully invalidated / soft-deleted).
1230
- */
1231
- private _readVersionedCurrent(scope: MemoryScopeKey): IMemoryRecord<unknown> | undefined {
1232
- return selectCurrentVersion(this._versionsForEntity(scope));
1233
- }
1234
-
1235
- /**
1236
- * Every persisted version of the entity whose subtree is `scope`. All version
1237
- * files for one entity live under exactly that scope (which encodes the
1238
- * entityId), so a scope filter over the index isolates one entity's versions.
1239
- */
1240
- private _versionsForEntity(scope: MemoryScopeKey): ReadonlyArray<IMemoryRecord<unknown>> {
1241
- return this._index
1242
- .entries()
1243
- .filter((entry) => entry.scope === scope)
1244
- .map((entry) => entry.record);
1245
- }
1246
-
1247
- /**
1248
- * Versioned write (invalidate-don't-delete). Builds the new version's content
1249
- * (a first version from the incoming record, or a merge of the incoming patch
1250
- * over the current version), persists it as a NEW version file, then sets
1251
- * `invalid_at` on the prior current version.
1252
- *
1253
- * Durability order: the new version is persisted FIRST. It carries the highest
1254
- * `seq`, so a crash before the prior-version invalidation completes still
1255
- * resolves the new version as current (`selectCurrentVersion` breaks a
1256
- * two-current tie by highest `seq`), and `asOf` reads stay correct because each
1257
- * version's `valid_at` lower-bounds its interval.
1258
- */
1259
- private async _putVersioned(
1260
- record: IMemoryRecord<unknown>,
1261
- body: string,
1262
- codec: ITemporalIdentityCodec,
1263
- scope: MemoryScopeKey
1264
- ): Promise<Result<IPutOutcome>> {
1265
- const envelope: IMemoryEnvelope = record.envelope;
1266
- const entityId: EntityId = envelope.entityId;
1267
- const kind: Kind = envelope.kind;
1268
- // Snapshot the entity's versions BEFORE the write. `priorCurrents` is every
1269
- // still-current version at snapshot time — normally one, but two-or-more if a
1270
- // prior invalidation partially failed; invalidating all of them lets the write
1271
- // self-heal a stuck state (P2-7).
1272
- const versions: ReadonlyArray<IMemoryRecord<unknown>> = this._versionsForEntity(scope);
1273
- const priorCurrents: ReadonlyArray<IMemoryRecord<unknown>> = versions.filter(isVersionCurrent);
1274
- const current: IMemoryRecord<unknown> | undefined = selectCurrentVersion(versions);
1275
- const policy: IWritePolicy = this._policyFor(kind);
1276
- // Same single-owner read as the flat path (see `_writeResolved`).
1277
- const dedupScope: DedupScope = this.dedupScopeFor(kind);
1278
- return this._contentHash(kind, body, envelope.links).thenOnSuccess((hash) => {
1279
- // Entity-scoped dedup: a re-put is a no-op only when the CURRENT content AND
1280
- // its mutable metadata are unchanged (does not spawn a redundant version).
1281
- // A metadata-only revision (tags/provenance — declared mutable by
1282
- // TemporalVersionedPolicy) must NOT be swallowed here: it mints a new
1283
- // version via the applyUpdate merge in `_buildVersionedRecord`, mirroring
1284
- // the flat path. Content-scoped dedup is not a versioning concern, so only
1285
- // the entity granularity is honored here.
1286
- if (
1287
- dedupScope === 'entity' &&
1288
- current !== undefined &&
1289
- current.envelope.contentHash === hash &&
1290
- this._isMutableMetadataUnchanged(current, record)
1291
- ) {
1292
- return Promise.resolve(succeed<IPutOutcome>({ record: current, evicted: [] }));
1293
- }
1294
- // On the versioned path the admission cohort is the entity's ENTIRE version
1295
- // history (no target id to exclude — the new version does not exist yet),
1296
- // which differs from the flat path's "cohort excluding target id" shape.
1297
- return policy.admit(record, versions).thenOnSuccess((decision) => {
1298
- if (decision.decision === 'reject') {
1299
- return Promise.resolve(fail<IPutOutcome>(`memory put: rejected by policy: ${decision.reason}`));
1300
- }
1301
- // No culling on the versioned path — history is retained (invalidate-don't-delete).
1302
- const now: number = this._clock();
1303
- const seq: number = ++this._seq;
1304
- // World-truth start of the new version. Each prior current version's
1305
- // world-truth interval CLOSES at this same instant (not at `now`) so a
1306
- // backdated/future-dated `valid_at` leaves no gap or overlap on the
1307
- // valid-time axis.
1308
- const validAt: number = envelope.temporal?.valid_at ?? now;
1309
- return codec
1310
- .encodeVersion(entityId, seq)
1311
- .withErrorFormat((msg) => `memory put '${entityId}': ${msg}`)
1312
- .thenOnSuccess((versionStem) =>
1313
- this._buildVersionedRecord(record, body, current, policy, hash, versionStem, validAt, now, seq)
1314
- .onSuccess((built) => succeed(this._stampRank(built)))
1315
- .thenOnSuccess((built) => this._embedOnWrite(built, scope))
1316
- .thenOnSuccess((built) => this._embedFragmentsOnWrite(built, scope))
1317
- .onSuccess((embeddedBuilt) => this._persist(embeddedBuilt, scope, versionStem))
1318
- .onSuccess((persisted) =>
1319
- this._invalidateCurrents(scope, priorCurrents, validAt, now).onSuccess(() =>
1320
- succeed(persisted)
1321
- )
1322
- )
1323
- .onSuccess((persisted) => succeed({ record: persisted, evicted: [] }))
1324
- );
1325
- });
1326
- });
1327
- }
1328
-
1329
- /**
1330
- * Invalidate a set of still-current versions (close each world-truth interval at
1331
- * `invalidAt`; stamp transaction-time `updated` = `now`). Chained so a mid-list
1332
- * failure propagates. Invalidating every prior current — not just the
1333
- * highest-`seq` pick — self-heals a state where a previous write's invalidation
1334
- * only partially completed (P2-7).
1335
- */
1336
- private _invalidateCurrents(
1337
- scope: MemoryScopeKey,
1338
- currents: ReadonlyArray<IMemoryRecord<unknown>>,
1339
- invalidAt: number,
1340
- now: number
1341
- ): Result<true> {
1342
- return currents.reduce<Result<true>>(
1343
- (acc, version) =>
1344
- acc.onSuccess(() =>
1345
- this._invalidateVersion(scope, version, invalidAt, now).onSuccess(() => succeed(true))
1346
- ),
1347
- succeed(true)
1348
- );
1349
- }
1350
-
1351
- /**
1352
- * Build the new version to persist. A first version takes the incoming
1353
- * content verbatim (its dedup `hash` is reused); a subsequent version projects
1354
- * the incoming record's mutable fields into a merge-patch and lets the policy
1355
- * merge them over the CURRENT version (the merge-patch-under-versioning
1356
- * contract), recomputing the content hash from the policy's output. Each
1357
- * version is its own record with its own transaction time (`created` = `now`)
1358
- * and a store-minted `id` = the version stem, so `id === filename stem` holds.
1359
- */
1360
- private _buildVersionedRecord(
1361
- incoming: IMemoryRecord<unknown>,
1362
- body: string,
1363
- current: IMemoryRecord<unknown> | undefined,
1364
- policy: IWritePolicy,
1365
- // Reused only for the first-version branch (its content is taken verbatim); a
1366
- // subsequent version recomputes its hash from the merge-patched content.
1367
- firstVersionHash: string,
1368
- versionStem: string,
1369
- validAt: number,
1370
- now: number,
1371
- seq: number
1372
- ): Result<IMemoryRecord<string>> {
1373
- const mintedId: MemoryId = versionStem as MemoryId;
1374
- if (current === undefined) {
1375
- const envelope: IMemoryEnvelope = {
1376
- ...incoming.envelope,
1377
- id: mintedId,
1378
- entityId: incoming.envelope.entityId,
1379
- created: now,
1380
- updated: now,
1381
- seq,
1382
- contentHash: firstVersionHash,
1383
- temporal: { valid_at: validAt }
1384
- };
1385
- return succeed({ envelope, body });
1386
- }
1387
- const patch: Record<string, unknown> = this._projectMutablePatch(incoming, policy.mutableFields);
1388
- return policy
1389
- .applyUpdate(current, patch)
1390
- .withErrorFormat((msg) => `memory put '${incoming.envelope.entityId}': update failed: ${msg}`)
1391
- .onSuccess((updated) => {
1392
- if (typeof updated.body !== 'string') {
1393
- return fail(
1394
- `memory put '${
1395
- incoming.envelope.entityId
1396
- }': policy returned a non-string body (${typeof updated.body})`
1397
- );
1398
- }
1399
- const finalBody: string = updated.body;
1400
- return this._contentHash(updated.envelope.kind, finalBody, updated.envelope.links).onSuccess(
1401
- (finalHash) => {
1402
- const envelope: IMemoryEnvelope = {
1403
- ...updated.envelope,
1404
- id: mintedId,
1405
- entityId: incoming.envelope.entityId,
1406
- created: now,
1407
- updated: now,
1408
- seq,
1409
- contentHash: finalHash,
1410
- temporal: { valid_at: validAt }
1411
- };
1412
- return succeed({ envelope, body: finalBody });
1413
- }
1414
- );
1415
- });
1416
- }
1417
-
1418
- /**
1419
- * Set `invalid_at` on a prior current version (invalidate-don't-delete) and
1420
- * rewrite its file + index entry. The content hash is unchanged — `invalid_at`
1421
- * is temporal metadata, not part of `{ kind, body, links }` — so the version's
1422
- * identity is stable.
1423
- */
1424
- private _invalidateVersion(
1425
- scope: MemoryScopeKey,
1426
- version: IMemoryRecord<unknown>,
1427
- invalidAt: number,
1428
- now: number
1429
- ): Result<IMemoryRecord<unknown>> {
1430
- /* c8 ignore start -- defensive: every persisted version carries a string body (only string bodies are written) */
1431
- if (typeof version.body !== 'string') {
1432
- return fail(`memory put: cannot invalidate version '${version.envelope.id}': non-string body`);
1433
- }
1434
- /* c8 ignore stop */
1435
- // `invalid_at` is the WORLD-TRUTH close of the interval (the superseding version's
1436
- // `valid_at`, or the delete instant); `updated` is the transaction-time stamp.
1437
- const invalidated: IMemoryRecord<string> = {
1438
- envelope: {
1439
- ...version.envelope,
1440
- updated: now,
1441
- temporal: { ...version.envelope.temporal, invalid_at: invalidAt }
1442
- },
1443
- body: version.body
1444
- };
1445
- return this._persist(invalidated, scope, version.envelope.id);
1446
- }
1447
-
1448
- /**
1449
- * Versioned delete: SOFT delete (invalidate-don't-delete). Sets `invalid_at` on
1450
- * the current version, leaving the entity's history intact and the entity with
1451
- * no current version. Returns the invalidated version's {@link MemoryId}. Fails
1452
- * with "no record found" when there is no current version — matching the flat
1453
- * delete's not-found semantics. History is retained deliberately: temporal
1454
- * kinds exist to preserve the audit trail (and the L3 `contradicts` interlock
1455
- * builds on it), so a hard delete would defeat the purpose.
1456
- */
1457
- private async _deleteVersioned(entityId: EntityId, scope: MemoryScopeKey): Promise<Result<MemoryId>> {
1458
- const versions: ReadonlyArray<IMemoryRecord<unknown>> = this._versionsForEntity(scope);
1459
- const currents: ReadonlyArray<IMemoryRecord<unknown>> = versions.filter(isVersionCurrent);
1460
- const current: IMemoryRecord<unknown> | undefined = selectCurrentVersion(versions);
1461
- if (current === undefined) {
1462
- return fail(`memory delete '${entityId}': no record found`);
1463
- }
1464
- // A delete closes the world-truth interval at the delete instant (`now` for both
1465
- // the transaction stamp and the `invalid_at` boundary). Every still-current
1466
- // version is invalidated (self-heals a stuck two-current state — P2-7); the
1467
- // highest-`seq` current's id is returned.
1468
- const now: number = this._clock();
1469
- return this._invalidateCurrents(scope, currents, now, now).onSuccess(() => succeed(current.envelope.id));
1470
- }
1471
-
1472
- /** Evict (physically delete) a single record file by id, patching the index. */
1473
- private _evict(scope: MemoryScopeKey, id: MemoryId): Result<MemoryId> {
1474
- return this._readRecord(scope, id).onSuccess((existing) => {
1475
- if (existing === undefined) {
1476
- return fail(`memory put: cannot evict '${id}' in scope '${scope}': not found`);
1477
- }
1478
- return this._deleteFile(scope, id)
1479
- .onSuccess(() => this._index.patch('delete', { scope, record: existing }))
1480
- .onSuccess(() => succeed(id));
1481
- });
1482
- }
1483
-
1484
- /** Project the incoming record's mutable fields into a merge-patch. */
1485
- private _projectMutablePatch(
1486
- record: IMemoryRecord<unknown>,
1487
- mutableFields: ReadonlyArray<string>
1488
- ): Record<string, unknown> {
1489
- const patch: Record<string, unknown> = {};
1490
- for (const field of mutableFields) {
1491
- const accessor: ((record: IMemoryRecord<unknown>) => unknown) | undefined =
1492
- FileTreeMemoryStore._mutableFieldAccessors.get(field);
1493
- if (accessor !== undefined) {
1494
- const value: unknown = accessor(record);
1495
- if (value !== undefined) {
1496
- patch[field] = value;
1497
- }
1498
- }
1499
- }
1500
- return patch;
1501
- }
1502
-
1503
- /**
1504
- * The admission cohort for a write: every indexed record in `scope` of `kind`
1505
- * except the one at `idStem` (the record being written or updated). This is
1506
- * the set a per-kind cap (e.g. {@link MemoryCapCullPolicy}) counts against, so
1507
- * a bounded-ring policy can keep a per-scope/per-kind family within
1508
- * `maxRecords`. Excluding the target id makes the post-write count uniform
1509
- * across first-writes and updates.
1510
- */
1511
- private _admissionCohort(
1512
- scope: MemoryScopeKey,
1513
- kind: Kind,
1514
- idStem: string
1515
- ): ReadonlyArray<IMemoryRecord<unknown>> {
1516
- return this._index
1517
- .entries()
1518
- .filter(
1519
- (entry) =>
1520
- entry.scope === scope && entry.record.envelope.kind === kind && entry.record.envelope.id !== idStem
1521
- )
1522
- .map((entry) => entry.record);
1523
- }
1524
-
1525
- /**
1526
- * Find a record in `scope` whose `contentHash` equals `hash`, if any,
1527
- * optionally excluding a specific id. `excludeId` lets the content-scoped
1528
- * dedup skip the same-id record so it does not shadow the LWW update path.
1529
- */
1530
- private _findByContentHash(
1531
- scope: MemoryScopeKey,
1532
- hash: string,
1533
- excludeId?: string
1534
- ): IMemoryRecord<unknown> | undefined {
1535
- const match: IIndexedMemoryRecord | undefined = this._index
1536
- .entries()
1537
- .find(
1538
- (entry) =>
1539
- entry.scope === scope &&
1540
- entry.record.envelope.contentHash === hash &&
1541
- entry.record.envelope.id !== excludeId
1542
- );
1543
- return match?.record;
1544
- }
1545
-
1546
- /**
1547
- * True when `incoming`'s caller-authored mutable metadata (`tags` / `provenance`)
1548
- * canonically equals `existing`'s. `body` and `links` are covered by the content
1549
- * hash; `embeddingRef` is store-derived (not caller metadata) and is deliberately
1550
- * excluded so a same-content re-put is not treated as changed merely because the
1551
- * store already stamped an embedding. Used to keep an identical re-put a no-op
1552
- * without swallowing a genuine metadata revision.
1553
- *
1554
- * Canonicalization never fails for a validated record (`tags`/`provenance` are
1555
- * always plain JSON); a failure is defaulted to a non-matching sentinel so the
1556
- * write flows to `applyUpdate` (which re-validates) rather than silently
1557
- * no-op-ing on an un-canonicalizable value.
1558
- */
1559
- private _isMutableMetadataUnchanged(
1560
- existing: IMemoryRecord<unknown>,
1561
- incoming: IMemoryRecord<unknown>
1562
- ): boolean {
1563
- const key = (record: IMemoryRecord<unknown>): string =>
1564
- this._hasher
1565
- .canonicalize({ tags: record.envelope.tags, provenance: record.envelope.provenance })
1566
- .orDefault('');
1567
- const existingKey: string = key(existing);
1568
- return existingKey !== '' && existingKey === key(incoming);
1569
- }
1570
-
1571
- private _contentHash(kind: Kind, body: string, links: IMemoryEnvelope['links']): Result<string> {
1572
- return this._hasher.computeHash({ kind, body, links });
1573
- }
1574
-
1575
- /**
1576
- * Stamp the store-computed {@link IMemoryEnvelope.rank} onto a fully-built,
1577
- * fully-stamped record by running the kind's registered {@link RankProjector}.
1578
- * Runs on the SAME resolved (post-merge) record whose `contentHash` was just
1579
- * computed, so `rank` is always consistent with the current body — no separate
1580
- * write path and no consumer write-discipline rule. A no-op pass-through
1581
- * (byte-identical record) when the kind has no projector — the additive,
1582
- * zero-overhead-when-unwired default. The projector is a host callback: a throw
1583
- * is logged at `warn` and the record is stamped with NO `rank` (the field is
1584
- * explicitly cleared, so a throw on an update drops a now-stale prior rank rather
1585
- * than preserving it), so a ranking bug never loses an authoritative write.
1586
- */
1587
- private _stampRank(record: IMemoryRecord<string>): IMemoryRecord<string> {
1588
- const projector: RankProjector | undefined = this._rankProjectors.get(record.envelope.kind);
1589
- if (projector === undefined) {
1590
- return record;
1591
- }
1592
- try {
1593
- const rank: number = projector(record);
1594
- return { envelope: { ...record.envelope, rank }, body: record.body };
1595
- } catch (err) {
1596
- this._warnSwallowed(
1597
- `memory put '${record.envelope.id}': rank projector threw (swallowed; rank cleared): ${String(err)}`
1598
- );
1599
- // Explicitly CLEAR `rank` (not `return record`): on an update the merged
1600
- // envelope carries the prior version's `rank` (the write-policy merge does
1601
- // not touch `rank`), so returning it verbatim would keep a stale value
1602
- // inconsistent with the revised body. Clearing honors the staleness
1603
- // contract — an uncomputable rank means the record sorts last as "unranked".
1604
- return { envelope: { ...record.envelope, rank: undefined }, body: record.body };
1605
- }
1606
- }
1607
-
1608
- private _codecFor(kind: Kind): Result<IIdentityCodec> {
1609
- const codec: IIdentityCodec | undefined = this._codecs.get(kind) ?? this._defaultCodec;
1610
- if (codec === undefined) {
1611
- return fail(`no identity codec registered for kind '${kind}'`);
1612
- }
1613
- return succeed(codec);
1614
- }
1615
-
1616
- private _policyFor(kind: Kind): IWritePolicy {
1617
- return this._writePolicies.get(kind) ?? this._defaultPolicy;
1618
- }
1619
-
1620
- /** {@inheritDoc IMemoryStore.dedupScopeFor} */
1621
- public dedupScopeFor(kind: Kind): DedupScope {
1622
- return this._policyFor(kind).dedupScope ?? DEFAULT_DEDUP_SCOPE;
1623
- }
1624
-
1625
- /**
1626
- * Read and validate the record at `<scope>/<idStem>.md`, returning `undefined`
1627
- * when the scope directory or file is absent. Verifies the on-disk id ↔
1628
- * filename round-trip on every load.
1629
- */
1630
- private _readRecord(scope: MemoryScopeKey, idStem: string): Result<IMemoryRecord<unknown> | undefined> {
1631
- return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
1632
- if (scopeDir === undefined) {
1633
- return succeed(undefined);
1634
- }
1635
- return scopeDir.getChildren().onSuccess((children) => {
1636
- const targetName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;
1637
- const file: FileTree.IFileTreeFileItem | undefined = children.find(
1638
- (c): c is FileTree.IFileTreeFileItem => c.type === 'file' && c.name === targetName
1639
- );
1640
- if (file === undefined) {
1641
- return succeed(undefined);
1642
- }
1643
- return file
1644
- .getRawContents()
1645
- .onSuccess((raw) => parseMemoryFile(raw, this._registry))
1646
- .onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord));
1647
- });
1648
- });
1649
- }
1650
-
1651
- /**
1652
- * Enforce `envelope.id === filename stem`, the codec round-trip, AND that the
1653
- * envelope's own `entityId` agrees with the id decoded from the subtree scope.
1654
- * The round-trip only validates (scope, stem) consistency; the codec derives
1655
- * `entityId` from the scope path and never reads the envelope's `entityId`
1656
- * field, so a tampered/corrupt file whose frontmatter declares a foreign
1657
- * `entityId` would otherwise load undetected — and `entityId` is trusted
1658
- * verbatim downstream (e.g. merge-into re-addressing). Cross-check it here.
1659
- */
1660
- private _verifyLoaded(
1661
- scope: MemoryScopeKey,
1662
- file: FileTree.IFileTreeFileItem,
1663
- record: IMemoryRecord<unknown>
1664
- ): Result<IMemoryRecord<unknown>> {
1665
- if (record.envelope.id !== file.baseName) {
1666
- return fail(
1667
- `memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`
1668
- );
1669
- }
1670
- return this._codecFor(record.envelope.kind)
1671
- .onSuccess((codec) =>
1672
- codec.verifyRoundTrip(scope, file.baseName).onSuccess(() => codec.decode(scope, file.baseName))
1673
- )
1674
- .withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
1675
- .onSuccess((decodedEntityId) => {
1676
- if (decodedEntityId !== record.envelope.entityId) {
1677
- return fail(
1678
- `memory file '${file.absolutePath}': envelope entityId '${record.envelope.entityId}' does not match scope-derived entityId '${decodedEntityId}'`
1679
- );
1680
- }
1681
- return succeed(record);
1682
- });
1683
- }
1684
-
1685
- /**
1686
- * Resolve the directory for a scope, returning `undefined` when it does not
1687
- * exist. Navigation only — does not create. Folds the path segments through
1688
- * `getChildren` so an absent segment short-circuits to `undefined`.
1689
- */
1690
- private _resolveScopeDir(scope: MemoryScopeKey): Result<FileTree.IFileTreeDirectoryItem | undefined> {
1691
- return this._scopeEncoding(scope).onSuccess((encoded) => {
1692
- const segments: string[] = encoded.split('/').filter((s) => s.length > 0);
1693
- return segments.reduce<Result<FileTree.IFileTreeDirectoryItem | undefined>>(
1694
- (acc, segment) =>
1695
- acc.onSuccess((current) => {
1696
- if (current === undefined) {
1697
- return succeed(undefined);
1698
- }
1699
- return current
1700
- .getChildren()
1701
- .onSuccess((children) =>
1702
- succeed(
1703
- children.find(
1704
- (c): c is FileTree.IFileTreeDirectoryItem => c.type === 'directory' && c.name === segment
1705
- )
1706
- )
1707
- );
1708
- }),
1709
- succeed(this._root)
1710
- );
1711
- });
1712
- }
1713
-
1714
- /** Ensure the scope directory exists, creating segments as needed. */
1715
- private _ensureScopeDir(scope: MemoryScopeKey): Result<FileTree.IMutableFileTreeDirectoryItem> {
1716
- return this._scopeEncoding(scope).onSuccess((encoded) => {
1717
- const segments: string[] = encoded.split('/').filter((s) => s.length > 0);
1718
- return segments.reduce<Result<FileTree.IMutableFileTreeDirectoryItem>>(
1719
- (acc, segment) =>
1720
- acc.onSuccess((current) =>
1721
- current.getChildren().onSuccess((children) => {
1722
- const existing: FileTree.FileTreeItem | undefined = children.find(
1723
- (c) => c.type === 'directory' && c.name === segment
1724
- );
1725
- if (existing === undefined) {
1726
- return current.createChildDirectory(segment);
1727
- }
1728
- /* c8 ignore next 3 -- defensive: a child of a mutable in-memory/fs tree is itself mutable; the guard protects against a read-only adapter handed in as root */
1729
- if (!FileTree.isMutableDirectoryItem(existing)) {
1730
- return fail(`${existing.absolutePath}: directory is not mutable`);
1731
- }
1732
- return succeed(existing);
1733
- })
1734
- ),
1735
- succeed(this._root)
1736
- );
1737
- });
1738
- }
1739
-
1740
- /** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
1741
- private _writeFile(scope: MemoryScopeKey, idStem: string, raw: string): Result<true> {
1742
- return this._ensureScopeDir(scope).onSuccess((scopeDir) =>
1743
- scopeDir.getChildren().onSuccess((children) => {
1744
- const fileName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;
1745
- const existing: FileTree.FileTreeItem | undefined = children.find(
1746
- (c) => c.type === 'file' && c.name === fileName
1747
- );
1748
- if (existing === undefined) {
1749
- return scopeDir.createChildFile(fileName, raw).onSuccess(() => succeed(true));
1750
- }
1751
- /* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
1752
- if (!FileTree.isMutableFileItem(existing)) {
1753
- return fail(`${existing.absolutePath}: file is not mutable`);
1754
- }
1755
- return existing.setRawContents(raw).onSuccess(() => succeed(true));
1756
- })
1757
- );
1758
- }
1759
-
1760
- /**
1761
- * Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
1762
- * guards are unreachable through the callers (`delete` / `_evict` both read the
1763
- * record first, so the directory and file exist) but are kept so a future
1764
- * direct caller degrades loudly rather than silently.
1765
- */
1766
- private _deleteFile(scope: MemoryScopeKey, idStem: string): Result<true> {
1767
- return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
1768
- /* c8 ignore next 3 -- unreachable: callers read the record (hence the scope dir) first */
1769
- if (scopeDir === undefined) {
1770
- return fail(`memory delete: scope '${scope}' not found`);
1771
- }
1772
- const fileName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;
1773
- return scopeDir.getChildren().onSuccess((children) => {
1774
- const file: FileTree.FileTreeItem | undefined = children.find(
1775
- (c) => c.type === 'file' && c.name === fileName
1776
- );
1777
- /* c8 ignore next 3 -- unreachable: callers read the record (hence the file) first */
1778
- if (file === undefined) {
1779
- return fail(`memory delete: file '${fileName}' not found in scope '${scope}'`);
1780
- }
1781
- /* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
1782
- if (!FileTree.isMutableFileItem(file)) {
1783
- return fail(`${file.absolutePath}: file is not mutable`);
1784
- }
1785
- return file.delete().onSuccess(() => succeed(true));
1786
- });
1787
- });
1788
- }
1789
-
1790
- /**
1791
- * Walk the FileTree once and rebuild the index. Also resumes the `seq`
1792
- * counter past the highest persisted `seq` so new writes stay monotonic.
1793
- *
1794
- * In `'skip'` mode each per-record failure is captured structurally on
1795
- * `this._skippedRecords` (path + scope + path-tagged error) at its failure
1796
- * site and logged at `warn`; the walk keeps every record that loaded.
1797
- */
1798
- private _initialIndex(onRecordError: MemoryRecordErrorMode): Result<true> {
1799
- return this._collectEntries(this._root, [], onRecordError).onSuccess((entries) =>
1800
- this._index.rebuild(entries).onSuccess(() => {
1801
- for (const entry of entries) {
1802
- if (entry.record.envelope.seq > this._seq) {
1803
- this._seq = entry.record.envelope.seq;
1804
- }
1805
- }
1806
- return succeed(true);
1807
- })
1808
- );
1809
- }
1810
-
1811
- /** Recursively collect every `.md` record under `dir` (scope = path segments). */
1812
- private _collectEntries(
1813
- dir: FileTree.IFileTreeDirectoryItem,
1814
- scopeSegments: ReadonlyArray<string>,
1815
- onRecordError: MemoryRecordErrorMode
1816
- ): Result<ReadonlyArray<IIndexedMemoryRecord>> {
1817
- return dir.getChildren().onSuccess((children) => {
1818
- const results: Result<ReadonlyArray<IIndexedMemoryRecord>>[] = children.map((child) => {
1819
- if (child.type === 'directory') {
1820
- return this._collectEntries(child, [...scopeSegments, child.name], onRecordError);
1821
- }
1822
- if (!child.name.endsWith(MEMORY_FILE_EXTENSION) || scopeSegments.length === 0) {
1823
- // Skip non-record files and any record-shaped file sitting at the root
1824
- // (records always live under at least one scope segment).
1825
- return succeed<ReadonlyArray<IIndexedMemoryRecord>>([]);
1826
- }
1827
- const scope: MemoryScopeKey = scopeSegments.join('/') as MemoryScopeKey;
1828
- return this._loadRecordFile(scope, child, onRecordError);
1829
- });
1830
- // `'skip'` mode: keep every record that parsed, drop the ones that failed
1831
- // in a single pass (each failure is captured on `this._skippedRecords` and
1832
- // warn-logged at its site in `_loadRecordFile`). `.orDefault([])` covers
1833
- // the all-invalid-subtree edge where `mapSuccess` returns Failure because
1834
- // no element succeeded. `'fail'` mode: `mapResults` fails the whole open on
1835
- // any bad record — byte-identical to the historical load path.
1836
- if (onRecordError === 'skip') {
1837
- return succeed<ReadonlyArray<IIndexedMemoryRecord>>(mapSuccess(results).orDefault([]).flat());
1838
- }
1839
- return mapResults(results).onSuccess((perChild) =>
1840
- succeed<ReadonlyArray<IIndexedMemoryRecord>>(perChild.flat())
1841
- );
1842
- });
1843
- }
1844
-
1845
- /**
1846
- * Load and verify one record file. On failure in `'skip'` mode, records the
1847
- * structured {@link ISkippedRecord} identity (path + scope + path-tagged
1848
- * error) and logs the skip at `warn`; the failure is still returned so the
1849
- * caller's `mapSuccess` drops it from the loaded set. In `'fail'` mode the
1850
- * failure passes through untouched so the historical error is byte-identical.
1851
- */
1852
- private _loadRecordFile(
1853
- scope: MemoryScopeKey,
1854
- child: FileTree.IFileTreeFileItem,
1855
- onRecordError: MemoryRecordErrorMode
1856
- ): Result<ReadonlyArray<IIndexedMemoryRecord>> {
1857
- return child
1858
- .getRawContents()
1859
- .onSuccess((raw) => parseMemoryFile(raw, this._registry))
1860
- .onSuccess((parsedRecord) => this._verifyLoaded(scope, child, parsedRecord))
1861
- .onSuccess((verified) => succeed<ReadonlyArray<IIndexedMemoryRecord>>([{ scope, record: verified }]))
1862
- .onFailure((message) => {
1863
- if (onRecordError === 'skip') {
1864
- const path: string = `${scope}/${child.name}`;
1865
- const error: string = `memory record '${path}': ${message}`;
1866
- this._skippedRecords.push({ path, scope, error });
1867
- this._warnSwallowed(error);
1868
- }
1869
- return fail(message);
1870
- });
1871
- }
1872
- }