@remnic/core 9.7.7 → 9.7.9

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 (383) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -6
  2. package/dist/access-admin-ops-surface.js +31 -29
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-boundary.d.ts +6 -6
  5. package/dist/access-boundary.js +32 -30
  6. package/dist/access-cli.js +88 -86
  7. package/dist/access-cli.js.map +1 -1
  8. package/dist/access-http.d.ts +6 -6
  9. package/dist/access-http.js +39 -37
  10. package/dist/access-identity-continuity-surface.d.ts +5 -5
  11. package/dist/access-identity-continuity-surface.js +31 -29
  12. package/dist/access-lcm-surface.d.ts +6 -6
  13. package/dist/access-lcm-surface.js +31 -29
  14. package/dist/access-mcp.d.ts +6 -6
  15. package/dist/access-mcp.js +36 -34
  16. package/dist/access-observe-write-surface.d.ts +6 -6
  17. package/dist/access-observe-write-surface.js +31 -29
  18. package/dist/access-operations-batch.js +34 -32
  19. package/dist/access-operations.d.ts +6 -6
  20. package/dist/access-operations.js +35 -33
  21. package/dist/access-recall-surface.d.ts +6 -6
  22. package/dist/access-recall-surface.js +31 -29
  23. package/dist/{access-service-CxDZXTKt.d.ts → access-service-DSuzikHQ.d.ts} +4 -4
  24. package/dist/access-service.d.ts +6 -6
  25. package/dist/access-service.js +31 -29
  26. package/dist/access-surface-catalog.d.ts +6 -6
  27. package/dist/action-confidence.d.ts +1 -1
  28. package/dist/active-memory-bridge.d.ts +1 -1
  29. package/dist/active-recall.d.ts +1 -1
  30. package/dist/active-recall.js +1 -1
  31. package/dist/adapters/index.js +4 -4
  32. package/dist/adapters/registry.js +2 -2
  33. package/dist/{auto-sync-676N6N6Y.js → auto-sync-AAMTVZMK.js} +5 -5
  34. package/dist/behavior-learner.d.ts +1 -1
  35. package/dist/behavior-signals.d.ts +1 -1
  36. package/dist/bootstrap.d.ts +5 -5
  37. package/dist/briefing.d.ts +2 -2
  38. package/dist/briefing.js +14 -12
  39. package/dist/buffer-surprise-report.d.ts +1 -1
  40. package/dist/buffer.d.ts +2 -2
  41. package/dist/bulk-import/index.d.ts +3 -3
  42. package/dist/calibration.d.ts +1 -1
  43. package/dist/calibration.js +2 -2
  44. package/dist/capabilities.d.ts +1 -1
  45. package/dist/{capsule-merge-GK5E647P.js → capsule-merge-Z2HUO6IK.js} +10 -2
  46. package/dist/capsule-merge-Z2HUO6IK.js.map +1 -0
  47. package/dist/{catalog-DzU9HowE.d.ts → catalog-BNwjACa5.d.ts} +1 -1
  48. package/dist/causal-behavior.d.ts +1 -1
  49. package/dist/causal-consolidation.d.ts +1 -1
  50. package/dist/causal-consolidation.js +15 -13
  51. package/dist/causal-consolidation.js.map +1 -1
  52. package/dist/causal-trajectory-graph.d.ts +1 -1
  53. package/dist/{chunk-TZJVTH3F.js → chunk-27VEKQQA.js} +14 -14
  54. package/dist/{chunk-JO5AT6QT.js → chunk-3ZVFLMFQ.js} +1 -1
  55. package/dist/chunk-3ZVFLMFQ.js.map +1 -0
  56. package/dist/{chunk-AD2KK5MT.js → chunk-5CFHYQU3.js} +9 -3
  57. package/dist/chunk-5CFHYQU3.js.map +1 -0
  58. package/dist/{chunk-7XH7VJN4.js → chunk-5S7Z5SHA.js} +2 -2
  59. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  60. package/dist/{chunk-EJCVMPIM.js → chunk-7LMEY5KZ.js} +15 -1
  61. package/dist/chunk-7LMEY5KZ.js.map +1 -0
  62. package/dist/{chunk-3F4N4WDH.js → chunk-AHLPZ2SF.js} +2 -2
  63. package/dist/{chunk-AH3M6F4X.js → chunk-CDGGQGUU.js} +2 -2
  64. package/dist/{chunk-ZHKD4EZM.js → chunk-CK6RI52P.js} +3 -3
  65. package/dist/{chunk-6C35R7P4.js → chunk-DJCKXI4M.js} +2 -2
  66. package/dist/{chunk-GL3SMLMZ.js → chunk-DLPXS7BE.js} +109 -85
  67. package/dist/chunk-DLPXS7BE.js.map +1 -0
  68. package/dist/{chunk-NMMKRVUF.js → chunk-EIKBKQ6L.js} +6 -6
  69. package/dist/{chunk-O3BZ6PFY.js → chunk-F3FC7SMO.js} +2 -2
  70. package/dist/{chunk-ZW6OKHVX.js → chunk-FKCJH7KP.js} +2 -2
  71. package/dist/{chunk-DQPPRXFO.js → chunk-HQ7QZJM5.js} +2 -2
  72. package/dist/{chunk-UYZUJ4EB.js → chunk-I2THZUAF.js} +2 -2
  73. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  74. package/dist/{chunk-KIPLG7S7.js → chunk-JKLJ6A7W.js} +2 -2
  75. package/dist/{chunk-HXDKC3BI.js → chunk-JU3GHO6J.js} +2 -2
  76. package/dist/{chunk-74MJH7I3.js → chunk-KGNFFCZJ.js} +12 -3
  77. package/dist/chunk-KGNFFCZJ.js.map +1 -0
  78. package/dist/{chunk-Q6JPMCPO.js → chunk-KKASNHGF.js} +62 -57
  79. package/dist/chunk-KKASNHGF.js.map +1 -0
  80. package/dist/{chunk-R2SJDIPO.js → chunk-KPXAJ77O.js} +9 -7
  81. package/dist/chunk-KPXAJ77O.js.map +1 -0
  82. package/dist/{chunk-SYNYI4ZM.js → chunk-L5HJQC36.js} +2 -2
  83. package/dist/{chunk-VXMWDXIJ.js → chunk-LC7OODWZ.js} +2 -2
  84. package/dist/{chunk-CNWC274T.js → chunk-MLT5EG6K.js} +28 -28
  85. package/dist/{chunk-M5QKGHCR.js → chunk-OC35KQIN.js} +6 -2
  86. package/dist/chunk-OC35KQIN.js.map +1 -0
  87. package/dist/chunk-OKUEZOGW.js +27 -0
  88. package/dist/chunk-OKUEZOGW.js.map +1 -0
  89. package/dist/{chunk-PKGWJFNR.js → chunk-OVMS3DCB.js} +2 -2
  90. package/dist/chunk-PDJTHRAW.js +36 -0
  91. package/dist/chunk-PDJTHRAW.js.map +1 -0
  92. package/dist/{chunk-ZUFJ7ZTR.js → chunk-PHXCPVSX.js} +6 -2
  93. package/dist/chunk-PHXCPVSX.js.map +1 -0
  94. package/dist/{chunk-57INMZ6F.js → chunk-POUD5BLV.js} +2 -2
  95. package/dist/{chunk-KTOH7EJE.js → chunk-PV7E2FGY.js} +307 -43
  96. package/dist/chunk-PV7E2FGY.js.map +1 -0
  97. package/dist/{chunk-LRGSLV7M.js → chunk-PYHOAXTI.js} +4 -4
  98. package/dist/{chunk-WKE4VXSZ.js → chunk-QKVX4Y6D.js} +2 -2
  99. package/dist/{chunk-FXGC5UUH.js → chunk-RT7IP3D3.js} +2 -2
  100. package/dist/{chunk-CHVU4RE5.js → chunk-SKXBOZDF.js} +3 -3
  101. package/dist/{chunk-TVRPRFUB.js → chunk-TY43GBX2.js} +9 -7
  102. package/dist/chunk-TY43GBX2.js.map +1 -0
  103. package/dist/{chunk-UVIQHSS7.js → chunk-UP25ZZCN.js} +9 -3
  104. package/dist/chunk-UP25ZZCN.js.map +1 -0
  105. package/dist/{chunk-GKCCHHZK.js → chunk-UXKMMNJD.js} +4 -4
  106. package/dist/{chunk-XFTPUXCE.js → chunk-UXWGPODH.js} +4 -4
  107. package/dist/{chunk-2FSHROYK.js → chunk-V53GJUSL.js} +8 -8
  108. package/dist/{chunk-LKT755LP.js → chunk-VA4WSW54.js} +2 -2
  109. package/dist/{chunk-FYKIEOG6.js → chunk-VCFADTFS.js} +3 -3
  110. package/dist/{chunk-VF4XKTX3.js → chunk-VZ5OJE4X.js} +10 -2
  111. package/dist/chunk-VZ5OJE4X.js.map +1 -0
  112. package/dist/{chunk-AWJ2FHCF.js → chunk-X47LWMKA.js} +49 -21
  113. package/dist/chunk-X47LWMKA.js.map +1 -0
  114. package/dist/{chunk-VMGMY2AI.js → chunk-XG5CGR3K.js} +8 -8
  115. package/dist/{chunk-VMGMY2AI.js.map → chunk-XG5CGR3K.js.map} +1 -1
  116. package/dist/{chunk-T7VNN3ZM.js → chunk-XVLEGMJI.js} +2 -2
  117. package/dist/{chunk-CRFTLUWT.js → chunk-Y5U35ZVS.js} +2 -2
  118. package/dist/{chunk-UHGHTOR5.js → chunk-YGAOLHKN.js} +7 -7
  119. package/dist/{chunk-QDCJNRKK.js → chunk-YXOAQAH2.js} +3 -3
  120. package/dist/{cli-BqyGVyOe.d.ts → cli-DACyuK8-.d.ts} +5 -5
  121. package/dist/cli.d.ts +8 -8
  122. package/dist/cli.js +65 -63
  123. package/dist/compounding/engine.d.ts +2 -2
  124. package/dist/compounding/engine.js +14 -12
  125. package/dist/compounding/preference-consolidator.d.ts +1 -1
  126. package/dist/compression-optimizer.d.ts +1 -1
  127. package/dist/config.d.ts +1 -1
  128. package/dist/config.js +1 -1
  129. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  130. package/dist/connectors/codex-materialize-runner.js +14 -12
  131. package/dist/connectors/codex-materialize.d.ts +1 -1
  132. package/dist/connectors/index.d.ts +1 -1
  133. package/dist/connectors/index.js +14 -12
  134. package/dist/consolidation-provenance-check.d.ts +2 -2
  135. package/dist/consolidation-provenance-check.js +6 -2
  136. package/dist/consolidation-undo.d.ts +2 -2
  137. package/dist/consolidation-undo.js +8 -1
  138. package/dist/consolidation-undo.js.map +1 -1
  139. package/dist/contradiction/index.d.ts +2 -2
  140. package/dist/conversation-index/backend.d.ts +1 -1
  141. package/dist/conversation-index/chunker.d.ts +1 -1
  142. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  143. package/dist/conversation-index/indexer.d.ts +1 -1
  144. package/dist/conversation-index/search.d.ts +1 -1
  145. package/dist/day-summary.d.ts +1 -1
  146. package/dist/delinearize.d.ts +1 -1
  147. package/dist/direct-answer-wiring.d.ts +1 -1
  148. package/dist/direct-answer.d.ts +1 -1
  149. package/dist/embedding-fallback.d.ts +1 -1
  150. package/dist/enrichment/index.d.ts +1 -1
  151. package/dist/entity-retrieval.d.ts +2 -2
  152. package/dist/entity-retrieval.js +14 -12
  153. package/dist/entity-schema.d.ts +1 -1
  154. package/dist/explicit-capture.d.ts +5 -5
  155. package/dist/extraction-faithfulness.d.ts +1 -1
  156. package/dist/extraction-faithfulness.js +3 -3
  157. package/dist/extraction-judge-telemetry.d.ts +1 -1
  158. package/dist/extraction-judge-training.d.ts +1 -1
  159. package/dist/extraction-judge.d.ts +1 -1
  160. package/dist/extraction-judge.js +3 -3
  161. package/dist/extraction.d.ts +1 -1
  162. package/dist/extraction.js +3 -3
  163. package/dist/fallback-llm.d.ts +1 -1
  164. package/dist/fallback-llm.js +2 -2
  165. package/dist/{forget-KHF7A44H.js → forget-CQCLB5LB.js} +2 -2
  166. package/dist/graph-dashboard-diff.d.ts +1 -1
  167. package/dist/graph-dashboard-key.d.ts +1 -1
  168. package/dist/graph-dashboard-parser.d.ts +1 -1
  169. package/dist/graph-edge-reinforcement.d.ts +1 -1
  170. package/dist/graph-snapshot.d.ts +1 -1
  171. package/dist/graph.d.ts +1 -1
  172. package/dist/identity-continuity.d.ts +1 -1
  173. package/dist/importance.d.ts +1 -1
  174. package/dist/importers/index.d.ts +1 -1
  175. package/dist/in-flight-reads.d.ts +19 -0
  176. package/dist/in-flight-reads.js +16 -0
  177. package/dist/in-flight-reads.js.map +1 -0
  178. package/dist/index.d.ts +13 -13
  179. package/dist/index.js +105 -103
  180. package/dist/intent.d.ts +1 -1
  181. package/dist/lcm/engine.d.ts +1 -1
  182. package/dist/lcm/engine.js +2 -2
  183. package/dist/lcm/index.d.ts +1 -1
  184. package/dist/lcm/index.js +7 -7
  185. package/dist/lcm/tools.d.ts +1 -1
  186. package/dist/lifecycle.d.ts +1 -1
  187. package/dist/live-connectors-runner.d.ts +1 -1
  188. package/dist/local-llm.d.ts +1 -1
  189. package/dist/local-model-endpoint.d.ts +1 -1
  190. package/dist/maintenance/memory-governance.d.ts +1 -1
  191. package/dist/maintenance/memory-governance.js +14 -12
  192. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +14 -12
  193. package/dist/maintenance/rebuild-memory-projection.js +15 -13
  194. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  195. package/dist/memory-action-policy.d.ts +1 -1
  196. package/dist/memory-cache.d.ts +24 -13
  197. package/dist/memory-cache.js +3 -1
  198. package/dist/memory-corpus-version.d.ts +19 -0
  199. package/dist/memory-corpus-version.js +13 -0
  200. package/dist/memory-corpus-version.js.map +1 -0
  201. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  202. package/dist/memory-projection-store.d.ts +1 -1
  203. package/dist/memory-provenance.d.ts +1 -1
  204. package/dist/memory-worth-outcomes.d.ts +2 -2
  205. package/dist/models-json.d.ts +1 -1
  206. package/dist/namespaces/migrate.d.ts +3 -3
  207. package/dist/namespaces/migrate.js +21 -19
  208. package/dist/namespaces/principal.d.ts +1 -1
  209. package/dist/namespaces/search.d.ts +1 -1
  210. package/dist/namespaces/search.js +7 -7
  211. package/dist/namespaces/storage.d.ts +3 -3
  212. package/dist/namespaces/storage.js +14 -12
  213. package/dist/native-knowledge.d.ts +1 -1
  214. package/dist/operator-toolkit.d.ts +2 -2
  215. package/dist/operator-toolkit.js +26 -24
  216. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  217. package/dist/orchestration/maintenance.d.ts +2 -2
  218. package/dist/orchestration/maintenance.js +16 -14
  219. package/dist/{orchestrator-37Cx7yyw.d.ts → orchestrator-D83AvrPf.d.ts} +4 -4
  220. package/dist/orchestrator.d.ts +5 -5
  221. package/dist/orchestrator.js +88 -86
  222. package/dist/page-versioning.js +5 -1
  223. package/dist/patterns-cli.d.ts +1 -1
  224. package/dist/{pipeline-C6onljHc.d.ts → pipeline-BSqGpYl1.d.ts} +1 -1
  225. package/dist/policy-runtime.d.ts +1 -1
  226. package/dist/provenance.d.ts +1 -1
  227. package/dist/{qmd-J08G7MEm.d.ts → qmd-CutnpRVe.d.ts} +1 -1
  228. package/dist/qmd-preflight.d.ts +2 -2
  229. package/dist/qmd-recall-cache.d.ts +1 -1
  230. package/dist/qmd.d.ts +2 -2
  231. package/dist/qmd.js +2 -2
  232. package/dist/recall-disclosure-escalation.d.ts +1 -1
  233. package/dist/recall-explain-renderer.d.ts +1 -1
  234. package/dist/recall-explain-renderer.js +3 -3
  235. package/dist/recall-planner-llm.d.ts +1 -1
  236. package/dist/recall-planner-llm.js +2 -2
  237. package/dist/recall-state.d.ts +1 -1
  238. package/dist/recall-tag-filter.d.ts +1 -1
  239. package/dist/recall-timings.d.ts +1 -1
  240. package/dist/recall-xray-cli.d.ts +1 -1
  241. package/dist/recall-xray-cli.js +4 -4
  242. package/dist/recall-xray-renderer.d.ts +1 -1
  243. package/dist/recall-xray-renderer.js +3 -3
  244. package/dist/recall-xray.d.ts +1 -1
  245. package/dist/recall-xray.js +2 -2
  246. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  247. package/dist/replay/normalizers/claude.d.ts +1 -1
  248. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  249. package/dist/replay/normalizers/shared.d.ts +1 -1
  250. package/dist/replay/runner.d.ts +1 -1
  251. package/dist/replay/types.d.ts +1 -1
  252. package/dist/resolve-auth-token.d.ts +1 -1
  253. package/dist/resume-bundles.js +2 -2
  254. package/dist/retrieval-agents.d.ts +2 -2
  255. package/dist/retrieval-tiers.d.ts +1 -1
  256. package/dist/routing/engine.d.ts +1 -1
  257. package/dist/routing/store.d.ts +1 -1
  258. package/dist/search/embed-helper.d.ts +1 -1
  259. package/dist/search/factory.d.ts +1 -1
  260. package/dist/search/factory.js +6 -6
  261. package/dist/search/index.d.ts +1 -1
  262. package/dist/search/index.js +12 -12
  263. package/dist/search/lancedb-backend.d.ts +1 -1
  264. package/dist/search/meilisearch-backend.d.ts +1 -1
  265. package/dist/search/noop-backend.d.ts +1 -1
  266. package/dist/search/orama-backend.d.ts +1 -1
  267. package/dist/search/port.d.ts +1 -1
  268. package/dist/search/remote-backend.d.ts +1 -1
  269. package/dist/{semantic-consolidation-BFtoVosl.d.ts → semantic-consolidation-btIVRC4l.d.ts} +1 -1
  270. package/dist/semantic-consolidation.d.ts +2 -2
  271. package/dist/semantic-consolidation.js +15 -13
  272. package/dist/semantic-rule-promotion.js +14 -12
  273. package/dist/semantic-rule-verifier.d.ts +5 -1
  274. package/dist/semantic-rule-verifier.js +14 -12
  275. package/dist/session-observer-bands.d.ts +1 -1
  276. package/dist/session-observer-state.d.ts +1 -1
  277. package/dist/shared-context/manager.d.ts +1 -1
  278. package/dist/signal.d.ts +1 -1
  279. package/dist/{storage-CXjwBr69.d.ts → storage-BVv_Mqa0.d.ts} +133 -3
  280. package/dist/storage.d.ts +2 -2
  281. package/dist/storage.js +13 -11
  282. package/dist/summarizer.d.ts +1 -1
  283. package/dist/summarizer.js +3 -3
  284. package/dist/summary-snapshot.d.ts +1 -1
  285. package/dist/temporal-supersession.d.ts +2 -2
  286. package/dist/temporal-timeline-recall.d.ts +1 -1
  287. package/dist/temporal-validity.d.ts +1 -1
  288. package/dist/threading.d.ts +1 -1
  289. package/dist/tier-migration.d.ts +2 -2
  290. package/dist/tier-routing.d.ts +1 -1
  291. package/dist/topics.d.ts +1 -1
  292. package/dist/transcript.d.ts +1 -1
  293. package/dist/transfer/capsule-import.js +6 -2
  294. package/dist/transfer/import-sqlite.js +2 -2
  295. package/dist/trust-score-stage.d.ts +1 -1
  296. package/dist/trust-score.d.ts +1 -1
  297. package/dist/{types-DpxqC8dI.d.ts → types-Cmkz7csa.d.ts} +27 -0
  298. package/dist/types.d.ts +1 -1
  299. package/dist/types.js +1 -1
  300. package/dist/utility-runtime.d.ts +1 -1
  301. package/dist/verified-recall.d.ts +4 -0
  302. package/dist/verified-recall.js +14 -12
  303. package/package.json +12 -2
  304. package/src/binary-lifecycle/pipeline.ts +22 -0
  305. package/src/cli.ts +8 -0
  306. package/src/compounding/engine.ts +9 -1
  307. package/src/config.ts +14 -0
  308. package/src/consolidation-undo.ts +8 -0
  309. package/src/curation/index.ts +9 -0
  310. package/src/in-flight-reads.ts +63 -0
  311. package/src/maintenance/memory-governance.ts +6 -0
  312. package/src/memory-cache-hot.test.ts +131 -0
  313. package/src/memory-cache.ts +146 -22
  314. package/src/memory-corpus-version.ts +45 -0
  315. package/src/namespaces/storage.ts +20 -1
  316. package/src/operator-toolkit.ts +7 -1
  317. package/src/orchestration/recall-internal.ts +6 -3
  318. package/src/orchestrator.ts +25 -1
  319. package/src/page-versioning.ts +14 -1
  320. package/src/provenance-frontmatter.test.ts +6 -0
  321. package/src/review/index.ts +16 -8
  322. package/src/semantic-rule-promotion.ts +9 -1
  323. package/src/semantic-rule-verifier.ts +19 -6
  324. package/src/spaces/index.ts +10 -0
  325. package/src/storage/memory-read-store.ts +9 -2
  326. package/src/storage-contract/cache-coherence.test.ts +242 -0
  327. package/src/storage-contract/namespace-routing.test.ts +26 -1
  328. package/src/storage.ts +513 -31
  329. package/src/transfer/capsule-cache-coherence.test.ts +107 -0
  330. package/src/transfer/capsule-import.ts +8 -0
  331. package/src/transfer/capsule-merge.ts +7 -0
  332. package/src/types.ts +27 -0
  333. package/src/verified-recall.ts +20 -6
  334. package/dist/capsule-merge-GK5E647P.js.map +0 -1
  335. package/dist/chunk-74MJH7I3.js.map +0 -1
  336. package/dist/chunk-AD2KK5MT.js.map +0 -1
  337. package/dist/chunk-AWJ2FHCF.js.map +0 -1
  338. package/dist/chunk-EJCVMPIM.js.map +0 -1
  339. package/dist/chunk-GL3SMLMZ.js.map +0 -1
  340. package/dist/chunk-JO5AT6QT.js.map +0 -1
  341. package/dist/chunk-KTOH7EJE.js.map +0 -1
  342. package/dist/chunk-M5QKGHCR.js.map +0 -1
  343. package/dist/chunk-Q6JPMCPO.js.map +0 -1
  344. package/dist/chunk-R2SJDIPO.js.map +0 -1
  345. package/dist/chunk-TVRPRFUB.js.map +0 -1
  346. package/dist/chunk-UVIQHSS7.js.map +0 -1
  347. package/dist/chunk-VF4XKTX3.js.map +0 -1
  348. package/dist/chunk-ZUFJ7ZTR.js.map +0 -1
  349. /package/dist/{auto-sync-676N6N6Y.js.map → auto-sync-AAMTVZMK.js.map} +0 -0
  350. /package/dist/{chunk-TZJVTH3F.js.map → chunk-27VEKQQA.js.map} +0 -0
  351. /package/dist/{chunk-7XH7VJN4.js.map → chunk-5S7Z5SHA.js.map} +0 -0
  352. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  353. /package/dist/{chunk-3F4N4WDH.js.map → chunk-AHLPZ2SF.js.map} +0 -0
  354. /package/dist/{chunk-AH3M6F4X.js.map → chunk-CDGGQGUU.js.map} +0 -0
  355. /package/dist/{chunk-ZHKD4EZM.js.map → chunk-CK6RI52P.js.map} +0 -0
  356. /package/dist/{chunk-6C35R7P4.js.map → chunk-DJCKXI4M.js.map} +0 -0
  357. /package/dist/{chunk-NMMKRVUF.js.map → chunk-EIKBKQ6L.js.map} +0 -0
  358. /package/dist/{chunk-O3BZ6PFY.js.map → chunk-F3FC7SMO.js.map} +0 -0
  359. /package/dist/{chunk-ZW6OKHVX.js.map → chunk-FKCJH7KP.js.map} +0 -0
  360. /package/dist/{chunk-DQPPRXFO.js.map → chunk-HQ7QZJM5.js.map} +0 -0
  361. /package/dist/{chunk-UYZUJ4EB.js.map → chunk-I2THZUAF.js.map} +0 -0
  362. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  363. /package/dist/{chunk-KIPLG7S7.js.map → chunk-JKLJ6A7W.js.map} +0 -0
  364. /package/dist/{chunk-HXDKC3BI.js.map → chunk-JU3GHO6J.js.map} +0 -0
  365. /package/dist/{chunk-SYNYI4ZM.js.map → chunk-L5HJQC36.js.map} +0 -0
  366. /package/dist/{chunk-VXMWDXIJ.js.map → chunk-LC7OODWZ.js.map} +0 -0
  367. /package/dist/{chunk-CNWC274T.js.map → chunk-MLT5EG6K.js.map} +0 -0
  368. /package/dist/{chunk-PKGWJFNR.js.map → chunk-OVMS3DCB.js.map} +0 -0
  369. /package/dist/{chunk-57INMZ6F.js.map → chunk-POUD5BLV.js.map} +0 -0
  370. /package/dist/{chunk-LRGSLV7M.js.map → chunk-PYHOAXTI.js.map} +0 -0
  371. /package/dist/{chunk-WKE4VXSZ.js.map → chunk-QKVX4Y6D.js.map} +0 -0
  372. /package/dist/{chunk-FXGC5UUH.js.map → chunk-RT7IP3D3.js.map} +0 -0
  373. /package/dist/{chunk-CHVU4RE5.js.map → chunk-SKXBOZDF.js.map} +0 -0
  374. /package/dist/{chunk-GKCCHHZK.js.map → chunk-UXKMMNJD.js.map} +0 -0
  375. /package/dist/{chunk-XFTPUXCE.js.map → chunk-UXWGPODH.js.map} +0 -0
  376. /package/dist/{chunk-2FSHROYK.js.map → chunk-V53GJUSL.js.map} +0 -0
  377. /package/dist/{chunk-LKT755LP.js.map → chunk-VA4WSW54.js.map} +0 -0
  378. /package/dist/{chunk-FYKIEOG6.js.map → chunk-VCFADTFS.js.map} +0 -0
  379. /package/dist/{chunk-T7VNN3ZM.js.map → chunk-XVLEGMJI.js.map} +0 -0
  380. /package/dist/{chunk-CRFTLUWT.js.map → chunk-Y5U35ZVS.js.map} +0 -0
  381. /package/dist/{chunk-UHGHTOR5.js.map → chunk-YGAOLHKN.js.map} +0 -0
  382. /package/dist/{chunk-QDCJNRKK.js.map → chunk-YXOAQAH2.js.map} +0 -0
  383. /package/dist/{forget-KHF7A44H.js.map → forget-CQCLB5LB.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.7.7",
3
+ "version": "9.7.9",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1090,6 +1090,16 @@
1090
1090
  "remnic-source": "./src/importance.ts",
1091
1091
  "import": "./dist/importance.js"
1092
1092
  },
1093
+ "./in-flight-reads": {
1094
+ "types": "./dist/in-flight-reads.d.ts",
1095
+ "remnic-source": "./src/in-flight-reads.ts",
1096
+ "import": "./dist/in-flight-reads.js"
1097
+ },
1098
+ "./in-flight-reads.js": {
1099
+ "types": "./dist/in-flight-reads.d.ts",
1100
+ "remnic-source": "./src/in-flight-reads.ts",
1101
+ "import": "./dist/in-flight-reads.js"
1102
+ },
1093
1103
  "./index": {
1094
1104
  "types": "./dist/index.d.ts",
1095
1105
  "remnic-source": "./src/index.ts",
@@ -3000,7 +3010,7 @@
3000
3010
  "core"
3001
3011
  ],
3002
3012
  "peerDependencies": {
3003
- "@remnic/coding-graph": "^9.7.7"
3013
+ "@remnic/coding-graph": "^9.7.9"
3004
3014
  },
3005
3015
  "peerDependenciesMeta": {
3006
3016
  "@remnic/coding-graph": {
@@ -18,6 +18,21 @@ import type {
18
18
  import type { BinaryStorageBackend } from "./backend.js";
19
19
  import { readManifest, writeManifest } from "./manifest.js";
20
20
  import { scanForBinaries } from "./scanner.js";
21
+ import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
22
+ import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
23
+
24
+ /**
25
+ * True when `mdPath` is an active recall-corpus memory file under `memoryDir`
26
+ * (a top-level RECALL_FALLBACK_DIRS category), so a redirect rewrite of it must
27
+ * bump the corpus sentinel to self-heal a warm hot-memories cache (#1902).
28
+ * entities/, artifacts/, and other non-recall paths need no bump.
29
+ */
30
+ function isRecallCorpusMarkdown(mdPath: string, memoryDir: string): boolean {
31
+ const rel = path.relative(memoryDir, mdPath);
32
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
33
+ const top = rel.split(path.sep)[0];
34
+ return (RECALL_FALLBACK_DIRS as readonly string[]).includes(top);
35
+ }
21
36
 
22
37
  /** Minimal logger interface so we don't depend on the full logger module. */
23
38
  interface PipelineLogger {
@@ -305,6 +320,13 @@ async function stageRedirect(
305
320
  for (const update of updates) {
306
321
  try {
307
322
  await writeMarkdownFile(update.mdPath, update.content);
323
+ // The redirect rewrote an on-disk memory file out-of-band (not through a
324
+ // StorageManager mutation). Bump the corpus sentinel per write so a warm
325
+ // hot-memories cache rescans — mid-batch too, never serving pre-redirect
326
+ // content (#1902). Only recall-corpus files affect that cache.
327
+ if (isRecallCorpusMarkdown(update.mdPath, memoryDir)) {
328
+ bumpMemoryCorpusVersionForDir(memoryDir);
329
+ }
308
330
  } catch (err) {
309
331
  writeFailCount++;
310
332
  const msg = `redirect write failed for ${update.mdPath}: ${err instanceof Error ? err.message : String(err)}`;
package/src/cli.ts CHANGED
@@ -10,6 +10,7 @@ import { resolveNamespaceCapabilities,
10
10
  resolveIdentityContinuityCapabilities,
11
11
  resolveSecurityCapabilities, resolveEvalCapabilities, resolveConsolidationCapabilities, resolveRecallAuxiliaryCapabilities } from "./capabilities.js";
12
12
  import { ThreadingManager } from "./threading.js";
13
+ import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
13
14
  import { utcDayRange } from "./transcript.js";
14
15
  import { runWearablesCliCommand } from "./wearables/cli.js";
15
16
  import { registerResearchStatusCommands } from "./cli/research-status-commands.js";
@@ -7495,6 +7496,11 @@ export function registerCli(
7495
7496
  try {
7496
7497
  await unlink(filePath);
7497
7498
  deleted += 1;
7499
+ // Per-delete corpus bump (Cursor Medium, #1902): dedupe removes
7500
+ // active memory files directly (bypassing StorageManager), so bump
7501
+ // each delete to keep a same-process or peer readAllMemories from
7502
+ // recalling a duplicate that was already removed mid-batch.
7503
+ bumpMemoryCorpusVersionForDir(memoryDir);
7498
7504
  } catch (err) {
7499
7505
  console.log(` failed to delete ${filePath}: ${String(err)}`);
7500
7506
  }
@@ -7556,6 +7562,8 @@ export function registerCli(
7556
7562
  try {
7557
7563
  await unlink(filePath);
7558
7564
  deleted += 1;
7565
+ // Per-delete corpus bump (Cursor Medium, #1902) — see dedupe-exact.
7566
+ bumpMemoryCorpusVersionForDir(memoryDir);
7559
7567
  } catch (err) {
7560
7568
  console.log(` failed to delete ${filePath}: ${String(err)}`);
7561
7569
  }
@@ -321,7 +321,15 @@ function normalizePromotionWhitespace(value: string): string {
321
321
  }
322
322
 
323
323
  function stripTrailingPromotionPunctuation(value: string): string {
324
- return value.replace(/[,:;]+$/g, "").trim();
324
+ // Linear trailing-punctuation strip (no `/[,:;]+$/` — CodeQL flags that as a
325
+ // polynomial-backtracking ReDoS on long runs of ',;:'). Walk back once, O(n).
326
+ let end = value.length;
327
+ while (end > 0) {
328
+ const ch = value[end - 1];
329
+ if (ch === "," || ch === ":" || ch === ";") end--;
330
+ else break;
331
+ }
332
+ return value.slice(0, end).trim();
325
333
  }
326
334
 
327
335
  function extractExplicitIfThenRule(value: string): string | null {
package/src/config.ts CHANGED
@@ -2106,6 +2106,20 @@ export function parseConfig(
2106
2106
  // opt out with recallMemoryWorthFilterEnabled=false.
2107
2107
  recallMemoryWorthFilterEnabled:
2108
2108
  coerceBool(cfg.recallMemoryWorthFilterEnabled) ?? true,
2109
+ // Hot-memories result cache (issue #1902). Default on: readAllMemories()
2110
+ // serves from the version-keyed in-process corpus cache. coerceBool
2111
+ // handles "false"/"0" string forms; set false to force disk scans.
2112
+ hotMemoriesCacheEnabled: coerceBool(cfg.hotMemoriesCacheEnabled) ?? true,
2113
+ // External-edit safety net (issue #1902): the version sentinel gives
2114
+ // immediate coherence for cooperating writers, but direct user/git/editor
2115
+ // writes to memory files (which the project supports) don't bump it. Bound
2116
+ // the hot cache's staleness with a TTL so such edits self-heal. Default 60s;
2117
+ // 0 disables the TTL (version invalidation only — max perf for pure-daemon
2118
+ // deployments with no external edits). coerceNumber handles string forms.
2119
+ hotMemoriesCacheTtlMs: (() => {
2120
+ const n = coerceNumber(cfg.hotMemoriesCacheTtlMs);
2121
+ return n !== undefined && n >= 0 ? n : 60_000;
2122
+ })(),
2109
2123
  recallMemoryWorthHalfLifeMs: (() => {
2110
2124
  const n = coerceNumber(cfg.recallMemoryWorthHalfLifeMs);
2111
2125
  return n !== undefined && n >= 0 ? n : 0;
@@ -25,6 +25,7 @@ import { mkdir, writeFile, access, realpath, lstat } from "node:fs/promises";
25
25
  import { constants as fsConstants } from "node:fs";
26
26
  import type { StorageManager } from "./storage.js";
27
27
  import type { VersioningConfig } from "./page-versioning.js";
28
+ import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
28
29
  import { getVersion } from "./page-versioning.js";
29
30
 
30
31
  /**
@@ -646,6 +647,13 @@ export async function runConsolidationUndo(options: {
646
647
  // write fails with EEXIST instead of silently overwriting the new
647
648
  // file (PR #637 round-11 review, codex P1).
648
649
  await writeFile(p.sourcePath, p.content, { encoding: "utf-8", flag: "wx" });
650
+ // Restore writes an active memory file back out-of-band (direct wx write,
651
+ // not a StorageManager mutation). Bump the corpus sentinel per successful
652
+ // restore so a warm hot-memories cache rescans — a later restore failure
653
+ // returns before archiveMemory() runs, so without this the already-written
654
+ // restores would stay invisible to recall until an unrelated mutation or
655
+ // restart (esp. with hotMemoriesCacheTtlMs: 0). Issue #1902, Codex Medium.
656
+ bumpMemoryCorpusVersionForDir(memoryDir);
649
657
  result.restores.push({
650
658
  entry: p.entry,
651
659
  sourcePath: p.sourcePath,
@@ -9,6 +9,7 @@ import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import crypto from "node:crypto";
11
11
  import { getCategoryDir, ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
12
+ import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
12
13
 
13
14
  // ── Types ────────────────────────────────────────────────────────────────────
14
15
 
@@ -202,6 +203,10 @@ export async function curate(options: CurateOptions): Promise<CurateResult> {
202
203
  }
203
204
  }
204
205
 
206
+ // Per-statement corpus bumps happen in writeStatement (below) so a
207
+ // same-process readAllMemories mid-batch never serves a stale/partial corpus
208
+ // (Cursor Medium, #1902); no after-loop bump needed here.
209
+
205
210
  return {
206
211
  statements,
207
212
  filesProcessed,
@@ -471,6 +476,10 @@ function writeStatement(stmt: CuratedStatement, memoryDir: string): string {
471
476
  const body = `${frontmatter}\n\n${stmt.content}\n`;
472
477
 
473
478
  fs.writeFileSync(filePath, body);
479
+ // Bump the corpus sentinel per write (out-of-band create) so a concurrent
480
+ // same-process readAllMemories rescans and sees this statement immediately,
481
+ // never a stale/partial corpus mid-batch (Cursor Medium, #1902).
482
+ bumpMemoryCorpusVersionForDir(memoryDir);
474
483
  return filePath;
475
484
  }
476
485
 
@@ -0,0 +1,63 @@
1
+ import type { MemoryFile } from "./types.js";
2
+
3
+ /**
4
+ * In-flight readAllMemories() dedup registry (issue #1902).
5
+ *
6
+ * NOT a cache layer — it holds transient per-(directory, secure-store key) scan
7
+ * promises so temporally-overlapping reads collapse onto ONE disk scan; the slot
8
+ * is deleted when the promise settles. It lives in its own module (not
9
+ * memory-cache.ts) so it is not swept by the ALL_CACHE_LAYERS invalidation
10
+ * contract, yet remains importable by both StorageManager and the out-of-band
11
+ * corpus-bump helper (memory-corpus-version.ts).
12
+ *
13
+ * Keyed by baseDir AND secure-store key identity (Codex P1 / Cursor Medium):
14
+ * an unlocked manager's decrypted scan must NOT be handed to a locked or
15
+ * differently-keyed manager for the same dir — that would bypass the keyId
16
+ * isolation getCachedMemories enforces. Locked/plaintext stores use keyId "".
17
+ */
18
+ const inFlightReadsByKey = new Map<string, Promise<MemoryFile[]>>();
19
+
20
+ const DIR_SEP = "\u0000";
21
+
22
+ function composeKey(baseDir: string, keyId: string): string {
23
+ return `${baseDir}${DIR_SEP}${keyId}`;
24
+ }
25
+
26
+ export function getInFlightRead(baseDir: string, keyId = ""): Promise<MemoryFile[]> | undefined {
27
+ return inFlightReadsByKey.get(composeKey(baseDir, keyId));
28
+ }
29
+
30
+ export function setInFlightRead(baseDir: string, keyId: string, read: Promise<MemoryFile[]>): void {
31
+ inFlightReadsByKey.set(composeKey(baseDir, keyId), read);
32
+ }
33
+
34
+ /**
35
+ * Delete the in-flight slot for one (baseDir, keyId). When `expected` is given,
36
+ * only deletes if the slot still holds that exact promise (owner-only clear).
37
+ */
38
+ export function deleteInFlightRead(
39
+ baseDir: string,
40
+ keyId = "",
41
+ expected?: Promise<MemoryFile[]>,
42
+ ): void {
43
+ const key = composeKey(baseDir, keyId);
44
+ if (expected === undefined || inFlightReadsByKey.get(key) === expected) {
45
+ inFlightReadsByKey.delete(key);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Drop EVERY in-flight slot for `baseDir` across all secure-store identities.
51
+ * Used by mutations / invalidation / out-of-band corpus bumps: after the corpus
52
+ * changes, no reader of any key identity may attach to a pre-mutation scan.
53
+ */
54
+ export function deleteInFlightReadsForDir(baseDir: string): void {
55
+ const prefix = `${baseDir}${DIR_SEP}`;
56
+ for (const key of inFlightReadsByKey.keys()) {
57
+ if (key.startsWith(prefix)) inFlightReadsByKey.delete(key);
58
+ }
59
+ }
60
+
61
+ export function clearInFlightReads(): void {
62
+ inFlightReadsByKey.clear();
63
+ }
@@ -4,6 +4,7 @@ import { StorageManager } from "../storage.js";
4
4
  import { decideLifecycleTransition } from "../lifecycle.js";
5
5
  import type { MemoryFile, MemoryStatus } from "../types.js";
6
6
  import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
7
+ import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
7
8
  import { assertPathInsideRoot } from "../utils/path-containment.js";
8
9
 
9
10
  export type MemoryGovernanceMode = "shadow" | "apply";
@@ -1015,6 +1016,11 @@ export async function restoreMemoryGovernanceRun(
1015
1016
  await mkdir(path.dirname(entry.originalPath), { recursive: true });
1016
1017
  await writeFile(entry.originalPath, entry.beforeRaw, "utf-8");
1017
1018
  restoredActions += 1;
1019
+ // Restore writes/removes memory files directly (out-of-band — not through a
1020
+ // StorageManager mutation). Bump the corpus sentinel PER entry so a
1021
+ // same-process or peer readAllMemories rescans mid-batch and never serves a
1022
+ // partially-restored corpus (Cursor Medium, #1902).
1023
+ bumpMemoryCorpusVersionForDir(options.memoryDir);
1018
1024
  }
1019
1025
 
1020
1026
  return {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Issue #1902 — unit tests for the hot-memories result cache layer.
3
+ *
4
+ * These pin the low-level cache contract that StorageManager.readAllMemories()
5
+ * relies on: version-keyed round-trip, the version-0 and version-mismatch null
6
+ * guards, in-place patch-on-write / patch-on-delete, and the two invalidation
7
+ * shapes (wholesale drop vs derived/global-only keep).
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import test from "node:test";
12
+
13
+ import {
14
+ clearMemoryCache,
15
+ getCachedMemories,
16
+ invalidateAllForDir,
17
+ invalidateDerivedAndGlobalForDir,
18
+ setCachedMemories,
19
+ updateCacheOnDelete,
20
+ updateCacheOnWrite,
21
+ } from "./memory-cache.js";
22
+ import type { MemoryFile } from "./types.js";
23
+
24
+ function mem(p: string, content = "x"): MemoryFile {
25
+ return {
26
+ path: p,
27
+ frontmatter: { id: p, category: "fact" },
28
+ content,
29
+ } as unknown as MemoryFile;
30
+ }
31
+
32
+ test("hot cache: round-trips at a matching version", () => {
33
+ const dir = "/tmp/remnic-hot-unit-a";
34
+ clearMemoryCache();
35
+ setCachedMemories(dir, [mem("a.md"), mem("b.md")], 3);
36
+ const got = getCachedMemories(dir, 3);
37
+ assert.ok(got, "expected a cache hit at the matching version");
38
+ assert.deepEqual(got!.map((m) => m.path).sort(), ["a.md", "b.md"]);
39
+ clearMemoryCache();
40
+ });
41
+
42
+ test("hot cache: version 0 always misses (fresh/test dirs)", () => {
43
+ const dir = "/tmp/remnic-hot-unit-b";
44
+ clearMemoryCache();
45
+ setCachedMemories(dir, [mem("a.md")], 0);
46
+ assert.equal(getCachedMemories(dir, 0), null, "version 0 must never serve a cache hit");
47
+ clearMemoryCache();
48
+ });
49
+
50
+ test("hot cache: version mismatch misses (never serves stale)", () => {
51
+ const dir = "/tmp/remnic-hot-unit-c";
52
+ clearMemoryCache();
53
+ setCachedMemories(dir, [mem("a.md")], 5);
54
+ assert.ok(getCachedMemories(dir, 5), "same version hits");
55
+ assert.equal(getCachedMemories(dir, 6), null, "a bumped version must miss");
56
+ clearMemoryCache();
57
+ });
58
+
59
+ test("hot cache: updateCacheOnWrite / updateCacheOnDelete mutate the entry in place", () => {
60
+ const dir = "/tmp/remnic-hot-unit-d";
61
+ clearMemoryCache();
62
+ setCachedMemories(dir, [mem("a.md")], 2);
63
+ updateCacheOnWrite(dir, mem("b.md", "new"));
64
+ const afterWrite = getCachedMemories(dir, 2);
65
+ assert.deepEqual(afterWrite!.map((m) => m.path).sort(), ["a.md", "b.md"], "write adds in place");
66
+ updateCacheOnWrite(dir, mem("a.md", "updated"));
67
+ assert.equal(
68
+ getCachedMemories(dir, 2)!.find((m) => m.path === "a.md")!.content,
69
+ "updated",
70
+ "write to an existing path replaces in place",
71
+ );
72
+ updateCacheOnDelete(dir, "a.md");
73
+ assert.deepEqual(getCachedMemories(dir, 2)!.map((m) => m.path), ["b.md"], "delete removes in place");
74
+ clearMemoryCache();
75
+ });
76
+
77
+ test("hot cache: wholesale invalidateAllForDir drops the hot layer; invalidateDerivedAndGlobalForDir keeps it", () => {
78
+ const dir = "/tmp/remnic-hot-unit-e";
79
+ clearMemoryCache();
80
+ setCachedMemories(dir, [mem("a.md")], 4);
81
+ invalidateDerivedAndGlobalForDir(dir);
82
+ assert.ok(
83
+ getCachedMemories(dir, 4),
84
+ "invalidateDerivedAndGlobalForDir must NOT drop the hot layer (patch-on-write keeps it warm)",
85
+ );
86
+ invalidateAllForDir(dir);
87
+ assert.equal(getCachedMemories(dir, 4), null, "invalidateAllForDir must drop the hot layer");
88
+ clearMemoryCache();
89
+ });
90
+
91
+ test("hot cache: keyId scopes entries — a mismatched secure-store key misses (#1902 Codex P1)", () => {
92
+ const dir = "/tmp/remnic-hot-unit-keyid";
93
+ clearMemoryCache();
94
+ // A keyed (unlocked encrypted) manager warms the cache with decrypted content.
95
+ setCachedMemories(dir, [mem("secret.md", "decrypted")], 5, "secure-store:key:1");
96
+ // A locked/unkeyed manager for the SAME dir must NOT read the decrypted entry.
97
+ assert.equal(
98
+ getCachedMemories(dir, 5, ""),
99
+ null,
100
+ "a manager without the matching key must miss the decrypted cache entry",
101
+ );
102
+ // A different key id also misses.
103
+ assert.equal(getCachedMemories(dir, 5, "secure-store:key:2"), null, "a different key id misses");
104
+ // The matching key id hits.
105
+ const got = getCachedMemories(dir, 5, "secure-store:key:1");
106
+ assert.ok(got, "the matching key id serves the cached corpus");
107
+ assert.deepEqual(got!.map((m) => m.path), ["secret.md"]);
108
+ clearMemoryCache();
109
+ });
110
+
111
+ test("hot cache: TTL expires an entry so external edits self-heal (#1902 Codex P1)", (t) => {
112
+ const dir = "/tmp/remnic-hot-unit-ttl";
113
+ clearMemoryCache();
114
+ // Deterministic time control (no real timers): the entry's loadedAt is stamped
115
+ // from the mocked clock at set time; advancing it past the TTL forces a miss.
116
+ t.mock.timers.enable({ apis: ["Date"] });
117
+ try {
118
+ setCachedMemories(dir, [mem("a.md")], 9, "");
119
+ // ttl=0 disables the TTL: always a hit at the matching version.
120
+ assert.ok(getCachedMemories(dir, 9, "", 0), "ttl=0 disables expiry");
121
+ // Fresh entry within a generous TTL still hits.
122
+ assert.ok(getCachedMemories(dir, 9, "", 60_000), "fresh entry within TTL hits");
123
+ // Advance past the TTL: the entry is now a miss, forcing a rescan that would
124
+ // pick up any external filesystem edit that bypassed the version sentinel.
125
+ t.mock.timers.tick(120_000);
126
+ assert.equal(getCachedMemories(dir, 9, "", 60_000), null, "entry older than ttl is a miss");
127
+ } finally {
128
+ t.mock.timers.reset();
129
+ clearMemoryCache();
130
+ }
131
+ });
@@ -5,49 +5,107 @@ interface CacheEntry {
5
5
  memories: Map<string, MemoryFile>; // keyed by file path
6
6
  version: number;
7
7
  loadedAt: number;
8
+ // Secure-store key identity that decrypted these memories (issue #1902, Codex
9
+ // P1). "" for plaintext/locked stores. A manager only reads an entry whose
10
+ // keyId matches its own, so a locked/unkeyed instance never sees content
11
+ // another instance decrypted under a key for the same baseDir.
12
+ keyId: string;
13
+ // Effective TTL (ms) this entry was stored under (issue #1902, Codex Medium),
14
+ // so the opportunistic sweep can evict abandoned corpora that are never
15
+ // revisited. 0 = no TTL (never swept by age).
16
+ ttlMs: number;
8
17
  }
9
18
 
10
19
  // Module-level singleton — shared across all StorageManager instances and sessions
11
20
  const hotCacheByDir = new Map<string, CacheEntry>();
12
21
  const archiveCacheByDir = new Map<string, CacheEntry>();
13
22
 
14
- export function getCachedMemories(baseDir: string, currentVersion: number): MemoryFile[] | null {
23
+ /**
24
+ * Evict every hot entry whose own TTL has elapsed (issue #1902, Codex Medium).
25
+ * Called opportunistically on each set so a long-running process that reads many
26
+ * namespace/temporary roots and never revisits some of them still releases their
27
+ * (potentially hundreds-of-MB) corpora within the TTL, instead of holding them
28
+ * resident until an explicit global clear. O(entries) over a small map.
29
+ */
30
+ function sweepExpiredHotEntries(now: number): void {
31
+ for (const [dir, entry] of hotCacheByDir) {
32
+ if (entry.ttlMs > 0 && now - entry.loadedAt > entry.ttlMs) hotCacheByDir.delete(dir);
33
+ }
34
+ }
35
+
36
+ export function getCachedMemories(
37
+ baseDir: string,
38
+ currentVersion: number,
39
+ keyId = "",
40
+ ttlMs = 0,
41
+ ): MemoryFile[] | null {
15
42
  // Don't serve from cache when version tracking is unavailable (version=0).
16
43
  // This ensures tests and fresh installs without a version file always read disk.
17
44
  if (currentVersion === 0) return null;
18
45
  const entry = hotCacheByDir.get(baseDir);
19
- if (!entry || entry.version !== currentVersion) return null;
46
+ if (!entry || entry.version !== currentVersion || entry.keyId !== keyId) return null;
47
+ // TTL safety net (issue #1902): bound staleness from external filesystem edits
48
+ // that bypass the version sentinel. ttlMs <= 0 disables the check. On expiry
49
+ // evict the entry (not just return null) so a revisited-but-stale dir releases
50
+ // its resident corpus instead of lingering until an explicit clear.
51
+ if (ttlMs > 0 && Date.now() - entry.loadedAt > ttlMs) {
52
+ hotCacheByDir.delete(baseDir);
53
+ return null;
54
+ }
20
55
  return [...entry.memories.values()];
21
56
  }
22
57
 
23
- export function setCachedMemories(baseDir: string, memories: MemoryFile[], version: number): void {
58
+ export function setCachedMemories(
59
+ baseDir: string,
60
+ memories: MemoryFile[],
61
+ version: number,
62
+ keyId = "",
63
+ ttlMs = 0,
64
+ ): void {
24
65
  const map = new Map<string, MemoryFile>();
25
66
  for (const m of memories) map.set(m.path, m);
26
- hotCacheByDir.set(baseDir, { memories: map, version, loadedAt: Date.now() });
67
+ const now = Date.now();
68
+ hotCacheByDir.set(baseDir, { memories: map, version, loadedAt: now, keyId, ttlMs });
69
+ // Release abandoned corpora that expired but were never revisited (Codex Medium).
70
+ sweepExpiredHotEntries(now);
27
71
  }
28
72
 
29
- export function updateCacheOnWrite(baseDir: string, memory: MemoryFile): void {
73
+ export function updateCacheOnWrite(baseDir: string, memory: MemoryFile, keyId = ""): void {
30
74
  const entry = hotCacheByDir.get(baseDir);
31
- if (entry) entry.memories.set(memory.path, memory);
75
+ // Only mutate an entry that belongs to THIS key identity (issue #1902, Codex
76
+ // Medium). A concurrent differently-keyed manager may have replaced the
77
+ // single hotCacheByDir slot during an awaited parse; inserting our decrypted
78
+ // MemoryFile into its entry would leak plaintext across the key boundary.
79
+ if (entry && entry.keyId === keyId) entry.memories.set(memory.path, memory);
32
80
  }
33
81
 
34
- export function updateCacheOnDelete(baseDir: string, filePath: string): void {
82
+ export function updateCacheOnDelete(baseDir: string, filePath: string, keyId = ""): void {
35
83
  const entry = hotCacheByDir.get(baseDir);
36
- if (entry) entry.memories.delete(filePath);
84
+ if (entry && entry.keyId === keyId) entry.memories.delete(filePath);
37
85
  }
38
-
39
86
  // Archive cache — same pattern, separate store
40
- export function getCachedArchivedMemories(baseDir: string, currentVersion: number): MemoryFile[] | null {
87
+ export function getCachedArchivedMemories(
88
+ baseDir: string,
89
+ currentVersion: number,
90
+ keyId = "",
91
+ ): MemoryFile[] | null {
41
92
  if (currentVersion === 0) return null;
42
93
  const entry = archiveCacheByDir.get(baseDir);
43
- if (!entry || entry.version !== currentVersion) return null;
94
+ if (!entry || entry.version !== currentVersion || entry.keyId !== keyId) return null;
44
95
  return [...entry.memories.values()];
45
96
  }
46
97
 
47
- export function setCachedArchivedMemories(baseDir: string, memories: MemoryFile[], version: number): void {
98
+ export function setCachedArchivedMemories(
99
+ baseDir: string,
100
+ memories: MemoryFile[],
101
+ version: number,
102
+ keyId = "",
103
+ ): void {
48
104
  const map = new Map<string, MemoryFile>();
49
105
  for (const m of memories) map.set(m.path, m);
50
- archiveCacheByDir.set(baseDir, { memories: map, version, loadedAt: Date.now() });
106
+ // ttlMs: 0 the archive cache has no age TTL; it is version-keyed and cleared
107
+ // via invalidation. The field satisfies the shared CacheEntry shape (#1902).
108
+ archiveCacheByDir.set(baseDir, { memories: map, version, loadedAt: Date.now(), keyId, ttlMs: 0 });
51
109
  }
52
110
 
53
111
  // Entity cache — same pattern as memory cache, but keyed by schema-aware parse inputs.
@@ -108,41 +166,77 @@ export function invalidateCachedEntities(baseDir: string): void {
108
166
  interface DerivedCacheEntry<T> {
109
167
  data: T;
110
168
  sourceVersion: number; // matches the hot cache version it was derived from
169
+ keyId: string; // secure-store key identity of the corpus it was derived from (#1902)
170
+ loadedAt: number; // build time — bounds staleness from external edits via ttlMs (#1902)
111
171
  }
112
172
 
113
173
  const episodeMapByDir = new Map<string, DerivedCacheEntry<Map<string, MemoryFile>>>();
114
174
  const ruleMemoriesByDir = new Map<string, DerivedCacheEntry<{ all: MemoryFile[]; byId: Map<string, MemoryFile> }>>();
115
175
 
116
- /** Get a pre-filtered Map of episode memories (keyed by ID). Derived from hot cache. */
117
- export function getCachedEpisodeMap(baseDir: string, currentVersion: number): Map<string, MemoryFile> | null {
176
+ export function getCachedEpisodeMap(
177
+ baseDir: string,
178
+ currentVersion: number,
179
+ keyId = "",
180
+ ttlMs = 0,
181
+ ): Map<string, MemoryFile> | null {
118
182
  if (currentVersion === 0) return null;
119
183
  const entry = episodeMapByDir.get(baseDir);
120
- if (!entry || entry.sourceVersion !== currentVersion) return null;
184
+ if (!entry || entry.sourceVersion !== currentVersion || entry.keyId !== keyId) return null;
185
+ // TTL safety net (issue #1902): the version sentinel doesn't move on a direct
186
+ // external edit, so bound how long a derived view is served before a rebuild
187
+ // (which re-reads via readAllMemories, itself TTL-bounded). ttlMs <= 0 disables.
188
+ if (ttlMs > 0 && Date.now() - entry.loadedAt > ttlMs) {
189
+ episodeMapByDir.delete(baseDir);
190
+ return null;
191
+ }
121
192
  return entry.data;
122
193
  }
123
194
 
124
195
  /** Build and cache the episode memory map from the full memory list. */
125
- export function setCachedEpisodeMap(baseDir: string, memories: MemoryFile[], version: number): Map<string, MemoryFile> {
196
+ export function setCachedEpisodeMap(
197
+ baseDir: string,
198
+ memories: MemoryFile[],
199
+ version: number,
200
+ cache = true,
201
+ keyId = "",
202
+ ): Map<string, MemoryFile> {
126
203
  const map = new Map<string, MemoryFile>();
127
204
  for (const m of memories) {
128
205
  if (m.frontmatter.status === "archived" || m.frontmatter.status === "forgotten") continue;
129
206
  if (m.frontmatter.memoryKind !== "episode") continue;
130
207
  map.set(m.frontmatter.id, m);
131
208
  }
132
- episodeMapByDir.set(baseDir, { data: map, sourceVersion: version });
209
+ if (cache) episodeMapByDir.set(baseDir, { data: map, sourceVersion: version, keyId, loadedAt: Date.now() });
133
210
  return map;
134
211
  }
135
212
 
136
213
  /** Get pre-filtered rule memories. Derived from hot cache. */
137
- export function getCachedRuleMemories(baseDir: string, currentVersion: number): { all: MemoryFile[]; byId: Map<string, MemoryFile> } | null {
214
+ export function getCachedRuleMemories(
215
+ baseDir: string,
216
+ currentVersion: number,
217
+ keyId = "",
218
+ ttlMs = 0,
219
+ ): { all: MemoryFile[]; byId: Map<string, MemoryFile> } | null {
138
220
  if (currentVersion === 0) return null;
139
221
  const entry = ruleMemoriesByDir.get(baseDir);
140
- if (!entry || entry.sourceVersion !== currentVersion) return null;
222
+ if (!entry || entry.sourceVersion !== currentVersion || entry.keyId !== keyId) return null;
223
+ // TTL safety net (issue #1902): bound staleness from direct external edits
224
+ // that don't move the version sentinel. ttlMs <= 0 disables.
225
+ if (ttlMs > 0 && Date.now() - entry.loadedAt > ttlMs) {
226
+ ruleMemoriesByDir.delete(baseDir);
227
+ return null;
228
+ }
141
229
  return entry.data;
142
230
  }
143
231
 
144
232
  /** Build and cache the rule memories from the full memory list. */
145
- export function setCachedRuleMemories(baseDir: string, memories: MemoryFile[], version: number): { all: MemoryFile[]; byId: Map<string, MemoryFile> } {
233
+ export function setCachedRuleMemories(
234
+ baseDir: string,
235
+ memories: MemoryFile[],
236
+ version: number,
237
+ cache = true,
238
+ keyId = "",
239
+ ): { all: MemoryFile[]; byId: Map<string, MemoryFile> } {
146
240
  const byId = new Map<string, MemoryFile>();
147
241
  const all: MemoryFile[] = [];
148
242
  for (const m of memories) {
@@ -156,7 +250,7 @@ export function setCachedRuleMemories(baseDir: string, memories: MemoryFile[], v
156
250
  }
157
251
  }
158
252
  const result = { all, byId };
159
- ruleMemoriesByDir.set(baseDir, { data: result, sourceVersion: version });
253
+ if (cache) ruleMemoriesByDir.set(baseDir, { data: result, sourceVersion: version, keyId, loadedAt: Date.now() });
160
254
  return result;
161
255
  }
162
256
 
@@ -295,6 +389,36 @@ export function invalidateAllForDir(baseDir: string): void {
295
389
  }
296
390
  }
297
391
 
392
+ /**
393
+ * Invalidate every cache layer for `baseDir` EXCEPT the hot-memories layer
394
+ * (issue #1902). Single-file write paths use this instead of
395
+ * `invalidateAllForDir` because they patch the hot-memories entry in place
396
+ * (via `updateCacheOnWrite`/`updateCacheOnDelete`) and re-key it to the bumped
397
+ * version — so dropping it here would defeat the whole warm-cache fast path.
398
+ * The derived episode/rule views and the global QMD caches are cheap to
399
+ * rebuild and are not patched, so they are still evicted here. Every other
400
+ * layer remains reachable for the wholesale `invalidateAllForDir` /
401
+ * `clearMemoryCache` chokepoint used by bulk/ambiguous mutations.
402
+ */
403
+ export function invalidateDerivedAndGlobalForDir(baseDir: string): void {
404
+ // Preserve the layers a single-file write intentionally keeps warm (issue
405
+ // #1902, Codex Medium): hot-memories is patched + re-keyed in place; the
406
+ // entities layer is keyed on memory-status, which plain create/content writes
407
+ // deliberately do NOT bump (so entity retrieval stays warm); and the
408
+ // archive-memories layer is version-keyed on the archive tier, untouched by a
409
+ // hot-tier write. Evict ONLY the derived (episode/rule) and global (QMD) views.
410
+ for (const layer of ALL_CACHE_LAYERS) {
411
+ if (
412
+ layer.name === "hot-memories" ||
413
+ layer.name === "entities" ||
414
+ layer.name === "archive-memories"
415
+ ) {
416
+ continue;
417
+ }
418
+ layer.invalidateForDir(baseDir);
419
+ }
420
+ }
421
+
298
422
  export function clearMemoryCache(baseDir?: string): void {
299
423
  if (baseDir) {
300
424
  invalidateAllForDir(baseDir);