@fgv/ts-agent-memory 5.1.0-36

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 (331) hide show
  1. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +9 -0
  2. package/.rush/temp/f6a88bfdd66517ccb98c2c7ae1be6e6fe9e15d38.tar.log +282 -0
  3. package/.rush/temp/operation/build/all.log +9 -0
  4. package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
  5. package/.rush/temp/operation/build/state.json +3 -0
  6. package/.rush/temp/shrinkwrap-deps.json +688 -0
  7. package/LICENSE +21 -0
  8. package/README.md +45 -0
  9. package/config/api-extractor.json +343 -0
  10. package/config/jest.config.json +14 -0
  11. package/config/rig.json +4 -0
  12. package/dist/index.js +12 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/packlets/converters/bodyConverterRegistry.js +51 -0
  15. package/dist/packlets/converters/bodyConverterRegistry.js.map +1 -0
  16. package/dist/packlets/converters/envelopeConverter.js +159 -0
  17. package/dist/packlets/converters/envelopeConverter.js.map +1 -0
  18. package/dist/packlets/converters/index.js +7 -0
  19. package/dist/packlets/converters/index.js.map +1 -0
  20. package/dist/packlets/index/index.js +6 -0
  21. package/dist/packlets/index/index.js.map +1 -0
  22. package/dist/packlets/index/memoryIndex.js +170 -0
  23. package/dist/packlets/index/memoryIndex.js.map +1 -0
  24. package/dist/packlets/observe/index.js +7 -0
  25. package/dist/packlets/observe/index.js.map +1 -0
  26. package/dist/packlets/observe/memoryObservationStore.js +119 -0
  27. package/dist/packlets/observe/memoryObservationStore.js.map +1 -0
  28. package/dist/packlets/observe/observer.js +6 -0
  29. package/dist/packlets/observe/observer.js.map +1 -0
  30. package/dist/packlets/retrieve/hybridRetriever.js +135 -0
  31. package/dist/packlets/retrieve/hybridRetriever.js.map +1 -0
  32. package/dist/packlets/retrieve/index.js +12 -0
  33. package/dist/packlets/retrieve/index.js.map +1 -0
  34. package/dist/packlets/retrieve/linkTraversalRetriever.js +143 -0
  35. package/dist/packlets/retrieve/linkTraversalRetriever.js.map +1 -0
  36. package/dist/packlets/retrieve/recencyRetriever.js +33 -0
  37. package/dist/packlets/retrieve/recencyRetriever.js.map +1 -0
  38. package/dist/packlets/retrieve/retriever.js +110 -0
  39. package/dist/packlets/retrieve/retriever.js.map +1 -0
  40. package/dist/packlets/retrieve/semanticRetriever.js +86 -0
  41. package/dist/packlets/retrieve/semanticRetriever.js.map +1 -0
  42. package/dist/packlets/retrieve/structuredFilterRetriever.js +37 -0
  43. package/dist/packlets/retrieve/structuredFilterRetriever.js.map +1 -0
  44. package/dist/packlets/retrieve/tagRetriever.js +37 -0
  45. package/dist/packlets/retrieve/tagRetriever.js.map +1 -0
  46. package/dist/packlets/store/fileTreeMemoryStore.js +698 -0
  47. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -0
  48. package/dist/packlets/store/index.js +7 -0
  49. package/dist/packlets/store/index.js.map +1 -0
  50. package/dist/packlets/store/scopeEncoding.js +31 -0
  51. package/dist/packlets/store/scopeEncoding.js.map +1 -0
  52. package/dist/packlets/types/envelope.js +6 -0
  53. package/dist/packlets/types/envelope.js.map +1 -0
  54. package/dist/packlets/types/filenameSafety.js +52 -0
  55. package/dist/packlets/types/filenameSafety.js.map +1 -0
  56. package/dist/packlets/types/identityCodec.js +184 -0
  57. package/dist/packlets/types/identityCodec.js.map +1 -0
  58. package/dist/packlets/types/ids.js +67 -0
  59. package/dist/packlets/types/ids.js.map +1 -0
  60. package/dist/packlets/types/index.js +10 -0
  61. package/dist/packlets/types/index.js.map +1 -0
  62. package/dist/packlets/types/writePolicy.js +263 -0
  63. package/dist/packlets/types/writePolicy.js.map +1 -0
  64. package/dist/packlets/vector/inMemoryCosineIndex.js +150 -0
  65. package/dist/packlets/vector/inMemoryCosineIndex.js.map +1 -0
  66. package/dist/packlets/vector/index.js +7 -0
  67. package/dist/packlets/vector/index.js.map +1 -0
  68. package/dist/packlets/vector/vectorIndex.js +6 -0
  69. package/dist/packlets/vector/vectorIndex.js.map +1 -0
  70. package/dist/test/unit/converters/bodyConverterRegistry.test.js +72 -0
  71. package/dist/test/unit/converters/bodyConverterRegistry.test.js.map +1 -0
  72. package/dist/test/unit/converters/envelopeConverter.test.js +196 -0
  73. package/dist/test/unit/converters/envelopeConverter.test.js.map +1 -0
  74. package/dist/test/unit/index/memoryIndex.test.js +152 -0
  75. package/dist/test/unit/index/memoryIndex.test.js.map +1 -0
  76. package/dist/test/unit/observe/memoryObservationStore.test.js +118 -0
  77. package/dist/test/unit/observe/memoryObservationStore.test.js.map +1 -0
  78. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js +182 -0
  79. package/dist/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -0
  80. package/dist/test/unit/retrieve/retrievers.test.js +506 -0
  81. package/dist/test/unit/retrieve/retrievers.test.js.map +1 -0
  82. package/dist/test/unit/store/embedOnWrite.test.js +260 -0
  83. package/dist/test/unit/store/embedOnWrite.test.js.map +1 -0
  84. package/dist/test/unit/store/fileTreeMemoryStore.test.js +647 -0
  85. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -0
  86. package/dist/test/unit/store/observations.test.js +239 -0
  87. package/dist/test/unit/store/observations.test.js.map +1 -0
  88. package/dist/test/unit/store/scopeEncoding.test.js +24 -0
  89. package/dist/test/unit/store/scopeEncoding.test.js.map +1 -0
  90. package/dist/test/unit/types/identityCodec.test.js +187 -0
  91. package/dist/test/unit/types/identityCodec.test.js.map +1 -0
  92. package/dist/test/unit/types/ids.test.js +84 -0
  93. package/dist/test/unit/types/ids.test.js.map +1 -0
  94. package/dist/test/unit/types/writePolicy.test.js +241 -0
  95. package/dist/test/unit/types/writePolicy.test.js.map +1 -0
  96. package/dist/test/unit/vector/inMemoryCosineIndex.test.js +192 -0
  97. package/dist/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -0
  98. package/dist/test/unit/vector/vectorIndex.test.js +42 -0
  99. package/dist/test/unit/vector/vectorIndex.test.js.map +1 -0
  100. package/dist/ts-agent-memory.d.ts +1901 -0
  101. package/dist/tsdoc-metadata.json +11 -0
  102. package/eslint.config.js +15 -0
  103. package/etc/ts-agent-memory.api.md +525 -0
  104. package/lib/index.d.ts +8 -0
  105. package/lib/index.d.ts.map +1 -0
  106. package/lib/index.js +28 -0
  107. package/lib/index.js.map +1 -0
  108. package/lib/packlets/converters/bodyConverterRegistry.d.ts +64 -0
  109. package/lib/packlets/converters/bodyConverterRegistry.d.ts.map +1 -0
  110. package/lib/packlets/converters/bodyConverterRegistry.js +55 -0
  111. package/lib/packlets/converters/bodyConverterRegistry.js.map +1 -0
  112. package/lib/packlets/converters/envelopeConverter.d.ts +70 -0
  113. package/lib/packlets/converters/envelopeConverter.d.ts.map +1 -0
  114. package/lib/packlets/converters/envelopeConverter.js +166 -0
  115. package/lib/packlets/converters/envelopeConverter.js.map +1 -0
  116. package/lib/packlets/converters/index.d.ts +3 -0
  117. package/lib/packlets/converters/index.d.ts.map +1 -0
  118. package/lib/packlets/converters/index.js +23 -0
  119. package/lib/packlets/converters/index.js.map +1 -0
  120. package/lib/packlets/index/index.d.ts +2 -0
  121. package/lib/packlets/index/index.d.ts.map +1 -0
  122. package/lib/packlets/index/index.js +22 -0
  123. package/lib/packlets/index/index.js.map +1 -0
  124. package/lib/packlets/index/memoryIndex.d.ts +127 -0
  125. package/lib/packlets/index/memoryIndex.d.ts.map +1 -0
  126. package/lib/packlets/index/memoryIndex.js +174 -0
  127. package/lib/packlets/index/memoryIndex.js.map +1 -0
  128. package/lib/packlets/observe/index.d.ts +3 -0
  129. package/lib/packlets/observe/index.d.ts.map +1 -0
  130. package/lib/packlets/observe/index.js +23 -0
  131. package/lib/packlets/observe/index.js.map +1 -0
  132. package/lib/packlets/observe/memoryObservationStore.d.ts +91 -0
  133. package/lib/packlets/observe/memoryObservationStore.d.ts.map +1 -0
  134. package/lib/packlets/observe/memoryObservationStore.js +123 -0
  135. package/lib/packlets/observe/memoryObservationStore.js.map +1 -0
  136. package/lib/packlets/observe/observer.d.ts +110 -0
  137. package/lib/packlets/observe/observer.d.ts.map +1 -0
  138. package/lib/packlets/observe/observer.js +7 -0
  139. package/lib/packlets/observe/observer.js.map +1 -0
  140. package/lib/packlets/retrieve/hybridRetriever.d.ts +79 -0
  141. package/lib/packlets/retrieve/hybridRetriever.d.ts.map +1 -0
  142. package/lib/packlets/retrieve/hybridRetriever.js +140 -0
  143. package/lib/packlets/retrieve/hybridRetriever.js.map +1 -0
  144. package/lib/packlets/retrieve/index.d.ts +8 -0
  145. package/lib/packlets/retrieve/index.d.ts.map +1 -0
  146. package/lib/packlets/retrieve/index.js +28 -0
  147. package/lib/packlets/retrieve/index.js.map +1 -0
  148. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts +61 -0
  149. package/lib/packlets/retrieve/linkTraversalRetriever.d.ts.map +1 -0
  150. package/lib/packlets/retrieve/linkTraversalRetriever.js +147 -0
  151. package/lib/packlets/retrieve/linkTraversalRetriever.js.map +1 -0
  152. package/lib/packlets/retrieve/recencyRetriever.d.ts +21 -0
  153. package/lib/packlets/retrieve/recencyRetriever.d.ts.map +1 -0
  154. package/lib/packlets/retrieve/recencyRetriever.js +37 -0
  155. package/lib/packlets/retrieve/recencyRetriever.js.map +1 -0
  156. package/lib/packlets/retrieve/retriever.d.ts +133 -0
  157. package/lib/packlets/retrieve/retriever.d.ts.map +1 -0
  158. package/lib/packlets/retrieve/retriever.js +119 -0
  159. package/lib/packlets/retrieve/retriever.js.map +1 -0
  160. package/lib/packlets/retrieve/semanticRetriever.d.ts +69 -0
  161. package/lib/packlets/retrieve/semanticRetriever.d.ts.map +1 -0
  162. package/lib/packlets/retrieve/semanticRetriever.js +90 -0
  163. package/lib/packlets/retrieve/semanticRetriever.js.map +1 -0
  164. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts +22 -0
  165. package/lib/packlets/retrieve/structuredFilterRetriever.d.ts.map +1 -0
  166. package/lib/packlets/retrieve/structuredFilterRetriever.js +41 -0
  167. package/lib/packlets/retrieve/structuredFilterRetriever.js.map +1 -0
  168. package/lib/packlets/retrieve/tagRetriever.d.ts +22 -0
  169. package/lib/packlets/retrieve/tagRetriever.d.ts.map +1 -0
  170. package/lib/packlets/retrieve/tagRetriever.js +41 -0
  171. package/lib/packlets/retrieve/tagRetriever.js.map +1 -0
  172. package/lib/packlets/store/fileTreeMemoryStore.d.ts +327 -0
  173. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -0
  174. package/lib/packlets/store/fileTreeMemoryStore.js +702 -0
  175. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -0
  176. package/lib/packlets/store/index.d.ts +3 -0
  177. package/lib/packlets/store/index.d.ts.map +1 -0
  178. package/lib/packlets/store/index.js +23 -0
  179. package/lib/packlets/store/index.js.map +1 -0
  180. package/lib/packlets/store/scopeEncoding.d.ts +19 -0
  181. package/lib/packlets/store/scopeEncoding.d.ts.map +1 -0
  182. package/lib/packlets/store/scopeEncoding.js +34 -0
  183. package/lib/packlets/store/scopeEncoding.js.map +1 -0
  184. package/lib/packlets/types/envelope.d.ts +119 -0
  185. package/lib/packlets/types/envelope.d.ts.map +1 -0
  186. package/lib/packlets/types/envelope.js +7 -0
  187. package/lib/packlets/types/envelope.js.map +1 -0
  188. package/lib/packlets/types/filenameSafety.d.ts +16 -0
  189. package/lib/packlets/types/filenameSafety.d.ts.map +1 -0
  190. package/lib/packlets/types/filenameSafety.js +55 -0
  191. package/lib/packlets/types/filenameSafety.js.map +1 -0
  192. package/lib/packlets/types/identityCodec.d.ts +136 -0
  193. package/lib/packlets/types/identityCodec.d.ts.map +1 -0
  194. package/lib/packlets/types/identityCodec.js +190 -0
  195. package/lib/packlets/types/identityCodec.js.map +1 -0
  196. package/lib/packlets/types/ids.d.ts +55 -0
  197. package/lib/packlets/types/ids.d.ts.map +1 -0
  198. package/lib/packlets/types/ids.js +70 -0
  199. package/lib/packlets/types/ids.js.map +1 -0
  200. package/lib/packlets/types/index.d.ts +6 -0
  201. package/lib/packlets/types/index.d.ts.map +1 -0
  202. package/lib/packlets/types/index.js +26 -0
  203. package/lib/packlets/types/index.js.map +1 -0
  204. package/lib/packlets/types/writePolicy.d.ts +213 -0
  205. package/lib/packlets/types/writePolicy.d.ts.map +1 -0
  206. package/lib/packlets/types/writePolicy.js +268 -0
  207. package/lib/packlets/types/writePolicy.js.map +1 -0
  208. package/lib/packlets/vector/inMemoryCosineIndex.d.ts +69 -0
  209. package/lib/packlets/vector/inMemoryCosineIndex.d.ts.map +1 -0
  210. package/lib/packlets/vector/inMemoryCosineIndex.js +154 -0
  211. package/lib/packlets/vector/inMemoryCosineIndex.js.map +1 -0
  212. package/lib/packlets/vector/index.d.ts +3 -0
  213. package/lib/packlets/vector/index.d.ts.map +1 -0
  214. package/lib/packlets/vector/index.js +23 -0
  215. package/lib/packlets/vector/index.js.map +1 -0
  216. package/lib/packlets/vector/vectorIndex.d.ts +68 -0
  217. package/lib/packlets/vector/vectorIndex.d.ts.map +1 -0
  218. package/lib/packlets/vector/vectorIndex.js +7 -0
  219. package/lib/packlets/vector/vectorIndex.js.map +1 -0
  220. package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts +2 -0
  221. package/lib/test/unit/converters/bodyConverterRegistry.test.d.ts.map +1 -0
  222. package/lib/test/unit/converters/bodyConverterRegistry.test.js +74 -0
  223. package/lib/test/unit/converters/bodyConverterRegistry.test.js.map +1 -0
  224. package/lib/test/unit/converters/envelopeConverter.test.d.ts +2 -0
  225. package/lib/test/unit/converters/envelopeConverter.test.d.ts.map +1 -0
  226. package/lib/test/unit/converters/envelopeConverter.test.js +198 -0
  227. package/lib/test/unit/converters/envelopeConverter.test.js.map +1 -0
  228. package/lib/test/unit/index/memoryIndex.test.d.ts +2 -0
  229. package/lib/test/unit/index/memoryIndex.test.d.ts.map +1 -0
  230. package/lib/test/unit/index/memoryIndex.test.js +154 -0
  231. package/lib/test/unit/index/memoryIndex.test.js.map +1 -0
  232. package/lib/test/unit/observe/memoryObservationStore.test.d.ts +2 -0
  233. package/lib/test/unit/observe/memoryObservationStore.test.d.ts.map +1 -0
  234. package/lib/test/unit/observe/memoryObservationStore.test.js +120 -0
  235. package/lib/test/unit/observe/memoryObservationStore.test.js.map +1 -0
  236. package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts +2 -0
  237. package/lib/test/unit/retrieve/linkTraversalRetriever.test.d.ts.map +1 -0
  238. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js +184 -0
  239. package/lib/test/unit/retrieve/linkTraversalRetriever.test.js.map +1 -0
  240. package/lib/test/unit/retrieve/retrievers.test.d.ts +2 -0
  241. package/lib/test/unit/retrieve/retrievers.test.d.ts.map +1 -0
  242. package/lib/test/unit/retrieve/retrievers.test.js +508 -0
  243. package/lib/test/unit/retrieve/retrievers.test.js.map +1 -0
  244. package/lib/test/unit/store/embedOnWrite.test.d.ts +2 -0
  245. package/lib/test/unit/store/embedOnWrite.test.d.ts.map +1 -0
  246. package/lib/test/unit/store/embedOnWrite.test.js +262 -0
  247. package/lib/test/unit/store/embedOnWrite.test.js.map +1 -0
  248. package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts +2 -0
  249. package/lib/test/unit/store/fileTreeMemoryStore.test.d.ts.map +1 -0
  250. package/lib/test/unit/store/fileTreeMemoryStore.test.js +649 -0
  251. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -0
  252. package/lib/test/unit/store/observations.test.d.ts +2 -0
  253. package/lib/test/unit/store/observations.test.d.ts.map +1 -0
  254. package/lib/test/unit/store/observations.test.js +241 -0
  255. package/lib/test/unit/store/observations.test.js.map +1 -0
  256. package/lib/test/unit/store/scopeEncoding.test.d.ts +2 -0
  257. package/lib/test/unit/store/scopeEncoding.test.d.ts.map +1 -0
  258. package/lib/test/unit/store/scopeEncoding.test.js +26 -0
  259. package/lib/test/unit/store/scopeEncoding.test.js.map +1 -0
  260. package/lib/test/unit/types/identityCodec.test.d.ts +2 -0
  261. package/lib/test/unit/types/identityCodec.test.d.ts.map +1 -0
  262. package/lib/test/unit/types/identityCodec.test.js +189 -0
  263. package/lib/test/unit/types/identityCodec.test.js.map +1 -0
  264. package/lib/test/unit/types/ids.test.d.ts +2 -0
  265. package/lib/test/unit/types/ids.test.d.ts.map +1 -0
  266. package/lib/test/unit/types/ids.test.js +86 -0
  267. package/lib/test/unit/types/ids.test.js.map +1 -0
  268. package/lib/test/unit/types/writePolicy.test.d.ts +2 -0
  269. package/lib/test/unit/types/writePolicy.test.d.ts.map +1 -0
  270. package/lib/test/unit/types/writePolicy.test.js +243 -0
  271. package/lib/test/unit/types/writePolicy.test.js.map +1 -0
  272. package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts +2 -0
  273. package/lib/test/unit/vector/inMemoryCosineIndex.test.d.ts.map +1 -0
  274. package/lib/test/unit/vector/inMemoryCosineIndex.test.js +194 -0
  275. package/lib/test/unit/vector/inMemoryCosineIndex.test.js.map +1 -0
  276. package/lib/test/unit/vector/vectorIndex.test.d.ts +2 -0
  277. package/lib/test/unit/vector/vectorIndex.test.d.ts.map +1 -0
  278. package/lib/test/unit/vector/vectorIndex.test.js +44 -0
  279. package/lib/test/unit/vector/vectorIndex.test.js.map +1 -0
  280. package/package.json +81 -0
  281. package/rush-logs/ts-agent-memory.build.cache.log +3 -0
  282. package/rush-logs/ts-agent-memory.build.log +9 -0
  283. package/src/index.ts +12 -0
  284. package/src/packlets/converters/bodyConverterRegistry.ts +105 -0
  285. package/src/packlets/converters/envelopeConverter.ts +210 -0
  286. package/src/packlets/converters/index.ts +7 -0
  287. package/src/packlets/index/index.ts +6 -0
  288. package/src/packlets/index/memoryIndex.ts +268 -0
  289. package/src/packlets/observe/index.ts +7 -0
  290. package/src/packlets/observe/memoryObservationStore.ts +153 -0
  291. package/src/packlets/observe/observer.ts +119 -0
  292. package/src/packlets/retrieve/hybridRetriever.ts +181 -0
  293. package/src/packlets/retrieve/index.ts +12 -0
  294. package/src/packlets/retrieve/linkTraversalRetriever.ts +169 -0
  295. package/src/packlets/retrieve/recencyRetriever.ts +54 -0
  296. package/src/packlets/retrieve/retriever.ts +207 -0
  297. package/src/packlets/retrieve/semanticRetriever.ts +147 -0
  298. package/src/packlets/retrieve/structuredFilterRetriever.ts +58 -0
  299. package/src/packlets/retrieve/tagRetriever.ts +58 -0
  300. package/src/packlets/store/fileTreeMemoryStore.ts +1073 -0
  301. package/src/packlets/store/index.ts +7 -0
  302. package/src/packlets/store/scopeEncoding.ts +36 -0
  303. package/src/packlets/types/envelope.ts +138 -0
  304. package/src/packlets/types/filenameSafety.ts +57 -0
  305. package/src/packlets/types/identityCodec.ts +263 -0
  306. package/src/packlets/types/ids.ts +124 -0
  307. package/src/packlets/types/index.ts +10 -0
  308. package/src/packlets/types/writePolicy.ts +447 -0
  309. package/src/packlets/vector/inMemoryCosineIndex.ts +173 -0
  310. package/src/packlets/vector/index.ts +7 -0
  311. package/src/packlets/vector/vectorIndex.ts +78 -0
  312. package/src/test/unit/converters/bodyConverterRegistry.test.ts +89 -0
  313. package/src/test/unit/converters/envelopeConverter.test.ts +261 -0
  314. package/src/test/unit/index/memoryIndex.test.ts +187 -0
  315. package/src/test/unit/observe/memoryObservationStore.test.ts +158 -0
  316. package/src/test/unit/retrieve/linkTraversalRetriever.test.ts +230 -0
  317. package/src/test/unit/retrieve/retrievers.test.ts +662 -0
  318. package/src/test/unit/store/embedOnWrite.test.ts +346 -0
  319. package/src/test/unit/store/fileTreeMemoryStore.test.ts +875 -0
  320. package/src/test/unit/store/observations.test.ts +290 -0
  321. package/src/test/unit/store/scopeEncoding.test.ts +37 -0
  322. package/src/test/unit/types/identityCodec.test.ts +266 -0
  323. package/src/test/unit/types/ids.test.ts +94 -0
  324. package/src/test/unit/types/writePolicy.test.ts +325 -0
  325. package/src/test/unit/vector/inMemoryCosineIndex.test.ts +242 -0
  326. package/src/test/unit/vector/vectorIndex.test.ts +48 -0
  327. package/temp/build/lint/_eslint-5eVG3S6w.json +182 -0
  328. package/temp/build/typescript/ts_8nwakTlr.json +1 -0
  329. package/temp/ts-agent-memory.api.json +12438 -0
  330. package/temp/ts-agent-memory.api.md +525 -0
  331. package/tsconfig.json +8 -0
@@ -0,0 +1,1073 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { Hash, Logging, Result, fail, mapResults, 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
+ IIdentityCodec,
14
+ IMemoryEnvelope,
15
+ IMemoryRecord,
16
+ IProvenance,
17
+ IWritePolicy,
18
+ Kind,
19
+ KnowledgeLwwPolicy,
20
+ MemoryId,
21
+ MemoryScopeKey,
22
+ Tag
23
+ } from '../types';
24
+ import { IBodyConverterRegistry as IRegistry, parseMemoryFile, serializeMemoryFile } from '../converters';
25
+ import { IIndexedMemoryRecord, IMemoryIndex, MemoryIndex } from '../index';
26
+ import {
27
+ IMemoryObservationRecord,
28
+ IMemoryObserver,
29
+ MemoryObservationOutcome,
30
+ MemoryObservationPhase
31
+ } from '../observe';
32
+ import { IVectorIndex, MemoryEmbedder } from '../vector';
33
+ import { defaultMemoryScopeEncoding } from './scopeEncoding';
34
+
35
+ /** The on-disk extension for a memory record file. */
36
+ const MEMORY_FILE_EXTENSION: string = '.md';
37
+
38
+ /**
39
+ * Filter for {@link IMemoryStore.list}. All present fields are ANDed together.
40
+ * @public
41
+ */
42
+ export interface IMemoryStoreListFilter {
43
+ /** Restrict to records in this scope. */
44
+ readonly scope?: MemoryScopeKey;
45
+ /** Restrict to records of this kind. */
46
+ readonly kind?: Kind;
47
+ /** Restrict to records carrying this tag (exact match). */
48
+ readonly tag?: Tag;
49
+ /**
50
+ * For temporal kinds: return only records valid at this epoch ms. No-op in
51
+ * B1 (no temporal kinds wired).
52
+ */
53
+ readonly asOf?: number;
54
+ }
55
+
56
+ /**
57
+ * The writable, FileTree-backed, content-hash-deduped memory store.
58
+ * @public
59
+ */
60
+ export interface IMemoryStore {
61
+ /**
62
+ * Keyed read by entity id. Resolves `entityId` to a storage address via the
63
+ * registered {@link IIdentityCodec} for `kind`. Returns `undefined` when no
64
+ * record exists.
65
+ */
66
+ get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
67
+
68
+ /**
69
+ * Direct read by `(scope, MemoryId)`. Returns `undefined` when not found.
70
+ */
71
+ getById(scope: MemoryScopeKey, id: MemoryId): Promise<Result<IMemoryRecord<unknown> | undefined>>;
72
+
73
+ /**
74
+ * List records, filtered in-memory over the derived index.
75
+ */
76
+ list(filter?: IMemoryStoreListFilter): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>>;
77
+
78
+ /**
79
+ * Write a record. Validates the body, computes a content hash, deduplicates
80
+ * (scope-wide, before policy), applies the kind's {@link IWritePolicy}, stamps
81
+ * transaction-time metadata (`created` / `updated` / `seq` / `contentHash`),
82
+ * writes the file, and patches the index. Returns the written record — or the
83
+ * existing record unchanged on a dedup no-op.
84
+ */
85
+ put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>>;
86
+
87
+ /**
88
+ * Delete a record by `(kind, entityId)`. Non-temporal kinds physically delete
89
+ * the file. Returns the {@link MemoryId} of the deleted record.
90
+ */
91
+ delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>>;
92
+ }
93
+
94
+ /**
95
+ * Parameters for {@link FileTreeMemoryStore.create}.
96
+ * @public
97
+ */
98
+ export interface IFileTreeMemoryStoreCreateParams {
99
+ /** Root directory under which scope-encoded sub-trees live. Must be mutable. */
100
+ readonly root: FileTree.IMutableFileTreeDirectoryItem;
101
+ /** Per-kind body converter registry. Gates every body on write and read. */
102
+ readonly registry: IRegistry;
103
+ /** Per-kind write policies. Kinds without an entry use a default LWW policy. */
104
+ readonly writePolicies?: ReadonlyMap<Kind, IWritePolicy>;
105
+ /** Per-kind identity codecs. */
106
+ readonly codecs?: ReadonlyMap<Kind, IIdentityCodec>;
107
+ /** Default codec for kinds without an explicit entry. */
108
+ readonly defaultCodec?: IIdentityCodec;
109
+ /** Scope encoding. Defaults to {@link defaultMemoryScopeEncoding}. */
110
+ readonly scopeEncoding?: (scope: MemoryScopeKey) => Result<string>;
111
+ /**
112
+ * Transaction-time clock. Defaults to `Date.now`. Injectable so tests can
113
+ * make `created` / `updated` deterministic. Also stamps observation
114
+ * `timestamp`s.
115
+ */
116
+ readonly clock?: () => number;
117
+ /**
118
+ * Optional observers fired once per public `get` / `put` / `delete` call.
119
+ * Purely additive — when absent, no observation records are produced and the
120
+ * store behaves exactly as it did without this parameter. Observer errors
121
+ * never affect the store operation (swallowed, logged to {@link
122
+ * IFileTreeMemoryStoreCreateParams.logger | logger} at `warn`).
123
+ */
124
+ readonly observers?: ReadonlyArray<IMemoryObserver>;
125
+ /**
126
+ * Diagnostic logger for swallowed observer failures. Defaults to a
127
+ * `Logging.NoOpLogger`.
128
+ */
129
+ readonly logger?: Logging.ILogger;
130
+ /**
131
+ * Optional vector index for semantic recall. Wired together with
132
+ * {@link IFileTreeMemoryStoreCreateParams.embed | embed}: when both are present
133
+ * the store embeds each written record and maintains the index on
134
+ * `put` / `delete` / cap-cull eviction. Absent (or `embed` absent) → no
135
+ * embedding work happens and the store behaves exactly as it does without this
136
+ * parameter (the additive, zero-overhead-when-unwired default — mirrors the
137
+ * observer hook).
138
+ */
139
+ readonly vectorIndex?: IVectorIndex;
140
+ /**
141
+ * Optional embedder applied to each record on write, wired together with
142
+ * {@link IFileTreeMemoryStoreCreateParams.vectorIndex | vectorIndex}. The
143
+ * consumer supplies it (e.g. `callProviderEmbedding` or in-process
144
+ * transformers); the store never calls an embedding provider directly, so the
145
+ * core stays embedder-agnostic. Embedding/index maintenance is **best-effort**:
146
+ * a failed (or throwing) `embed` / `add` / `remove` is logged at `warn` via
147
+ * {@link IFileTreeMemoryStoreCreateParams.logger | logger} and the record
148
+ * operation still succeeds — the vector index is a derived view that a later
149
+ * `rebuild` reconciles, so a vector failure never fails an authoritative write.
150
+ */
151
+ readonly embed?: MemoryEmbedder;
152
+ }
153
+
154
+ /**
155
+ * The internal outcome of a locked `put`: the written (or existing, on a
156
+ * dedup no-op) record plus the {@link MemoryId}s evicted by a `cull-oldest`
157
+ * admission decision. The eviction file-deletes and index patches happen
158
+ * inside the write-lock; the `evicted` ids are surfaced so the public `put`
159
+ * can fire one `'delete'` observation per evicted record AFTER the lock
160
+ * releases — consistent with the post-op firing of the other observation
161
+ * hooks, and avoiding observer re-entrancy into the still-held write-lock.
162
+ */
163
+ interface IPutOutcome {
164
+ readonly record: IMemoryRecord<unknown>;
165
+ readonly evicted: ReadonlyArray<MemoryId>;
166
+ }
167
+
168
+ /**
169
+ * The resolved storage address + content hash for a put, produced by the
170
+ * synchronous prefix of `_putLocked` and handed to the async write tail.
171
+ */
172
+ interface IResolvedWriteAddress {
173
+ readonly scope: MemoryScopeKey;
174
+ readonly idStem: string;
175
+ readonly hash: string;
176
+ }
177
+
178
+ interface IInternalParams {
179
+ readonly root: FileTree.IMutableFileTreeDirectoryItem;
180
+ readonly registry: IRegistry;
181
+ readonly writePolicies: ReadonlyMap<Kind, IWritePolicy>;
182
+ readonly codecs: ReadonlyMap<Kind, IIdentityCodec>;
183
+ readonly defaultCodec?: IIdentityCodec;
184
+ readonly defaultPolicy: IWritePolicy;
185
+ readonly scopeEncoding: (scope: MemoryScopeKey) => Result<string>;
186
+ readonly clock: () => number;
187
+ readonly index: IMemoryIndex;
188
+ readonly observers: ReadonlyArray<IMemoryObserver>;
189
+ readonly logger: Logging.ILogger;
190
+ readonly vectorIndex?: IVectorIndex;
191
+ readonly embed?: MemoryEmbedder;
192
+ }
193
+
194
+ /**
195
+ * Flat-layout, FileTree-backed {@link IMemoryStore}. The FileTree is the source
196
+ * of truth; the {@link IMemoryIndex} is a derived in-memory view patched on
197
+ * every write. Concurrent writes are serialized through a per-instance async
198
+ * write-lock so the index and the on-disk files never interleave.
199
+ *
200
+ * @remarks
201
+ * B1 supports flat (non-versioned) layout only and string (markdown) bodies.
202
+ * A codec reporting `isVersioned: true`, or a non-string body, fails loudly —
203
+ * the versioned/temporal write path is a fast-follow.
204
+ * @public
205
+ */
206
+ export class FileTreeMemoryStore implements IMemoryStore {
207
+ private readonly _root: FileTree.IMutableFileTreeDirectoryItem;
208
+ private readonly _registry: IRegistry;
209
+ private readonly _writePolicies: ReadonlyMap<Kind, IWritePolicy>;
210
+ private readonly _codecs: ReadonlyMap<Kind, IIdentityCodec>;
211
+ private readonly _defaultCodec: IIdentityCodec | undefined;
212
+ private readonly _defaultPolicy: IWritePolicy;
213
+ private readonly _scopeEncoding: (scope: MemoryScopeKey) => Result<string>;
214
+ private readonly _clock: () => number;
215
+ private readonly _index: IMemoryIndex;
216
+ private readonly _hasher: Hash.Crc32Normalizer;
217
+ private readonly _observers: ReadonlyArray<IMemoryObserver>;
218
+ private readonly _logger: Logging.ILogger;
219
+ private readonly _vectorIndex: IVectorIndex | undefined;
220
+ private readonly _embed: MemoryEmbedder | undefined;
221
+
222
+ /** Monotonic write counter; incremented inside the write-lock on each put. */
223
+ private _seq: number;
224
+ /**
225
+ * Monotonic observation-sequence counter. A distinct authority from `_seq`
226
+ * (the envelope write counter): it numbers the audit stream so a single
227
+ * {@link MemoryObservationStore} fed by this store sees strictly increasing
228
+ * `seq`, satisfying the ring buffer's cursor contract.
229
+ */
230
+ private _observationSeq: number;
231
+ /** Tail of the write-lock promise chain that serializes mutating ops. */
232
+ private _writeTail: Promise<unknown>;
233
+
234
+ /**
235
+ * The record-level mutable-field vocabulary: maps a declared mutable field
236
+ * name to its canonical location on a record. Used to project an incoming
237
+ * record into a merge-patch on update. B1 ships the knowledge-LWW surface
238
+ * (body + envelope metadata); body-internal mutable fields (Phase-C cap-cull)
239
+ * extend this map.
240
+ */
241
+ private static readonly _mutableFieldAccessors: ReadonlyMap<
242
+ string,
243
+ (record: IMemoryRecord<unknown>) => unknown
244
+ > = new Map<string, (record: IMemoryRecord<unknown>) => unknown>([
245
+ ['body', (r) => r.body],
246
+ ['tags', (r) => r.envelope.tags],
247
+ ['links', (r) => r.envelope.links],
248
+ ['provenance', (r) => r.envelope.provenance],
249
+ ['embeddingRef', (r) => r.envelope.embeddingRef]
250
+ ]);
251
+
252
+ private constructor(params: IInternalParams) {
253
+ this._root = params.root;
254
+ this._registry = params.registry;
255
+ this._writePolicies = params.writePolicies;
256
+ this._codecs = params.codecs;
257
+ this._defaultCodec = params.defaultCodec;
258
+ this._defaultPolicy = params.defaultPolicy;
259
+ this._scopeEncoding = params.scopeEncoding;
260
+ this._clock = params.clock;
261
+ this._index = params.index;
262
+ this._hasher = new Hash.Crc32Normalizer();
263
+ this._observers = params.observers;
264
+ this._logger = params.logger;
265
+ this._vectorIndex = params.vectorIndex;
266
+ this._embed = params.embed;
267
+ this._seq = 0;
268
+ this._observationSeq = 0;
269
+ this._writeTail = Promise.resolve();
270
+ }
271
+
272
+ /**
273
+ * Family-convention factory. Builds the derived index and a default LWW
274
+ * policy, then performs an initial FileTree walk so an existing vault is
275
+ * indexed (and the `seq` counter resumes past the highest persisted `seq`).
276
+ */
277
+ public static create(params: IFileTreeMemoryStoreCreateParams): Result<FileTreeMemoryStore> {
278
+ return KnowledgeLwwPolicy.create().onSuccess((defaultPolicy) =>
279
+ MemoryIndex.create().onSuccess((index) => {
280
+ const store: FileTreeMemoryStore = new FileTreeMemoryStore({
281
+ root: params.root,
282
+ registry: params.registry,
283
+ writePolicies: params.writePolicies ?? new Map<Kind, IWritePolicy>(),
284
+ codecs: params.codecs ?? new Map<Kind, IIdentityCodec>(),
285
+ defaultCodec: params.defaultCodec,
286
+ defaultPolicy,
287
+ scopeEncoding: params.scopeEncoding ?? defaultMemoryScopeEncoding,
288
+ clock: params.clock ?? Date.now,
289
+ index,
290
+ observers: params.observers ?? [],
291
+ logger: params.logger ?? new Logging.NoOpLogger(),
292
+ vectorIndex: params.vectorIndex,
293
+ embed: params.embed
294
+ });
295
+ return store._initialIndex().onSuccess(() => succeed(store));
296
+ })
297
+ );
298
+ }
299
+
300
+ /** {@inheritDoc IMemoryStore.get} */
301
+ public async get(kind: Kind, entityId: EntityId): Promise<Result<IMemoryRecord<unknown> | undefined>> {
302
+ const result: Result<IMemoryRecord<unknown> | undefined> = this._codecFor(kind).onSuccess((codec) =>
303
+ codec.encode(entityId).onSuccess((addr) => {
304
+ if (addr.isVersioned) {
305
+ return fail(`memory get '${entityId}': versioned/temporal layout not yet supported`);
306
+ }
307
+ return this._readRecord(addr.scope, addr.idStem);
308
+ })
309
+ );
310
+ await this._fireObservation('read', kind, entityId, {
311
+ outcome: result.isSuccess() ? 'success' : 'failure',
312
+ id: result.isSuccess() ? result.value?.envelope.id : undefined,
313
+ error: result.isFailure() ? result.message : undefined
314
+ });
315
+ return result;
316
+ }
317
+
318
+ /** {@inheritDoc IMemoryStore.getById} */
319
+ public async getById(
320
+ scope: MemoryScopeKey,
321
+ id: MemoryId
322
+ ): Promise<Result<IMemoryRecord<unknown> | undefined>> {
323
+ return this._readRecord(scope, id);
324
+ }
325
+
326
+ /** {@inheritDoc IMemoryStore.list} */
327
+ public async list(filter?: IMemoryStoreListFilter): Promise<Result<ReadonlyArray<IMemoryRecord<unknown>>>> {
328
+ const matches: IMemoryRecord<unknown>[] = this._index
329
+ .entries()
330
+ .filter((entry) => {
331
+ if (filter?.scope !== undefined && entry.scope !== filter.scope) {
332
+ return false;
333
+ }
334
+ if (filter?.kind !== undefined && entry.record.envelope.kind !== filter.kind) {
335
+ return false;
336
+ }
337
+ if (filter?.tag !== undefined && !entry.record.envelope.tags.includes(filter.tag)) {
338
+ return false;
339
+ }
340
+ return true;
341
+ })
342
+ .map((entry) => entry.record);
343
+ return succeed(matches);
344
+ }
345
+
346
+ /** {@inheritDoc IMemoryStore.put} */
347
+ public async put(record: IMemoryRecord<unknown>): Promise<Result<IMemoryRecord<unknown>>> {
348
+ const result: Result<IPutOutcome> = await this._enqueue(() => this._putLocked(record));
349
+ await this._fireObservation('write', record.envelope.kind, record.envelope.entityId, {
350
+ outcome: result.isSuccess() ? 'success' : 'failure',
351
+ id: result.isSuccess() ? result.value.record.envelope.id : record.envelope.id,
352
+ provenance: record.envelope.provenance,
353
+ error: result.isFailure() ? result.message : undefined
354
+ });
355
+ // Fire one `'delete'` observation per record evicted by cap-cull. The
356
+ // evicted records are always in the same `(scope, kind)` cohort as the
357
+ // incoming record, so resolving the scope from the incoming `entityId`
358
+ // yields the evicted record's scope; the observation records the evicted
359
+ // record's own `id`. A no-op / first-write / reject path yields no
360
+ // evictions, so this loop is empty there.
361
+ if (result.isSuccess()) {
362
+ for (const evictedId of result.value.evicted) {
363
+ await this._fireObservation('delete', record.envelope.kind, record.envelope.entityId, {
364
+ outcome: 'success',
365
+ id: evictedId
366
+ });
367
+ }
368
+ }
369
+ return result.onSuccess((outcome) => succeed(outcome.record));
370
+ }
371
+
372
+ /** {@inheritDoc IMemoryStore.delete} */
373
+ public async delete(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>> {
374
+ const result: Result<MemoryId> = await this._enqueue(() => this._deleteLocked(kind, entityId));
375
+ await this._fireObservation('delete', kind, entityId, {
376
+ outcome: result.isSuccess() ? 'success' : 'failure',
377
+ id: result.isSuccess() ? result.value : undefined,
378
+ error: result.isFailure() ? result.message : undefined
379
+ });
380
+ return result;
381
+ }
382
+
383
+ /**
384
+ * Serialize a mutating task behind the write-lock. Tasks run in submission
385
+ * order; a failed task does not break the chain for subsequent ones.
386
+ */
387
+ private _enqueue<T>(task: () => Promise<Result<T>>): Promise<Result<T>> {
388
+ const result: Promise<Result<T>> = this._writeTail.then(task);
389
+ this._writeTail = result.then(
390
+ () => undefined,
391
+ () => undefined
392
+ );
393
+ return result;
394
+ }
395
+
396
+ /**
397
+ * Build and fan out one observation record for a completed op. A no-op when no
398
+ * observers are wired (the additive-default path pays nothing). The store is
399
+ * the seq authority — it mints `seq` / `timestamp` so every observer sees the
400
+ * same record. The scope is resolved best-effort via the codec.
401
+ */
402
+ private async _fireObservation(
403
+ phase: MemoryObservationPhase,
404
+ kind: Kind,
405
+ entityId: EntityId,
406
+ details: {
407
+ readonly outcome: MemoryObservationOutcome;
408
+ readonly id?: MemoryId;
409
+ readonly provenance?: IProvenance;
410
+ readonly error?: string;
411
+ }
412
+ ): Promise<void> {
413
+ if (this._observers.length === 0) {
414
+ return;
415
+ }
416
+ const record: IMemoryObservationRecord = {
417
+ seq: ++this._observationSeq,
418
+ timestamp: this._clock(),
419
+ phase,
420
+ scope: this._scopeBestEffort(kind, entityId),
421
+ id: details.id,
422
+ kind,
423
+ outcome: details.outcome,
424
+ error: details.error,
425
+ provenance: details.provenance
426
+ };
427
+ const awaited: Promise<void>[] = [];
428
+ for (const observer of this._observers) {
429
+ if (observer.fireAndForget === true) {
430
+ // Intentionally not awaited: a fire-and-forget observer must not extend
431
+ // the store op's latency. `_safeObserve` swallows internally; the
432
+ // `.catch` keeps the detached promise from being flagged as floating.
433
+ this._safeObserve(observer, record).catch(() => undefined);
434
+ } else {
435
+ awaited.push(this._safeObserve(observer, record));
436
+ }
437
+ }
438
+ await Promise.all(awaited);
439
+ }
440
+
441
+ /** Invoke one observer, swallowing any failure or throw (logged at `warn`). */
442
+ private async _safeObserve(observer: IMemoryObserver, record: IMemoryObservationRecord): Promise<void> {
443
+ try {
444
+ const observed: Result<unknown> = await observer.observe(record);
445
+ if (observed.isFailure()) {
446
+ this._warnSwallowed(`memory observer failed (swallowed): ${observed.message}`);
447
+ }
448
+ } catch (error) {
449
+ this._warnSwallowed(`memory observer threw (swallowed): ${String(error)}`);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Log a swallowed-issue warning (observer failure or best-effort vector
455
+ * maintenance), tolerating a logger that itself throws — diagnostic logging
456
+ * must never make a store op reject.
457
+ */
458
+ private _warnSwallowed(message: string): void {
459
+ try {
460
+ this._logger.warn(message);
461
+ } catch {
462
+ // Diagnostic logging must not affect the store operation.
463
+ }
464
+ }
465
+
466
+ /** Resolve a scope for an observation, best-effort (undefined when unresolvable). */
467
+ private _scopeBestEffort(kind: Kind, entityId: EntityId): MemoryScopeKey | undefined {
468
+ return this._codecFor(kind)
469
+ .onSuccess((codec) => codec.encode(entityId))
470
+ .onSuccess((addr) => succeed(addr.scope))
471
+ .orDefault();
472
+ }
473
+
474
+ private async _putLocked(record: IMemoryRecord<unknown>): Promise<Result<IPutOutcome>> {
475
+ const envelope: IMemoryEnvelope = record.envelope;
476
+ if (typeof record.body !== 'string') {
477
+ return fail(
478
+ `memory put '${envelope.id}': only string (markdown) bodies are supported (got ${typeof record.body})`
479
+ );
480
+ }
481
+ const body: string = record.body;
482
+ // Resolve the synchronous prefix (body validation → codec address → content
483
+ // hash) into a small descriptor, then bridge to the async write tail (which
484
+ // embeds on write). Keeping the sync prefix intact preserves the exact
485
+ // dedup/policy ordering of the original; only the embed step is new.
486
+ return this._registry
487
+ .convert(envelope.kind, body)
488
+ .withErrorFormat((msg) => `memory put '${envelope.id}': invalid body: ${msg}`)
489
+ .onSuccess(() => this._codecFor(envelope.kind))
490
+ .onSuccess(
491
+ (codec): Result<IResolvedWriteAddress> =>
492
+ codec.encode(envelope.entityId).onSuccess((addr): Result<IResolvedWriteAddress> => {
493
+ if (addr.isVersioned) {
494
+ return fail(`memory put '${envelope.entityId}': versioned/temporal layout not yet supported`);
495
+ }
496
+ if (envelope.id !== addr.idStem) {
497
+ return fail(
498
+ `memory put: envelope id '${envelope.id}' does not match codec-derived stem '${addr.idStem}'`
499
+ );
500
+ }
501
+ return this._contentHash(envelope.kind, body, envelope.links).onSuccess((hash) =>
502
+ succeed({ scope: addr.scope, idStem: addr.idStem, hash })
503
+ );
504
+ })
505
+ )
506
+ .thenOnSuccess((resolved) =>
507
+ this._writeResolved(record, body, resolved.scope, resolved.idStem, resolved.hash)
508
+ );
509
+ }
510
+
511
+ /**
512
+ * Run dedup → policy → stamp → embed → write for a resolved address and content
513
+ * hash. Async because the embed-on-write hook (when wired) does a network call
514
+ * or in-process inference; the whole chain runs inside the write-lock so the
515
+ * vector index, the on-disk file, and the derived index never interleave.
516
+ */
517
+ private async _writeResolved(
518
+ record: IMemoryRecord<unknown>,
519
+ body: string,
520
+ scope: MemoryScopeKey,
521
+ idStem: string,
522
+ hash: string
523
+ ): Promise<Result<IPutOutcome>> {
524
+ const policy: IWritePolicy = this._policyFor(record.envelope.kind);
525
+ const dedupScope: DedupScope = policy.dedupScope ?? DEFAULT_DEDUP_SCOPE;
526
+ // Content-hash dedup runs BEFORE policy. Its granularity is the kind's
527
+ // `dedupScope`:
528
+ // - 'content': an identical { kind, body, links } triple ANYWHERE in the
529
+ // scope (even under a different id) is a no-op (knowledge family).
530
+ // - 'entity' (default): only an identical re-put of the SAME id is a no-op;
531
+ // two distinct entities with identical content never collapse (experience
532
+ // families). The same-id check is folded into the `_readRecord` below.
533
+ // (Tags / provenance are metadata and are NOT part of the hash; see
534
+ // design-lock §2.5.)
535
+ if (dedupScope === 'content') {
536
+ const duplicate: IMemoryRecord<unknown> | undefined = this._findByContentHash(scope, hash);
537
+ if (duplicate !== undefined) {
538
+ return succeed({ record: duplicate, evicted: [] });
539
+ }
540
+ }
541
+ return this._readRecord(scope, idStem).thenOnSuccess((existing) => {
542
+ if (dedupScope === 'entity' && existing !== undefined && existing.envelope.contentHash === hash) {
543
+ // Entity-scoped dedup: an identical re-put of the same entity is a no-op.
544
+ return Promise.resolve(succeed({ record: existing, evicted: [] }));
545
+ }
546
+ // The admission cohort is the set of records the policy's cap applies to:
547
+ // every record in this scope of this kind EXCEPT the target id. On a first
548
+ // write the post-write count is `cohort.length + 1`; on an update the prior
549
+ // same-id record is excluded so the count is still `cohort.length + 1`
550
+ // (a replace, not a grow). The same-id `existing` record is threaded
551
+ // separately into `_buildRecord` for the merge-patch. Knowledge LWW ignores
552
+ // this argument, so its behavior is unchanged by the wider cohort.
553
+ const cohort: ReadonlyArray<IMemoryRecord<unknown>> = this._admissionCohort(
554
+ scope,
555
+ record.envelope.kind,
556
+ idStem
557
+ );
558
+ return policy
559
+ .admit(record, cohort)
560
+ .thenOnSuccess((decision) =>
561
+ this._admitWrite(record, body, scope, idStem, hash, policy, existing, decision)
562
+ );
563
+ });
564
+ }
565
+
566
+ /**
567
+ * Build → embed → persist → evict for an admitted write.
568
+ *
569
+ * The durable record store is authoritative; the vector index is a **derived,
570
+ * rebuildable** view, so vector maintenance is **best-effort** — a failed embed
571
+ * or `add` is logged and the durable write still succeeds (the index can be
572
+ * rebuilt via {@link InMemoryCosineIndex.rebuild}). Only genuine record-store
573
+ * failures (body/codec/policy, persist, file eviction) fail the `put`.
574
+ *
575
+ * Ordering: the embed + `add` run immediately before the single `_persist` so
576
+ * the index-returned `embeddingRef` lands in one durable write (a post-persist
577
+ * stamp would need a second write whose failure path is effectively untestable).
578
+ * Build and persist the replacement BEFORE evicting the cull-oldest cohort, so a
579
+ * later eviction failure never loses data with nothing written in its place.
580
+ */
581
+ private async _admitWrite(
582
+ record: IMemoryRecord<unknown>,
583
+ body: string,
584
+ scope: MemoryScopeKey,
585
+ idStem: string,
586
+ hash: string,
587
+ policy: IWritePolicy,
588
+ existing: IMemoryRecord<unknown> | undefined,
589
+ decision: AdmissionDecision
590
+ ): Promise<Result<IPutOutcome>> {
591
+ if (decision.decision === 'reject') {
592
+ return fail(`memory put: rejected by policy: ${decision.reason}`);
593
+ }
594
+ return this._buildRecord(record, body, existing, policy, hash)
595
+ .thenOnSuccess((built) => this._embedOnWrite(built))
596
+ .onSuccess((embeddedBuilt) => this._persist(embeddedBuilt, scope, idStem))
597
+ .thenOnSuccess(async (persisted) => {
598
+ // Everything after the authoritative `_persist` commit is best-effort and
599
+ // never turns a committed write into a `Failure`:
600
+ // - a cull-oldest eviction that fails is logged (the per-(scope,kind)
601
+ // cap may be transiently exceeded; the next write's admission restores
602
+ // it), and only the successfully-evicted ids flow onward;
603
+ // - vector pruning of the evicted cohort is likewise best-effort.
604
+ const evicted: ReadonlyArray<MemoryId> = this._applyEvictions(decision, scope);
605
+ await this._removeEvictedVectors(evicted);
606
+ return succeed({ record: persisted, evicted });
607
+ });
608
+ }
609
+
610
+ /**
611
+ * Best-effort embed-on-write. When a vector index AND an embedder are wired,
612
+ * embeds the built record, `add`s the vector (replace semantics handle a same-id
613
+ * re-embed — no explicit remove), and stamps the returned `embeddingRef`. A
614
+ * failure (returned `fail` OR a thrown/rejected hook) is logged and the
615
+ * unembedded record is returned unchanged — the put still persists, and the
616
+ * derived index is reconciled by a later `rebuild`. A pass-through no-op when
617
+ * unwired (byte-identical record).
618
+ *
619
+ * Always succeeds (`Result` is the chain's shape, never a vector-induced
620
+ * failure).
621
+ */
622
+ private async _embedOnWrite(built: IMemoryRecord<string>): Promise<Result<IMemoryRecord<string>>> {
623
+ if (this._vectorIndex === undefined || this._embed === undefined) {
624
+ return succeed(built);
625
+ }
626
+ const vectorIndex: IVectorIndex = this._vectorIndex;
627
+ const embed: MemoryEmbedder = this._embed;
628
+ const embedded: Result<Float32Array> = await this._tryVectorOp(
629
+ () => embed(built),
630
+ `embedding '${built.envelope.id}'`
631
+ );
632
+ if (embedded.isFailure()) {
633
+ return succeed(built);
634
+ }
635
+ const added: Result<string> = await this._tryVectorOp(
636
+ () => vectorIndex.add(built.envelope.id, embedded.value),
637
+ `vector add for '${built.envelope.id}'`
638
+ );
639
+ if (added.isFailure()) {
640
+ return succeed(built);
641
+ }
642
+ return succeed({ envelope: { ...built.envelope, embeddingRef: added.value }, body: built.body });
643
+ }
644
+
645
+ /**
646
+ * Evict the records named by a `cull-oldest` decision, best-effort. Runs only
647
+ * after the authoritative `_persist`, so a failed eviction is logged (never
648
+ * fatal) and the cap self-corrects on the next admission. Returns the ids that
649
+ * were actually evicted (so observations / vector pruning cover only those).
650
+ * `accept` / `reject` decisions evict nothing.
651
+ */
652
+ private _applyEvictions(decision: AdmissionDecision, scope: MemoryScopeKey): ReadonlyArray<MemoryId> {
653
+ if (decision.decision !== 'cull-oldest') {
654
+ return [];
655
+ }
656
+ const evicted: MemoryId[] = [];
657
+ for (const id of decision.evict) {
658
+ const result: Result<MemoryId> = this._evict(scope, id);
659
+ if (result.isSuccess()) {
660
+ evicted.push(result.value);
661
+ } else {
662
+ this._warnSwallowed(
663
+ `memory put: best-effort eviction of '${id}' failed (cap may be transiently exceeded; restored on the next write): ${result.message}`
664
+ );
665
+ }
666
+ }
667
+ return evicted;
668
+ }
669
+
670
+ /** Best-effort vector removal for each evicted record (never fails the put). */
671
+ private async _removeEvictedVectors(evicted: ReadonlyArray<MemoryId>): Promise<void> {
672
+ for (const id of evicted) {
673
+ await this._removeVectorBestEffort(id);
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Run a consumer-supplied vector hook, normalizing a thrown/rejected hook into a
679
+ * `Failure` and logging any failure at `warn`. Best-effort: the caller proceeds
680
+ * regardless, since the index is rebuildable.
681
+ */
682
+ private async _tryVectorOp<T>(op: () => Promise<Result<T>>, label: string): Promise<Result<T>> {
683
+ let result: Result<T>;
684
+ try {
685
+ result = await op();
686
+ } catch (err) {
687
+ result = fail(`${label} threw: ${String(err)}`);
688
+ }
689
+ if (result.isFailure()) {
690
+ this._warnSwallowed(
691
+ `memory: ${label} failed (best-effort; vector index left for rebuild): ${result.message}`
692
+ );
693
+ }
694
+ return result;
695
+ }
696
+
697
+ /**
698
+ * Best-effort vector removal. A no-op unless the full vector lifecycle is wired
699
+ * (both an index AND an embedder), so an unwired store does no vector work and
700
+ * behaves byte-identically. Failures are logged, never surfaced — a committed
701
+ * delete/eviction must not fail because a derived index could not be pruned.
702
+ */
703
+ private async _removeVectorBestEffort(id: MemoryId): Promise<void> {
704
+ if (this._vectorIndex === undefined || this._embed === undefined) {
705
+ return;
706
+ }
707
+ const vectorIndex: IVectorIndex = this._vectorIndex;
708
+ await this._tryVectorOp(() => vectorIndex.remove(id), `vector removal for '${id}'`);
709
+ }
710
+
711
+ /**
712
+ * Build the record to persist. On a first write the incoming envelope is the
713
+ * base (final content equals the incoming content, so the dedup `hash` is
714
+ * reused). On an update the incoming record's mutable fields are projected into
715
+ * a merge-patch and the policy's `applyUpdate` merges them over the existing
716
+ * record (preserving `created`); the persisted body and `contentHash` are then
717
+ * taken from the policy's actual output, so a body-transforming policy is never
718
+ * bypassed and the stored hash always matches the stored `{ kind, body, links }`.
719
+ * The store stamps the transaction-time metadata it owns
720
+ * (`created` / `updated` / `seq` / `contentHash`).
721
+ */
722
+ private _buildRecord(
723
+ incoming: IMemoryRecord<unknown>,
724
+ body: string,
725
+ existing: IMemoryRecord<unknown> | undefined,
726
+ policy: IWritePolicy,
727
+ hash: string
728
+ ): Result<IMemoryRecord<string>> {
729
+ const now: number = this._clock();
730
+ const seq: number = ++this._seq;
731
+ if (existing === undefined) {
732
+ return succeed({
733
+ envelope: { ...incoming.envelope, created: now, updated: now, seq, contentHash: hash },
734
+ body
735
+ });
736
+ }
737
+ const patch: Record<string, unknown> = this._projectMutablePatch(incoming, policy.mutableFields);
738
+ return policy
739
+ .applyUpdate(existing, patch)
740
+ .withErrorFormat((msg) => `memory put '${incoming.envelope.id}': update failed: ${msg}`)
741
+ .onSuccess((updated) => {
742
+ if (typeof updated.body !== 'string') {
743
+ return fail(
744
+ `memory put '${incoming.envelope.id}': policy returned a non-string body (${typeof updated.body})`
745
+ );
746
+ }
747
+ const finalBody: string = updated.body;
748
+ return this._contentHash(updated.envelope.kind, finalBody, updated.envelope.links).onSuccess(
749
+ (finalHash) =>
750
+ succeed({
751
+ envelope: {
752
+ ...updated.envelope,
753
+ created: existing.envelope.created,
754
+ updated: now,
755
+ seq,
756
+ contentHash: finalHash
757
+ },
758
+ body: finalBody
759
+ })
760
+ );
761
+ });
762
+ }
763
+
764
+ /** Serialize and write a fully-stamped record, then patch the index. */
765
+ private _persist(
766
+ record: IMemoryRecord<string>,
767
+ scope: MemoryScopeKey,
768
+ idStem: string
769
+ ): Result<IMemoryRecord<unknown>> {
770
+ return serializeMemoryFile(record.envelope, record.body)
771
+ .onSuccess((raw) => this._writeFile(scope, idStem, raw))
772
+ .onSuccess(() => this._index.patch('put', { scope, record }))
773
+ .onSuccess(() => succeed(record));
774
+ }
775
+
776
+ private async _deleteLocked(kind: Kind, entityId: EntityId): Promise<Result<MemoryId>> {
777
+ return this._codecFor(kind)
778
+ .onSuccess((codec) => codec.encode(entityId))
779
+ .thenOnSuccess((addr) => {
780
+ if (addr.isVersioned) {
781
+ return Promise.resolve(
782
+ fail<MemoryId>(`memory delete '${entityId}': versioned/temporal layout not yet supported`)
783
+ );
784
+ }
785
+ return this._readRecord(addr.scope, addr.idStem).thenOnSuccess((existing) => {
786
+ if (existing === undefined) {
787
+ return Promise.resolve(fail<MemoryId>(`memory delete '${entityId}': no record found`));
788
+ }
789
+ // Delete the record file + index entry (authoritative), then prune the
790
+ // vector best-effort: a committed delete must not fail because the
791
+ // derived index could not be pruned.
792
+ return this._deleteFile(addr.scope, addr.idStem)
793
+ .onSuccess(() => this._index.patch('delete', { scope: addr.scope, record: existing }))
794
+ .thenOnSuccess(async () => {
795
+ await this._removeVectorBestEffort(existing.envelope.id);
796
+ return succeed(existing.envelope.id);
797
+ });
798
+ });
799
+ });
800
+ }
801
+
802
+ /** Evict (physically delete) a single record file by id, patching the index. */
803
+ private _evict(scope: MemoryScopeKey, id: MemoryId): Result<MemoryId> {
804
+ return this._readRecord(scope, id).onSuccess((existing) => {
805
+ if (existing === undefined) {
806
+ return fail(`memory put: cannot evict '${id}' in scope '${scope}': not found`);
807
+ }
808
+ return this._deleteFile(scope, id)
809
+ .onSuccess(() => this._index.patch('delete', { scope, record: existing }))
810
+ .onSuccess(() => succeed(id));
811
+ });
812
+ }
813
+
814
+ /** Project the incoming record's mutable fields into a merge-patch. */
815
+ private _projectMutablePatch(
816
+ record: IMemoryRecord<unknown>,
817
+ mutableFields: ReadonlyArray<string>
818
+ ): Record<string, unknown> {
819
+ const patch: Record<string, unknown> = {};
820
+ for (const field of mutableFields) {
821
+ const accessor: ((record: IMemoryRecord<unknown>) => unknown) | undefined =
822
+ FileTreeMemoryStore._mutableFieldAccessors.get(field);
823
+ if (accessor !== undefined) {
824
+ const value: unknown = accessor(record);
825
+ if (value !== undefined) {
826
+ patch[field] = value;
827
+ }
828
+ }
829
+ }
830
+ return patch;
831
+ }
832
+
833
+ /**
834
+ * The admission cohort for a write: every indexed record in `scope` of `kind`
835
+ * except the one at `idStem` (the record being written or updated). This is
836
+ * the set a per-kind cap (e.g. {@link MemoryCapCullPolicy}) counts against, so
837
+ * a bounded-ring policy can keep a per-scope/per-kind family within
838
+ * `maxRecords`. Excluding the target id makes the post-write count uniform
839
+ * across first-writes and updates.
840
+ */
841
+ private _admissionCohort(
842
+ scope: MemoryScopeKey,
843
+ kind: Kind,
844
+ idStem: string
845
+ ): ReadonlyArray<IMemoryRecord<unknown>> {
846
+ return this._index
847
+ .entries()
848
+ .filter(
849
+ (entry) =>
850
+ entry.scope === scope && entry.record.envelope.kind === kind && entry.record.envelope.id !== idStem
851
+ )
852
+ .map((entry) => entry.record);
853
+ }
854
+
855
+ /** Find a record in `scope` whose `contentHash` equals `hash`, if any. */
856
+ private _findByContentHash(scope: MemoryScopeKey, hash: string): IMemoryRecord<unknown> | undefined {
857
+ const match: IIndexedMemoryRecord | undefined = this._index
858
+ .entries()
859
+ .find((entry) => entry.scope === scope && entry.record.envelope.contentHash === hash);
860
+ return match?.record;
861
+ }
862
+
863
+ private _contentHash(kind: Kind, body: string, links: IMemoryEnvelope['links']): Result<string> {
864
+ return this._hasher.computeHash({ kind, body, links });
865
+ }
866
+
867
+ private _codecFor(kind: Kind): Result<IIdentityCodec> {
868
+ const codec: IIdentityCodec | undefined = this._codecs.get(kind) ?? this._defaultCodec;
869
+ if (codec === undefined) {
870
+ return fail(`no identity codec registered for kind '${kind}'`);
871
+ }
872
+ return succeed(codec);
873
+ }
874
+
875
+ private _policyFor(kind: Kind): IWritePolicy {
876
+ return this._writePolicies.get(kind) ?? this._defaultPolicy;
877
+ }
878
+
879
+ /**
880
+ * Read and validate the record at `<scope>/<idStem>.md`, returning `undefined`
881
+ * when the scope directory or file is absent. Verifies the on-disk id ↔
882
+ * filename round-trip on every load.
883
+ */
884
+ private _readRecord(scope: MemoryScopeKey, idStem: string): Result<IMemoryRecord<unknown> | undefined> {
885
+ return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
886
+ if (scopeDir === undefined) {
887
+ return succeed(undefined);
888
+ }
889
+ return scopeDir.getChildren().onSuccess((children) => {
890
+ const targetName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;
891
+ const file: FileTree.IFileTreeFileItem | undefined = children.find(
892
+ (c): c is FileTree.IFileTreeFileItem => c.type === 'file' && c.name === targetName
893
+ );
894
+ if (file === undefined) {
895
+ return succeed(undefined);
896
+ }
897
+ return file
898
+ .getRawContents()
899
+ .onSuccess((raw) => parseMemoryFile(raw, this._registry))
900
+ .onSuccess((parsedRecord) => this._verifyLoaded(scope, file, parsedRecord));
901
+ });
902
+ });
903
+ }
904
+
905
+ /** Enforce `envelope.id === filename stem` and the codec round-trip on load. */
906
+ private _verifyLoaded(
907
+ scope: MemoryScopeKey,
908
+ file: FileTree.IFileTreeFileItem,
909
+ record: IMemoryRecord<unknown>
910
+ ): Result<IMemoryRecord<unknown>> {
911
+ if (record.envelope.id !== file.baseName) {
912
+ return fail(
913
+ `memory file '${file.absolutePath}': envelope id '${record.envelope.id}' does not match filename stem '${file.baseName}'`
914
+ );
915
+ }
916
+ return this._codecFor(record.envelope.kind)
917
+ .onSuccess((codec) => codec.verifyRoundTrip(scope, file.baseName))
918
+ .withErrorFormat((msg) => `memory file '${file.absolutePath}': ${msg}`)
919
+ .onSuccess(() => succeed(record));
920
+ }
921
+
922
+ /**
923
+ * Resolve the directory for a scope, returning `undefined` when it does not
924
+ * exist. Navigation only — does not create. Folds the path segments through
925
+ * `getChildren` so an absent segment short-circuits to `undefined`.
926
+ */
927
+ private _resolveScopeDir(scope: MemoryScopeKey): Result<FileTree.IFileTreeDirectoryItem | undefined> {
928
+ return this._scopeEncoding(scope).onSuccess((encoded) => {
929
+ const segments: string[] = encoded.split('/').filter((s) => s.length > 0);
930
+ return segments.reduce<Result<FileTree.IFileTreeDirectoryItem | undefined>>(
931
+ (acc, segment) =>
932
+ acc.onSuccess((current) => {
933
+ if (current === undefined) {
934
+ return succeed(undefined);
935
+ }
936
+ return current
937
+ .getChildren()
938
+ .onSuccess((children) =>
939
+ succeed(
940
+ children.find(
941
+ (c): c is FileTree.IFileTreeDirectoryItem => c.type === 'directory' && c.name === segment
942
+ )
943
+ )
944
+ );
945
+ }),
946
+ succeed(this._root)
947
+ );
948
+ });
949
+ }
950
+
951
+ /** Ensure the scope directory exists, creating segments as needed. */
952
+ private _ensureScopeDir(scope: MemoryScopeKey): Result<FileTree.IMutableFileTreeDirectoryItem> {
953
+ return this._scopeEncoding(scope).onSuccess((encoded) => {
954
+ const segments: string[] = encoded.split('/').filter((s) => s.length > 0);
955
+ return segments.reduce<Result<FileTree.IMutableFileTreeDirectoryItem>>(
956
+ (acc, segment) =>
957
+ acc.onSuccess((current) =>
958
+ current.getChildren().onSuccess((children) => {
959
+ const existing: FileTree.FileTreeItem | undefined = children.find(
960
+ (c) => c.type === 'directory' && c.name === segment
961
+ );
962
+ if (existing === undefined) {
963
+ return current.createChildDirectory(segment);
964
+ }
965
+ /* 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 */
966
+ if (!FileTree.isMutableDirectoryItem(existing)) {
967
+ return fail(`${existing.absolutePath}: directory is not mutable`);
968
+ }
969
+ return succeed(existing);
970
+ })
971
+ ),
972
+ succeed(this._root)
973
+ );
974
+ });
975
+ }
976
+
977
+ /** Write (create or overwrite) `<scope>/<idStem>.md` with `raw`. */
978
+ private _writeFile(scope: MemoryScopeKey, idStem: string, raw: string): Result<true> {
979
+ return this._ensureScopeDir(scope).onSuccess((scopeDir) =>
980
+ scopeDir.getChildren().onSuccess((children) => {
981
+ const fileName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;
982
+ const existing: FileTree.FileTreeItem | undefined = children.find(
983
+ (c) => c.type === 'file' && c.name === fileName
984
+ );
985
+ if (existing === undefined) {
986
+ return scopeDir.createChildFile(fileName, raw).onSuccess(() => succeed(true));
987
+ }
988
+ /* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
989
+ if (!FileTree.isMutableFileItem(existing)) {
990
+ return fail(`${existing.absolutePath}: file is not mutable`);
991
+ }
992
+ return existing.setRawContents(raw).onSuccess(() => succeed(true));
993
+ })
994
+ );
995
+ }
996
+
997
+ /**
998
+ * Physically delete `<scope>/<idStem>.md`. The scope-missing and file-missing
999
+ * guards are unreachable through the callers (`delete` / `_evict` both read the
1000
+ * record first, so the directory and file exist) but are kept so a future
1001
+ * direct caller degrades loudly rather than silently.
1002
+ */
1003
+ private _deleteFile(scope: MemoryScopeKey, idStem: string): Result<true> {
1004
+ return this._resolveScopeDir(scope).onSuccess((scopeDir) => {
1005
+ /* c8 ignore next 3 -- unreachable: callers read the record (hence the scope dir) first */
1006
+ if (scopeDir === undefined) {
1007
+ return fail(`memory delete: scope '${scope}' not found`);
1008
+ }
1009
+ const fileName: string = `${idStem}${MEMORY_FILE_EXTENSION}`;
1010
+ return scopeDir.getChildren().onSuccess((children) => {
1011
+ const file: FileTree.FileTreeItem | undefined = children.find(
1012
+ (c) => c.type === 'file' && c.name === fileName
1013
+ );
1014
+ /* c8 ignore next 3 -- unreachable: callers read the record (hence the file) first */
1015
+ if (file === undefined) {
1016
+ return fail(`memory delete: file '${fileName}' not found in scope '${scope}'`);
1017
+ }
1018
+ /* c8 ignore next 3 -- defensive: a file in a mutable tree is mutable; guards a read-only adapter */
1019
+ if (!FileTree.isMutableFileItem(file)) {
1020
+ return fail(`${file.absolutePath}: file is not mutable`);
1021
+ }
1022
+ return file.delete().onSuccess(() => succeed(true));
1023
+ });
1024
+ });
1025
+ }
1026
+
1027
+ /**
1028
+ * Walk the FileTree once and rebuild the index. Also resumes the `seq`
1029
+ * counter past the highest persisted `seq` so new writes stay monotonic.
1030
+ */
1031
+ private _initialIndex(): Result<true> {
1032
+ return this._collectEntries(this._root, []).onSuccess((entries) =>
1033
+ this._index.rebuild(entries).onSuccess(() => {
1034
+ for (const entry of entries) {
1035
+ if (entry.record.envelope.seq > this._seq) {
1036
+ this._seq = entry.record.envelope.seq;
1037
+ }
1038
+ }
1039
+ return succeed(true);
1040
+ })
1041
+ );
1042
+ }
1043
+
1044
+ /** Recursively collect every `.md` record under `dir` (scope = path segments). */
1045
+ private _collectEntries(
1046
+ dir: FileTree.IFileTreeDirectoryItem,
1047
+ scopeSegments: ReadonlyArray<string>
1048
+ ): Result<ReadonlyArray<IIndexedMemoryRecord>> {
1049
+ return dir.getChildren().onSuccess((children) => {
1050
+ const results: Result<ReadonlyArray<IIndexedMemoryRecord>>[] = children.map((child) => {
1051
+ if (child.type === 'directory') {
1052
+ return this._collectEntries(child, [...scopeSegments, child.name]);
1053
+ }
1054
+ if (!child.name.endsWith(MEMORY_FILE_EXTENSION) || scopeSegments.length === 0) {
1055
+ // Skip non-record files and any record-shaped file sitting at the root
1056
+ // (records always live under at least one scope segment).
1057
+ return succeed<ReadonlyArray<IIndexedMemoryRecord>>([]);
1058
+ }
1059
+ const scope: MemoryScopeKey = scopeSegments.join('/') as MemoryScopeKey;
1060
+ return child
1061
+ .getRawContents()
1062
+ .onSuccess((raw) => parseMemoryFile(raw, this._registry))
1063
+ .onSuccess((parsedRecord) => this._verifyLoaded(scope, child, parsedRecord))
1064
+ .onSuccess((verified) =>
1065
+ succeed<ReadonlyArray<IIndexedMemoryRecord>>([{ scope, record: verified }])
1066
+ );
1067
+ });
1068
+ return mapResults(results).onSuccess((perChild) =>
1069
+ succeed<ReadonlyArray<IIndexedMemoryRecord>>(perChild.flat())
1070
+ );
1071
+ });
1072
+ }
1073
+ }