claude-memory-layer 1.0.27 → 1.0.29

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/.env.example +7 -0
  2. package/AGENTS.md +11 -0
  3. package/README.md +374 -49
  4. package/benchmarks/replay/anonymized-real-sessions.json +48 -0
  5. package/dist/cli/index.js +10097 -6003
  6. package/dist/cli/index.js.map +4 -4
  7. package/dist/core/index.js +9745 -5587
  8. package/dist/core/index.js.map +4 -4
  9. package/dist/hooks/post-tool-use.js +6545 -5270
  10. package/dist/hooks/post-tool-use.js.map +4 -4
  11. package/dist/hooks/semantic-daemon.js +6646 -5354
  12. package/dist/hooks/semantic-daemon.js.map +4 -4
  13. package/dist/hooks/session-end.js +6618 -5347
  14. package/dist/hooks/session-end.js.map +4 -4
  15. package/dist/hooks/session-start.js +6619 -5354
  16. package/dist/hooks/session-start.js.map +4 -4
  17. package/dist/hooks/stop.js +6614 -5325
  18. package/dist/hooks/stop.js.map +4 -4
  19. package/dist/hooks/user-prompt-submit.js +6702 -5356
  20. package/dist/hooks/user-prompt-submit.js.map +4 -4
  21. package/dist/index.js +13537 -0
  22. package/dist/index.js.map +7 -0
  23. package/dist/mcp/index.js +20770 -0
  24. package/dist/mcp/index.js.map +7 -0
  25. package/dist/server/api/index.js +6632 -5319
  26. package/dist/server/api/index.js.map +4 -4
  27. package/dist/server/index.js +6667 -5340
  28. package/dist/server/index.js.map +4 -4
  29. package/dist/services/memory-service.js +6568 -5350
  30. package/dist/services/memory-service.js.map +4 -4
  31. package/dist/ui/assets/js/bootstrap.js +244 -0
  32. package/dist/ui/assets/js/chat.js +373 -0
  33. package/dist/ui/assets/js/disclosure.js +232 -0
  34. package/dist/ui/assets/js/modals.js +298 -0
  35. package/dist/ui/assets/js/overview.js +655 -0
  36. package/dist/ui/assets/js/state.js +72 -0
  37. package/dist/ui/assets/js/views.js +468 -0
  38. package/dist/ui/index.html +43 -1
  39. package/dist/ui/index.ts +3 -0
  40. package/dist/ui/style.css +222 -0
  41. package/docs/ARCHITECTURE_COMPARISON_AND_RECOMMENDATIONS.md +627 -0
  42. package/docs/HERMES_MEMORY_INGESTION_ANALYSIS.md +440 -0
  43. package/docs/MEMORY_USEFULNESS_AUDIT.md +371 -0
  44. package/docs/MEMORY_USEFULNESS_AUDIT_RAW.json +80 -0
  45. package/docs/MEMSEARCH_PROJECT_STRUCTURE_ANALYSIS.md +333 -0
  46. package/docs/PRODUCT_VALIDATION_MATRIX.md +82 -0
  47. package/docs/PROJECT_STRUCTURE_ANALYSIS.md +421 -0
  48. package/docs/REFACTORING_MILESTONES_AND_ISSUES.md +501 -0
  49. package/docs/REFACTORING_PLAN_THIN_CORE.md +414 -0
  50. package/docs/REFERENCE_PROJECT_ANALYSES.md +25 -0
  51. package/docs/SUPERLOCALMEMORY_PROJECT_STRUCTURE_ANALYSIS.md +452 -0
  52. package/docs/TARGET_ARCHITECTURE_AND_FOLDER_STRUCTURE.md +446 -0
  53. package/docs/architecture/comparison-index.md +47 -0
  54. package/docs/reports/codex-real-data-validation-20260505T040447Z.md +46 -0
  55. package/package.json +12 -5
  56. package/scripts/build.ts +25 -8
  57. package/scripts/generate-session-qrels.ts +126 -0
  58. package/scripts/postinstall-embedding-backend.cjs +142 -0
  59. package/scripts/replay-retrieval-benchmark.ts +69 -0
  60. package/specs/thin-core-refactor/context.md +275 -0
  61. package/specs/thin-core-refactor/plan.md +536 -0
  62. package/specs/thin-core-refactor/spec.md +465 -0
  63. package/src/adapters/claude/capture/index.ts +3 -0
  64. package/src/adapters/claude/context/index.ts +3 -0
  65. package/src/adapters/claude/hooks/index.ts +21 -0
  66. package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
  67. package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
  68. package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
  69. package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
  70. package/src/adapters/claude/hooks/session-end.ts +59 -0
  71. package/src/adapters/claude/hooks/session-start.ts +73 -0
  72. package/src/adapters/claude/hooks/stop.ts +128 -0
  73. package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
  74. package/src/adapters/claude/index.ts +4 -0
  75. package/src/adapters/claude/transcript/index.ts +4 -0
  76. package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
  77. package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
  78. package/src/apps/cli/claude-settings-hooks.ts +138 -0
  79. package/src/apps/cli/codex-import-runner.ts +125 -0
  80. package/src/apps/cli/codex-validation-output.ts +95 -0
  81. package/src/apps/cli/hermes-import-runner.ts +130 -0
  82. package/src/apps/cli/hermes-validation-output.ts +91 -0
  83. package/src/apps/cli/index.ts +1731 -0
  84. package/src/apps/cli/mcp-install.ts +106 -0
  85. package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
  86. package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
  87. package/src/apps/dashboard/assets/js/chat.js +373 -0
  88. package/src/apps/dashboard/assets/js/disclosure.js +232 -0
  89. package/src/apps/dashboard/assets/js/modals.js +298 -0
  90. package/src/apps/dashboard/assets/js/overview.js +655 -0
  91. package/src/apps/dashboard/assets/js/state.js +72 -0
  92. package/src/apps/dashboard/assets/js/views.js +468 -0
  93. package/src/{ui → apps/dashboard}/index.html +43 -1
  94. package/src/apps/dashboard/index.ts +3 -0
  95. package/src/{ui → apps/dashboard}/style.css +222 -0
  96. package/src/apps/index.ts +5 -0
  97. package/src/apps/server/api/chat.ts +244 -0
  98. package/src/apps/server/api/citations.ts +105 -0
  99. package/src/apps/server/api/events.ts +137 -0
  100. package/src/apps/server/api/health.ts +53 -0
  101. package/src/apps/server/api/index.ts +26 -0
  102. package/src/apps/server/api/projects.ts +74 -0
  103. package/src/apps/server/api/search.ts +184 -0
  104. package/src/apps/server/api/sessions.ts +115 -0
  105. package/src/apps/server/api/stats.ts +723 -0
  106. package/src/apps/server/api/turns.ts +143 -0
  107. package/src/apps/server/api/utils.ts +65 -0
  108. package/src/apps/server/index.ts +111 -0
  109. package/src/cli/index.ts +2 -1311
  110. package/src/cli/retrieval-disclosure-output.ts +2 -0
  111. package/src/compat/index.ts +5 -0
  112. package/src/core/derive/fact-deriver.ts +170 -0
  113. package/src/core/derive/index.ts +2 -0
  114. package/src/core/derive/summary-deriver.ts +76 -0
  115. package/src/core/embedder.ts +4 -152
  116. package/src/core/engine/embedding-maintenance-service.ts +187 -0
  117. package/src/core/engine/endless-memory-services.ts +4 -0
  118. package/src/core/engine/index.ts +19 -0
  119. package/src/core/engine/memory-engine-services.ts +170 -0
  120. package/src/core/engine/memory-ingest-service.ts +317 -0
  121. package/src/core/engine/memory-query-service.ts +173 -0
  122. package/src/core/engine/memory-runtime-service.ts +162 -0
  123. package/src/core/engine/memory-service-composition.ts +231 -0
  124. package/src/core/engine/retrieval-analytics-service.ts +181 -0
  125. package/src/core/engine/retrieval-disclosure-service.ts +420 -0
  126. package/src/core/engine/retrieval-orchestrator.ts +377 -0
  127. package/src/core/engine/retrieval-services.ts +176 -0
  128. package/src/core/engine/shared-memory-services.ts +4 -0
  129. package/src/core/entity-repo.ts +1 -3
  130. package/src/core/event-store.ts +3 -3
  131. package/src/core/evidence-aligner.ts +2 -2
  132. package/src/core/external-market-context.ts +582 -0
  133. package/src/core/graduation.ts +2 -3
  134. package/src/core/index.ts +21 -0
  135. package/src/core/matcher.ts +2 -4
  136. package/src/core/model/memory-fact.ts +30 -0
  137. package/src/core/model/memory-rule.ts +14 -0
  138. package/src/core/model/memory-summary.ts +21 -0
  139. package/src/core/model/raw-event.ts +28 -0
  140. package/src/core/model/retrieval-result.ts +35 -0
  141. package/src/core/privacy/filter.ts +21 -10
  142. package/src/core/product-validation-matrix.ts +314 -0
  143. package/src/core/progressive-retriever.ts +1 -2
  144. package/src/core/registry/project-path.ts +54 -0
  145. package/src/core/registry/session-registry.ts +69 -0
  146. package/src/core/replay-evaluator.ts +625 -0
  147. package/src/core/retrieval-benchmark.ts +117 -0
  148. package/src/core/retrieval-quality.ts +109 -0
  149. package/src/core/retriever.ts +53 -15
  150. package/src/core/session-qrels.ts +360 -0
  151. package/src/core/shared-event-store.ts +1 -1
  152. package/src/core/sqlite-event-store.ts +35 -11
  153. package/src/core/task/blocker-resolver.ts +2 -2
  154. package/src/core/task/task-resolver.ts +0 -1
  155. package/src/core/vector-outbox.ts +1 -10
  156. package/src/core/vector-worker.ts +1 -1
  157. package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
  158. package/src/extensions/endless-memory/index.ts +1 -0
  159. package/src/extensions/index.ts +5 -0
  160. package/src/extensions/mcp/handlers.ts +960 -0
  161. package/src/extensions/mcp/index.ts +48 -0
  162. package/src/extensions/mcp/tools.ts +252 -0
  163. package/src/extensions/shared-memory/index.ts +1 -0
  164. package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
  165. package/src/extensions/vector/embedder.ts +197 -0
  166. package/src/extensions/vector/index.ts +1 -0
  167. package/src/hooks/post-tool-use.ts +3 -236
  168. package/src/hooks/semantic-daemon-client.ts +1 -208
  169. package/src/hooks/semantic-daemon.ts +6 -271
  170. package/src/hooks/session-end.ts +4 -79
  171. package/src/hooks/session-start.ts +4 -73
  172. package/src/hooks/stop.ts +3 -173
  173. package/src/hooks/user-prompt-submit.ts +3 -338
  174. package/src/index.ts +13 -0
  175. package/src/mcp/handlers.ts +2 -212
  176. package/src/mcp/index.ts +3 -46
  177. package/src/mcp/tools.ts +2 -78
  178. package/src/server/api/chat.ts +2 -244
  179. package/src/server/api/citations.ts +2 -105
  180. package/src/server/api/events.ts +2 -137
  181. package/src/server/api/health.ts +2 -53
  182. package/src/server/api/index.ts +2 -26
  183. package/src/server/api/projects.ts +2 -74
  184. package/src/server/api/search.ts +2 -102
  185. package/src/server/api/sessions.ts +2 -115
  186. package/src/server/api/stats.ts +2 -724
  187. package/src/server/api/turns.ts +2 -143
  188. package/src/server/api/utils.ts +2 -46
  189. package/src/server/index.ts +2 -100
  190. package/src/services/bootstrap-organizer.ts +46 -26
  191. package/src/services/codex-session-history-importer.ts +521 -29
  192. package/src/services/hermes-session-history-importer.ts +733 -0
  193. package/src/services/memory-service-config.ts +36 -0
  194. package/src/services/memory-service-registry.ts +150 -0
  195. package/src/services/memory-service.ts +211 -1325
  196. package/src/services/session-history-importer.ts +58 -14
  197. package/tests/README.md +23 -0
  198. package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
  199. package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
  200. package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
  201. package/tests/apps/app-layer-boundary.test.ts +48 -0
  202. package/tests/apps/claude-settings-hooks.test.ts +107 -0
  203. package/tests/apps/cli-disclosure-output.test.ts +212 -0
  204. package/tests/apps/codex-import-runner.test.ts +99 -0
  205. package/tests/apps/codex-validation-output.test.ts +100 -0
  206. package/tests/apps/hermes-import-runner.test.ts +99 -0
  207. package/tests/apps/mcp-install-command.test.ts +59 -0
  208. package/tests/apps/package-build-entrypoints.test.ts +30 -0
  209. package/tests/apps/postinstall-embedding-backend.test.ts +167 -0
  210. package/tests/apps/search-api-disclosure.test.ts +162 -0
  211. package/tests/apps/stats-api-lightweight.test.ts +67 -0
  212. package/tests/apps/ui-disclosure-output.test.ts +140 -0
  213. package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
  214. package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
  215. package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
  216. package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
  217. package/tests/core/embedding-maintenance-service.test.ts +282 -0
  218. package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
  219. package/tests/core/external-market-context.test.ts +209 -0
  220. package/tests/core/fact-deriver.test.ts +79 -0
  221. package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
  222. package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
  223. package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
  224. package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
  225. package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
  226. package/tests/core/memory-engine-services.test.ts +240 -0
  227. package/tests/core/memory-ingest-service.test.ts +296 -0
  228. package/tests/core/memory-query-service.test.ts +129 -0
  229. package/tests/core/memory-runtime-service.test.ts +201 -0
  230. package/tests/core/memory-service-composition.test.ts +192 -0
  231. package/tests/core/memory-service-config.test.ts +41 -0
  232. package/tests/core/memory-service-facade.test.ts +30 -0
  233. package/tests/core/memory-service-registry.test.ts +206 -0
  234. package/tests/core/product-validation-matrix.test.ts +61 -0
  235. package/tests/core/project-registry.test.ts +78 -0
  236. package/tests/core/replay-evaluator.test.ts +181 -0
  237. package/tests/core/retrieval-analytics-service.test.ts +210 -0
  238. package/tests/core/retrieval-benchmark.test.ts +93 -0
  239. package/tests/core/retrieval-disclosure-service.test.ts +264 -0
  240. package/tests/core/retrieval-orchestrator.test.ts +403 -0
  241. package/tests/core/retrieval-quality.test.ts +31 -0
  242. package/tests/core/retrieval-services.test.ts +185 -0
  243. package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
  244. package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
  245. package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
  246. package/tests/core/session-history-importer-filter.test.ts +78 -0
  247. package/tests/core/session-qrels.test.ts +250 -0
  248. package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
  249. package/tests/core/summary-deriver.test.ts +66 -0
  250. package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
  251. package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
  252. package/tests/extensions/endless-memory-services.test.ts +325 -0
  253. package/tests/extensions/mcp-context-tools.test.ts +905 -0
  254. package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
  255. package/tests/extensions/mcp-package-build.test.ts +22 -0
  256. package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
  257. package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
  258. package/tests/extensions/shared-memory-services.test.ts +309 -0
  259. package/tests/extensions/vector-extension-boundary.test.ts +21 -0
  260. package/.claude/settings.local.json +0 -25
  261. package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
  262. package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
  263. package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
  264. package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
  265. package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
  266. package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
  267. package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
  268. package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
  269. package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
  270. package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
  271. package/.npm-cache/_update-notifier-last-checked +0 -0
  272. package/bootstrap-kb/decisions/decisions.md +0 -244
  273. package/bootstrap-kb/glossary/glossary.md +0 -46
  274. package/bootstrap-kb/modules/.claude-plugin.md +0 -22
  275. package/bootstrap-kb/modules/agents.md.md +0 -15
  276. package/bootstrap-kb/modules/claude.md.md +0 -15
  277. package/bootstrap-kb/modules/context.md.md +0 -15
  278. package/bootstrap-kb/modules/docs.md +0 -18
  279. package/bootstrap-kb/modules/handoff.md.md +0 -15
  280. package/bootstrap-kb/modules/package-lock.json.md +0 -15
  281. package/bootstrap-kb/modules/package.json.md +0 -15
  282. package/bootstrap-kb/modules/plan.md.md +0 -15
  283. package/bootstrap-kb/modules/readme.md.md +0 -15
  284. package/bootstrap-kb/modules/scripts.md +0 -26
  285. package/bootstrap-kb/modules/spec.md.md +0 -15
  286. package/bootstrap-kb/modules/specs.md +0 -20
  287. package/bootstrap-kb/modules/src.md +0 -51
  288. package/bootstrap-kb/modules/tests.md +0 -42
  289. package/bootstrap-kb/modules/tsconfig.json.md +0 -15
  290. package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
  291. package/bootstrap-kb/overview/overview.md +0 -40
  292. package/bootstrap-kb/sources/manifest.json +0 -950
  293. package/bootstrap-kb/sources/manifest.md +0 -227
  294. package/bootstrap-kb/timeline/timeline.md +0 -57
  295. package/claude-memory-layer-1.0.14.tgz +0 -0
  296. package/d.sh +0 -3
  297. package/deploy.sh +0 -3
  298. package/dist/ui/app.js +0 -2101
  299. package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
  300. package/memory/_index.md +0 -419
  301. package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
  302. package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
  303. package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
  304. package/memory/agent_response/uncategorized/2026-03-05.md +0 -157
  305. package/memory/default/uncategorized/2026-02-25.md +0 -4839
  306. package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
  307. package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
  308. package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
  309. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
  310. package/memory/specs/citations-system/2026-02-25.md +0 -1121
  311. package/memory/specs/endless-mode/2026-02-25.md +0 -1392
  312. package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
  313. package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
  314. package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
  315. package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
  316. package/memory/specs/private-tags/2026-02-25.md +0 -1057
  317. package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
  318. package/memory/specs/task-entity-system/2026-02-25.md +0 -924
  319. package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
  320. package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
  321. package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
  322. package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
  323. package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
  324. package/memory/tool_observation/uncategorized/2026-03-05.md +0 -33
  325. package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
  326. package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
  327. package/memory/user_prompt/uncategorized/2026-03-05.md +0 -6
  328. package/specs/optional-duckdb/context.md +0 -77
  329. package/specs/optional-duckdb/plan.md +0 -142
  330. package/specs/optional-duckdb/spec.md +0 -35
  331. package/src/ui/app.js +0 -2101
@@ -0,0 +1,201 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ createMemoryRuntimeService,
5
+ type MemoryRuntimeServicesFactories
6
+ } from '../../src/core/engine/memory-runtime-service.js';
7
+ import type { EventStore } from '../../src/core/event-store.js';
8
+ import type { Embedder } from '../../src/core/embedder.js';
9
+ import type { GraduationPipeline } from '../../src/core/graduation.js';
10
+ import type { Retriever } from '../../src/core/retriever.js';
11
+ import type { VectorStore } from '../../src/core/vector-store.js';
12
+
13
+ function makeHarness(options?: { readOnly?: boolean; lightweightMode?: boolean; embeddingOnly?: boolean }) {
14
+ const calls: string[] = [];
15
+ const eventStore = { marker: 'event-store' } as unknown as EventStore;
16
+ const vectorStore = {
17
+ initialize: async () => { calls.push('vector.initialize'); }
18
+ } as unknown as VectorStore;
19
+ const embedder = {
20
+ initialize: async () => { calls.push('embedder.initialize'); }
21
+ } as unknown as Embedder;
22
+ const graduation = {
23
+ marker: 'graduation',
24
+ recordAccess: (eventId: string, sessionId: string, confidence: number = 1.0) => {
25
+ calls.push(`graduation.recordAccess:${eventId}:${sessionId}:${confidence}`);
26
+ }
27
+ } as unknown as GraduationPipeline;
28
+ const retriever = {
29
+ setGraduationPipeline: (pipeline: GraduationPipeline) => {
30
+ calls.push(pipeline === graduation ? 'retriever.setGraduationPipeline' : 'retriever.setGraduationPipeline:unknown');
31
+ }
32
+ } as unknown as Retriever;
33
+
34
+ const vectorWorker = {
35
+ start: () => { calls.push('vectorWorker.start'); },
36
+ stop: () => { calls.push('vectorWorker.stop'); },
37
+ processAll: async () => {
38
+ calls.push('vectorWorker.processAll');
39
+ return 3;
40
+ }
41
+ };
42
+ const graduationWorker = {
43
+ start: () => { calls.push('graduationWorker.start'); },
44
+ stop: () => { calls.push('graduationWorker.stop'); },
45
+ forceRun: async () => {
46
+ calls.push('graduationWorker.forceRun');
47
+ return { evaluated: 2, graduated: 1, byLevel: { L0: 1 } };
48
+ }
49
+ };
50
+
51
+ const factories: MemoryRuntimeServicesFactories = {
52
+ createVectorWorker: (receivedEventStore, receivedVectorStore, receivedEmbedder) => {
53
+ calls.push(
54
+ receivedEventStore === eventStore && receivedVectorStore === vectorStore && receivedEmbedder === embedder
55
+ ? 'createVectorWorker'
56
+ : 'createVectorWorker:unknown'
57
+ );
58
+ return vectorWorker as unknown as ReturnType<NonNullable<MemoryRuntimeServicesFactories['createVectorWorker']>>;
59
+ },
60
+ createGraduationWorker: (receivedEventStore, receivedGraduation) => {
61
+ calls.push(
62
+ receivedEventStore === eventStore && receivedGraduation === graduation
63
+ ? 'createGraduationWorker'
64
+ : 'createGraduationWorker:unknown'
65
+ );
66
+ return graduationWorker as unknown as ReturnType<NonNullable<MemoryRuntimeServicesFactories['createGraduationWorker']>>;
67
+ }
68
+ };
69
+
70
+ const service = createMemoryRuntimeService({
71
+ sqliteStore: {
72
+ initialize: async () => { calls.push('sqlite.initialize'); },
73
+ close: async () => { calls.push('sqlite.close'); }
74
+ },
75
+ eventStore,
76
+ vectorStore,
77
+ embedder,
78
+ retriever,
79
+ graduation,
80
+ endlessMemoryServices: {
81
+ initializeFromSavedMode: async () => { calls.push('endless.initializeFromSavedMode'); },
82
+ shutdown: () => { calls.push('endless.shutdown'); }
83
+ },
84
+ sharedMemoryServices: {
85
+ initialize: async () => { calls.push('shared.initialize'); },
86
+ close: async () => { calls.push('shared.close'); }
87
+ },
88
+ readOnly: options?.readOnly ?? false,
89
+ lightweightMode: options?.lightweightMode ?? false,
90
+ embeddingOnly: options?.embeddingOnly ?? false,
91
+ factories
92
+ });
93
+
94
+ return { service, calls, vectorWorker };
95
+ }
96
+
97
+ describe('createMemoryRuntimeService', () => {
98
+ it('initializes only sqlite in lightweight mode and keeps initialize idempotent', async () => {
99
+ const harness = makeHarness({ lightweightMode: true });
100
+
101
+ await harness.service.initialize();
102
+ await harness.service.initialize();
103
+
104
+ expect(harness.service.isInitialized()).toBe(true);
105
+ expect(harness.calls).toEqual(['sqlite.initialize']);
106
+ await expect(harness.service.processPendingEmbeddings()).resolves.toBe(0);
107
+ await expect(harness.service.forceGraduation()).resolves.toEqual({ evaluated: 0, graduated: 0, byLevel: {} });
108
+ });
109
+
110
+ it('records memory access through the graduation pipeline without initializing runtime workers', () => {
111
+ const harness = makeHarness();
112
+
113
+ harness.service.recordMemoryAccess('event-1', 'session-2', 0.42);
114
+
115
+ expect(harness.service.isInitialized()).toBe(false);
116
+ expect(harness.calls).toEqual(['graduation.recordAccess:event-1:session-2:0.42']);
117
+ });
118
+
119
+ it('starts vector and graduation workers plus writable lifecycle services by default', async () => {
120
+ const harness = makeHarness();
121
+
122
+ await harness.service.initialize();
123
+
124
+ expect(harness.calls).toEqual([
125
+ 'sqlite.initialize',
126
+ 'vector.initialize',
127
+ 'embedder.initialize',
128
+ 'createVectorWorker',
129
+ 'vectorWorker.start',
130
+ 'retriever.setGraduationPipeline',
131
+ 'createGraduationWorker',
132
+ 'graduationWorker.start',
133
+ 'endless.initializeFromSavedMode',
134
+ 'shared.initialize'
135
+ ]);
136
+ expect(harness.service.getVectorWorker()).toBe(harness.vectorWorker);
137
+ await expect(harness.service.processPendingEmbeddings()).resolves.toBe(3);
138
+ await expect(harness.service.forceGraduation()).resolves.toEqual({ evaluated: 2, graduated: 1, byLevel: { L0: 1 } });
139
+ });
140
+
141
+ it('skips workers and write lifecycle services in read-only mode', async () => {
142
+ const harness = makeHarness({ readOnly: true });
143
+
144
+ await harness.service.initialize();
145
+
146
+ expect(harness.calls).toEqual([
147
+ 'sqlite.initialize',
148
+ 'vector.initialize',
149
+ 'embedder.initialize'
150
+ ]);
151
+ expect(harness.service.getVectorWorker()).toBeNull();
152
+ });
153
+
154
+ it('embedding-only mode starts vector worker but skips graduation worker', async () => {
155
+ const harness = makeHarness({ embeddingOnly: true });
156
+
157
+ await harness.service.initialize();
158
+
159
+ expect(harness.calls).toEqual([
160
+ 'sqlite.initialize',
161
+ 'vector.initialize',
162
+ 'embedder.initialize',
163
+ 'createVectorWorker',
164
+ 'vectorWorker.start',
165
+ 'endless.initializeFromSavedMode',
166
+ 'shared.initialize'
167
+ ]);
168
+ await expect(harness.service.forceGraduation()).resolves.toEqual({ evaluated: 0, graduated: 0, byLevel: {} });
169
+ });
170
+
171
+ it('returns a fresh empty graduation result when no graduation worker exists', async () => {
172
+ const harness = makeHarness({ embeddingOnly: true });
173
+ await harness.service.initialize();
174
+
175
+ const first = await harness.service.forceGraduation();
176
+ first.byLevel.L0 = 99;
177
+ first.evaluated = 99;
178
+
179
+ const second = await harness.service.forceGraduation();
180
+
181
+ expect(second).toEqual({ evaluated: 0, graduated: 0, byLevel: {} });
182
+ expect(second).not.toBe(first);
183
+ expect(second.byLevel).not.toBe(first.byLevel);
184
+ });
185
+
186
+ it('shuts down workers and backing services in the historical MemoryService order', async () => {
187
+ const harness = makeHarness();
188
+ await harness.service.initialize();
189
+ harness.calls.length = 0;
190
+
191
+ await harness.service.shutdown();
192
+
193
+ expect(harness.calls).toEqual([
194
+ 'graduationWorker.stop',
195
+ 'endless.shutdown',
196
+ 'vectorWorker.stop',
197
+ 'shared.close',
198
+ 'sqlite.close'
199
+ ]);
200
+ });
201
+ });
@@ -0,0 +1,192 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ createMemoryServiceComposition,
5
+ type MemoryServiceCompositionFactories
6
+ } from '../../src/core/engine/memory-service-composition.js';
7
+ import type { SharedStoreConfig, ToolObservationPayload } from '../../src/core/types.js';
8
+
9
+ const sharedStoreConfig: SharedStoreConfig = {
10
+ enabled: true,
11
+ autoPromote: true,
12
+ searchShared: true,
13
+ minConfidenceForPromotion: 0.8,
14
+ sharedStoragePath: '~/shared-memory'
15
+ };
16
+
17
+ describe('createMemoryServiceComposition', () => {
18
+ it('wires engine, endless, shared, runtime, and embedding maintenance services for MemoryService', async () => {
19
+ const initialize = vi.fn(async () => undefined);
20
+ let projectHash: string | null = 'project-hash-1';
21
+ let projectPath: string | null = '/repo/project';
22
+
23
+ const sqliteStore = {
24
+ clearEmbeddingOutbox: vi.fn(async () => undefined),
25
+ getEventsPage: vi.fn(async () => [{ id: 'event-1', content: 'event content' }]),
26
+ enqueueForEmbedding: vi.fn(async () => undefined)
27
+ };
28
+ const vectorStore = { name: 'vector-store' };
29
+ const embedder = { getModelName: vi.fn(() => 'test-embedding-model') };
30
+ const retriever = { setSharedStores: vi.fn() };
31
+ const graduation = { name: 'graduation' };
32
+ const ingestService = { name: 'ingest' };
33
+ const queryService = { name: 'query' };
34
+ const retrievalOrchestrator = { name: 'retrieval-orchestrator' };
35
+ const retrievalDisclosureService = { name: 'retrieval-disclosure' };
36
+ const retrievalAnalyticsService = { name: 'retrieval-analytics' };
37
+ const endlessMemoryServices = { name: 'endless-memory' };
38
+ const sharedMemoryServices = {
39
+ isEnabled: vi.fn(() => true),
40
+ getEntryForDisclosure: vi.fn(async (entryId: string) => ({ id: entryId }))
41
+ };
42
+ const runtimeService = { getVectorWorker: vi.fn(() => ({ name: 'vector-worker' })) };
43
+ const embeddingMaintenanceService = { getEmbeddingModelName: vi.fn(() => 'maintenance-model') };
44
+
45
+ const factories: MemoryServiceCompositionFactories = {
46
+ expandPath: vi.fn((targetPath: string) => targetPath.replace('~', '/home/test')),
47
+ createToolObservationEmbedding: vi.fn((toolName: string) => `embedding:${toolName}`),
48
+ createMemoryEngineServices: vi.fn(() => ({
49
+ storagePath: '/expanded/storage',
50
+ sqliteStore,
51
+ vectorStore,
52
+ embedder,
53
+ matcher: { name: 'matcher' },
54
+ retriever,
55
+ retrievalOrchestrator,
56
+ retrievalDisclosureService,
57
+ retrievalAnalyticsService,
58
+ graduation,
59
+ mdMirror: { name: 'md-mirror' },
60
+ ingestService,
61
+ queryService
62
+ }) as any),
63
+ createEndlessMemoryServices: vi.fn(() => endlessMemoryServices as any),
64
+ createSharedMemoryServices: vi.fn(() => sharedMemoryServices as any),
65
+ createMemoryRuntimeService: vi.fn(() => runtimeService as any),
66
+ createEmbeddingMaintenanceService: vi.fn(() => embeddingMaintenanceService as any)
67
+ };
68
+
69
+ const composition = createMemoryServiceComposition({
70
+ config: {
71
+ storagePath: '~/memory-store',
72
+ embeddingModel: 'explicit-model',
73
+ readOnly: true,
74
+ lightweightMode: true,
75
+ embeddingOnly: true,
76
+ projectHash,
77
+ projectPath,
78
+ sharedStoreConfig
79
+ },
80
+ defaultSharedStoragePath: '/default/shared',
81
+ initialize,
82
+ getProjectHash: () => projectHash,
83
+ getProjectPath: () => projectPath,
84
+ factories
85
+ });
86
+
87
+ expect(factories.expandPath).toHaveBeenCalledWith('~/memory-store');
88
+ expect(factories.createMemoryEngineServices).toHaveBeenCalledWith(expect.objectContaining({
89
+ storagePath: '/home/test/memory-store',
90
+ readOnly: true,
91
+ embeddingModel: 'explicit-model',
92
+ cwd: '/repo/project',
93
+ initialize,
94
+ getProjectHash: expect.any(Function),
95
+ getProjectPath: expect.any(Function),
96
+ hasSharedStore: expect.any(Function),
97
+ sharedStore: expect.objectContaining({ get: expect.any(Function) }),
98
+ createToolObservationEmbedding: expect.any(Function)
99
+ }));
100
+
101
+ const engineOptions = vi.mocked(factories.createMemoryEngineServices).mock.calls[0][0];
102
+ expect(engineOptions.createToolObservationEmbedding({
103
+ toolName: 'Read',
104
+ metadata: { path: 'README.md' },
105
+ success: true
106
+ } as ToolObservationPayload)).toBe('embedding:Read');
107
+ expect(factories.createToolObservationEmbedding).toHaveBeenCalledWith(
108
+ 'Read',
109
+ { path: 'README.md' },
110
+ true
111
+ );
112
+
113
+ expect(factories.createEndlessMemoryServices).toHaveBeenCalledWith({
114
+ eventStore: sqliteStore,
115
+ configStore: sqliteStore,
116
+ initialize
117
+ });
118
+ expect(factories.createSharedMemoryServices).toHaveBeenCalledWith({
119
+ config: sharedStoreConfig,
120
+ defaultSharedStoragePath: '/default/shared',
121
+ readOnly: true,
122
+ expandPath: expect.any(Function),
123
+ embedder,
124
+ retriever
125
+ });
126
+ expect(factories.createMemoryRuntimeService).toHaveBeenCalledWith(expect.objectContaining({
127
+ sqliteStore,
128
+ eventStore: sqliteStore,
129
+ vectorStore,
130
+ embedder,
131
+ retriever,
132
+ graduation,
133
+ endlessMemoryServices,
134
+ sharedMemoryServices,
135
+ readOnly: true,
136
+ lightweightMode: true,
137
+ embeddingOnly: true
138
+ }));
139
+ expect(factories.createEmbeddingMaintenanceService).toHaveBeenCalledWith(expect.objectContaining({
140
+ storagePath: '/home/test/memory-store',
141
+ initialize,
142
+ getEmbeddingModelName: expect.any(Function),
143
+ vectorStore,
144
+ eventStore: expect.objectContaining({
145
+ clearEmbeddingOutbox: expect.any(Function),
146
+ getEventsPage: expect.any(Function),
147
+ enqueueForEmbedding: expect.any(Function)
148
+ }),
149
+ getVectorWorker: expect.any(Function)
150
+ }));
151
+
152
+ const embeddingOptions = vi.mocked(factories.createEmbeddingMaintenanceService).mock.calls[0][0];
153
+ expect(embeddingOptions.getEmbeddingModelName()).toBe('test-embedding-model');
154
+ expect(embeddingOptions.getVectorWorker()).toEqual({ name: 'vector-worker' });
155
+ await embeddingOptions.eventStore.clearEmbeddingOutbox();
156
+ await expect(embeddingOptions.eventStore.getEventsPage(10, 0)).resolves.toEqual([
157
+ { id: 'event-1', content: 'event content' }
158
+ ]);
159
+ await embeddingOptions.eventStore.enqueueForEmbedding('event-1', 'event content');
160
+ expect(sqliteStore.clearEmbeddingOutbox).toHaveBeenCalled();
161
+ expect(sqliteStore.getEventsPage).toHaveBeenCalledWith(10, 0);
162
+ expect(sqliteStore.enqueueForEmbedding).toHaveBeenCalledWith('event-1', 'event content');
163
+
164
+ expect(await engineOptions.sharedStore?.get('shared-1')).toEqual({ id: 'shared-1' });
165
+ expect(engineOptions.hasSharedStore()).toBe(true);
166
+ projectHash = 'project-hash-2';
167
+ projectPath = '/repo/other';
168
+ expect(engineOptions.getProjectHash()).toBe('project-hash-2');
169
+ expect(engineOptions.getProjectPath?.()).toBe('/repo/other');
170
+
171
+ expect(composition).toMatchObject({
172
+ storagePath: '/home/test/memory-store',
173
+ readOnly: true,
174
+ lightweightMode: true,
175
+ embeddingOnly: true,
176
+ sqliteStore,
177
+ vectorStore,
178
+ embedder,
179
+ retriever,
180
+ retrievalOrchestrator,
181
+ retrievalDisclosureService,
182
+ retrievalAnalyticsService,
183
+ graduation,
184
+ ingestService,
185
+ queryService,
186
+ endlessMemoryServices,
187
+ sharedMemoryServices,
188
+ runtimeService,
189
+ embeddingMaintenanceService
190
+ });
191
+ });
192
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ DEFAULT_ENABLED_SHARED_STORE_CONFIG as CONFIG_DEFAULT_ENABLED_SHARED_STORE_CONFIG,
5
+ DISABLED_SHARED_STORE_CONFIG as CONFIG_DISABLED_SHARED_STORE_CONFIG,
6
+ DEFAULT_SHARED_STORAGE_PATH as CONFIG_DEFAULT_SHARED_STORAGE_PATH
7
+ } from '../../src/services/memory-service-config.js';
8
+ import {
9
+ DEFAULT_ENABLED_SHARED_STORE_CONFIG as FACADE_DEFAULT_ENABLED_SHARED_STORE_CONFIG,
10
+ DISABLED_SHARED_STORE_CONFIG as FACADE_DISABLED_SHARED_STORE_CONFIG
11
+ } from '../../src/services/memory-service.js';
12
+
13
+ /**
14
+ * MemoryService should stay a thin compatibility facade. Shared defaults are
15
+ * owned by memory-service-config so registries can import them without pulling
16
+ * in the MemoryService class and creating a service-locator cycle.
17
+ */
18
+ describe('memory-service-config', () => {
19
+ it('owns disabled shared-store defaults while memory-service preserves the public export', () => {
20
+ expect(CONFIG_DISABLED_SHARED_STORE_CONFIG).toMatchObject({
21
+ enabled: false,
22
+ autoPromote: false,
23
+ searchShared: false,
24
+ minConfidenceForPromotion: 0.8
25
+ });
26
+ expect(CONFIG_DISABLED_SHARED_STORE_CONFIG.sharedStoragePath).toContain('.claude-code');
27
+ expect(CONFIG_DISABLED_SHARED_STORE_CONFIG.sharedStoragePath).toContain('shared');
28
+ expect(FACADE_DISABLED_SHARED_STORE_CONFIG).toBe(CONFIG_DISABLED_SHARED_STORE_CONFIG);
29
+ });
30
+
31
+ it('owns enabled shared-store defaults while memory-service preserves the public export', () => {
32
+ expect(CONFIG_DEFAULT_ENABLED_SHARED_STORE_CONFIG).toEqual({
33
+ enabled: true,
34
+ autoPromote: true,
35
+ searchShared: true,
36
+ minConfidenceForPromotion: 0.8,
37
+ sharedStoragePath: CONFIG_DEFAULT_SHARED_STORAGE_PATH
38
+ });
39
+ expect(FACADE_DEFAULT_ENABLED_SHARED_STORE_CONFIG).toBe(CONFIG_DEFAULT_ENABLED_SHARED_STORE_CONFIG);
40
+ });
41
+ });
@@ -0,0 +1,30 @@
1
+ import { mkdtemp } from 'fs/promises';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import {
8
+ DISABLED_SHARED_STORE_CONFIG,
9
+ MemoryService
10
+ } from '../../src/services/memory-service.js';
11
+
12
+ describe('MemoryService facade delegation', () => {
13
+ it('reads the embedding model name through the embedding maintenance facade', async () => {
14
+ const storagePath = await mkdtemp(path.join(os.tmpdir(), 'memory-service-facade-'));
15
+ const service = new MemoryService({
16
+ storagePath,
17
+ sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
18
+ });
19
+ const serviceInternals = service as unknown as {
20
+ embedder: { getModelName(): string };
21
+ embeddingMaintenanceService: { getEmbeddingModelName(): string };
22
+ };
23
+ serviceInternals.embedder = { getModelName: () => 'direct-embedder-model' };
24
+ serviceInternals.embeddingMaintenanceService = {
25
+ getEmbeddingModelName: () => 'maintenance-facade-model'
26
+ };
27
+
28
+ expect(service.getEmbeddingModelName()).toBe('maintenance-facade-model');
29
+ });
30
+ });
@@ -0,0 +1,206 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createMemoryServiceRegistry } from '../../src/services/memory-service-registry.js';
5
+ import type { SharedStoreConfig } from '../../src/core/types.js';
6
+
7
+ interface FakeService {
8
+ config: Record<string, unknown>;
9
+ }
10
+
11
+ const disabledSharedStoreConfig: SharedStoreConfig = {
12
+ enabled: false,
13
+ autoPromote: false,
14
+ searchShared: false,
15
+ minConfidenceForPromotion: 0.8,
16
+ sharedStoragePath: '/home/test/.claude-code/memory/shared'
17
+ };
18
+
19
+ function createRegistry(sessionProject?: { projectHash: string; projectPath: string }) {
20
+ const createdConfigs: Array<Record<string, unknown>> = [];
21
+ const registry = createMemoryServiceRegistry<FakeService>({
22
+ createService: (config) => {
23
+ createdConfigs.push(config as unknown as Record<string, unknown>);
24
+ return { config: config as unknown as Record<string, unknown> };
25
+ },
26
+ hashProjectPath: (projectPath) => `hash:${projectPath}`,
27
+ getProjectStoragePath: (projectPath) => `/storage/${projectPath}`,
28
+ getSessionProject: (sessionId) => sessionId === 'known-session' ? sessionProject ?? null : null,
29
+ homedir: () => '/home/test',
30
+ disabledSharedStoreConfig
31
+ });
32
+
33
+ return { registry, createdConfigs };
34
+ }
35
+
36
+ describe('createMemoryServiceRegistry', () => {
37
+ it('stays independent from the MemoryService facade to avoid service-locator cycles', () => {
38
+ const source = readFileSync('src/services/memory-service-registry.ts', 'utf8');
39
+
40
+ expect(source).not.toContain("from './memory-service.js'");
41
+ expect(source).not.toContain('from "./memory-service.js"');
42
+ });
43
+
44
+ it('caches the default writable service with global storage and disabled shared store', () => {
45
+ const { registry, createdConfigs } = createRegistry();
46
+
47
+ const first = registry.getDefaultMemoryService();
48
+ const second = registry.getDefaultMemoryService();
49
+
50
+ expect(first).toBe(second);
51
+ expect(createdConfigs).toHaveLength(1);
52
+ expect(first.config).toMatchObject({
53
+ storagePath: '~/.claude-code/memory',
54
+ analyticsEnabled: false,
55
+ sharedStoreConfig: disabledSharedStoreConfig
56
+ });
57
+ });
58
+
59
+ it('creates fresh read-only services to avoid holding locks', () => {
60
+ const { registry, createdConfigs } = createRegistry();
61
+
62
+ const first = registry.getReadOnlyMemoryService();
63
+ const second = registry.getReadOnlyMemoryService();
64
+
65
+ expect(first).not.toBe(second);
66
+ expect(createdConfigs).toHaveLength(2);
67
+ expect(first.config).toMatchObject({
68
+ storagePath: '~/.claude-code/memory',
69
+ readOnly: true,
70
+ analyticsEnabled: false,
71
+ sharedStoreConfig: disabledSharedStoreConfig
72
+ });
73
+ });
74
+
75
+ it('caches project services by project hash and preserves project scope config', () => {
76
+ const { registry, createdConfigs } = createRegistry();
77
+
78
+ const first = registry.getMemoryServiceForProject('/workspace/app');
79
+ const second = registry.getMemoryServiceForProject('/workspace/app');
80
+
81
+ expect(first).toBe(second);
82
+ expect(createdConfigs).toHaveLength(1);
83
+ expect(first.config).toMatchObject({
84
+ storagePath: '/storage//workspace/app',
85
+ projectHash: 'hash:/workspace/app',
86
+ projectPath: '/workspace/app',
87
+ analyticsEnabled: false,
88
+ sharedStoreConfig: disabledSharedStoreConfig
89
+ });
90
+ });
91
+
92
+ it('preserves the first shared store config for cached project services', () => {
93
+ const { registry, createdConfigs } = createRegistry();
94
+ const firstSharedConfig: SharedStoreConfig = {
95
+ ...disabledSharedStoreConfig,
96
+ enabled: true,
97
+ searchShared: true,
98
+ sharedStoragePath: '/shared/first'
99
+ };
100
+ const laterSharedConfig: SharedStoreConfig = {
101
+ ...disabledSharedStoreConfig,
102
+ enabled: true,
103
+ autoPromote: true,
104
+ sharedStoragePath: '/shared/later'
105
+ };
106
+
107
+ const first = registry.getMemoryServiceForProject('/workspace/app', firstSharedConfig);
108
+ const second = registry.getMemoryServiceForProject('/workspace/app', laterSharedConfig);
109
+
110
+ expect(second).toBe(first);
111
+ expect(createdConfigs).toHaveLength(1);
112
+ expect(first.config.sharedStoreConfig).toBe(firstSharedConfig);
113
+ expect(first.config.sharedStoreConfig).not.toBe(laterSharedConfig);
114
+ });
115
+
116
+ it('resolves session services from the session registry and falls back to global service', () => {
117
+ const { registry } = createRegistry({
118
+ projectHash: 'registered-hash',
119
+ projectPath: '/workspace/registered'
120
+ });
121
+
122
+ const projectService = registry.getMemoryServiceForSession('known-session');
123
+ const directService = registry.getMemoryServiceForProject('/workspace/registered');
124
+ const fallbackService = registry.getMemoryServiceForSession('unknown-session');
125
+
126
+ expect(projectService).toBe(directService);
127
+ expect(fallbackService).toBe(registry.getDefaultMemoryService());
128
+ });
129
+
130
+ it('keeps registry methods safe when callers destructure a custom registry', () => {
131
+ const { registry } = createRegistry({
132
+ projectHash: 'registered-hash',
133
+ projectPath: '/workspace/registered'
134
+ });
135
+ const {
136
+ getDefaultMemoryService,
137
+ getMemoryServiceForProject,
138
+ getMemoryServiceForSession
139
+ } = registry;
140
+
141
+ const projectService = getMemoryServiceForSession('known-session');
142
+ const directService = getMemoryServiceForProject('/workspace/registered');
143
+ const fallbackService = getMemoryServiceForSession('unknown-session');
144
+
145
+ expect(projectService).toBe(directService);
146
+ expect(fallbackService).toBe(getDefaultMemoryService());
147
+ });
148
+
149
+ it('caches lightweight services separately for project and global sessions', () => {
150
+ const { registry } = createRegistry({
151
+ projectHash: 'registered-hash',
152
+ projectPath: '/workspace/registered'
153
+ });
154
+
155
+ const projectService = registry.getLightweightMemoryService('known-session');
156
+ const sameProjectService = registry.getLightweightMemoryService('known-session');
157
+ const globalService = registry.getLightweightMemoryService('unknown-session');
158
+
159
+ expect(projectService).toBe(sameProjectService);
160
+ expect(projectService).not.toBe(globalService);
161
+ expect(projectService.config).toMatchObject({
162
+ storagePath: '/storage//workspace/registered',
163
+ projectHash: 'registered-hash',
164
+ projectPath: '/workspace/registered',
165
+ lightweightMode: true,
166
+ analyticsEnabled: false,
167
+ sharedStoreConfig: disabledSharedStoreConfig
168
+ });
169
+ expect(globalService.config).toMatchObject({
170
+ storagePath: '/home/test/.claude-code/memory',
171
+ lightweightMode: true,
172
+ analyticsEnabled: false,
173
+ sharedStoreConfig: disabledSharedStoreConfig
174
+ });
175
+ });
176
+
177
+ it('caches lightweight project services for CLI read paths without reusing the full project service', () => {
178
+ const { registry, createdConfigs } = createRegistry();
179
+
180
+ const fullProjectService = registry.getMemoryServiceForProject('/workspace/app');
181
+ const lightweightProjectService = registry.getLightweightMemoryServiceForProject('/workspace/app');
182
+ const sameLightweightProjectService = registry.getLightweightMemoryServiceForProject('/workspace/app');
183
+
184
+ expect(lightweightProjectService).toBe(sameLightweightProjectService);
185
+ expect(lightweightProjectService).not.toBe(fullProjectService);
186
+ expect(createdConfigs).toHaveLength(2);
187
+ expect(lightweightProjectService.config).toMatchObject({
188
+ storagePath: '/storage//workspace/app',
189
+ projectHash: 'hash:/workspace/app',
190
+ projectPath: '/workspace/app',
191
+ lightweightMode: true,
192
+ analyticsEnabled: false,
193
+ sharedStoreConfig: disabledSharedStoreConfig
194
+ });
195
+ });
196
+
197
+ it('creates uncached services through the explicit factory facade', () => {
198
+ const { registry } = createRegistry();
199
+
200
+ const first = registry.createMemoryService({ storagePath: '/tmp/a' });
201
+ const second = registry.createMemoryService({ storagePath: '/tmp/a' });
202
+
203
+ expect(first).not.toBe(second);
204
+ expect(first.config).toMatchObject({ storagePath: '/tmp/a' });
205
+ });
206
+ });