nlm-memory 0.19.0 → 0.20.1

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 (398) hide show
  1. package/README.md +17 -10
  2. package/dist/cli/digest.js +21 -0
  3. package/dist/cli/digest.js.map +1 -1
  4. package/dist/cli/init.d.ts +12 -0
  5. package/dist/cli/init.js +49 -0
  6. package/dist/cli/init.js.map +1 -0
  7. package/dist/cli/nlm.d.ts +13 -1
  8. package/dist/cli/nlm.js +766 -55
  9. package/dist/cli/nlm.js.map +1 -1
  10. package/dist/core/actions/overlay.d.ts +20 -6
  11. package/dist/core/actions/overlay.js +32 -18
  12. package/dist/core/actions/overlay.js.map +1 -1
  13. package/dist/core/adapters/claude-code.js +6 -0
  14. package/dist/core/adapters/claude-code.js.map +1 -1
  15. package/dist/core/classifier/hierarchical-classify.d.ts +6 -2
  16. package/dist/core/classifier/hierarchical-classify.js +10 -6
  17. package/dist/core/classifier/hierarchical-classify.js.map +1 -1
  18. package/dist/core/classifier/prompt.js +3 -3
  19. package/dist/core/digest/compose.d.ts +10 -0
  20. package/dist/core/digest/compose.js +7 -1
  21. package/dist/core/digest/compose.js.map +1 -1
  22. package/dist/core/embedding/embed-backfill.d.ts +13 -2
  23. package/dist/core/embedding/embed-backfill.js +181 -43
  24. package/dist/core/embedding/embed-backfill.js.map +1 -1
  25. package/dist/core/embedding/embedding-config.d.ts +17 -0
  26. package/dist/core/embedding/embedding-config.js +12 -0
  27. package/dist/core/embedding/embedding-config.js.map +1 -0
  28. package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
  29. package/dist/core/embedding/pg-embed-backfill.js +264 -0
  30. package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
  31. package/dist/core/entities/dedup-suggest.d.ts +25 -0
  32. package/dist/core/entities/dedup-suggest.js +107 -0
  33. package/dist/core/entities/dedup-suggest.js.map +1 -0
  34. package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
  35. package/dist/core/eval/classifier-fixture-eval.js +217 -0
  36. package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
  37. package/dist/core/eval/extraction-scoring.d.ts +63 -0
  38. package/dist/core/eval/extraction-scoring.js +79 -0
  39. package/dist/core/eval/extraction-scoring.js.map +1 -0
  40. package/dist/core/eval/run-eval.d.ts +40 -0
  41. package/dist/core/eval/run-eval.js +34 -0
  42. package/dist/core/eval/run-eval.js.map +1 -0
  43. package/dist/core/exemplars/capture-from-session.d.ts +1 -0
  44. package/dist/core/exemplars/capture-from-session.js +6 -2
  45. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  46. package/dist/core/exemplars/diff-parse.d.ts +23 -0
  47. package/dist/core/exemplars/diff-parse.js +73 -0
  48. package/dist/core/exemplars/diff-parse.js.map +1 -0
  49. package/dist/core/exemplars/embed-backfill.d.ts +36 -0
  50. package/dist/core/exemplars/embed-backfill.js +85 -0
  51. package/dist/core/exemplars/embed-backfill.js.map +1 -0
  52. package/dist/core/exemplars/embed-text.d.ts +15 -0
  53. package/dist/core/exemplars/embed-text.js +18 -0
  54. package/dist/core/exemplars/embed-text.js.map +1 -0
  55. package/dist/core/exemplars/extract-exemplar.js +2 -65
  56. package/dist/core/exemplars/extract-exemplar.js.map +1 -1
  57. package/dist/core/exemplars/recall-code.js +4 -0
  58. package/dist/core/exemplars/recall-code.js.map +1 -1
  59. package/dist/core/exemplars/warm-embedder.d.ts +14 -0
  60. package/dist/core/exemplars/warm-embedder.js +18 -0
  61. package/dist/core/exemplars/warm-embedder.js.map +1 -0
  62. package/dist/core/health/corpus-state.d.ts +8 -0
  63. package/dist/core/health/corpus-state.js +11 -0
  64. package/dist/core/health/corpus-state.js.map +1 -0
  65. package/dist/core/health/embed-inflight.d.ts +9 -0
  66. package/dist/core/health/embed-inflight.js +30 -0
  67. package/dist/core/health/embed-inflight.js.map +1 -0
  68. package/dist/core/health/embedding-lane-state.d.ts +6 -0
  69. package/dist/core/health/embedding-lane-state.js +18 -0
  70. package/dist/core/health/embedding-lane-state.js.map +1 -0
  71. package/dist/core/health/warmup-state.d.ts +12 -0
  72. package/dist/core/health/warmup-state.js +12 -0
  73. package/dist/core/health/warmup-state.js.map +1 -0
  74. package/dist/core/hook/gate.d.ts +13 -6
  75. package/dist/core/hook/gate.js +47 -6
  76. package/dist/core/hook/gate.js.map +1 -1
  77. package/dist/core/hook/hook-log.d.ts +5 -0
  78. package/dist/core/hook/hook-log.js.map +1 -1
  79. package/dist/core/hook/memo.d.ts +12 -0
  80. package/dist/core/hook/memo.js +41 -1
  81. package/dist/core/hook/memo.js.map +1 -1
  82. package/dist/core/hook/pointer-block.d.ts +1 -0
  83. package/dist/core/hook/pointer-block.js +23 -2
  84. package/dist/core/hook/pointer-block.js.map +1 -1
  85. package/dist/core/hook/query-extract.d.ts +2 -1
  86. package/dist/core/hook/query-extract.js +10 -1
  87. package/dist/core/hook/query-extract.js.map +1 -1
  88. package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
  89. package/dist/core/hook/resolve-conversation-by-query.js +98 -0
  90. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
  91. package/dist/core/hook/select.d.ts +8 -0
  92. package/dist/core/hook/select.js +9 -2
  93. package/dist/core/hook/select.js.map +1 -1
  94. package/dist/core/hook/transcript.js +16 -2
  95. package/dist/core/hook/transcript.js.map +1 -1
  96. package/dist/core/ingest/ingest-session.d.ts +5 -0
  97. package/dist/core/ingest/ingest-session.js +3 -0
  98. package/dist/core/ingest/ingest-session.js.map +1 -1
  99. package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
  100. package/dist/core/ingest/reclassify-oversized.js +3 -0
  101. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  102. package/dist/core/ingest/reprocess.d.ts +96 -0
  103. package/dist/core/ingest/reprocess.js +275 -0
  104. package/dist/core/ingest/reprocess.js.map +1 -0
  105. package/dist/core/integrity/check-invariants.d.ts +6 -1
  106. package/dist/core/integrity/check-invariants.js +209 -3
  107. package/dist/core/integrity/check-invariants.js.map +1 -1
  108. package/dist/core/metrics/corpus-stats.d.ts +49 -0
  109. package/dist/core/metrics/corpus-stats.js +111 -0
  110. package/dist/core/metrics/corpus-stats.js.map +1 -0
  111. package/dist/core/metrics/re-derivation.d.ts +48 -0
  112. package/dist/core/metrics/re-derivation.js +87 -0
  113. package/dist/core/metrics/re-derivation.js.map +1 -0
  114. package/dist/core/recall/citation-log.d.ts +8 -0
  115. package/dist/core/recall/citation-log.js +17 -0
  116. package/dist/core/recall/citation-log.js.map +1 -1
  117. package/dist/core/recall/query-intent.d.ts +7 -0
  118. package/dist/core/recall/query-intent.js +25 -0
  119. package/dist/core/recall/query-intent.js.map +1 -0
  120. package/dist/core/recall/query-log.d.ts +4 -0
  121. package/dist/core/recall/query-log.js +4 -0
  122. package/dist/core/recall/query-log.js.map +1 -1
  123. package/dist/core/recall/recall-service.d.ts +37 -1
  124. package/dist/core/recall/recall-service.js +106 -63
  125. package/dist/core/recall/recall-service.js.map +1 -1
  126. package/dist/core/recall/related-exemplars.d.ts +1 -0
  127. package/dist/core/recall/related-exemplars.js +4 -1
  128. package/dist/core/recall/related-exemplars.js.map +1 -1
  129. package/dist/core/recall/related-facts.js +2 -2
  130. package/dist/core/recall/related-facts.js.map +1 -1
  131. package/dist/core/recall/reranker.d.ts +2 -1
  132. package/dist/core/recall/reranker.js +3 -3
  133. package/dist/core/recall/reranker.js.map +1 -1
  134. package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
  135. package/dist/core/recall-facts/fact-query-log.js +2 -0
  136. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  137. package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
  138. package/dist/core/recall-facts/fact-recall-service.js +21 -8
  139. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  140. package/dist/core/scheduler/scan-once.d.ts +6 -6
  141. package/dist/core/scheduler/scan-once.js +13 -49
  142. package/dist/core/scheduler/scan-once.js.map +1 -1
  143. package/dist/core/scheduler/scheduler.d.ts +10 -0
  144. package/dist/core/scheduler/scheduler.js +80 -28
  145. package/dist/core/scheduler/scheduler.js.map +1 -1
  146. package/dist/core/signals/code-signal.d.ts +49 -0
  147. package/dist/core/signals/code-signal.js +53 -0
  148. package/dist/core/signals/code-signal.js.map +1 -0
  149. package/dist/core/storage/db-restore.js +3 -1
  150. package/dist/core/storage/db-restore.js.map +1 -1
  151. package/dist/core/storage/fact-batch.d.ts +9 -0
  152. package/dist/core/storage/fact-batch.js +14 -0
  153. package/dist/core/storage/fact-batch.js.map +1 -0
  154. package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
  155. package/dist/core/storage/pg-code-exemplar-store.js +7 -0
  156. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  157. package/dist/core/storage/pg-embedding-config.d.ts +29 -0
  158. package/dist/core/storage/pg-embedding-config.js +53 -0
  159. package/dist/core/storage/pg-embedding-config.js.map +1 -0
  160. package/dist/core/storage/pg-entity-store.d.ts +12 -0
  161. package/dist/core/storage/pg-entity-store.js +79 -0
  162. package/dist/core/storage/pg-entity-store.js.map +1 -0
  163. package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
  164. package/dist/core/storage/pg-fact-ingest.js +34 -0
  165. package/dist/core/storage/pg-fact-ingest.js.map +1 -0
  166. package/dist/core/storage/pg-fact-store.d.ts +3 -0
  167. package/dist/core/storage/pg-fact-store.js +26 -30
  168. package/dist/core/storage/pg-fact-store.js.map +1 -1
  169. package/dist/core/storage/pg-migrate.d.ts +18 -0
  170. package/dist/core/storage/pg-migrate.js +73 -0
  171. package/dist/core/storage/pg-migrate.js.map +1 -0
  172. package/dist/core/storage/pg-session-store.d.ts +9 -0
  173. package/dist/core/storage/pg-session-store.js +204 -76
  174. package/dist/core/storage/pg-session-store.js.map +1 -1
  175. package/dist/core/storage/pg-storage.d.ts +9 -7
  176. package/dist/core/storage/pg-storage.js +20 -50
  177. package/dist/core/storage/pg-storage.js.map +1 -1
  178. package/dist/core/storage/pg-workstream-store.d.ts +24 -0
  179. package/dist/core/storage/pg-workstream-store.js +96 -0
  180. package/dist/core/storage/pg-workstream-store.js.map +1 -0
  181. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
  182. package/dist/core/storage/sqlite-code-exemplar-store.js +11 -0
  183. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  184. package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
  185. package/dist/core/storage/sqlite-embedding-config.js +27 -0
  186. package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
  187. package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
  188. package/dist/core/storage/sqlite-entity-store.js +96 -0
  189. package/dist/core/storage/sqlite-entity-store.js.map +1 -0
  190. package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
  191. package/dist/core/storage/sqlite-fact-store.js +76 -25
  192. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  193. package/dist/core/storage/sqlite-session-store.d.ts +20 -5
  194. package/dist/core/storage/sqlite-session-store.js +183 -68
  195. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  196. package/dist/core/storage/sqlite-storage.d.ts +8 -8
  197. package/dist/core/storage/sqlite-storage.js +21 -32
  198. package/dist/core/storage/sqlite-storage.js.map +1 -1
  199. package/dist/core/storage/sqlite-workstream-store.d.ts +24 -0
  200. package/dist/core/storage/sqlite-workstream-store.js +98 -0
  201. package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
  202. package/dist/core/util/with-timeout.d.ts +5 -0
  203. package/dist/core/util/with-timeout.js +20 -0
  204. package/dist/core/util/with-timeout.js.map +1 -0
  205. package/dist/core/work-digest/active-spans.d.ts +8 -0
  206. package/dist/core/work-digest/active-spans.js +29 -0
  207. package/dist/core/work-digest/active-spans.js.map +1 -0
  208. package/dist/core/work-digest/attribute.d.ts +15 -0
  209. package/dist/core/work-digest/attribute.js +76 -0
  210. package/dist/core/work-digest/attribute.js.map +1 -0
  211. package/dist/core/work-digest/build-work-digest.d.ts +12 -0
  212. package/dist/core/work-digest/build-work-digest.js +69 -0
  213. package/dist/core/work-digest/build-work-digest.js.map +1 -0
  214. package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
  215. package/dist/core/work-digest/compose-work-digest.js +52 -0
  216. package/dist/core/work-digest/compose-work-digest.js.map +1 -0
  217. package/dist/core/work-digest/merge-active.d.ts +10 -0
  218. package/dist/core/work-digest/merge-active.js +22 -0
  219. package/dist/core/work-digest/merge-active.js.map +1 -0
  220. package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
  221. package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
  222. package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
  223. package/dist/core/work-digest/topics.d.ts +18 -0
  224. package/dist/core/work-digest/topics.js +26 -0
  225. package/dist/core/work-digest/topics.js.map +1 -0
  226. package/dist/core/work-digest/types.d.ts +45 -0
  227. package/dist/core/work-digest/types.js +2 -0
  228. package/dist/core/work-digest/types.js.map +1 -0
  229. package/dist/core/workstream/bind.d.ts +25 -0
  230. package/dist/core/workstream/bind.js +23 -0
  231. package/dist/core/workstream/bind.js.map +1 -0
  232. package/dist/core/workstream/compose-recall.d.ts +2 -0
  233. package/dist/core/workstream/compose-recall.js +39 -0
  234. package/dist/core/workstream/compose-recall.js.map +1 -0
  235. package/dist/core/workstream/merge-suggest.d.ts +19 -0
  236. package/dist/core/workstream/merge-suggest.js +61 -0
  237. package/dist/core/workstream/merge-suggest.js.map +1 -0
  238. package/dist/core/workstream/model.d.ts +20 -0
  239. package/dist/core/workstream/model.js +9 -0
  240. package/dist/core/workstream/model.js.map +1 -0
  241. package/dist/core/workstream/name-match.d.ts +8 -0
  242. package/dist/core/workstream/name-match.js +18 -0
  243. package/dist/core/workstream/name-match.js.map +1 -0
  244. package/dist/core/workstream/resolve.d.ts +10 -0
  245. package/dist/core/workstream/resolve.js +21 -0
  246. package/dist/core/workstream/resolve.js.map +1 -0
  247. package/dist/core/workstream/rollup.d.ts +12 -0
  248. package/dist/core/workstream/rollup.js +17 -0
  249. package/dist/core/workstream/rollup.js.map +1 -0
  250. package/dist/core/workstream/work-topics.d.ts +10 -0
  251. package/dist/core/workstream/work-topics.js +55 -0
  252. package/dist/core/workstream/work-topics.js.map +1 -0
  253. package/dist/hook/hook-helpers.d.ts +4 -0
  254. package/dist/hook/hook-helpers.js +36 -0
  255. package/dist/hook/hook-helpers.js.map +1 -0
  256. package/dist/hook/pre-compact-hook.d.ts +23 -0
  257. package/dist/hook/pre-compact-hook.js +66 -0
  258. package/dist/hook/pre-compact-hook.js.map +1 -0
  259. package/dist/hook/prompt-recall-hook.d.ts +37 -0
  260. package/dist/hook/prompt-recall-hook.js +120 -20
  261. package/dist/hook/prompt-recall-hook.js.map +1 -1
  262. package/dist/hook/recall-gate.d.ts +29 -0
  263. package/dist/hook/recall-gate.js +50 -0
  264. package/dist/hook/recall-gate.js.map +1 -0
  265. package/dist/hook/recall-over-http.d.ts +1 -1
  266. package/dist/hook/recall-over-http.js +11 -11
  267. package/dist/hook/recall-over-http.js.map +1 -1
  268. package/dist/hook/recent-context.d.ts +25 -0
  269. package/dist/hook/recent-context.js +104 -0
  270. package/dist/hook/recent-context.js.map +1 -0
  271. package/dist/hook/score-floor.d.ts +27 -0
  272. package/dist/hook/score-floor.js +42 -0
  273. package/dist/hook/score-floor.js.map +1 -0
  274. package/dist/hook/session-end-hook.js +8 -29
  275. package/dist/hook/session-end-hook.js.map +1 -1
  276. package/dist/hook/session-start-hook.js +24 -59
  277. package/dist/hook/session-start-hook.js.map +1 -1
  278. package/dist/hook/stop-hook.js +22 -59
  279. package/dist/hook/stop-hook.js.map +1 -1
  280. package/dist/hook/subagent-start-hook.d.ts +28 -0
  281. package/dist/hook/subagent-start-hook.js +81 -0
  282. package/dist/hook/subagent-start-hook.js.map +1 -0
  283. package/dist/http/app.js +28 -8
  284. package/dist/http/app.js.map +1 -1
  285. package/dist/install/setup.js +2 -0
  286. package/dist/install/setup.js.map +1 -1
  287. package/dist/llm/build-classifier.d.ts +22 -0
  288. package/dist/llm/build-classifier.js +42 -0
  289. package/dist/llm/build-classifier.js.map +1 -0
  290. package/dist/llm/build-embedder.d.ts +11 -0
  291. package/dist/llm/build-embedder.js +42 -0
  292. package/dist/llm/build-embedder.js.map +1 -0
  293. package/dist/llm/bundled-embedder-client.d.ts +37 -0
  294. package/dist/llm/bundled-embedder-client.js +90 -0
  295. package/dist/llm/bundled-embedder-client.js.map +1 -0
  296. package/dist/llm/classifier-box.d.ts +13 -5
  297. package/dist/llm/classifier-box.js +28 -6
  298. package/dist/llm/classifier-box.js.map +1 -1
  299. package/dist/llm/classifier-egress.d.ts +18 -0
  300. package/dist/llm/classifier-egress.js +59 -0
  301. package/dist/llm/classifier-egress.js.map +1 -0
  302. package/dist/llm/client-shared.d.ts +4 -0
  303. package/dist/llm/client-shared.js +39 -0
  304. package/dist/llm/client-shared.js.map +1 -0
  305. package/dist/llm/deepseek-client.d.ts +20 -0
  306. package/dist/llm/deepseek-client.js +59 -45
  307. package/dist/llm/deepseek-client.js.map +1 -1
  308. package/dist/llm/embedder-info.d.ts +15 -0
  309. package/dist/llm/embedder-info.js +27 -0
  310. package/dist/llm/embedder-info.js.map +1 -0
  311. package/dist/llm/model-quirks.d.ts +4 -0
  312. package/dist/llm/model-quirks.js +7 -0
  313. package/dist/llm/model-quirks.js.map +1 -0
  314. package/dist/llm/naming.d.ts +5 -0
  315. package/dist/llm/naming.js +20 -0
  316. package/dist/llm/naming.js.map +1 -0
  317. package/dist/llm/ollama-client.d.ts +7 -8
  318. package/dist/llm/ollama-client.js +59 -51
  319. package/dist/llm/ollama-client.js.map +1 -1
  320. package/dist/llm/ollama-code-embedder.d.ts +8 -3
  321. package/dist/llm/ollama-code-embedder.js +14 -24
  322. package/dist/llm/ollama-code-embedder.js.map +1 -1
  323. package/dist/llm/openai-code-embedder-client.d.ts +25 -0
  324. package/dist/llm/openai-code-embedder-client.js +52 -0
  325. package/dist/llm/openai-code-embedder-client.js.map +1 -0
  326. package/dist/llm/openai-embedder-client.d.ts +41 -0
  327. package/dist/llm/openai-embedder-client.js +83 -0
  328. package/dist/llm/openai-embedder-client.js.map +1 -0
  329. package/dist/mcp/server.d.ts +47 -2
  330. package/dist/mcp/server.js +303 -18
  331. package/dist/mcp/server.js.map +1 -1
  332. package/dist/ports/code-embedder.d.ts +6 -1
  333. package/dist/ports/code-exemplar-store.d.ts +5 -0
  334. package/dist/ports/entity-store.d.ts +35 -0
  335. package/dist/ports/entity-store.js +7 -0
  336. package/dist/ports/entity-store.js.map +1 -0
  337. package/dist/ports/fact-store.d.ts +0 -0
  338. package/dist/ports/fact-store.js.map +1 -1
  339. package/dist/ports/llm-client.d.ts +9 -1
  340. package/dist/ports/session-store.d.ts +12 -0
  341. package/dist/ports/storage.d.ts +7 -30
  342. package/dist/ports/storage.js +3 -7
  343. package/dist/ports/storage.js.map +1 -1
  344. package/dist/ports/transcript-adapter.d.ts +2 -0
  345. package/dist/ports/workstream-store.d.ts +21 -0
  346. package/dist/ports/workstream-store.js +2 -0
  347. package/dist/ports/workstream-store.js.map +1 -0
  348. package/dist/shared/types.d.ts +23 -2
  349. package/fixtures/classifier-gold/README.md +94 -0
  350. package/fixtures/classifier-gold/reference.json +318 -0
  351. package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
  352. package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
  353. package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
  354. package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
  355. package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
  356. package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
  357. package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
  358. package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
  359. package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
  360. package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
  361. package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
  362. package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
  363. package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
  364. package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
  365. package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
  366. package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
  367. package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
  368. package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
  369. package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
  370. package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
  371. package/migrations/000_initial_schema.sql +1 -1
  372. package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
  373. package/migrations/025_workstreams.sql +30 -0
  374. package/migrations/026_embedding_config.sql +12 -0
  375. package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
  376. package/migrations/028_classifier_provenance.sql +5 -0
  377. package/migrations/pg/001_initial.sql +34 -0
  378. package/migrations/pg/019_split_replaces.sql +16 -7
  379. package/migrations/pg/025_workstreams.sql +34 -0
  380. package/migrations/pg/026_embedding_config.sql +12 -0
  381. package/migrations/pg/028_classifier_provenance.sql +5 -0
  382. package/migrations/pg/029_entity_variants.sql +9 -0
  383. package/nlm/index.js +395 -37
  384. package/package.json +12 -2
  385. package/plugin/.codex-plugin/plugin.json +1 -1
  386. package/plugin/scripts/prompt-recall-hook.mjs +402 -41
  387. package/plugin/scripts/session-start-hook.mjs +286 -60
  388. package/plugin/scripts/stop-hook.mjs +85 -65
  389. package/plugin-hermes-agent/__init__.py +2 -1
  390. package/templates/agent-contract/README.md +24 -0
  391. package/templates/agent-contract/claude-code.md +40 -0
  392. package/templates/agent-contract/generic.md +38 -0
  393. package/dist/core/embedding/embed-normalize.d.ts +0 -28
  394. package/dist/core/embedding/embed-normalize.js +0 -98
  395. package/dist/core/embedding/embed-normalize.js.map +0 -1
  396. package/dist/core/storage/pg-tx-context.d.ts +0 -52
  397. package/dist/core/storage/pg-tx-context.js +0 -149
  398. package/dist/core/storage/pg-tx-context.js.map +0 -1
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Version-gated PostgreSQL migration runner. Reads versioned *.sql files from
3
+ * a directory, applies any whose integer prefix is not yet in schema_migrations,
4
+ * and returns the list of newly applied versions. Idempotent: re-running on an
5
+ * up-to-date database is a no-op.
6
+ *
7
+ * Migration files that contain their own BEGIN/COMMIT markers have those lines
8
+ * stripped; the runner wraps each file in a single transaction instead. A file
9
+ * whose first line is `-- nlm:no-wrap` is executed as-is (manages its own
10
+ * transaction). No pg migration files use that convention today; the check is
11
+ * kept for parity with the sqlite runner.
12
+ */
13
+ import { readFileSync, readdirSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ const FILE_PATTERN = /^(\d+)_([a-z0-9_-]+)\.sql$/i;
16
+ function stripTransactionMarkers(sql) {
17
+ return sql
18
+ .split("\n")
19
+ .filter((line) => !/^\s*(?:BEGIN|COMMIT)\s*;\s*$/.test(line))
20
+ .join("\n");
21
+ }
22
+ export async function runMigrationsPg(pool, migrationsDir) {
23
+ await pool.query(`
24
+ CREATE TABLE IF NOT EXISTS schema_migrations (
25
+ version INTEGER PRIMARY KEY,
26
+ name TEXT NOT NULL,
27
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
28
+ )
29
+ `);
30
+ const { rows: appliedRows } = await pool.query("SELECT version FROM schema_migrations");
31
+ const applied = new Set(appliedRows.map((r) => r.version));
32
+ const files = readdirSync(migrationsDir)
33
+ .filter((f) => FILE_PATTERN.test(f))
34
+ .sort();
35
+ const newlyApplied = [];
36
+ for (const file of files) {
37
+ const match = FILE_PATTERN.exec(file);
38
+ if (!match)
39
+ continue;
40
+ const version = Number(match[1]);
41
+ const name = match[2] ?? file;
42
+ if (applied.has(version))
43
+ continue;
44
+ const rawSql = readFileSync(join(migrationsDir, file), "utf8");
45
+ const noWrap = rawSql.startsWith("-- nlm:no-wrap");
46
+ const client = await pool.connect();
47
+ try {
48
+ if (noWrap) {
49
+ await client.query(rawSql);
50
+ await client.query("INSERT INTO schema_migrations (version, name) VALUES ($1, $2) ON CONFLICT DO NOTHING", [version, name]);
51
+ }
52
+ else {
53
+ const sql = stripTransactionMarkers(rawSql);
54
+ await client.query("BEGIN");
55
+ try {
56
+ await client.query(sql);
57
+ await client.query("INSERT INTO schema_migrations (version, name) VALUES ($1, $2) ON CONFLICT DO NOTHING", [version, name]);
58
+ await client.query("COMMIT");
59
+ }
60
+ catch (err) {
61
+ await client.query("ROLLBACK");
62
+ throw new Error(`Migration ${file} failed: ${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ }
65
+ }
66
+ finally {
67
+ client.release();
68
+ }
69
+ newlyApplied.push({ version, name });
70
+ }
71
+ return newlyApplied;
72
+ }
73
+ //# sourceMappingURL=pg-migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-migrate.js","sourceRoot":"","sources":["../../../src/core/storage/pg-migrate.ts"],"names":[],"mappings":"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"}
@@ -13,14 +13,23 @@ import type { PgFactStore } from "./pg-fact-store.js";
13
13
  export declare class PgSessionStore implements SessionStore {
14
14
  readonly pool: Pool;
15
15
  constructor(pool: Pool);
16
+ private overlayCache;
17
+ private overlayCacheAt;
18
+ invalidateOverlayCache(): void;
19
+ private overlay;
16
20
  list(filter?: SessionFilter): Promise<ReadonlyArray<Session>>;
17
21
  getById(sessionId: string): Promise<Session | null>;
18
22
  getByIds(ids: ReadonlyArray<string>): Promise<ReadonlyArray<Session>>;
23
+ listByDateRange(fromIso: string, toIso: string): Promise<ReadonlyArray<Session>>;
19
24
  semanticSearch(queryVector: Float32Array, limit: number, opts?: SearchOptions): Promise<ReadonlyArray<SemanticNeighbor>>;
20
25
  keywordSearch(query: string, limit: number, opts?: SearchOptions): Promise<ReadonlyArray<KeywordNeighbor>>;
21
26
  resolveSuccessors(ids: ReadonlyArray<string>): Promise<Map<string, string>>;
22
27
  updateStatus(sessionId: string, status: SessionStatus): Promise<void>;
23
28
  markSuperseded(predecessorId: string, successorId: string): Promise<void>;
29
+ setWorkstreamBinding(sessionId: string, workstreamId: string | null, source: import("../workstream/model.js").BindingSource | null, confidence: number | null): Promise<void>;
30
+ listSessionIdsByWorkstreams(workstreamIds: ReadonlyArray<string>): Promise<ReadonlyArray<string>>;
31
+ getEntities(sessionId: string): Promise<ReadonlyArray<string>>;
32
+ getWorkstreamIds(sessionIds: ReadonlyArray<string>): Promise<Map<string, string | null>>;
24
33
  recentWrites(limit: number): Promise<RecentWrite[]>;
25
34
  recentMarkers(limit: number): Promise<RecentMarker[]>;
26
35
  insertSession(record: IngestRecord, embedder?: import("../../ports/llm-client.js").LLMClient | null, supersedes?: Supersedes | null, factSink?: {
@@ -5,7 +5,36 @@
5
5
  * and recentMarkers() for the /live HTTP endpoints, and insertSession() +
6
6
  * insertSessionForTest() for ingest and test seeding.
7
7
  */
8
+ import { tokenize } from "../recall/tokenize.js";
9
+ import { ingestSessionFactsOnClient } from "./pg-fact-ingest.js";
8
10
  import { chunkSessionText } from "../embedding/chunk-body.js";
11
+ import { batchWinners } from "./fact-batch.js";
12
+ import { loadActionOverlayPg, openQuestionId } from "../actions/overlay.js";
13
+ import { liveSessionStatus } from "./live-status.js";
14
+ /**
15
+ * PG mirror of SQLite findContinuesPredecessor: the most recent prior session
16
+ * whose distinct entity-set is identical to the new session's. Runs on the
17
+ * ingest transaction's own client. Returns null when there is no entity-set or
18
+ * no exact-set match, leaving the pair unlinked for the re_derivation_rate metric.
19
+ */
20
+ async function findContinuesPredecessorPg(client, newId, rawEntities) {
21
+ const entities = [...new Set(rawEntities.map((e) => e.trim()).filter(Boolean))];
22
+ if (entities.length === 0)
23
+ return null;
24
+ const placeholders = entities.map((_, i) => `$${i + 2}`).join(",");
25
+ const res = await client.query(`SELECT s.id AS id
26
+ FROM sessions s
27
+ JOIN session_entities se ON se.session_id = s.id
28
+ WHERE s.id != $1
29
+ AND se.entity_canonical IN (${placeholders})
30
+ GROUP BY s.id
31
+ HAVING COUNT(DISTINCT se.entity_canonical) = $${entities.length + 2}
32
+ AND COUNT(DISTINCT se.entity_canonical)
33
+ = (SELECT COUNT(*) FROM session_entities x WHERE x.session_id = s.id)
34
+ ORDER BY s.started_at DESC, s.id DESC
35
+ LIMIT 1`, [newId, ...entities, entities.length]);
36
+ return res.rows[0]?.id ?? null;
37
+ }
9
38
  export class PgSessionStore {
10
39
  pool;
11
40
  // `pool` is public-readonly so PG-native sibling helpers (actions-log Pg
@@ -13,18 +42,36 @@ export class PgSessionStore {
13
42
  constructor(pool) {
14
43
  this.pool = pool;
15
44
  }
45
+ overlayCache = null;
46
+ overlayCacheAt = 0;
47
+ invalidateOverlayCache() {
48
+ this.overlayCache = null;
49
+ }
50
+ async overlay() {
51
+ // TTL backstop: explicit invalidation covers the daemon's own writers; the
52
+ // 30s expiry bounds staleness if another process ever writes actions to
53
+ // this database.
54
+ if (this.overlayCache !== null && Date.now() - this.overlayCacheAt < 30_000) {
55
+ return this.overlayCache;
56
+ }
57
+ this.overlayCache = await loadActionOverlayPg(this.pool);
58
+ this.overlayCacheAt = Date.now();
59
+ return this.overlayCache;
60
+ }
16
61
  async list(filter) {
17
62
  const result = await this.pool.query(`SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
18
- label, summary, status, transcript_kind, transcript_path, body
63
+ label, summary, status, transcript_kind, transcript_path, body,
64
+ classifier_provider, classifier_model, classifier_confidence
19
65
  FROM sessions ORDER BY started_at ASC`);
20
66
  if (result.rows.length === 0)
21
67
  return [];
22
68
  const ids = result.rows.map((r) => r.id);
23
- const [entitiesMap, markersMap] = await Promise.all([
69
+ const [entitiesMap, markersMap, overlay] = await Promise.all([
24
70
  this.loadEntities(ids),
25
71
  this.loadMarkers(ids),
72
+ this.overlay(),
26
73
  ]);
27
- const sessions = result.rows.map((r) => rowToSession(r, entitiesMap, markersMap));
74
+ const sessions = result.rows.map((r) => rowToSession(r, entitiesMap, markersMap, overlay));
28
75
  if (!filter)
29
76
  return sessions;
30
77
  return sessions.filter((s) => {
@@ -39,17 +86,19 @@ export class PgSessionStore {
39
86
  }
40
87
  async getById(sessionId) {
41
88
  const result = await this.pool.query(`SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
42
- label, summary, status, transcript_kind, transcript_path, body
89
+ label, summary, status, transcript_kind, transcript_path, body,
90
+ classifier_provider, classifier_model, classifier_confidence
43
91
  FROM sessions WHERE id = $1`, [sessionId]);
44
92
  if (!result.rows[0])
45
93
  return null;
46
- const [entitiesMap, markersMap, edgesMap] = await Promise.all([
94
+ const [entitiesMap, markersMap, edgesMap, overlay] = await Promise.all([
47
95
  this.loadEntities([sessionId]),
48
96
  this.loadMarkers([sessionId]),
49
97
  this.loadEdges([sessionId]),
98
+ this.overlay(),
50
99
  ]);
51
100
  const edges = edgesMap.get(sessionId);
52
- return rowToSession(result.rows[0], entitiesMap, markersMap, edges);
101
+ return rowToSession(result.rows[0], entitiesMap, markersMap, overlay, edges);
53
102
  }
54
103
  async getByIds(ids) {
55
104
  if (ids.length === 0)
@@ -61,41 +110,84 @@ export class PgSessionStore {
61
110
  if (result.rows.length === 0)
62
111
  return [];
63
112
  const foundIds = result.rows.map((r) => r.id);
64
- const [entitiesMap, markersMap] = await Promise.all([
113
+ const [entitiesMap, markersMap, overlay] = await Promise.all([
65
114
  this.loadEntities(foundIds),
66
115
  this.loadMarkers(foundIds),
116
+ this.overlay(),
117
+ ]);
118
+ return result.rows.map((r) => rowToSession({ ...r, body: null }, entitiesMap, markersMap, overlay));
119
+ }
120
+ async listByDateRange(fromIso, toIso) {
121
+ const result = await this.pool.query(`SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
122
+ label, summary, status, transcript_kind, transcript_path, workstream_id
123
+ FROM sessions
124
+ WHERE started_at < $1 AND (ended_at IS NULL OR ended_at >= $2)
125
+ ORDER BY started_at ASC`, [toIso, fromIso]);
126
+ if (result.rows.length === 0)
127
+ return [];
128
+ const ids = result.rows.map((r) => r.id);
129
+ const [entitiesMap, markersMap, overlay] = await Promise.all([
130
+ this.loadEntities(ids),
131
+ this.loadMarkers(ids),
132
+ this.overlay(),
67
133
  ]);
68
- return result.rows.map((r) => rowToSession({ ...r, body: null }, entitiesMap, markersMap));
134
+ return result.rows.map((r) => rowToSession({ ...r, body: null }, entitiesMap, markersMap, overlay));
69
135
  }
70
136
  async semanticSearch(queryVector, limit, opts) {
137
+ const wsIds = opts?.workstreamIds;
138
+ if (wsIds !== undefined && wsIds.length === 0)
139
+ return [];
71
140
  const k = Math.max(1, Math.trunc(limit));
72
141
  const vecStr = `[${Array.from(queryVector).join(",")}]`;
73
142
  const statusFilter = opts?.includeSuperseded === true
74
143
  ? "s.status != 'replaced'"
75
144
  : "s.status NOT IN ('superseded', 'replaced')";
145
+ const params = [vecStr];
146
+ let wsClause = "";
147
+ if (wsIds?.length) {
148
+ params.push(wsIds);
149
+ wsClause = `AND s.workstream_id = ANY($${params.length}::text[])`;
150
+ }
151
+ params.push(k);
152
+ const limitParam = `$${params.length}`;
76
153
  const result = await this.pool.query(`SELECT sec.session_id, MIN(sec.embedding <-> $1::vector) AS distance, s.status
77
154
  FROM session_embedding_chunks sec
78
155
  JOIN sessions s ON s.id = sec.session_id
156
+ WHERE TRUE ${wsClause}
79
157
  GROUP BY sec.session_id, s.status
80
158
  HAVING ${statusFilter}
81
159
  ORDER BY distance
82
- LIMIT $2`, [vecStr, k]);
160
+ LIMIT ${limitParam}`, params);
83
161
  return result.rows.map((r) => ({ sessionId: r.session_id, distance: r.distance }));
84
162
  }
85
163
  async keywordSearch(query, limit, opts) {
86
- if (!query.trim())
164
+ const wsIds = opts?.workstreamIds;
165
+ if (wsIds !== undefined && wsIds.length === 0)
166
+ return [];
167
+ const terms = tokenize(query).map(sanitizeTsToken).filter(Boolean);
168
+ if (terms.length === 0)
87
169
  return [];
170
+ const tsQuery = terms.join(" OR ");
88
171
  const k = Math.max(1, Math.trunc(limit));
89
172
  const statusFilter = opts?.includeSuperseded === true
90
173
  ? "status != 'replaced'"
91
174
  : "status NOT IN ('superseded', 'replaced')";
175
+ const params = [tsQuery];
176
+ let wsClause = "";
177
+ if (wsIds?.length) {
178
+ params.push(wsIds);
179
+ wsClause = `AND workstream_id = ANY($${params.length}::text[])`;
180
+ }
181
+ params.push(k);
182
+ const limitParam = `$${params.length}`;
92
183
  const result = await this.pool.query(`SELECT id AS session_id,
93
184
  ts_rank_cd(fts_vector, websearch_to_tsquery('english', $1)) AS score
94
185
  FROM sessions
95
186
  WHERE fts_vector @@ websearch_to_tsquery('english', $1)
96
187
  AND ${statusFilter}
188
+ ${wsClause}
97
189
  ORDER BY score DESC
98
- LIMIT $2`, [query, k]);
190
+ LIMIT ${limitParam}`, params);
99
191
  return result.rows.map((r) => ({ sessionId: r.session_id, score: r.score }));
100
192
  }
101
193
  async resolveSuccessors(ids) {
@@ -176,7 +268,11 @@ export class PgSessionStore {
176
268
  AND s.superseded_by IS NULL
177
269
  )
178
270
  `;
179
- await client.query(cascadeSQL, [predecessorId, successorId]);
271
+ const cascaded = await client.query(cascadeSQL + " RETURNING p.id", [predecessorId, successorId]);
272
+ const cascadedIds = cascaded.rows.map((r) => r.id);
273
+ if (cascadedIds.length > 0) {
274
+ await client.query("DELETE FROM fact_embeddings WHERE fact_id = ANY($1)", [cascadedIds]);
275
+ }
180
276
  await client.query("COMMIT");
181
277
  }
182
278
  catch (err) {
@@ -187,6 +283,29 @@ export class PgSessionStore {
187
283
  client.release();
188
284
  }
189
285
  }
286
+ async setWorkstreamBinding(sessionId, workstreamId, source, confidence) {
287
+ await this.pool.query("UPDATE sessions SET workstream_id = $1, binding_source = $2, binding_confidence = $3, updated_at = NOW() WHERE id = $4", [workstreamId, source, confidence, sessionId]);
288
+ }
289
+ async listSessionIdsByWorkstreams(workstreamIds) {
290
+ if (workstreamIds.length === 0)
291
+ return [];
292
+ const ph = workstreamIds.map((_, i) => `$${i + 1}`).join(",");
293
+ const r = await this.pool.query(`SELECT id FROM sessions WHERE workstream_id IN (${ph}) ORDER BY started_at ASC`, [...workstreamIds]);
294
+ return r.rows.map((row) => row.id);
295
+ }
296
+ async getEntities(sessionId) {
297
+ return (await this.loadEntities([sessionId])).get(sessionId) ?? [];
298
+ }
299
+ async getWorkstreamIds(sessionIds) {
300
+ const out = new Map();
301
+ if (sessionIds.length === 0)
302
+ return out;
303
+ const ph = sessionIds.map((_, i) => `$${i + 1}`).join(",");
304
+ const result = await this.pool.query(`SELECT id, workstream_id FROM sessions WHERE id IN (${ph})`, [...sessionIds]);
305
+ for (const r of result.rows)
306
+ out.set(r.id, r.workstream_id);
307
+ return out;
308
+ }
190
309
  async recentWrites(limit) {
191
310
  const result = await this.pool.query(`SELECT id, runtime, label, summary, created_at AS "createdAt"
192
311
  FROM sessions ORDER BY created_at DESC LIMIT $1`, [limit]);
@@ -222,8 +341,9 @@ export class PgSessionStore {
222
341
  await client.query(`INSERT INTO sessions (
223
342
  id, runtime, runtime_session_id, started_at, ended_at, duration_min,
224
343
  label, summary, body, status, transcript_kind, transcript_path,
225
- transcript_offset, transcript_length
226
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
344
+ transcript_offset, transcript_length,
345
+ classifier_provider, classifier_model, classifier_confidence
346
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
227
347
  ON CONFLICT (id) DO UPDATE SET
228
348
  ended_at = EXCLUDED.ended_at,
229
349
  duration_min = EXCLUDED.duration_min,
@@ -231,6 +351,9 @@ export class PgSessionStore {
231
351
  summary = EXCLUDED.summary,
232
352
  body = EXCLUDED.body,
233
353
  status = EXCLUDED.status,
354
+ classifier_provider = EXCLUDED.classifier_provider,
355
+ classifier_model = EXCLUDED.classifier_model,
356
+ classifier_confidence = EXCLUDED.classifier_confidence,
234
357
  updated_at = NOW()`, [
235
358
  record.id, record.runtime, record.runtimeSessionId,
236
359
  record.startedAt, record.endedAt, record.durationMin,
@@ -238,6 +361,9 @@ export class PgSessionStore {
238
361
  record.status === "idle" ? "active" : record.status,
239
362
  record.transcriptKind, record.transcriptPath,
240
363
  record.transcriptOffset, record.transcriptLength,
364
+ record.classifier?.provider ?? null,
365
+ record.classifier?.model ?? null,
366
+ record.classifier?.confidence ?? null,
241
367
  ]);
242
368
  await client.query("DELETE FROM markers WHERE session_id = $1", [record.id]);
243
369
  for (let i = 0; i < record.decisions.length; i++) {
@@ -246,17 +372,34 @@ export class PgSessionStore {
246
372
  for (let i = 0; i < record.openQuestions.length; i++) {
247
373
  await client.query("INSERT INTO markers (session_id, kind, text, position) VALUES ($1, 'open', $2, $3)", [record.id, record.openQuestions[i].trim(), i]);
248
374
  }
249
- for (const raw of record.entities) {
250
- const name = raw.trim();
251
- if (!name)
252
- continue;
375
+ // Replace entity-link semantics: delete the session's existing links, then
376
+ // re-insert for the new entity list. Without this, nlm reprocess amplifies
377
+ // stale links (ON CONFLICT DO NOTHING keeps dropped entities forever) and
378
+ // double-counts session_count on every re-ingest pass.
379
+ const rawNewEntities = [...new Set(record.entities.map((e) => e.trim()).filter(Boolean))];
380
+ // Resolve each extracted entity through entity_variants so merged surface
381
+ // forms bind to the canonical instead of resurrecting the retired source.
382
+ const variantRes = await client.query(`SELECT variant, canonical FROM entity_variants WHERE variant = ANY($1)`, [rawNewEntities]);
383
+ const variantMap = new Map(variantRes.rows.map((r) => [r.variant, r.canonical]));
384
+ const newEntities = rawNewEntities.map((name) => variantMap.get(name) ?? name);
385
+ const oldRes = await client.query("SELECT entity_canonical FROM session_entities WHERE session_id = $1", [record.id]);
386
+ const oldEntities = new Set(oldRes.rows.map((r) => r.entity_canonical));
387
+ await client.query("DELETE FROM session_entities WHERE session_id = $1", [record.id]);
388
+ for (const name of newEntities) {
253
389
  await client.query(`INSERT INTO entities (canonical, type, status, source, first_seen_session, last_seen_session, session_count)
254
390
  VALUES ($1, 'candidate', 'candidate', 'auto-detected', $2, $2, 0)
255
- ON CONFLICT (canonical) DO UPDATE SET
256
- last_seen_session = $2,
257
- session_count = entities.session_count + 1,
258
- updated_at = NOW()`, [name, record.id]);
259
- await client.query("INSERT INTO session_entities (session_id, entity_canonical) VALUES ($1, $2) ON CONFLICT DO NOTHING", [record.id, name]);
391
+ ON CONFLICT (canonical) DO NOTHING`, [name, record.id]);
392
+ // Update last_seen for entities newly added to this session; matches prior touch semantics.
393
+ if (!oldEntities.has(name)) {
394
+ await client.query("UPDATE entities SET last_seen_session = $1, updated_at = NOW() WHERE canonical = $2", [record.id, name]);
395
+ }
396
+ await client.query("INSERT INTO session_entities (session_id, entity_canonical) VALUES ($1, $2)", [record.id, name]);
397
+ }
398
+ // Recompute session_count exactly for every entity in (old union new) so
399
+ // counts reflect reality regardless of prior drift.
400
+ const allTouched = [...new Set([...oldEntities, ...newEntities])];
401
+ for (const name of allTouched) {
402
+ await client.query("UPDATE entities SET session_count = (SELECT COUNT(*) FROM session_entities WHERE entity_canonical = $1), updated_at = NOW() WHERE canonical = $1", [name]);
260
403
  }
261
404
  if (supersedes && supersedes.priorSessionId !== record.id) {
262
405
  const predecessorStatus = supersedes.kind === "replaces" ? "replaced" : "superseded";
@@ -264,35 +407,17 @@ export class PgSessionStore {
264
407
  VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`, [record.id, supersedes.priorSessionId, supersedes.kind]);
265
408
  await client.query("UPDATE sessions SET status = $1, updated_at = NOW() WHERE id = $2", [predecessorStatus, supersedes.priorSessionId]);
266
409
  }
267
- // Atomic session+facts ingest. Mirrors the SQLite factSink path
268
- // (sqlite-session-store.insertSession) and PgFactStore.ingestSessionFacts,
269
- // but runs on the session's own client so facts commit with the row.
270
- // Re-ingest is idempotent: DELETE-by-session clears prior facts, then the
271
- // batch UPDATE re-collapses every active (subject, predicate) sibling.
272
- if (factSink !== null) {
273
- await client.query("DELETE FROM facts WHERE source_session_id = $1", [record.id]);
274
- if (factSink.facts.length > 0) {
275
- for (const f of factSink.facts) {
276
- await client.query(`INSERT INTO facts (id, kind, subject, predicate, value, source_session_id,
277
- source_quote, created_at, superseded_by, confidence)
278
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [f.id, f.kind, f.subject, f.predicate, f.value, f.sourceSessionId,
279
- f.sourceQuote, f.createdAt, f.supersededBy, f.confidence]);
280
- }
281
- const values = factSink.facts
282
- .map((_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`)
283
- .join(", ");
284
- const params = [];
285
- for (const f of factSink.facts)
286
- params.push(f.subject, f.predicate, f.id);
287
- await client.query(`UPDATE facts AS old
288
- SET superseded_by = new_f.new_id
289
- FROM (VALUES ${values}) AS new_f(subject, predicate, new_id)
290
- WHERE old.subject = new_f.subject
291
- AND old.predicate = new_f.predicate
292
- AND old.superseded_by IS NULL
293
- AND old.id != new_f.new_id`, params);
410
+ else {
411
+ const priorId = await findContinuesPredecessorPg(client, record.id, record.entities);
412
+ if (priorId !== null) {
413
+ await client.query(`INSERT INTO session_edges (from_session, to_session, kind)
414
+ VALUES ($1, $2, 'continues') ON CONFLICT DO NOTHING`, [record.id, priorId]);
294
415
  }
295
416
  }
417
+ // Atomic session+facts ingest on the session's own client. Single source of truth: pg-fact-ingest.ts.
418
+ if (factSink !== null) {
419
+ await ingestSessionFactsOnClient(client, record.id, factSink.facts);
420
+ }
296
421
  await client.query("COMMIT");
297
422
  }
298
423
  catch (err) {
@@ -325,7 +450,7 @@ export class PgSessionStore {
325
450
  // SQLite path. A per-fact embed failure leaves the fact current but
326
451
  // semantically unreachable until a future re-ingest.
327
452
  if (factSink !== null) {
328
- for (const fact of factSink.facts) {
453
+ for (const fact of batchWinners(factSink.facts)) {
329
454
  const factText = `${fact.subject} ${fact.predicate} ${fact.value}`.trim();
330
455
  if (!factText)
331
456
  continue;
@@ -385,26 +510,7 @@ export class PgSessionStore {
385
510
  const client = await this.pool.connect();
386
511
  try {
387
512
  await client.query("BEGIN");
388
- await client.query("DELETE FROM facts WHERE source_session_id = $1", [sessionId]);
389
- if (facts.length > 0) {
390
- for (const f of facts) {
391
- await client.query(`INSERT INTO facts (id, kind, subject, predicate, value, source_session_id,
392
- source_quote, created_at, superseded_by, confidence)
393
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [f.id, f.kind, f.subject, f.predicate, f.value, f.sourceSessionId,
394
- f.sourceQuote, f.createdAt, f.supersededBy, f.confidence]);
395
- }
396
- const values = facts.map((_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`).join(", ");
397
- const supersedeParams = [];
398
- for (const f of facts)
399
- supersedeParams.push(f.subject, f.predicate, f.id);
400
- await client.query(`UPDATE facts AS old
401
- SET superseded_by = new_f.new_id
402
- FROM (VALUES ${values}) AS new_f(subject, predicate, new_id)
403
- WHERE old.subject = new_f.subject
404
- AND old.predicate = new_f.predicate
405
- AND old.superseded_by IS NULL
406
- AND old.id != new_f.new_id`, supersedeParams);
407
- }
513
+ await ingestSessionFactsOnClient(client, sessionId, facts);
408
514
  await client.query("COMMIT");
409
515
  }
410
516
  catch (err) {
@@ -415,7 +521,7 @@ export class PgSessionStore {
415
521
  client.release();
416
522
  }
417
523
  if (embedder) {
418
- for (const fact of facts) {
524
+ for (const fact of batchWinners(facts)) {
419
525
  const factText = `${fact.subject} ${fact.predicate} ${fact.value}`.trim();
420
526
  if (!factText)
421
527
  continue;
@@ -488,8 +594,26 @@ export class PgSessionStore {
488
594
  return out;
489
595
  }
490
596
  }
491
- function rowToSession(row, entitiesById, markersById, edges) {
597
+ function sanitizeTsToken(token) {
598
+ return token.replace(/[&|!():*'<]/g, "");
599
+ }
600
+ function rowToSession(row, entitiesById, markersById, overlay, edges) {
492
601
  const m = markersById.get(row.id);
602
+ const rawDecisions = m?.decisions ?? [];
603
+ const rawOpen = m?.open ?? [];
604
+ const activeOpen = [];
605
+ const promotedDecisions = [];
606
+ for (const text of rawOpen) {
607
+ const id = openQuestionId(row.id, text);
608
+ if (overlay.resolvedOpens.has(id))
609
+ continue;
610
+ const resolution = overlay.promotedOpens.get(id);
611
+ if (resolution !== undefined) {
612
+ promotedDecisions.push(resolution);
613
+ continue;
614
+ }
615
+ activeOpen.push(text);
616
+ }
493
617
  return {
494
618
  id: row.id,
495
619
  runtime: row.runtime,
@@ -499,16 +623,20 @@ function rowToSession(row, entitiesById, markersById, edges) {
499
623
  durationMin: row.duration_min,
500
624
  label: row.label,
501
625
  summary: row.summary,
502
- status: row.status,
626
+ status: liveSessionStatus(row.transcript_path, row.status),
503
627
  transcriptKind: row.transcript_kind ?? "",
504
628
  transcriptPath: row.transcript_path,
505
629
  body: row.body ?? "",
506
630
  entities: entitiesById.get(row.id) ?? [],
507
- decisions: m?.decisions ?? [],
508
- open: m?.open ?? [],
631
+ decisions: [...rawDecisions, ...promotedDecisions],
632
+ open: activeOpen,
509
633
  ...(edges !== undefined
510
634
  ? { supersededBy: edges.supersededBy, supersedes: edges.supersedes }
511
635
  : {}),
636
+ workstreamId: row.workstream_id ?? null,
637
+ classifierProvider: row.classifier_provider ?? null,
638
+ classifierModel: row.classifier_model ?? null,
639
+ classifierConfidence: row.classifier_confidence ?? null,
512
640
  };
513
641
  }
514
642
  //# sourceMappingURL=pg-session-store.js.map