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,34 @@
1
+ -- migrations/pg/025_workstreams.sql
2
+ -- PG parity for SQLite migration 025 (workstreams). Auto-applied by the
3
+ -- version-gated runner (runMigrationsPg). Idempotent: every DDL statement uses
4
+ -- IF NOT EXISTS or ADD COLUMN IF NOT EXISTS, so re-applying on an existing
5
+ -- schema is a no-op. Mirrors migrations/025_workstreams.sql.
6
+
7
+ BEGIN;
8
+
9
+ CREATE TABLE IF NOT EXISTS workstreams (
10
+ id TEXT PRIMARY KEY,
11
+ label TEXT NOT NULL,
12
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','merged','retired')),
13
+ merged_into TEXT REFERENCES workstreams(id),
14
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
15
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
16
+ last_session_at TIMESTAMPTZ
17
+ );
18
+
19
+ CREATE TABLE IF NOT EXISTS workstream_entities (
20
+ workstream_id TEXT NOT NULL REFERENCES workstreams(id) ON DELETE CASCADE,
21
+ entity_canonical TEXT NOT NULL REFERENCES entities(canonical),
22
+ session_count INTEGER NOT NULL DEFAULT 0,
23
+ PRIMARY KEY (workstream_id, entity_canonical)
24
+ );
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_workstream_entities_entity ON workstream_entities(entity_canonical);
27
+
28
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS workstream_id TEXT REFERENCES workstreams(id);
29
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS binding_source TEXT;
30
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS binding_confidence DOUBLE PRECISION;
31
+
32
+ CREATE INDEX IF NOT EXISTS idx_sessions_workstream ON sessions(workstream_id);
33
+
34
+ COMMIT;
@@ -0,0 +1,12 @@
1
+ -- Records which embedder produced the stored vectors, per lane.
2
+ -- lane 'prose' covers session chunks and fact embeddings (same embedder);
3
+ -- lane 'code' covers code exemplar embeddings.
4
+ -- Absence of a row means pre-tracking corpus: reconcile treats the running
5
+ -- embedder as canonical and records it.
6
+ CREATE TABLE IF NOT EXISTS embedding_config (
7
+ lane TEXT PRIMARY KEY CHECK (lane IN ('prose', 'code')),
8
+ provider TEXT NOT NULL,
9
+ model TEXT NOT NULL,
10
+ dim INTEGER NOT NULL,
11
+ updated_at TEXT NOT NULL
12
+ );
@@ -0,0 +1,5 @@
1
+ -- Which classifier produced this session's classification. NULL = classified
2
+ -- before provenance tracking; nlm reprocess treats NULL as eligible.
3
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS classifier_provider TEXT;
4
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS classifier_model TEXT;
5
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS classifier_confidence REAL;
@@ -0,0 +1,9 @@
1
+ -- Entity variant alias table. Tracks merged surface forms so re-ingest of an
2
+ -- old spelling binds to the canonical entity instead of resurrecting the source.
3
+ CREATE TABLE IF NOT EXISTS entity_variants (
4
+ variant TEXT PRIMARY KEY,
5
+ canonical TEXT NOT NULL REFERENCES entities(canonical) ON DELETE CASCADE,
6
+ source_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL
7
+ );
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_entity_variants_canonical ON entity_variants(canonical);
package/nlm/index.js CHANGED
@@ -181,30 +181,55 @@ var STOPWORDS = /* @__PURE__ */ new Set([
181
181
  ]);
182
182
  var MIN_CONTENT_WORDS = 2;
183
183
  var MIN_WORD_LEN = 3;
184
+ var SYSTEM_MESSAGE_PREFIX = /^<(task-notification|command-name|command-message|command-args|local-command-stdout|local-command-caveat|output-file|system-reminder)\b/;
184
185
  function extractRecallQuery(prompt) {
186
+ if (SYSTEM_MESSAGE_PREFIX.test(prompt.trim())) return null;
185
187
  const tokens = prompt.trim().split(/\s+/).map((t) => t.replace(/^[^\w-]+|[^\w-]+$/g, "")).filter((t) => t.length >= MIN_WORD_LEN);
186
188
  const contentWords = tokens.filter((t) => !STOPWORDS.has(t.toLowerCase()));
187
189
  if (contentWords.length < MIN_CONTENT_WORDS) return null;
188
190
  return contentWords.join(" ");
189
191
  }
190
192
 
193
+ // src/hook/hook-helpers.ts
194
+ function readStdin() {
195
+ return new Promise((resolve2) => {
196
+ let data = "";
197
+ process.stdin.setEncoding("utf8");
198
+ process.stdin.on("data", (chunk) => data += chunk);
199
+ process.stdin.on("end", () => resolve2(data));
200
+ process.stdin.on("error", () => resolve2(data));
201
+ });
202
+ }
203
+ async function fetchWithTimeout(url, init, ms) {
204
+ const controller = new AbortController();
205
+ const timer = setTimeout(() => controller.abort(), ms);
206
+ try {
207
+ return await fetch(url, { ...init, signal: controller.signal });
208
+ } finally {
209
+ clearTimeout(timer);
210
+ }
211
+ }
212
+ function hookModeFromEnv() {
213
+ return process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
214
+ }
215
+
191
216
  // src/hook/recall-over-http.ts
192
217
  var RECALL_LIMIT = 5;
193
218
  var RECALL_TIMEOUT_MS = 2e3;
194
- async function recallOverHttp(prompt, runtime, conversationId) {
219
+ async function recallOverHttp(prompt, runtime, conversationId, mode = "keyword") {
195
220
  const query = extractRecallQuery(prompt);
196
221
  if (query === null) return { hits: [], facts: [], exemplars: [] };
197
222
  const portValue = process.env["NLM_PORT"] ?? "3940";
198
- const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=keyword&limit=${RECALL_LIMIT}&withFacts=true&withExemplars=true` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "");
199
- const controller = new AbortController();
200
- const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
223
+ const url = (
224
+ // 127.0.0.1, not localhost: each hook is a fresh process with no connection
225
+ // reuse, and Node resolves localhost to IPv6 ::1 first — a measured ~50-300ms
226
+ // per-fire connect penalty vs ~3ms on the explicit IPv4 loopback.
227
+ `http://127.0.0.1:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=${mode}&limit=${RECALL_LIMIT}&withFacts=true&withExemplars=true` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "")
228
+ );
201
229
  try {
202
230
  const extra = { "x-recall-source": "hook" };
203
231
  if (runtime) extra["x-recall-runtime"] = runtime;
204
- const res = await fetch(url, {
205
- headers: hookAuthHeaders(extra),
206
- signal: controller.signal
207
- });
232
+ const res = await fetchWithTimeout(url, { headers: hookAuthHeaders(extra) }, RECALL_TIMEOUT_MS);
208
233
  if (!res.ok) return { hits: [], facts: [], exemplars: [] };
209
234
  let body;
210
235
  try {
@@ -232,8 +257,8 @@ async function recallOverHttp(prompt, runtime, conversationId) {
232
257
  taskContext: e.taskContext
233
258
  }));
234
259
  return { hits, facts, exemplars };
235
- } finally {
236
- clearTimeout(timer);
260
+ } catch {
261
+ return { hits: [], facts: [], exemplars: [] };
237
262
  }
238
263
  }
239
264
 
@@ -243,14 +268,192 @@ import { pathToFileURL } from "node:url";
243
268
  // src/core/hook/gate.ts
244
269
  var LEADING_FILLER = /^(please|can you|could you|would you|will you|i need you to|i'd like you to|i want you to|i would like you to|help me|let's|lets|hey|ok|okay)\b[\s,]*/i;
245
270
  var GENERATIVE_OPENER = /^(write|draft|create|compose|generate|brainstorm|design|outline|sketch|invent|rename|come up with)\b/i;
271
+ var HARNESS_TAGS = [
272
+ "ide_selection",
273
+ "ide_opened_file",
274
+ "ide_closed_file",
275
+ "ide_diagnostics",
276
+ "ide_recently_modified_file",
277
+ "task-notification",
278
+ "system-reminder",
279
+ "command-name",
280
+ "command-message",
281
+ "command-args",
282
+ "local-command-stdout",
283
+ "local-command-stderr"
284
+ ];
285
+ var HARNESS_BLOCK = new RegExp(
286
+ `<(${HARNESS_TAGS.join("|")})\\b[^>]*>[\\s\\S]*?(?:</\\1>|$)`,
287
+ "gi"
288
+ );
289
+ var ACK_ONLY = /^(yes|yep|yeah|sure|ok|okay|k|thanks|thank you|ty|done|nice|cool|great|perfect|continue|next|go|go ahead|do it|yes please|sounds good|good|got it|right|correct)\W*$/i;
290
+ function contentWordCount(s) {
291
+ const m = s.match(/[A-Za-z0-9]{2,}/g);
292
+ return m ? m.length : 0;
293
+ }
246
294
  function classifyPrompt(prompt) {
247
- let p = prompt.trim();
295
+ let p = prompt.replace(HARNESS_BLOCK, " ").replace(/\s+/g, " ").trim();
296
+ if (contentWordCount(p) === 0) return "skip";
297
+ if (ACK_ONLY.test(p)) return "skip";
248
298
  for (let i = 0; i < 3 && LEADING_FILLER.test(p); i++) {
249
299
  p = p.replace(LEADING_FILLER, "");
250
300
  }
301
+ p = p.trim();
302
+ if (contentWordCount(p) === 0) return "skip";
251
303
  return GENERATIVE_OPENER.test(p) ? "generative" : "evaluate";
252
304
  }
253
305
 
306
+ // src/hook/recent-context.ts
307
+ import { closeSync, existsSync as existsSync2, fstatSync, openSync, readSync } from "node:fs";
308
+ var DEFAULT_MAX_TURNS = 3;
309
+ var DEFAULT_MAX_BYTES = 64 * 1024;
310
+ var DEFAULT_PER_TURN_CHARS = 400;
311
+ var STOPWORDS2 = /* @__PURE__ */ new Set([
312
+ "the",
313
+ "a",
314
+ "an",
315
+ "and",
316
+ "or",
317
+ "but",
318
+ "is",
319
+ "are",
320
+ "was",
321
+ "were",
322
+ "be",
323
+ "been",
324
+ "being",
325
+ "do",
326
+ "does",
327
+ "did",
328
+ "doing",
329
+ "you",
330
+ "your",
331
+ "i",
332
+ "we",
333
+ "our",
334
+ "it",
335
+ "its",
336
+ "this",
337
+ "that",
338
+ "these",
339
+ "those",
340
+ "what",
341
+ "which",
342
+ "how",
343
+ "why",
344
+ "when",
345
+ "where",
346
+ "who",
347
+ "can",
348
+ "could",
349
+ "would",
350
+ "should",
351
+ "will",
352
+ "shall",
353
+ "may",
354
+ "might",
355
+ "must",
356
+ "please",
357
+ "to",
358
+ "of",
359
+ "in",
360
+ "on",
361
+ "for",
362
+ "with",
363
+ "so",
364
+ "now",
365
+ "then",
366
+ "here",
367
+ "there",
368
+ "just",
369
+ "also",
370
+ "as",
371
+ "at",
372
+ "by",
373
+ "if",
374
+ "my",
375
+ "me",
376
+ "us",
377
+ "ok",
378
+ "okay",
379
+ "yes",
380
+ "no",
381
+ "not",
382
+ "up",
383
+ "out",
384
+ "go",
385
+ "get",
386
+ "got",
387
+ "make",
388
+ "made",
389
+ "give",
390
+ "tell",
391
+ "want",
392
+ "need",
393
+ "think",
394
+ "know",
395
+ "let",
396
+ "lets",
397
+ "about",
398
+ "from",
399
+ "into"
400
+ ]);
401
+ function topicalWordCount(s) {
402
+ const tokens = s.toLowerCase().match(/[a-z0-9]{2,}/g) ?? [];
403
+ return tokens.filter((t) => !STOPWORDS2.has(t)).length;
404
+ }
405
+ function textOf(message) {
406
+ if (typeof message !== "object" || message === null) return "";
407
+ const content = message.content;
408
+ if (typeof content === "string") return content;
409
+ if (Array.isArray(content)) {
410
+ return content.filter(
411
+ (b) => typeof b === "object" && b !== null && b.type === "text"
412
+ ).map((b) => b.text).join(" ");
413
+ }
414
+ return "";
415
+ }
416
+ function tailRead(path, maxBytes) {
417
+ const fd = openSync(path, "r");
418
+ try {
419
+ const size = fstatSync(fd).size;
420
+ const start = size > maxBytes ? size - maxBytes : 0;
421
+ const len = size - start;
422
+ if (len <= 0) return "";
423
+ const buf = Buffer.alloc(len);
424
+ readSync(fd, buf, 0, len, start);
425
+ return buf.toString("utf8");
426
+ } finally {
427
+ closeSync(fd);
428
+ }
429
+ }
430
+ function recentConversationContext(transcriptPath, opts = {}) {
431
+ try {
432
+ if (!transcriptPath || !existsSync2(transcriptPath)) return "";
433
+ const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
434
+ const perTurnChars = opts.perTurnChars ?? DEFAULT_PER_TURN_CHARS;
435
+ const lines = tailRead(transcriptPath, opts.maxBytes ?? DEFAULT_MAX_BYTES).split("\n");
436
+ const turns = [];
437
+ for (let i = lines.length - 1; i >= 0 && turns.length < maxTurns; i--) {
438
+ const line = lines[i].trim();
439
+ if (!line) continue;
440
+ let evt;
441
+ try {
442
+ evt = JSON.parse(line);
443
+ } catch {
444
+ continue;
445
+ }
446
+ if (evt.type !== "user" && evt.type !== "assistant") continue;
447
+ const text = textOf(evt.message).trim();
448
+ if (!text) continue;
449
+ turns.unshift(text.slice(0, perTurnChars));
450
+ }
451
+ return turns.join(" ").trim();
452
+ } catch {
453
+ return "";
454
+ }
455
+ }
456
+
254
457
  // src/core/hook/hook-log.ts
255
458
  import { appendFileSync, mkdirSync } from "node:fs";
256
459
  import { homedir as homedir2 } from "node:os";
@@ -269,7 +472,15 @@ function appendHookLog(entry) {
269
472
  }
270
473
 
271
474
  // src/core/hook/memo.ts
272
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
475
+ import {
476
+ existsSync as existsSync3,
477
+ mkdirSync as mkdirSync2,
478
+ readdirSync,
479
+ readFileSync as readFileSync2,
480
+ rmSync,
481
+ statSync,
482
+ writeFileSync
483
+ } from "node:fs";
273
484
  import { homedir as homedir3 } from "node:os";
274
485
  import { join as join2 } from "node:path";
275
486
  function stateDir() {
@@ -282,7 +493,7 @@ function memoPath(conversationId) {
282
493
  function loadSurfaced(conversationId) {
283
494
  try {
284
495
  const path = memoPath(conversationId);
285
- if (!existsSync2(path)) return /* @__PURE__ */ new Set();
496
+ if (!existsSync3(path)) return /* @__PURE__ */ new Set();
286
497
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
287
498
  if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
288
499
  return new Set(parsed.filter((x) => typeof x === "string"));
@@ -301,6 +512,26 @@ function recordSurfaced(conversationId, ids) {
301
512
  }
302
513
 
303
514
  // src/core/hook/pointer-block.ts
515
+ function truncateSummary(s, max = 200) {
516
+ if (s.length <= max) return s;
517
+ const window = s.slice(0, max);
518
+ const last = window[window.length - 1];
519
+ if ((last === "." || last === "!" || last === "?") && window.length - 1 >= 60) {
520
+ return window;
521
+ }
522
+ for (let i = window.length - 2; i >= 60; i--) {
523
+ const c = window[i];
524
+ if ((c === "." || c === "!" || c === "?") && window[i + 1] === " ") {
525
+ return s.slice(0, i + 1);
526
+ }
527
+ }
528
+ for (let i = window.length - 1; i >= 60; i--) {
529
+ if (window[i] === " ") {
530
+ return s.slice(0, i) + " ...";
531
+ }
532
+ }
533
+ return window + " ...";
534
+ }
304
535
  function formatPointerBlock(hits, facts = [], exemplars = []) {
305
536
  if (hits.length === 0 && facts.length === 0 && exemplars.length === 0) return "";
306
537
  const out = [];
@@ -309,7 +540,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
309
540
  for (const h of hits) {
310
541
  const datePart = h.startedAt.slice(0, 10);
311
542
  if (h.summary) {
312
- out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${h.summary.slice(0, 120)}`);
543
+ out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${truncateSummary(h.summary)}`);
313
544
  } else {
314
545
  out.push(`- ${h.id} \xB7 ${h.label} (${datePart})`);
315
546
  }
@@ -328,7 +559,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
328
559
  out.push("## Related code exemplars (nlm-memory)");
329
560
  for (const e of exemplars) {
330
561
  const langPart = e.lang ? `${e.lang} \xB7 ` : "";
331
- out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${e.taskContext.slice(0, 120)}`);
562
+ out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${truncateSummary(e.taskContext)}`);
332
563
  }
333
564
  }
334
565
  const tools = exemplars.length > 0 ? "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved), recall_code (pull the full code for a related exemplar)." : "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved).";
@@ -337,25 +568,125 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
337
568
  }
338
569
 
339
570
  // src/core/hook/select.ts
571
+ function medianScore(hits) {
572
+ if (hits.length === 0) return 0;
573
+ const sorted = hits.map((h) => h.matchScore).sort((a, b) => a - b);
574
+ return sorted[Math.floor(sorted.length / 2)] ?? 0;
575
+ }
340
576
  function selectHits(params) {
341
- const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
577
+ const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap, relativeFloor = 0 } = params;
578
+ const relCut = relativeFloor > 0 ? relativeFloor * medianScore(hits) : 0;
342
579
  const eligible = hits.filter(
343
- (h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id)
580
+ (h) => h.matchScore >= scoreThreshold && h.matchScore >= relCut && !surfaced.has(h.id)
344
581
  );
345
582
  const budget = Math.max(0, perConversationCap - surfaced.size);
346
583
  const limit = Math.min(perFireCap, budget);
347
584
  return eligible.slice(0, limit);
348
585
  }
349
586
 
587
+ // src/hook/score-floor.ts
588
+ function parseScoreFloor(raw) {
589
+ if (raw === void 0) return 0;
590
+ const parsed = Number(raw);
591
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
592
+ return parsed;
593
+ }
594
+ function parseRelativeFloor(raw, fallback) {
595
+ if (raw === void 0) return fallback;
596
+ const parsed = Number(raw);
597
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
598
+ return parsed;
599
+ }
600
+
601
+ // src/hook/recall-gate.ts
602
+ var RECALL_GATE_MODEL = "qwen3.5:4b";
603
+ var RECALL_GATE_SYSTEM = `You are a recall GATE protecting against off-topic memory injection. Given a USER PROMPT and a CANDIDATE prior-session context, answer irrelevant ONLY when the candidate is CLEARLY about a completely different topic, project, or task than the prompt (e.g. the prompt is about debugging a website and the candidate is about a trading pipeline). If there is ANY plausible topical connection, or you are at all unsure, answer relevant. Dropping a useful memory is worse than keeping a marginal one. You do NOT see the assistant's answer. Output {"gate":"relevant"|"irrelevant"}.`;
604
+ var GATE_FORMAT = { type: "object", properties: { gate: { type: "string", enum: ["relevant", "irrelevant"] } }, required: ["gate"] };
605
+ var GATE_OPTS = { temperature: 0, top_p: 1, top_k: 0, presence_penalty: 0, frequency_penalty: 0 };
606
+ function parseRecallGateMode(env = process.env) {
607
+ const v = env["NLM_HOOK_RECALL_GATE"]?.trim();
608
+ return v === "shadow" || v === "live" ? v : void 0;
609
+ }
610
+ var GATE_TIMEOUT_MS = 4e3;
611
+ function makeOllamaGate(url, model = RECALL_GATE_MODEL, timeoutMs = GATE_TIMEOUT_MS) {
612
+ return async (prompt, candidate) => {
613
+ try {
614
+ const r = await fetchWithTimeout(
615
+ `${url}/api/chat`,
616
+ {
617
+ method: "POST",
618
+ headers: { "Content-Type": "application/json" },
619
+ body: JSON.stringify({
620
+ model,
621
+ stream: false,
622
+ think: false,
623
+ // Keep the gate model resident between fires so only the first fire
624
+ // pays the cold-load; subsequent gates are ~1 judge call.
625
+ keep_alive: "15m",
626
+ format: GATE_FORMAT,
627
+ options: GATE_OPTS,
628
+ messages: [
629
+ { role: "system", content: RECALL_GATE_SYSTEM },
630
+ { role: "user", content: `USER PROMPT:
631
+ ${prompt}
632
+
633
+ CANDIDATE CONTEXT:
634
+ ${candidate}` }
635
+ ]
636
+ })
637
+ },
638
+ timeoutMs
639
+ );
640
+ const d = await r.json();
641
+ const v = JSON.parse(d.message?.content ?? "{}").gate;
642
+ return v === "irrelevant" ? "irrelevant" : "relevant";
643
+ } catch {
644
+ return "relevant";
645
+ }
646
+ };
647
+ }
648
+
350
649
  // src/hook/prompt-recall-hook.ts
351
- var SCORE_THRESHOLD = 0;
650
+ var SCORE_THRESHOLD = parseScoreFloor(process.env["NLM_RECALL_SCORE_FLOOR"]);
651
+ var RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
352
652
  var PER_FIRE_CAP = 3;
353
653
  var PER_CONVERSATION_CAP = 10;
354
654
  var PROMPT_PREVIEW_CHARS = 200;
655
+ function parseHookDeadline(raw) {
656
+ if (raw === void 0) return 4e3;
657
+ const n = Number(raw);
658
+ return Number.isFinite(n) && n > 0 ? n : 4e3;
659
+ }
660
+ var HOOK_DEADLINE_MS = parseHookDeadline(process.env["NLM_HOOK_DEADLINE_MS"]);
661
+ async function withDeadline(p, ms, fallback) {
662
+ if (ms <= 0) return fallback;
663
+ let timer;
664
+ const timeout = new Promise((resolve2) => {
665
+ timer = setTimeout(() => resolve2(fallback), ms);
666
+ });
667
+ try {
668
+ return await Promise.race([p, timeout]);
669
+ } finally {
670
+ clearTimeout(timer);
671
+ }
672
+ }
355
673
  function hookRuntimeFromEnv(env = process.env) {
356
674
  const raw = env["NLM_HOOK_RUNTIME"]?.trim();
357
675
  return raw ? raw : "claude-code";
358
676
  }
677
+ function promptRecallEnabled(env = process.env) {
678
+ const raw = env["NLM_HOOK_PROMPT_RECALL"]?.trim();
679
+ if (raw === void 0 || raw === "") return false;
680
+ return raw.toLowerCase() !== "off";
681
+ }
682
+ function buildRecallQuery(input, env = process.env) {
683
+ if (env["NLM_HOOK_CONTEXT_RECALL"] !== "1") return input.prompt;
684
+ if (!input.transcriptPath) return input.prompt;
685
+ const minWords = Number.parseInt(env["NLM_HOOK_CONTEXT_MIN_WORDS"] ?? "3", 10);
686
+ if (topicalWordCount(input.prompt) >= minWords) return input.prompt;
687
+ const context = recentConversationContext(input.transcriptPath);
688
+ return context ? `${context} ${input.prompt}` : input.prompt;
689
+ }
359
690
  function normalizeRecall(raw) {
360
691
  if (Array.isArray(raw)) return { hits: raw, facts: [] };
361
692
  return raw;
@@ -363,7 +694,7 @@ function normalizeRecall(raw) {
363
694
  async function runHook(input, deps) {
364
695
  const gate = classifyPrompt(input.prompt);
365
696
  const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
366
- if (gate === "generative") {
697
+ if (gate === "generative" || gate === "skip") {
367
698
  appendHookLog({
368
699
  ts: (/* @__PURE__ */ new Date()).toISOString(),
369
700
  conversationId: input.conversationId,
@@ -376,9 +707,12 @@ async function runHook(input, deps) {
376
707
  });
377
708
  return "";
378
709
  }
710
+ const deadline = Date.now() + (deps.deadlineMs ?? HOOK_DEADLINE_MS);
379
711
  let fetched = { hits: [], facts: [] };
380
712
  try {
381
- fetched = normalizeRecall(await deps.recall(input.prompt));
713
+ fetched = normalizeRecall(
714
+ await withDeadline(deps.recall(buildRecallQuery(input)), deadline - Date.now(), { hits: [], facts: [] })
715
+ );
382
716
  } catch {
383
717
  fetched = { hits: [], facts: [] };
384
718
  }
@@ -388,10 +722,33 @@ async function runHook(input, deps) {
388
722
  hits,
389
723
  surfaced,
390
724
  scoreThreshold: SCORE_THRESHOLD,
725
+ relativeFloor: RELATIVE_FLOOR,
391
726
  perFireCap: PER_FIRE_CAP,
392
727
  perConversationCap: PER_CONVERSATION_CAP
393
728
  });
394
- const block = formatPointerBlock(selected, fetched.facts, fetched.exemplars);
729
+ let gateDecisions;
730
+ let injected = selected;
731
+ if (deps.recallGate && selected.length > 0) {
732
+ const g = deps.recallGate;
733
+ const toGate = g.maxCandidates ? selected.slice(0, g.maxCandidates) : selected;
734
+ const remaining = deadline - Date.now();
735
+ const gateFallback = toGate.map((h) => ({ id: h.id, gate: "relevant" }));
736
+ if (remaining <= 0) {
737
+ gateDecisions = gateFallback;
738
+ } else {
739
+ gateDecisions = await withDeadline(
740
+ Promise.all(toGate.map(async (h) => ({ id: h.id, gate: await g.judge(input.prompt, `${h.label}
741
+ ${h.summary ?? ""}`) }))),
742
+ remaining,
743
+ gateFallback
744
+ );
745
+ }
746
+ if (g.mode === "live") {
747
+ const drop = new Set(gateDecisions.filter((d) => d.gate === "irrelevant").map((d) => d.id));
748
+ injected = selected.filter((h) => !drop.has(h.id));
749
+ }
750
+ }
751
+ const block = formatPointerBlock(injected, fetched.facts, fetched.exemplars);
395
752
  const estTokens = Math.ceil(block.length / 4);
396
753
  appendHookLog({
397
754
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -399,25 +756,17 @@ async function runHook(input, deps) {
399
756
  promptPreview: preview,
400
757
  gate,
401
758
  hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
402
- wouldInject: selected.map((h) => h.id),
759
+ wouldInject: injected.map((h) => h.id),
403
760
  estTokens,
404
- mode: deps.mode
761
+ mode: deps.mode,
762
+ ...gateDecisions ? { gateDecisions } : {}
405
763
  });
406
- if (deps.mode === "live" && selected.length > 0) {
407
- recordSurfaced(input.conversationId, selected.map((h) => h.id));
764
+ if (deps.mode === "live" && injected.length > 0) {
765
+ recordSurfaced(input.conversationId, injected.map((h) => h.id));
408
766
  return block;
409
767
  }
410
768
  return "";
411
769
  }
412
- function readStdin() {
413
- return new Promise((resolve2) => {
414
- let data = "";
415
- process.stdin.setEncoding("utf8");
416
- process.stdin.on("data", (chunk) => data += chunk);
417
- process.stdin.on("end", () => resolve2(data));
418
- process.stdin.on("error", () => resolve2(data));
419
- });
420
- }
421
770
  async function main() {
422
771
  try {
423
772
  autoloadEnv();
@@ -425,12 +774,21 @@ async function main() {
425
774
  const payload = JSON.parse(raw);
426
775
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
427
776
  const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
777
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : void 0;
428
778
  if (!prompt) return;
429
- const mode = process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
779
+ if (!promptRecallEnabled()) return;
780
+ const mode = hookModeFromEnv();
430
781
  const runtime = hookRuntimeFromEnv();
782
+ const gateMode = parseRecallGateMode();
783
+ const gateUrl = process.env["OLLAMA_URL"] ?? "http://127.0.0.1:11434";
784
+ const gateTopN = Math.max(1, Number.parseInt(process.env["NLM_HOOK_RECALL_GATE_TOPN"] ?? "1", 10) || 1);
431
785
  const out = await runHook(
432
- { prompt, conversationId },
433
- { mode, recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId) }
786
+ { prompt, conversationId, ...transcriptPath ? { transcriptPath } : {} },
787
+ {
788
+ mode,
789
+ recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId),
790
+ ...gateMode ? { recallGate: { mode: gateMode, judge: makeOllamaGate(gateUrl), maxCandidates: gateTopN } } : {}
791
+ }
434
792
  );
435
793
  if (out) process.stdout.write(out);
436
794
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nlm-memory",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "Local-first non-linear memory operating system for AI operators.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -29,6 +29,8 @@
29
29
  "files": [
30
30
  "dist",
31
31
  "migrations",
32
+ "fixtures",
33
+ "templates",
32
34
  "plugin",
33
35
  "plugin-hermes-agent",
34
36
  "nlm",
@@ -52,16 +54,24 @@
52
54
  "test:watch": "vitest",
53
55
  "test:unit": "vitest run tests/unit",
54
56
  "test:integration": "vitest run tests/integration",
57
+ "test:pg": "vitest run --no-file-parallelism .pg.test.ts",
55
58
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
56
59
  "bench:longmemeval": "tsx scripts/longmemeval/run-harness.ts",
57
60
  "bench:classifier": "tsx scripts/longmemeval/run-harness.ts --classifier",
58
61
  "bench:compare": "tsx scripts/longmemeval/compare-classifiers.ts",
59
62
  "eval:signals": "tsx scripts/eval/signals-eval.ts",
60
- "eval:classifier": "tsx scripts/eval/classifier-eval.ts"
63
+ "eval:classifier": "tsx scripts/eval/classifier-eval.ts",
64
+ "eval:fact-recall": "tsx scripts/eval/fact-recall-eval.ts",
65
+ "eval:code-exemplar": "tsx scripts/eval/code-exemplar-eval.ts",
66
+ "eval:reranker": "tsx scripts/eval/reranker-ablation.ts",
67
+ "eval:floor": "tsx scripts/eval/floor-calibration.ts",
68
+ "eval:matcher": "tsx scripts/eval/tune-matcher.ts",
69
+ "eval:naming": "tsx scripts/eval/tune-naming.ts"
61
70
  },
62
71
  "dependencies": {
63
72
  "@clack/prompts": "^1.4.0",
64
73
  "@hono/node-server": "^2.0.3",
74
+ "@huggingface/transformers": "4.2.0",
65
75
  "@modelcontextprotocol/sdk": "^1.29.0",
66
76
  "@toon-format/toon": "^2.3.0",
67
77
  "better-sqlite3": "^12.10.0",