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
@@ -10,8 +10,9 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
12
  import * as readline from 'readline';
13
- import { randomUUID } from 'crypto';
14
- import { MemoryService, registerSession } from './memory-service.js';
13
+ import { createHash, randomUUID } from 'crypto';
14
+ import { MemoryService } from './memory-service.js';
15
+ import { registerSession } from '../core/registry/session-registry.js';
15
16
  import type { ImportOptions, ImportResult } from './session-history-importer.js';
16
17
 
17
18
  type CodexLogLine = {
@@ -37,6 +38,96 @@ type CodexContentBlock = {
37
38
  text?: unknown;
38
39
  };
39
40
 
41
+ export const CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS = 10_000;
42
+
43
+ export interface CodexSessionMeta {
44
+ sessionId: string | null;
45
+ cwd: string | null;
46
+ }
47
+
48
+ export interface CodexValidationOptions {
49
+ sessionsDir?: string;
50
+ projectPath?: string;
51
+ limit?: number;
52
+ maxContentChars?: number;
53
+ anonymizeProjects?: boolean;
54
+ now?: Date;
55
+ }
56
+
57
+ export interface CodexValidationSource {
58
+ sessionsDir: string;
59
+ projectPath?: string;
60
+ projectFilterApplied: boolean;
61
+ sourcePaths: string[];
62
+ }
63
+
64
+ export interface CodexValidationLimits {
65
+ sessionLimit?: number;
66
+ maxContentChars: number;
67
+ }
68
+
69
+ export interface CodexValidationTotals {
70
+ sessionsScanned: number;
71
+ sessionsMatched: number;
72
+ filesRead: number;
73
+ recordsRead: number;
74
+ messagesNormalized: number;
75
+ turnsNormalized: number;
76
+ userMessages: number;
77
+ assistantMessages: number;
78
+ malformedLines: number;
79
+ skippedUnsupportedRecords: number;
80
+ emptyAssistantMessages: number;
81
+ truncatedMessages: number;
82
+ missingProjectCwd: number;
83
+ warnings: number;
84
+ }
85
+
86
+ export interface CodexProjectSummary {
87
+ projectHash: string;
88
+ pathLabel: string;
89
+ sessions: number;
90
+ messagesNormalized: number;
91
+ turnsNormalized: number;
92
+ userMessages: number;
93
+ assistantMessages: number;
94
+ malformedLines: number;
95
+ skippedUnsupportedRecords: number;
96
+ truncatedMessages: number;
97
+ emptyAssistantMessages: number;
98
+ }
99
+
100
+ export interface CodexSessionReplaySummary {
101
+ sessionId: string;
102
+ filePath: string;
103
+ projectHash: string;
104
+ pathLabel: string;
105
+ matched: boolean;
106
+ recordsRead: number;
107
+ messagesNormalized: number;
108
+ turnsNormalized: number;
109
+ userMessages: number;
110
+ assistantMessages: number;
111
+ malformedLines: number;
112
+ skippedUnsupportedRecords: number;
113
+ emptyAssistantMessages: number;
114
+ truncatedMessages: number;
115
+ missingProjectCwd: boolean;
116
+ warnings: string[];
117
+ }
118
+
119
+ export interface CodexSessionValidationReport {
120
+ generatedAt: string;
121
+ dryRun: true;
122
+ willMutate: false;
123
+ source: CodexValidationSource;
124
+ limits: CodexValidationLimits;
125
+ totals: CodexValidationTotals;
126
+ topProjects: CodexProjectSummary[];
127
+ sessions: CodexSessionReplaySummary[];
128
+ warnings: string[];
129
+ }
130
+
40
131
  function isRecord(value: unknown): value is Record<string, unknown> {
41
132
  return typeof value === 'object' && value !== null;
42
133
  }
@@ -49,33 +140,416 @@ function normalizeMaybeRealpath(p: string): string {
49
140
  }
50
141
  }
51
142
 
52
- function extractTextFromContent(content: unknown): string | null {
53
- if (!Array.isArray(content)) return null;
143
+ interface CodexExtractedContent {
144
+ text: string | null;
145
+ originalLength: number;
146
+ truncated: boolean;
147
+ }
148
+
149
+ function extractCodexContentText(
150
+ content: unknown,
151
+ maxContentChars = CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS
152
+ ): CodexExtractedContent {
54
153
  const texts: string[] = [];
55
- for (const block of content) {
56
- if (!isRecord(block)) continue;
57
- const b = block as CodexContentBlock;
58
- const t = typeof b.type === 'string' ? b.type : '';
59
- if (t !== 'input_text' && t !== 'output_text' && t !== 'text') continue;
60
- if (typeof b.text === 'string' && b.text.length > 0) {
61
- texts.push(b.text);
154
+
155
+ if (typeof content === 'string') {
156
+ if (content.length > 0) texts.push(content);
157
+ } else if (Array.isArray(content)) {
158
+ for (const block of content) {
159
+ if (!isRecord(block)) continue;
160
+ const b = block as CodexContentBlock;
161
+ const t = typeof b.type === 'string' ? b.type : '';
162
+ if (t !== 'input_text' && t !== 'output_text' && t !== 'text') continue;
163
+ if (typeof b.text === 'string' && b.text.length > 0) {
164
+ texts.push(b.text);
165
+ }
166
+ }
167
+ }
168
+
169
+ if (texts.length === 0) {
170
+ return { text: null, originalLength: 0, truncated: false };
171
+ }
172
+
173
+ const merged = texts.join('\n');
174
+ const truncated = merged.length > maxContentChars;
175
+ return {
176
+ text: truncated ? `${merged.slice(0, maxContentChars)}...[truncated]` : merged,
177
+ originalLength: merged.length,
178
+ truncated
179
+ };
180
+ }
181
+
182
+ function extractTextFromContent(content: unknown): string | null {
183
+ return extractCodexContentText(content).text;
184
+ }
185
+
186
+ export function getDefaultCodexSessionsDir(): string {
187
+ return path.join(os.homedir(), '.codex', 'sessions');
188
+ }
189
+
190
+ export function listCodexSessionFilesRecursive(rootDir: string): string[] {
191
+ if (!fs.existsSync(rootDir)) return [];
192
+ const out: string[] = [];
193
+ const stack: string[] = [rootDir];
194
+
195
+ while (stack.length > 0) {
196
+ const dir = stack.pop()!;
197
+ let entries: fs.Dirent[];
198
+ try {
199
+ entries = fs.readdirSync(dir, { withFileTypes: true });
200
+ } catch {
201
+ continue;
202
+ }
203
+
204
+ for (const ent of entries) {
205
+ const fullPath = path.join(dir, ent.name);
206
+ if (ent.isDirectory()) {
207
+ stack.push(fullPath);
208
+ } else if (ent.isFile() && ent.name.endsWith('.jsonl')) {
209
+ out.push(fullPath);
210
+ }
211
+ }
212
+ }
213
+
214
+ return out.sort();
215
+ }
216
+
217
+ function getFileMtimeMs(filePath: string): number {
218
+ try {
219
+ return fs.statSync(filePath).mtimeMs;
220
+ } catch {
221
+ return 0;
222
+ }
223
+ }
224
+
225
+ function selectRecentCodexSessionFiles(files: string[], sessionLimit?: number): string[] {
226
+ if (sessionLimit === undefined) return files;
227
+ const limit = Number.isFinite(sessionLimit) && sessionLimit > 0 ? Math.floor(sessionLimit) : undefined;
228
+ if (limit === undefined) return files;
229
+ return [...files]
230
+ .sort((a, b) => getFileMtimeMs(b) - getFileMtimeMs(a) || b.localeCompare(a))
231
+ .slice(0, limit);
232
+ }
233
+
234
+ function normalizePositiveImportLimit(limit?: number): number | undefined {
235
+ if (limit === undefined) return undefined;
236
+ return Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : undefined;
237
+ }
238
+
239
+ function countStoredEntries(result: ImportResult): number {
240
+ return result.importedPrompts + result.importedResponses + result.skippedDuplicates;
241
+ }
242
+
243
+ export async function readCodexSessionMeta(filePath: string): Promise<CodexSessionMeta> {
244
+ const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' });
245
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
246
+
247
+ try {
248
+ let linesRead = 0;
249
+ for await (const line of rl) {
250
+ linesRead++;
251
+ if (!line.trim()) continue;
252
+ try {
253
+ const obj = JSON.parse(line) as CodexLogLine;
254
+ if (obj.type !== 'session_meta') continue;
255
+ if (!isRecord(obj.payload)) break;
256
+ const payload = obj.payload as CodexSessionMetaPayload;
257
+ const sessionId = typeof payload.id === 'string' ? payload.id : null;
258
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : null;
259
+ return { sessionId, cwd };
260
+ } catch {
261
+ // Ignore malformed preamble lines while looking for session_meta.
262
+ }
263
+
264
+ // session_meta is expected near the top; do not scan huge transcripts twice.
265
+ if (linesRead >= 25) break;
266
+ }
267
+ } finally {
268
+ rl.close();
269
+ fileStream.close();
270
+ }
271
+
272
+ return { sessionId: null, cwd: null };
273
+ }
274
+
275
+ export function deriveCodexSessionIdFromFileName(filePath: string): string | null {
276
+ const base = path.basename(filePath, '.jsonl');
277
+ // Common: rollout-<date>-<uuid>
278
+ const m = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
279
+ if (m?.[1]) return m[1];
280
+ return base.length > 0 ? base : null;
281
+ }
282
+
283
+ function createEmptyCodexValidationTotals(): CodexValidationTotals {
284
+ return {
285
+ sessionsScanned: 0,
286
+ sessionsMatched: 0,
287
+ filesRead: 0,
288
+ recordsRead: 0,
289
+ messagesNormalized: 0,
290
+ turnsNormalized: 0,
291
+ userMessages: 0,
292
+ assistantMessages: 0,
293
+ malformedLines: 0,
294
+ skippedUnsupportedRecords: 0,
295
+ emptyAssistantMessages: 0,
296
+ truncatedMessages: 0,
297
+ missingProjectCwd: 0,
298
+ warnings: 0
299
+ };
300
+ }
301
+
302
+ function projectHashFor(cwd: string | null): string {
303
+ return createHash('sha256').update(cwd ?? '(missing cwd)').digest('hex').slice(0, 12);
304
+ }
305
+
306
+ function projectPathLabel(cwd: string | null, anonymizeProjects: boolean): string {
307
+ const hash = projectHashFor(cwd);
308
+ if (anonymizeProjects) return cwd ? `project:${hash}` : `project:${hash}:missing-cwd`;
309
+ return cwd ?? '(missing cwd)';
310
+ }
311
+
312
+ function isProjectMatch(cwd: string | null, projectPath?: string): boolean {
313
+ if (!projectPath) return true;
314
+ if (!cwd) return false;
315
+ return normalizeMaybeRealpath(cwd) === normalizeMaybeRealpath(projectPath);
316
+ }
317
+
318
+ function addSessionToProject(projects: Map<string, CodexProjectSummary>, session: CodexSessionReplaySummary): void {
319
+ const existing = projects.get(session.projectHash) ?? {
320
+ projectHash: session.projectHash,
321
+ pathLabel: session.pathLabel,
322
+ sessions: 0,
323
+ messagesNormalized: 0,
324
+ turnsNormalized: 0,
325
+ userMessages: 0,
326
+ assistantMessages: 0,
327
+ malformedLines: 0,
328
+ skippedUnsupportedRecords: 0,
329
+ truncatedMessages: 0,
330
+ emptyAssistantMessages: 0
331
+ };
332
+
333
+ existing.sessions += 1;
334
+ existing.messagesNormalized += session.messagesNormalized;
335
+ existing.turnsNormalized += session.turnsNormalized;
336
+ existing.userMessages += session.userMessages;
337
+ existing.assistantMessages += session.assistantMessages;
338
+ existing.malformedLines += session.malformedLines;
339
+ existing.skippedUnsupportedRecords += session.skippedUnsupportedRecords;
340
+ existing.truncatedMessages += session.truncatedMessages;
341
+ existing.emptyAssistantMessages += session.emptyAssistantMessages;
342
+ projects.set(session.projectHash, existing);
343
+ }
344
+
345
+ function addSessionToTotals(totals: CodexValidationTotals, session: CodexSessionReplaySummary): void {
346
+ totals.filesRead += 1;
347
+ totals.recordsRead += session.recordsRead;
348
+ totals.messagesNormalized += session.messagesNormalized;
349
+ totals.turnsNormalized += session.turnsNormalized;
350
+ totals.userMessages += session.userMessages;
351
+ totals.assistantMessages += session.assistantMessages;
352
+ totals.malformedLines += session.malformedLines;
353
+ totals.skippedUnsupportedRecords += session.skippedUnsupportedRecords;
354
+ totals.emptyAssistantMessages += session.emptyAssistantMessages;
355
+ totals.truncatedMessages += session.truncatedMessages;
356
+ }
357
+
358
+ export async function normalizeCodexSessionFile(
359
+ filePath: string,
360
+ options: {
361
+ meta?: CodexSessionMeta;
362
+ matched?: boolean;
363
+ maxContentChars?: number;
364
+ anonymizeProjects?: boolean;
365
+ } = {}
366
+ ): Promise<CodexSessionReplaySummary> {
367
+ const meta = options.meta ?? await readCodexSessionMeta(filePath);
368
+ const sessionId = meta.sessionId ?? deriveCodexSessionIdFromFileName(filePath) ?? path.basename(filePath, '.jsonl');
369
+ const projectHash = projectHashFor(meta.cwd);
370
+ const pathLabel = projectPathLabel(meta.cwd, options.anonymizeProjects === true);
371
+ const summary: CodexSessionReplaySummary = {
372
+ sessionId,
373
+ filePath,
374
+ projectHash,
375
+ pathLabel,
376
+ matched: options.matched ?? true,
377
+ recordsRead: 0,
378
+ messagesNormalized: 0,
379
+ turnsNormalized: 0,
380
+ userMessages: 0,
381
+ assistantMessages: 0,
382
+ malformedLines: 0,
383
+ skippedUnsupportedRecords: 0,
384
+ emptyAssistantMessages: 0,
385
+ truncatedMessages: 0,
386
+ missingProjectCwd: !meta.cwd,
387
+ warnings: []
388
+ };
389
+
390
+ if (!meta.cwd) {
391
+ summary.warnings.push('session_meta missing cwd; project matching is unavailable for this session');
392
+ }
393
+
394
+ const maxContentChars = options.maxContentChars ?? CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS;
395
+ const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' });
396
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
397
+
398
+ try {
399
+ for await (const line of rl) {
400
+ if (!line.trim()) continue;
401
+ summary.recordsRead += 1;
402
+
403
+ let entry: CodexLogLine;
404
+ try {
405
+ entry = JSON.parse(line) as CodexLogLine;
406
+ } catch {
407
+ summary.malformedLines += 1;
408
+ continue;
409
+ }
410
+
411
+ if (entry.type === 'session_meta') continue;
412
+ if (entry.type !== 'response_item' || !isRecord(entry.payload)) {
413
+ summary.skippedUnsupportedRecords += 1;
414
+ continue;
415
+ }
416
+
417
+ const payload = entry.payload as CodexResponseItemMessagePayload;
418
+ if (payload.type !== 'message') {
419
+ summary.skippedUnsupportedRecords += 1;
420
+ continue;
421
+ }
422
+
423
+ const role = typeof payload.role === 'string' ? payload.role : null;
424
+ if (role !== 'user' && role !== 'assistant') {
425
+ summary.skippedUnsupportedRecords += 1;
426
+ continue;
427
+ }
428
+
429
+ const extracted = extractCodexContentText(payload.content, maxContentChars);
430
+ if (!extracted.text) {
431
+ if (role === 'assistant') {
432
+ summary.emptyAssistantMessages += 1;
433
+ } else {
434
+ summary.skippedUnsupportedRecords += 1;
435
+ }
436
+ continue;
437
+ }
438
+
439
+ if (extracted.truncated) {
440
+ summary.truncatedMessages += 1;
441
+ }
442
+
443
+ summary.messagesNormalized += 1;
444
+ if (role === 'user') {
445
+ summary.userMessages += 1;
446
+ summary.turnsNormalized += 1;
447
+ } else {
448
+ summary.assistantMessages += 1;
449
+ }
62
450
  }
451
+ } finally {
452
+ rl.close();
453
+ fileStream.close();
63
454
  }
64
- if (texts.length === 0) return null;
65
- return texts.join('\n');
455
+
456
+ return summary;
457
+ }
458
+
459
+ export async function validateCodexSessions(options: CodexValidationOptions = {}): Promise<CodexSessionValidationReport> {
460
+ const sessionsDir = path.resolve(options.sessionsDir ?? getDefaultCodexSessionsDir());
461
+ const maxContentChars = options.maxContentChars ?? CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS;
462
+ const report: CodexSessionValidationReport = {
463
+ generatedAt: (options.now ?? new Date()).toISOString(),
464
+ dryRun: true,
465
+ willMutate: false,
466
+ source: {
467
+ sessionsDir,
468
+ projectPath: options.projectPath,
469
+ projectFilterApplied: Boolean(options.projectPath),
470
+ sourcePaths: [sessionsDir]
471
+ },
472
+ limits: {
473
+ sessionLimit: options.limit,
474
+ maxContentChars
475
+ },
476
+ totals: createEmptyCodexValidationTotals(),
477
+ topProjects: [],
478
+ sessions: [],
479
+ warnings: []
480
+ };
481
+
482
+ if (!fs.existsSync(sessionsDir)) {
483
+ report.warnings.push(`Codex sessions directory not found: ${sessionsDir}`);
484
+ report.totals.warnings = report.warnings.length;
485
+ return report;
486
+ }
487
+
488
+ const sessionFiles = listCodexSessionFilesRecursive(sessionsDir);
489
+ const limitedFiles = typeof options.limit === 'number' && Number.isFinite(options.limit) && options.limit > 0
490
+ ? sessionFiles.slice(0, Math.floor(options.limit))
491
+ : sessionFiles;
492
+ const projects = new Map<string, CodexProjectSummary>();
493
+
494
+ for (const filePath of limitedFiles) {
495
+ const meta = await readCodexSessionMeta(filePath);
496
+ report.totals.sessionsScanned += 1;
497
+ if (!meta.cwd) {
498
+ report.totals.missingProjectCwd += 1;
499
+ }
500
+
501
+ const matched = isProjectMatch(meta.cwd, options.projectPath);
502
+ if (!matched) continue;
503
+
504
+ report.totals.sessionsMatched += 1;
505
+ const sessionSummary = await normalizeCodexSessionFile(filePath, {
506
+ meta,
507
+ matched,
508
+ maxContentChars,
509
+ anonymizeProjects: options.anonymizeProjects
510
+ });
511
+ report.sessions.push(sessionSummary);
512
+ addSessionToTotals(report.totals, sessionSummary);
513
+ addSessionToProject(projects, sessionSummary);
514
+ }
515
+
516
+ report.topProjects = [...projects.values()].sort((a, b) => {
517
+ const byMessages = b.messagesNormalized - a.messagesNormalized;
518
+ if (byMessages !== 0) return byMessages;
519
+ return b.sessions - a.sessions;
520
+ }).slice(0, 10);
521
+
522
+ if (report.totals.missingProjectCwd > 0) {
523
+ report.warnings.push(`${report.totals.missingProjectCwd} session(s) missing cwd; project matching only uses sessions with cwd`);
524
+ }
525
+ if (report.totals.malformedLines > 0) {
526
+ report.warnings.push(`${report.totals.malformedLines} malformed JSONL line(s) skipped`);
527
+ }
528
+ if (options.projectPath && report.totals.sessionsMatched === 0) {
529
+ report.warnings.push(`No Codex sessions matched project cwd: ${options.projectPath}`);
530
+ }
531
+ report.totals.warnings = report.warnings.length;
532
+
533
+ return report;
534
+ }
535
+
536
+ export interface CodexSessionHistoryImporterOptions {
537
+ sessionsDir?: string;
66
538
  }
67
539
 
68
540
  export class CodexSessionHistoryImporter {
69
541
  private readonly memoryService: MemoryService;
70
- private readonly codexDir: string;
542
+ private readonly sessionsRoot: string;
71
543
 
72
- constructor(memoryService: MemoryService) {
544
+ constructor(memoryService: MemoryService, options: CodexSessionHistoryImporterOptions = {}) {
73
545
  this.memoryService = memoryService;
74
- this.codexDir = path.join(os.homedir(), '.codex');
546
+ this.sessionsRoot = options.sessionsDir
547
+ ? path.resolve(options.sessionsDir)
548
+ : path.join(os.homedir(), '.codex', 'sessions');
75
549
  }
76
550
 
77
551
  private getSessionsRoot(): string {
78
- return path.join(this.codexDir, 'sessions');
552
+ return this.sessionsRoot;
79
553
  }
80
554
 
81
555
  private listSessionFilesRecursive(rootDir: string): string[] {
@@ -181,26 +655,33 @@ export class CodexSessionHistoryImporter {
181
655
  }
182
656
  }
183
657
 
184
- result.totalSessions = matchingFiles.length;
658
+ const selectedFiles = selectRecentCodexSessionFiles(matchingFiles, options.sessionLimit);
185
659
  onProgress?.({ phase: 'scan', message: `Found ${matchingFiles.length} Codex session(s) for this project` });
186
660
 
187
661
  const effectiveProjectPath = options.projectPath ?? projectPath;
662
+ const totalLimit = normalizePositiveImportLimit(options.limit);
663
+ let storedAcrossSessions = 0;
188
664
 
189
- for (let i = 0; i < matchingFiles.length; i++) {
190
- const filePath = matchingFiles[i];
665
+ for (let i = 0; i < selectedFiles.length; i++) {
666
+ if (totalLimit !== undefined && storedAcrossSessions >= totalLimit) break;
667
+ const filePath = selectedFiles[i];
668
+ const remainingLimit = totalLimit === undefined ? undefined : totalLimit - storedAcrossSessions;
191
669
  try {
192
- onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: matchingFiles.length, filePath });
670
+ onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: selectedFiles.length, filePath });
193
671
  const sessionResult = await this.importSessionFile(filePath, {
194
672
  ...options,
673
+ limit: remainingLimit,
195
674
  projectPath: effectiveProjectPath,
196
675
  _sessionIndex: i,
197
676
  } as ImportOptions & { _sessionIndex: number });
198
677
 
678
+ result.totalSessions++;
199
679
  result.totalMessages += sessionResult.totalMessages;
200
680
  result.importedPrompts += sessionResult.importedPrompts;
201
681
  result.importedResponses += sessionResult.importedResponses;
202
682
  result.skippedDuplicates += sessionResult.skippedDuplicates;
203
683
  result.errors.push(...sessionResult.errors);
684
+ storedAcrossSessions += countStoredEntries(sessionResult);
204
685
 
205
686
  onProgress?.({
206
687
  phase: 'session-done',
@@ -236,23 +717,31 @@ export class CodexSessionHistoryImporter {
236
717
 
237
718
  onProgress?.({ phase: 'scan', message: 'Scanning all Codex sessions...' });
238
719
  const sessionFiles = this.listSessionFilesRecursive(sessionsRoot);
239
- result.totalSessions = sessionFiles.length;
720
+ const selectedFiles = selectRecentCodexSessionFiles(sessionFiles, options.sessionLimit);
240
721
  onProgress?.({ phase: 'scan', message: `Found ${sessionFiles.length} Codex session file(s)` });
241
722
 
242
- for (let i = 0; i < sessionFiles.length; i++) {
243
- const filePath = sessionFiles[i];
723
+ const totalLimit = normalizePositiveImportLimit(options.limit);
724
+ let storedAcrossSessions = 0;
725
+
726
+ for (let i = 0; i < selectedFiles.length; i++) {
727
+ if (totalLimit !== undefined && storedAcrossSessions >= totalLimit) break;
728
+ const filePath = selectedFiles[i];
729
+ const remainingLimit = totalLimit === undefined ? undefined : totalLimit - storedAcrossSessions;
244
730
  try {
245
- onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: sessionFiles.length, filePath });
731
+ onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: selectedFiles.length, filePath });
246
732
  const sessionResult = await this.importSessionFile(filePath, {
247
733
  ...options,
734
+ limit: remainingLimit,
248
735
  _sessionIndex: i,
249
736
  } as ImportOptions & { _sessionIndex: number });
250
737
 
738
+ result.totalSessions++;
251
739
  result.totalMessages += sessionResult.totalMessages;
252
740
  result.importedPrompts += sessionResult.importedPrompts;
253
741
  result.importedResponses += sessionResult.importedResponses;
254
742
  result.skippedDuplicates += sessionResult.skippedDuplicates;
255
743
  result.errors.push(...sessionResult.errors);
744
+ storedAcrossSessions += countStoredEntries(sessionResult);
256
745
 
257
746
  onProgress?.({
258
747
  phase: 'session-done',
@@ -337,7 +826,7 @@ export class CodexSessionHistoryImporter {
337
826
  { importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId, source: 'codex' }
338
827
  );
339
828
 
340
- if (appendResult.isDuplicate) {
829
+ if (appendResult.success && appendResult.isDuplicate) {
341
830
  result.skippedDuplicates++;
342
831
  } else {
343
832
  result.importedResponses++;
@@ -374,7 +863,7 @@ export class CodexSessionHistoryImporter {
374
863
  { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId, source: 'codex' }
375
864
  );
376
865
 
377
- if (appendResult.isDuplicate) {
866
+ if (appendResult.success && appendResult.isDuplicate) {
378
867
  result.skippedDuplicates++;
379
868
  } else {
380
869
  result.importedPrompts++;
@@ -469,6 +958,9 @@ export class CodexSessionHistoryImporter {
469
958
  }
470
959
  }
471
960
 
472
- export function createCodexSessionHistoryImporter(memoryService: MemoryService): CodexSessionHistoryImporter {
473
- return new CodexSessionHistoryImporter(memoryService);
961
+ export function createCodexSessionHistoryImporter(
962
+ memoryService: MemoryService,
963
+ options: CodexSessionHistoryImporterOptions = {}
964
+ ): CodexSessionHistoryImporter {
965
+ return new CodexSessionHistoryImporter(memoryService, options);
474
966
  }