claude-memory-layer 1.0.27 → 1.0.28

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 (329) hide show
  1. package/.env.example +7 -0
  2. package/AGENTS.md +11 -0
  3. package/README.md +184 -41
  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 +9 -5
  56. package/scripts/build.ts +25 -8
  57. package/scripts/generate-session-qrels.ts +126 -0
  58. package/scripts/replay-retrieval-benchmark.ts +69 -0
  59. package/specs/thin-core-refactor/context.md +275 -0
  60. package/specs/thin-core-refactor/plan.md +536 -0
  61. package/specs/thin-core-refactor/spec.md +465 -0
  62. package/src/adapters/claude/capture/index.ts +3 -0
  63. package/src/adapters/claude/context/index.ts +3 -0
  64. package/src/adapters/claude/hooks/index.ts +21 -0
  65. package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
  66. package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
  67. package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
  68. package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
  69. package/src/adapters/claude/hooks/session-end.ts +59 -0
  70. package/src/adapters/claude/hooks/session-start.ts +73 -0
  71. package/src/adapters/claude/hooks/stop.ts +128 -0
  72. package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
  73. package/src/adapters/claude/index.ts +4 -0
  74. package/src/adapters/claude/transcript/index.ts +4 -0
  75. package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
  76. package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
  77. package/src/apps/cli/claude-settings-hooks.ts +138 -0
  78. package/src/apps/cli/codex-import-runner.ts +125 -0
  79. package/src/apps/cli/codex-validation-output.ts +95 -0
  80. package/src/apps/cli/hermes-import-runner.ts +130 -0
  81. package/src/apps/cli/hermes-validation-output.ts +91 -0
  82. package/src/apps/cli/index.ts +1731 -0
  83. package/src/apps/cli/mcp-install.ts +106 -0
  84. package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
  85. package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
  86. package/src/apps/dashboard/assets/js/chat.js +373 -0
  87. package/src/apps/dashboard/assets/js/disclosure.js +232 -0
  88. package/src/apps/dashboard/assets/js/modals.js +298 -0
  89. package/src/apps/dashboard/assets/js/overview.js +655 -0
  90. package/src/apps/dashboard/assets/js/state.js +72 -0
  91. package/src/apps/dashboard/assets/js/views.js +468 -0
  92. package/src/{ui → apps/dashboard}/index.html +43 -1
  93. package/src/apps/dashboard/index.ts +3 -0
  94. package/src/{ui → apps/dashboard}/style.css +222 -0
  95. package/src/apps/index.ts +5 -0
  96. package/src/apps/server/api/chat.ts +244 -0
  97. package/src/apps/server/api/citations.ts +105 -0
  98. package/src/apps/server/api/events.ts +137 -0
  99. package/src/apps/server/api/health.ts +53 -0
  100. package/src/apps/server/api/index.ts +26 -0
  101. package/src/apps/server/api/projects.ts +74 -0
  102. package/src/apps/server/api/search.ts +184 -0
  103. package/src/apps/server/api/sessions.ts +115 -0
  104. package/src/apps/server/api/stats.ts +723 -0
  105. package/src/apps/server/api/turns.ts +143 -0
  106. package/src/apps/server/api/utils.ts +65 -0
  107. package/src/apps/server/index.ts +111 -0
  108. package/src/cli/index.ts +2 -1311
  109. package/src/cli/retrieval-disclosure-output.ts +2 -0
  110. package/src/compat/index.ts +5 -0
  111. package/src/core/derive/fact-deriver.ts +170 -0
  112. package/src/core/derive/index.ts +2 -0
  113. package/src/core/derive/summary-deriver.ts +76 -0
  114. package/src/core/embedder.ts +4 -152
  115. package/src/core/engine/embedding-maintenance-service.ts +187 -0
  116. package/src/core/engine/endless-memory-services.ts +4 -0
  117. package/src/core/engine/index.ts +19 -0
  118. package/src/core/engine/memory-engine-services.ts +170 -0
  119. package/src/core/engine/memory-ingest-service.ts +317 -0
  120. package/src/core/engine/memory-query-service.ts +173 -0
  121. package/src/core/engine/memory-runtime-service.ts +162 -0
  122. package/src/core/engine/memory-service-composition.ts +231 -0
  123. package/src/core/engine/retrieval-analytics-service.ts +181 -0
  124. package/src/core/engine/retrieval-disclosure-service.ts +420 -0
  125. package/src/core/engine/retrieval-orchestrator.ts +377 -0
  126. package/src/core/engine/retrieval-services.ts +176 -0
  127. package/src/core/engine/shared-memory-services.ts +4 -0
  128. package/src/core/entity-repo.ts +1 -3
  129. package/src/core/event-store.ts +3 -3
  130. package/src/core/evidence-aligner.ts +2 -2
  131. package/src/core/external-market-context.ts +582 -0
  132. package/src/core/graduation.ts +2 -3
  133. package/src/core/index.ts +21 -0
  134. package/src/core/matcher.ts +2 -4
  135. package/src/core/model/memory-fact.ts +30 -0
  136. package/src/core/model/memory-rule.ts +14 -0
  137. package/src/core/model/memory-summary.ts +21 -0
  138. package/src/core/model/raw-event.ts +28 -0
  139. package/src/core/model/retrieval-result.ts +35 -0
  140. package/src/core/privacy/filter.ts +21 -10
  141. package/src/core/product-validation-matrix.ts +314 -0
  142. package/src/core/progressive-retriever.ts +1 -2
  143. package/src/core/registry/project-path.ts +54 -0
  144. package/src/core/registry/session-registry.ts +69 -0
  145. package/src/core/replay-evaluator.ts +625 -0
  146. package/src/core/retrieval-benchmark.ts +117 -0
  147. package/src/core/retrieval-quality.ts +109 -0
  148. package/src/core/retriever.ts +53 -15
  149. package/src/core/session-qrels.ts +360 -0
  150. package/src/core/shared-event-store.ts +1 -1
  151. package/src/core/sqlite-event-store.ts +35 -11
  152. package/src/core/task/blocker-resolver.ts +2 -2
  153. package/src/core/task/task-resolver.ts +0 -1
  154. package/src/core/vector-outbox.ts +1 -10
  155. package/src/core/vector-worker.ts +1 -1
  156. package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
  157. package/src/extensions/endless-memory/index.ts +1 -0
  158. package/src/extensions/index.ts +5 -0
  159. package/src/extensions/mcp/handlers.ts +960 -0
  160. package/src/extensions/mcp/index.ts +48 -0
  161. package/src/extensions/mcp/tools.ts +252 -0
  162. package/src/extensions/shared-memory/index.ts +1 -0
  163. package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
  164. package/src/extensions/vector/embedder.ts +197 -0
  165. package/src/extensions/vector/index.ts +1 -0
  166. package/src/hooks/post-tool-use.ts +3 -236
  167. package/src/hooks/semantic-daemon-client.ts +1 -208
  168. package/src/hooks/semantic-daemon.ts +6 -271
  169. package/src/hooks/session-end.ts +4 -79
  170. package/src/hooks/session-start.ts +4 -73
  171. package/src/hooks/stop.ts +3 -173
  172. package/src/hooks/user-prompt-submit.ts +3 -338
  173. package/src/index.ts +13 -0
  174. package/src/mcp/handlers.ts +2 -212
  175. package/src/mcp/index.ts +3 -46
  176. package/src/mcp/tools.ts +2 -78
  177. package/src/server/api/chat.ts +2 -244
  178. package/src/server/api/citations.ts +2 -105
  179. package/src/server/api/events.ts +2 -137
  180. package/src/server/api/health.ts +2 -53
  181. package/src/server/api/index.ts +2 -26
  182. package/src/server/api/projects.ts +2 -74
  183. package/src/server/api/search.ts +2 -102
  184. package/src/server/api/sessions.ts +2 -115
  185. package/src/server/api/stats.ts +2 -724
  186. package/src/server/api/turns.ts +2 -143
  187. package/src/server/api/utils.ts +2 -46
  188. package/src/server/index.ts +2 -100
  189. package/src/services/bootstrap-organizer.ts +46 -26
  190. package/src/services/codex-session-history-importer.ts +521 -29
  191. package/src/services/hermes-session-history-importer.ts +733 -0
  192. package/src/services/memory-service-config.ts +36 -0
  193. package/src/services/memory-service-registry.ts +150 -0
  194. package/src/services/memory-service.ts +211 -1325
  195. package/src/services/session-history-importer.ts +58 -14
  196. package/tests/README.md +23 -0
  197. package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
  198. package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
  199. package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
  200. package/tests/apps/app-layer-boundary.test.ts +48 -0
  201. package/tests/apps/claude-settings-hooks.test.ts +107 -0
  202. package/tests/apps/cli-disclosure-output.test.ts +212 -0
  203. package/tests/apps/codex-import-runner.test.ts +99 -0
  204. package/tests/apps/codex-validation-output.test.ts +100 -0
  205. package/tests/apps/hermes-import-runner.test.ts +99 -0
  206. package/tests/apps/mcp-install-command.test.ts +59 -0
  207. package/tests/apps/package-build-entrypoints.test.ts +30 -0
  208. package/tests/apps/search-api-disclosure.test.ts +162 -0
  209. package/tests/apps/stats-api-lightweight.test.ts +67 -0
  210. package/tests/apps/ui-disclosure-output.test.ts +140 -0
  211. package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
  212. package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
  213. package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
  214. package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
  215. package/tests/core/embedding-maintenance-service.test.ts +282 -0
  216. package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
  217. package/tests/core/external-market-context.test.ts +209 -0
  218. package/tests/core/fact-deriver.test.ts +79 -0
  219. package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
  220. package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
  221. package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
  222. package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
  223. package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
  224. package/tests/core/memory-engine-services.test.ts +240 -0
  225. package/tests/core/memory-ingest-service.test.ts +296 -0
  226. package/tests/core/memory-query-service.test.ts +129 -0
  227. package/tests/core/memory-runtime-service.test.ts +201 -0
  228. package/tests/core/memory-service-composition.test.ts +192 -0
  229. package/tests/core/memory-service-config.test.ts +41 -0
  230. package/tests/core/memory-service-facade.test.ts +30 -0
  231. package/tests/core/memory-service-registry.test.ts +206 -0
  232. package/tests/core/product-validation-matrix.test.ts +61 -0
  233. package/tests/core/project-registry.test.ts +78 -0
  234. package/tests/core/replay-evaluator.test.ts +181 -0
  235. package/tests/core/retrieval-analytics-service.test.ts +210 -0
  236. package/tests/core/retrieval-benchmark.test.ts +93 -0
  237. package/tests/core/retrieval-disclosure-service.test.ts +264 -0
  238. package/tests/core/retrieval-orchestrator.test.ts +403 -0
  239. package/tests/core/retrieval-quality.test.ts +31 -0
  240. package/tests/core/retrieval-services.test.ts +185 -0
  241. package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
  242. package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
  243. package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
  244. package/tests/core/session-history-importer-filter.test.ts +78 -0
  245. package/tests/core/session-qrels.test.ts +250 -0
  246. package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
  247. package/tests/core/summary-deriver.test.ts +66 -0
  248. package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
  249. package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
  250. package/tests/extensions/endless-memory-services.test.ts +325 -0
  251. package/tests/extensions/mcp-context-tools.test.ts +905 -0
  252. package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
  253. package/tests/extensions/mcp-package-build.test.ts +22 -0
  254. package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
  255. package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
  256. package/tests/extensions/shared-memory-services.test.ts +309 -0
  257. package/tests/extensions/vector-extension-boundary.test.ts +21 -0
  258. package/.claude/settings.local.json +0 -25
  259. package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
  260. package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
  261. package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
  262. package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
  263. package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
  264. package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
  265. package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
  266. package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
  267. package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
  268. package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
  269. package/.npm-cache/_update-notifier-last-checked +0 -0
  270. package/bootstrap-kb/decisions/decisions.md +0 -244
  271. package/bootstrap-kb/glossary/glossary.md +0 -46
  272. package/bootstrap-kb/modules/.claude-plugin.md +0 -22
  273. package/bootstrap-kb/modules/agents.md.md +0 -15
  274. package/bootstrap-kb/modules/claude.md.md +0 -15
  275. package/bootstrap-kb/modules/context.md.md +0 -15
  276. package/bootstrap-kb/modules/docs.md +0 -18
  277. package/bootstrap-kb/modules/handoff.md.md +0 -15
  278. package/bootstrap-kb/modules/package-lock.json.md +0 -15
  279. package/bootstrap-kb/modules/package.json.md +0 -15
  280. package/bootstrap-kb/modules/plan.md.md +0 -15
  281. package/bootstrap-kb/modules/readme.md.md +0 -15
  282. package/bootstrap-kb/modules/scripts.md +0 -26
  283. package/bootstrap-kb/modules/spec.md.md +0 -15
  284. package/bootstrap-kb/modules/specs.md +0 -20
  285. package/bootstrap-kb/modules/src.md +0 -51
  286. package/bootstrap-kb/modules/tests.md +0 -42
  287. package/bootstrap-kb/modules/tsconfig.json.md +0 -15
  288. package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
  289. package/bootstrap-kb/overview/overview.md +0 -40
  290. package/bootstrap-kb/sources/manifest.json +0 -950
  291. package/bootstrap-kb/sources/manifest.md +0 -227
  292. package/bootstrap-kb/timeline/timeline.md +0 -57
  293. package/claude-memory-layer-1.0.14.tgz +0 -0
  294. package/d.sh +0 -3
  295. package/deploy.sh +0 -3
  296. package/dist/ui/app.js +0 -2101
  297. package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
  298. package/memory/_index.md +0 -419
  299. package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
  300. package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
  301. package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
  302. package/memory/agent_response/uncategorized/2026-03-05.md +0 -157
  303. package/memory/default/uncategorized/2026-02-25.md +0 -4839
  304. package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
  305. package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
  306. package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
  307. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
  308. package/memory/specs/citations-system/2026-02-25.md +0 -1121
  309. package/memory/specs/endless-mode/2026-02-25.md +0 -1392
  310. package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
  311. package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
  312. package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
  313. package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
  314. package/memory/specs/private-tags/2026-02-25.md +0 -1057
  315. package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
  316. package/memory/specs/task-entity-system/2026-02-25.md +0 -924
  317. package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
  318. package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
  319. package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
  320. package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
  321. package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
  322. package/memory/tool_observation/uncategorized/2026-03-05.md +0 -33
  323. package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
  324. package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
  325. package/memory/user_prompt/uncategorized/2026-03-05.md +0 -6
  326. package/specs/optional-duckdb/context.md +0 -77
  327. package/specs/optional-duckdb/plan.md +0 -142
  328. package/specs/optional-duckdb/spec.md +0 -35
  329. 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
+ });