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,162 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Hono } from 'hono';
3
+
4
+ const mocks = vi.hoisted(() => {
5
+ const service = {
6
+ initialize: vi.fn(),
7
+ shutdown: vi.fn(),
8
+ retrieveMemories: vi.fn(),
9
+ searchDisclosure: vi.fn(),
10
+ expandDisclosure: vi.fn(),
11
+ sourceDisclosure: vi.fn()
12
+ };
13
+
14
+ const lightweightService = {
15
+ initialize: vi.fn(),
16
+ shutdown: vi.fn(),
17
+ searchDisclosure: vi.fn()
18
+ };
19
+
20
+ return {
21
+ service,
22
+ lightweightService,
23
+ getServiceFromQuery: vi.fn(() => service),
24
+ getLightweightServiceFromQuery: vi.fn(() => lightweightService)
25
+ };
26
+ });
27
+
28
+ vi.mock('../../src/apps/server/api/utils.js', () => ({
29
+ getServiceFromQuery: mocks.getServiceFromQuery,
30
+ getLightweightServiceFromQuery: mocks.getLightweightServiceFromQuery
31
+ }));
32
+
33
+ const { searchRouter } = await import('../../src/server/api/search.js');
34
+
35
+ function createApp() {
36
+ const app = new Hono();
37
+ app.route('/api/search', searchRouter);
38
+ return app;
39
+ }
40
+
41
+ describe('search disclosure API', () => {
42
+ beforeEach(() => {
43
+ mocks.service.initialize.mockReset().mockResolvedValue(undefined);
44
+ mocks.service.shutdown.mockReset().mockResolvedValue(undefined);
45
+ mocks.service.retrieveMemories.mockReset();
46
+ mocks.service.searchDisclosure.mockReset();
47
+ mocks.service.expandDisclosure.mockReset();
48
+ mocks.service.sourceDisclosure.mockReset();
49
+ mocks.lightweightService.initialize.mockReset().mockResolvedValue(undefined);
50
+ mocks.lightweightService.shutdown.mockReset().mockResolvedValue(undefined);
51
+ mocks.lightweightService.searchDisclosure.mockReset();
52
+ mocks.getServiceFromQuery.mockClear();
53
+ mocks.getLightweightServiceFromQuery.mockClear();
54
+ });
55
+
56
+ it('POST /api/search/disclosure delegates to MemoryService.searchDisclosure', async () => {
57
+ const responseBody = {
58
+ results: [{ id: 'event:e1', resultType: 'source', snippet: 'checkout fix', score: 0.91, reasons: ['semantic_match'], sourceRef: 'event:e1' }],
59
+ meta: { total: 1, usedVector: true, usedKeyword: true, fallbackApplied: false }
60
+ };
61
+ mocks.service.searchDisclosure.mockResolvedValue(responseBody);
62
+
63
+ const res = await createApp().request('/api/search/disclosure?project=abc12345', {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({ query: 'checkout fix', options: { topK: 3, includeShared: true } })
67
+ });
68
+
69
+ expect(res.status).toBe(200);
70
+ expect(await res.json()).toEqual(responseBody);
71
+ expect(mocks.getServiceFromQuery).toHaveBeenCalledTimes(1);
72
+ expect(mocks.service.initialize).toHaveBeenCalledTimes(1);
73
+ expect(mocks.service.searchDisclosure).toHaveBeenCalledWith('checkout fix', { topK: 3, includeShared: true });
74
+ expect(mocks.service.shutdown).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+
78
+ it('POST /api/search/disclosure uses lightweight service for explicit fast search', async () => {
79
+ const responseBody = {
80
+ results: [{ id: 'event:e1', resultType: 'source', snippet: 'checkout fix', score: 0.91, reasons: ['keyword_match'], sourceRef: 'event:e1' }],
81
+ meta: { total: 1, usedVector: false, usedKeyword: true, fallbackApplied: false }
82
+ };
83
+ mocks.lightweightService.searchDisclosure.mockResolvedValue(responseBody);
84
+
85
+ const res = await createApp().request('/api/search/disclosure?project=abc12345', {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ query: 'checkout fix', options: { topK: 3, strategy: 'fast', includeShared: true } })
89
+ });
90
+
91
+ expect(res.status).toBe(200);
92
+ expect(await res.json()).toEqual(responseBody);
93
+ expect(mocks.getLightweightServiceFromQuery).toHaveBeenCalledTimes(1);
94
+ expect(mocks.getServiceFromQuery).not.toHaveBeenCalled();
95
+ expect(mocks.lightweightService.initialize).toHaveBeenCalledTimes(1);
96
+ expect(mocks.lightweightService.searchDisclosure).toHaveBeenCalledWith('checkout fix', { topK: 3, strategy: 'fast', includeShared: true });
97
+ expect(mocks.lightweightService.shutdown).toHaveBeenCalledTimes(1);
98
+ expect(mocks.service.initialize).not.toHaveBeenCalled();
99
+ expect(mocks.service.searchDisclosure).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it('POST /api/search/disclosure rejects missing query', async () => {
103
+ const res = await createApp().request('/api/search/disclosure', {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ options: { topK: 3 } })
107
+ });
108
+
109
+ expect(res.status).toBe(400);
110
+ expect(await res.json()).toEqual({ error: 'Query is required' });
111
+ expect(mocks.getServiceFromQuery).not.toHaveBeenCalled();
112
+ expect(mocks.getLightweightServiceFromQuery).not.toHaveBeenCalled();
113
+ expect(mocks.service.searchDisclosure).not.toHaveBeenCalled();
114
+ expect(mocks.service.shutdown).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('GET /api/search/disclosure/:resultId/expand expands a disclosure result', async () => {
118
+ const responseBody = {
119
+ target: { id: 'event:e1', resultType: 'source', snippet: 'checkout fix', score: 1, reasons: ['continuity_link'] },
120
+ surroundingFacts: [],
121
+ relatedSources: [{ sourceRef: 'event:e1', sourceType: 'raw_event', eventIds: ['e1'] }]
122
+ };
123
+ mocks.service.expandDisclosure.mockResolvedValue(responseBody);
124
+
125
+ const res = await createApp().request('/api/search/disclosure/event:e1/expand?windowSize=2');
126
+
127
+ expect(res.status).toBe(200);
128
+ expect(await res.json()).toEqual(responseBody);
129
+ expect(mocks.service.initialize).not.toHaveBeenCalled();
130
+ expect(mocks.service.expandDisclosure).toHaveBeenCalledWith('event:e1', { windowSize: 2 });
131
+ expect(mocks.service.shutdown).toHaveBeenCalledTimes(1);
132
+ });
133
+
134
+ it('GET /api/search/disclosure/:resultId/source resolves a disclosure result source', async () => {
135
+ const responseBody = {
136
+ sourceRef: 'event:e1',
137
+ sourceType: 'raw_event',
138
+ eventIds: ['e1'],
139
+ rawEvents: [{ id: 'e1', content: 'checkout fix' }]
140
+ };
141
+ mocks.service.sourceDisclosure.mockResolvedValue(responseBody);
142
+
143
+ const res = await createApp().request('/api/search/disclosure/event:e1/source');
144
+
145
+ expect(res.status).toBe(200);
146
+ expect(await res.json()).toEqual(responseBody);
147
+ expect(mocks.service.initialize).not.toHaveBeenCalled();
148
+ expect(mocks.service.sourceDisclosure).toHaveBeenCalledWith('event:e1');
149
+ expect(mocks.service.shutdown).toHaveBeenCalledTimes(1);
150
+ });
151
+
152
+ it('GET /api/search/disclosure/:resultId/source returns 404 when source is missing', async () => {
153
+ mocks.service.sourceDisclosure.mockResolvedValue(null);
154
+
155
+ const res = await createApp().request('/api/search/disclosure/event:missing/source');
156
+
157
+ expect(res.status).toBe(404);
158
+ expect(await res.json()).toEqual({ error: 'Source not found' });
159
+ expect(mocks.service.initialize).not.toHaveBeenCalled();
160
+ expect(mocks.service.shutdown).toHaveBeenCalledTimes(1);
161
+ });
162
+ });
@@ -0,0 +1,67 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Hono } from 'hono';
3
+
4
+ const mocks = vi.hoisted(() => {
5
+ const service = {
6
+ initialize: vi.fn(),
7
+ shutdown: vi.fn(),
8
+ getStats: vi.fn(),
9
+ getRecentEvents: vi.fn(),
10
+ getRetrievalTraceStats: vi.fn()
11
+ };
12
+
13
+ return {
14
+ service,
15
+ getServiceFromQuery: vi.fn(),
16
+ getLightweightServiceFromQuery: vi.fn(() => service)
17
+ };
18
+ });
19
+
20
+ vi.mock('../../src/apps/server/api/utils.js', () => ({
21
+ getServiceFromQuery: mocks.getServiceFromQuery,
22
+ getLightweightServiceFromQuery: mocks.getLightweightServiceFromQuery
23
+ }));
24
+
25
+ vi.mock('../../src/services/memory-service.js', async (importOriginal) => {
26
+ const actual = await importOriginal<typeof import('../../src/services/memory-service.js')>();
27
+ return {
28
+ ...actual,
29
+ getMemoryServiceForProject: vi.fn(() => mocks.service)
30
+ };
31
+ });
32
+
33
+ const { statsRouter } = await import('../../src/server/api/stats.js');
34
+
35
+ function createApp() {
36
+ const app = new Hono();
37
+ app.route('/api/stats', statsRouter);
38
+ return app;
39
+ }
40
+
41
+ describe('stats API lightweight read paths', () => {
42
+ beforeEach(() => {
43
+ mocks.service.initialize.mockReset().mockResolvedValue(undefined);
44
+ mocks.service.shutdown.mockReset().mockResolvedValue(undefined);
45
+ mocks.service.getStats.mockReset().mockResolvedValue({ totalEvents: 2, vectorCount: 0, levelStats: [] });
46
+ mocks.service.getRecentEvents.mockReset().mockResolvedValue([
47
+ { id: 'e1', eventType: 'user_prompt', sessionId: 's1', timestamp: new Date('2026-05-01T00:00:00.000Z'), content: 'prompt', metadata: {} },
48
+ { id: 'e2', eventType: 'agent_response', sessionId: 's1', timestamp: new Date('2026-05-01T00:01:00.000Z'), content: 'response', metadata: {} }
49
+ ]);
50
+ mocks.service.getRetrievalTraceStats.mockReset().mockResolvedValue({ totalQueries: 0, avgCandidateCount: 0, avgSelectedCount: 0, selectionRate: 0 });
51
+ mocks.getServiceFromQuery.mockClear();
52
+ mocks.getLightweightServiceFromQuery.mockClear();
53
+ });
54
+
55
+ it('GET /api/stats uses the lightweight read-only service instead of full initialization service', async () => {
56
+ const res = await createApp().request('/api/stats?project=abc12345');
57
+
58
+ expect(res.status).toBe(200);
59
+ const body = await res.json();
60
+ expect(body.storage).toEqual({ eventCount: 2, vectorCount: 0 });
61
+ expect(body.sessions).toEqual({ total: 1 });
62
+ expect(mocks.getLightweightServiceFromQuery).toHaveBeenCalledTimes(1);
63
+ expect(mocks.getServiceFromQuery).not.toHaveBeenCalled();
64
+ expect(mocks.service.initialize).toHaveBeenCalledTimes(1);
65
+ expect(mocks.service.shutdown).toHaveBeenCalledTimes(1);
66
+ });
67
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import * as vm from 'node:vm';
5
+
6
+ class TestElement {
7
+ innerHTML = '';
8
+ textContent = '';
9
+ style: Record<string, string> = {};
10
+ disabled = false;
11
+ classList = { add() {}, remove() {}, toggle() {} };
12
+ dataset: Record<string, string> = {};
13
+ options: unknown[] = [];
14
+ addEventListener() {}
15
+ querySelectorAll() { return []; }
16
+ appendChild() {}
17
+ }
18
+
19
+ function loadDashboardWithElements(elements: Record<string, TestElement>) {
20
+ const dashboardDir = join(process.cwd(), 'src/apps/dashboard/assets/js');
21
+ const source = ['state.js', 'views.js', 'disclosure.js']
22
+ .map(file => readFileSync(join(dashboardDir, file), 'utf-8'))
23
+ .join('\n');
24
+ const context = {
25
+ console,
26
+ URL,
27
+ fetch: async () => ({ ok: true, json: async () => ({}) }),
28
+ window: { location: { origin: 'http://localhost:37777' } },
29
+ document: {
30
+ addEventListener() {},
31
+ getElementById(id: string) { return elements[id] ?? null; },
32
+ querySelectorAll() { return []; },
33
+ querySelector() { return null; },
34
+ createElement() { return new TestElement(); }
35
+ },
36
+ setTimeout,
37
+ clearTimeout
38
+ };
39
+
40
+ vm.runInNewContext(
41
+ `${source}\n;globalThis.__dashboardTestHooks = { state, renderDisclosureResults, renderDisclosureDrilldown };`,
42
+ context
43
+ );
44
+ return (context as unknown as { __dashboardTestHooks: {
45
+ state: Record<string, any>;
46
+ renderDisclosureResults: () => void;
47
+ renderDisclosureDrilldown: () => void;
48
+ }}).__dashboardTestHooks;
49
+ }
50
+
51
+ describe('dashboard retrieval disclosure provenance output', () => {
52
+ it('renders shared search results with source/project/topics provenance', () => {
53
+ const elements = { 'disclosure-results': new TestElement() };
54
+ const hooks = loadDashboardWithElements(elements);
55
+
56
+ hooks.state.isDisclosureLoading = false;
57
+ hooks.state.disclosureSelectedId = 'shared:shared-1';
58
+ hooks.state.disclosureMeta = { total: 1, usedVector: true, usedKeyword: true, fallbackApplied: false };
59
+ hooks.state.disclosureResults = [
60
+ {
61
+ id: 'shared:shared-1',
62
+ resultType: 'rule',
63
+ title: 'Shared checkout troubleshooting',
64
+ snippet: 'clear cache and retry',
65
+ score: 0.88,
66
+ reasons: ['semantic_match'],
67
+ sourceRef: 'shared:shared-1',
68
+ metadata: {
69
+ sourceProjectHash: 'project-a',
70
+ sourceEntryId: 'e-shared',
71
+ topics: ['checkout']
72
+ }
73
+ }
74
+ ];
75
+
76
+ hooks.renderDisclosureResults();
77
+
78
+ const html = elements['disclosure-results'].innerHTML;
79
+ expect(html).toContain('shared:shared-1');
80
+ expect(html).toContain('Shared checkout troubleshooting');
81
+ expect(html).toContain('sourceProjectHash');
82
+ expect(html).toContain('project-a');
83
+ expect(html).toContain('topics');
84
+ expect(html).toContain('checkout');
85
+ });
86
+
87
+ it('renders shared drilldown with explicit source metadata and no fake raw event', () => {
88
+ const elements = { 'disclosure-drilldown': new TestElement() };
89
+ const hooks = loadDashboardWithElements(elements);
90
+
91
+ hooks.state.disclosureSelectedId = 'shared:shared-1';
92
+ hooks.state.disclosureExpansion = {
93
+ target: {
94
+ id: 'shared:shared-1',
95
+ resultType: 'rule',
96
+ title: 'Shared checkout troubleshooting',
97
+ snippet: 'clear cache and retry',
98
+ score: 0.88,
99
+ reasons: ['semantic_match'],
100
+ sourceRef: 'shared:shared-1',
101
+ metadata: { sourceProjectHash: 'project-a', sourceEntryId: 'e-shared', topics: ['checkout'] }
102
+ },
103
+ relatedSources: [
104
+ {
105
+ sourceRef: 'shared:shared-1',
106
+ sourceType: 'shared_troubleshooting',
107
+ eventIds: [],
108
+ metadata: { sourceProjectHash: 'project-a', sourceEntryId: 'e-shared', topics: ['checkout'] }
109
+ }
110
+ ],
111
+ expandedContext: '[shared_troubleshooting] Shared checkout troubleshooting\nRoot cause: stale cache\nSolution: clear cache and retry'
112
+ };
113
+ hooks.state.disclosureSource = {
114
+ sourceRef: 'shared:shared-1',
115
+ sourceType: 'shared_troubleshooting',
116
+ eventIds: [],
117
+ rawEvents: [],
118
+ metadata: {
119
+ sourceProjectHash: 'project-a',
120
+ sourceEntryId: 'e-shared',
121
+ topics: ['checkout'],
122
+ rootCause: 'stale cache',
123
+ solution: 'clear cache and retry'
124
+ }
125
+ };
126
+
127
+ hooks.renderDisclosureDrilldown();
128
+
129
+ const html = elements['disclosure-drilldown'].innerHTML;
130
+ expect(html).toContain('shared_troubleshooting');
131
+ expect(html).toContain('Shared source provenance');
132
+ expect(html).toContain('sourceProjectHash');
133
+ expect(html).toContain('project-a');
134
+ expect(html).toContain('rootCause');
135
+ expect(html).toContain('stale cache');
136
+ expect(html).toContain('solution');
137
+ expect(html).toContain('clear cache and retry');
138
+ expect(html).toContain('No local raw events for this shared source.');
139
+ });
140
+ });
@@ -3,7 +3,7 @@ import * as fs from 'node:fs/promises';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { execSync } from 'node:child_process';
6
- import { bootstrapKnowledgeBase } from '../src/services/bootstrap-organizer.js';
6
+ import { bootstrapKnowledgeBase } from '../../src/services/bootstrap-organizer.js';
7
7
 
8
8
  async function makeTempRepo(): Promise<string> {
9
9
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'cml-bootstrap-'));
@@ -8,7 +8,7 @@ import {
8
8
  isSameCanonicalKey,
9
9
  makeDedupeKey,
10
10
  hashContent
11
- } from '../src/core/canonical-key.js';
11
+ } from '../../src/core/canonical-key.js';
12
12
 
13
13
  describe('makeCanonicalKey', () => {
14
14
  it('should normalize to lowercase', () => {
@@ -0,0 +1,185 @@
1
+ import { mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ import { createCodexSessionHistoryImporter, validateCodexSessions } from '../../src/services/codex-session-history-importer.js';
7
+
8
+ const tempDirs: string[] = [];
9
+
10
+ function tempDir() {
11
+ const dir = mkdtempSync(join(tmpdir(), 'cml-codex-validation-'));
12
+ tempDirs.push(dir);
13
+ return dir;
14
+ }
15
+
16
+ function writeJsonl(filePath: string, records: Array<string | Record<string, unknown>>) {
17
+ writeFileSync(
18
+ filePath,
19
+ records.map((record) => typeof record === 'string' ? record : JSON.stringify(record)).join('\n') + '\n',
20
+ 'utf8'
21
+ );
22
+ }
23
+
24
+ afterEach(() => {
25
+ for (const dir of tempDirs.splice(0)) {
26
+ rmSync(dir, { recursive: true, force: true });
27
+ }
28
+ });
29
+
30
+ describe('Codex session validation replay', () => {
31
+ it('dry-runs matching sessions by cwd without exposing transcript content', async () => {
32
+ const sessionsDir = tempDir();
33
+ const projectA = join(sessionsDir, 'project-a');
34
+ const projectB = join(sessionsDir, 'project-b');
35
+
36
+ writeJsonl(join(sessionsDir, 'rollout-2026-05-05T00-00-00-session-a.jsonl'), [
37
+ { type: 'session_meta', payload: { id: 'session-a', cwd: projectA, timestamp: '2026-05-05T00:00:00.000Z' } },
38
+ { type: 'response_item', timestamp: '2026-05-05T00:00:01.000Z', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'please implement the Codex validation replay flow' }] } },
39
+ { type: 'response_item', timestamp: '2026-05-05T00:00:02.000Z', payload: { type: 'function_call', name: 'shell', arguments: '{}' } },
40
+ { type: 'response_item', timestamp: '2026-05-05T00:00:03.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'validated answer that should never appear in aggregate reports' }] } },
41
+ { type: 'response_item', timestamp: '2026-05-05T00:00:04.000Z', payload: { type: 'message', role: 'assistant', content: [] } },
42
+ '{not valid json'
43
+ ]);
44
+
45
+ writeJsonl(join(sessionsDir, 'rollout-2026-05-05T00-00-00-session-b.jsonl'), [
46
+ { type: 'session_meta', payload: { id: 'session-b', cwd: projectB } },
47
+ { type: 'response_item', timestamp: '2026-05-05T00:01:01.000Z', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'this should not match project a' }] } }
48
+ ]);
49
+
50
+ writeJsonl(join(sessionsDir, 'rollout-2026-05-05T00-00-00-session-missing-cwd.jsonl'), [
51
+ { type: 'session_meta', payload: { id: 'session-missing-cwd' } },
52
+ { type: 'response_item', timestamp: '2026-05-05T00:02:01.000Z', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'missing cwd should be scanned but not project matched' }] } }
53
+ ]);
54
+
55
+ const report = await validateCodexSessions({ sessionsDir, projectPath: projectA });
56
+
57
+ expect(report.dryRun).toBe(true);
58
+ expect(report.willMutate).toBe(false);
59
+ expect(report.source.sessionsDir).toBe(sessionsDir);
60
+ expect(report.source.projectPath).toBe(projectA);
61
+ expect(report.totals.sessionsScanned).toBe(3);
62
+ expect(report.totals.sessionsMatched).toBe(1);
63
+ expect(report.totals.userMessages).toBe(1);
64
+ expect(report.totals.assistantMessages).toBe(1);
65
+ expect(report.totals.messagesNormalized).toBe(2);
66
+ expect(report.totals.turnsNormalized).toBe(1);
67
+ expect(report.totals.skippedUnsupportedRecords).toBe(1);
68
+ expect(report.totals.emptyAssistantMessages).toBe(1);
69
+ expect(report.totals.malformedLines).toBe(1);
70
+ expect(report.totals.missingProjectCwd).toBe(1);
71
+ expect(report.topProjects[0]).toMatchObject({ sessions: 1, userMessages: 1, assistantMessages: 1 });
72
+
73
+ const serialized = JSON.stringify(report);
74
+ expect(serialized).not.toContain('please implement the Codex validation replay flow');
75
+ expect(serialized).not.toContain('validated answer that should never appear');
76
+ expect(serialized).not.toContain('this should not match project a');
77
+ });
78
+
79
+ it('summarizes all sessions and counts malformed, unsupported, empty, and truncated content', async () => {
80
+ const sessionsDir = tempDir();
81
+ const projectA = join(sessionsDir, 'project-a');
82
+ const largeAssistantContent = 'A'.repeat(12_050);
83
+
84
+ writeJsonl(join(sessionsDir, 'rollout-2026-05-05T00-00-00-session-a.jsonl'), [
85
+ { type: 'session_meta', payload: { id: 'session-a', cwd: projectA } },
86
+ { type: 'response_item', timestamp: '2026-05-05T00:00:01.000Z', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'normalize all Codex sessions' }] } },
87
+ { type: 'response_item', timestamp: '2026-05-05T00:00:02.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: largeAssistantContent }] } },
88
+ { type: 'response_item', timestamp: '2026-05-05T00:00:03.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '' }] } },
89
+ { type: 'response_item', timestamp: '2026-05-05T00:00:04.000Z', payload: { type: 'reasoning', summary: [] } },
90
+ 'not-json'
91
+ ]);
92
+
93
+ writeJsonl(join(sessionsDir, 'rollout-2026-05-05T00-00-00-session-unknown.jsonl'), [
94
+ { type: 'session_meta', payload: { id: 'session-unknown' } },
95
+ { type: 'response_item', timestamp: '2026-05-05T00:01:01.000Z', payload: { type: 'message', role: 'user', content: 'string content from real-world Codex JSONL' } }
96
+ ]);
97
+
98
+ const report = await validateCodexSessions({ sessionsDir, maxContentChars: 10_000 });
99
+
100
+ expect(report.totals.sessionsScanned).toBe(2);
101
+ expect(report.totals.sessionsMatched).toBe(2);
102
+ expect(report.totals.userMessages).toBe(2);
103
+ expect(report.totals.assistantMessages).toBe(1);
104
+ expect(report.totals.messagesNormalized).toBe(3);
105
+ expect(report.totals.turnsNormalized).toBe(2);
106
+ expect(report.totals.truncatedMessages).toBe(1);
107
+ expect(report.totals.emptyAssistantMessages).toBe(1);
108
+ expect(report.totals.skippedUnsupportedRecords).toBe(1);
109
+ expect(report.totals.malformedLines).toBe(1);
110
+ expect(report.totals.missingProjectCwd).toBe(1);
111
+ expect(report.topProjects).toHaveLength(2);
112
+ expect(report.warnings.some((warning) => warning.includes('missing cwd'))).toBe(true);
113
+ });
114
+
115
+ it('imports only the most recently modified matching Codex session when sessionLimit is set', async () => {
116
+ const sessionsDir = tempDir();
117
+ const projectA = join(sessionsDir, 'project-a');
118
+ const oldFile = join(sessionsDir, 'rollout-2026-05-05T00-00-00-session-old.jsonl');
119
+ const latestFile = join(sessionsDir, 'rollout-2026-05-05T00-10-00-session-latest.jsonl');
120
+
121
+ writeJsonl(oldFile, [
122
+ { type: 'session_meta', payload: { id: 'session-old', cwd: projectA } },
123
+ { type: 'response_item', timestamp: '2026-05-05T00:00:01.000Z', payload: { type: 'message', role: 'user', content: 'old Codex project session should not be imported now' } }
124
+ ]);
125
+ writeJsonl(latestFile, [
126
+ { type: 'session_meta', payload: { id: 'session-latest', cwd: projectA } },
127
+ { type: 'response_item', timestamp: '2026-05-05T00:10:01.000Z', payload: { type: 'message', role: 'user', content: 'latest Codex project session should be imported now' } }
128
+ ]);
129
+ utimesSync(oldFile, new Date('2026-05-05T00:00:00.000Z'), new Date('2026-05-05T00:00:00.000Z'));
130
+ utimesSync(latestFile, new Date('2026-05-05T00:10:00.000Z'), new Date('2026-05-05T00:10:00.000Z'));
131
+
132
+ const memoryService = {
133
+ startSession: vi.fn(async (_sessionId: string, _projectPath?: string) => undefined),
134
+ endSession: vi.fn(async (_sessionId: string) => undefined),
135
+ deleteSessionEvents: vi.fn(async (_sessionId: string) => 0),
136
+ storeUserPrompt: vi.fn(async () => ({ success: true, isDuplicate: false })),
137
+ storeAgentResponse: vi.fn(async () => ({ success: true, isDuplicate: false }))
138
+ };
139
+ const importer = createCodexSessionHistoryImporter(memoryService as never, { sessionsDir });
140
+
141
+ const result = await importer.importProject(projectA, { sessionLimit: 1 });
142
+
143
+ expect(result.totalSessions).toBe(1);
144
+ expect(memoryService.startSession).toHaveBeenCalledTimes(1);
145
+ expect(memoryService.startSession).toHaveBeenCalledWith('session-latest', projectA);
146
+ expect(memoryService.storeUserPrompt).toHaveBeenCalledTimes(1);
147
+ });
148
+
149
+ it('applies Codex import limit across selected matching sessions', async () => {
150
+ const sessionsDir = tempDir();
151
+ const projectA = join(sessionsDir, 'project-a');
152
+ const oldFile = join(sessionsDir, 'rollout-2026-05-05T00-00-00-session-old.jsonl');
153
+ const latestFile = join(sessionsDir, 'rollout-2026-05-05T00-10-00-session-latest.jsonl');
154
+
155
+ writeJsonl(oldFile, [
156
+ { type: 'session_meta', payload: { id: 'session-old', cwd: projectA } },
157
+ { type: 'response_item', timestamp: '2026-05-05T00:00:01.000Z', payload: { type: 'message', role: 'user', content: 'old prompt should remain outside the global Codex import limit' } },
158
+ { type: 'response_item', timestamp: '2026-05-05T00:00:02.000Z', payload: { type: 'message', role: 'assistant', content: 'old assistant should remain outside the global Codex import limit' } }
159
+ ]);
160
+ writeJsonl(latestFile, [
161
+ { type: 'session_meta', payload: { id: 'session-latest', cwd: projectA } },
162
+ { type: 'response_item', timestamp: '2026-05-05T00:10:01.000Z', payload: { type: 'message', role: 'user', content: 'latest prompt should be imported first' } },
163
+ { type: 'response_item', timestamp: '2026-05-05T00:10:02.000Z', payload: { type: 'message', role: 'assistant', content: 'latest assistant should be imported first' } }
164
+ ]);
165
+ utimesSync(oldFile, new Date('2026-05-05T00:00:00.000Z'), new Date('2026-05-05T00:00:00.000Z'));
166
+ utimesSync(latestFile, new Date('2026-05-05T00:10:00.000Z'), new Date('2026-05-05T00:10:00.000Z'));
167
+
168
+ const memoryService = {
169
+ startSession: vi.fn(async (_sessionId: string, _projectPath?: string) => undefined),
170
+ endSession: vi.fn(async (_sessionId: string) => undefined),
171
+ deleteSessionEvents: vi.fn(async (_sessionId: string) => 0),
172
+ storeUserPrompt: vi.fn(async () => ({ success: true, isDuplicate: false })),
173
+ storeAgentResponse: vi.fn(async () => ({ success: true, isDuplicate: false }))
174
+ };
175
+ const importer = createCodexSessionHistoryImporter(memoryService as never, { sessionsDir });
176
+
177
+ const result = await importer.importProject(projectA, { sessionLimit: 2, limit: 2 });
178
+
179
+ expect(result.totalSessions).toBe(1);
180
+ expect(memoryService.startSession).toHaveBeenCalledTimes(1);
181
+ expect(memoryService.startSession).toHaveBeenCalledWith('session-latest', projectA);
182
+ expect(memoryService.storeUserPrompt).toHaveBeenCalledTimes(1);
183
+ expect(memoryService.storeAgentResponse).toHaveBeenCalledTimes(1);
184
+ });
185
+ });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
 
3
- import { ConsolidationWorker } from '../src/core/consolidation-worker.js';
4
- import type { EndlessModeConfig, MemoryEvent } from '../src/core/types.js';
3
+ import { ConsolidationWorker } from '../../src/core/consolidation-worker.js';
4
+ import type { EndlessModeConfig, MemoryEvent } from '../../src/core/types.js';
5
5
 
6
6
  function makeEvent(id: string, content: string, hoursAgo = 20): MemoryEvent {
7
7
  return {