nlm-memory 0.20.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 (392) 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/prompt.js +3 -3
  16. package/dist/core/digest/compose.d.ts +10 -0
  17. package/dist/core/digest/compose.js +7 -1
  18. package/dist/core/digest/compose.js.map +1 -1
  19. package/dist/core/embedding/embed-backfill.d.ts +13 -2
  20. package/dist/core/embedding/embed-backfill.js +181 -43
  21. package/dist/core/embedding/embed-backfill.js.map +1 -1
  22. package/dist/core/embedding/embedding-config.d.ts +17 -0
  23. package/dist/core/embedding/embedding-config.js +12 -0
  24. package/dist/core/embedding/embedding-config.js.map +1 -0
  25. package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
  26. package/dist/core/embedding/pg-embed-backfill.js +264 -0
  27. package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
  28. package/dist/core/entities/dedup-suggest.d.ts +25 -0
  29. package/dist/core/entities/dedup-suggest.js +107 -0
  30. package/dist/core/entities/dedup-suggest.js.map +1 -0
  31. package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
  32. package/dist/core/eval/classifier-fixture-eval.js +217 -0
  33. package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
  34. package/dist/core/eval/extraction-scoring.d.ts +63 -0
  35. package/dist/core/eval/extraction-scoring.js +79 -0
  36. package/dist/core/eval/extraction-scoring.js.map +1 -0
  37. package/dist/core/eval/run-eval.d.ts +40 -0
  38. package/dist/core/eval/run-eval.js +34 -0
  39. package/dist/core/eval/run-eval.js.map +1 -0
  40. package/dist/core/exemplars/capture-from-session.d.ts +1 -0
  41. package/dist/core/exemplars/capture-from-session.js +6 -2
  42. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  43. package/dist/core/exemplars/diff-parse.d.ts +23 -0
  44. package/dist/core/exemplars/diff-parse.js +73 -0
  45. package/dist/core/exemplars/diff-parse.js.map +1 -0
  46. package/dist/core/exemplars/embed-backfill.d.ts +36 -0
  47. package/dist/core/exemplars/embed-backfill.js +85 -0
  48. package/dist/core/exemplars/embed-backfill.js.map +1 -0
  49. package/dist/core/exemplars/embed-text.d.ts +15 -0
  50. package/dist/core/exemplars/embed-text.js +18 -0
  51. package/dist/core/exemplars/embed-text.js.map +1 -0
  52. package/dist/core/exemplars/extract-exemplar.js +2 -65
  53. package/dist/core/exemplars/extract-exemplar.js.map +1 -1
  54. package/dist/core/exemplars/recall-code.js +4 -0
  55. package/dist/core/exemplars/recall-code.js.map +1 -1
  56. package/dist/core/exemplars/warm-embedder.d.ts +14 -0
  57. package/dist/core/exemplars/warm-embedder.js +18 -0
  58. package/dist/core/exemplars/warm-embedder.js.map +1 -0
  59. package/dist/core/health/corpus-state.d.ts +8 -0
  60. package/dist/core/health/corpus-state.js +11 -0
  61. package/dist/core/health/corpus-state.js.map +1 -0
  62. package/dist/core/health/embed-inflight.d.ts +9 -0
  63. package/dist/core/health/embed-inflight.js +30 -0
  64. package/dist/core/health/embed-inflight.js.map +1 -0
  65. package/dist/core/health/embedding-lane-state.d.ts +6 -0
  66. package/dist/core/health/embedding-lane-state.js +18 -0
  67. package/dist/core/health/embedding-lane-state.js.map +1 -0
  68. package/dist/core/health/warmup-state.d.ts +12 -0
  69. package/dist/core/health/warmup-state.js +12 -0
  70. package/dist/core/health/warmup-state.js.map +1 -0
  71. package/dist/core/hook/gate.d.ts +13 -6
  72. package/dist/core/hook/gate.js +47 -6
  73. package/dist/core/hook/gate.js.map +1 -1
  74. package/dist/core/hook/hook-log.d.ts +5 -0
  75. package/dist/core/hook/hook-log.js.map +1 -1
  76. package/dist/core/hook/memo.d.ts +12 -0
  77. package/dist/core/hook/memo.js +41 -1
  78. package/dist/core/hook/memo.js.map +1 -1
  79. package/dist/core/hook/pointer-block.d.ts +1 -0
  80. package/dist/core/hook/pointer-block.js +23 -2
  81. package/dist/core/hook/pointer-block.js.map +1 -1
  82. package/dist/core/hook/query-extract.d.ts +2 -1
  83. package/dist/core/hook/query-extract.js +10 -1
  84. package/dist/core/hook/query-extract.js.map +1 -1
  85. package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
  86. package/dist/core/hook/resolve-conversation-by-query.js +98 -0
  87. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
  88. package/dist/core/hook/select.d.ts +8 -0
  89. package/dist/core/hook/select.js +9 -2
  90. package/dist/core/hook/select.js.map +1 -1
  91. package/dist/core/hook/transcript.js +16 -2
  92. package/dist/core/hook/transcript.js.map +1 -1
  93. package/dist/core/ingest/ingest-session.d.ts +5 -0
  94. package/dist/core/ingest/ingest-session.js +3 -0
  95. package/dist/core/ingest/ingest-session.js.map +1 -1
  96. package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
  97. package/dist/core/ingest/reclassify-oversized.js +3 -0
  98. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  99. package/dist/core/ingest/reprocess.d.ts +96 -0
  100. package/dist/core/ingest/reprocess.js +275 -0
  101. package/dist/core/ingest/reprocess.js.map +1 -0
  102. package/dist/core/integrity/check-invariants.d.ts +6 -1
  103. package/dist/core/integrity/check-invariants.js +209 -3
  104. package/dist/core/integrity/check-invariants.js.map +1 -1
  105. package/dist/core/metrics/corpus-stats.d.ts +49 -0
  106. package/dist/core/metrics/corpus-stats.js +111 -0
  107. package/dist/core/metrics/corpus-stats.js.map +1 -0
  108. package/dist/core/metrics/re-derivation.d.ts +48 -0
  109. package/dist/core/metrics/re-derivation.js +87 -0
  110. package/dist/core/metrics/re-derivation.js.map +1 -0
  111. package/dist/core/recall/citation-log.d.ts +8 -0
  112. package/dist/core/recall/citation-log.js +17 -0
  113. package/dist/core/recall/citation-log.js.map +1 -1
  114. package/dist/core/recall/query-intent.d.ts +7 -0
  115. package/dist/core/recall/query-intent.js +25 -0
  116. package/dist/core/recall/query-intent.js.map +1 -0
  117. package/dist/core/recall/query-log.d.ts +4 -0
  118. package/dist/core/recall/query-log.js +4 -0
  119. package/dist/core/recall/query-log.js.map +1 -1
  120. package/dist/core/recall/recall-service.d.ts +37 -1
  121. package/dist/core/recall/recall-service.js +106 -63
  122. package/dist/core/recall/recall-service.js.map +1 -1
  123. package/dist/core/recall/related-exemplars.d.ts +1 -0
  124. package/dist/core/recall/related-exemplars.js +4 -1
  125. package/dist/core/recall/related-exemplars.js.map +1 -1
  126. package/dist/core/recall/related-facts.js +2 -2
  127. package/dist/core/recall/related-facts.js.map +1 -1
  128. package/dist/core/recall/reranker.d.ts +2 -1
  129. package/dist/core/recall/reranker.js +3 -3
  130. package/dist/core/recall/reranker.js.map +1 -1
  131. package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
  132. package/dist/core/recall-facts/fact-query-log.js +2 -0
  133. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  134. package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
  135. package/dist/core/recall-facts/fact-recall-service.js +21 -8
  136. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  137. package/dist/core/scheduler/scan-once.d.ts +6 -6
  138. package/dist/core/scheduler/scan-once.js +13 -49
  139. package/dist/core/scheduler/scan-once.js.map +1 -1
  140. package/dist/core/scheduler/scheduler.d.ts +10 -0
  141. package/dist/core/scheduler/scheduler.js +75 -10
  142. package/dist/core/scheduler/scheduler.js.map +1 -1
  143. package/dist/core/signals/code-signal.d.ts +49 -0
  144. package/dist/core/signals/code-signal.js +53 -0
  145. package/dist/core/signals/code-signal.js.map +1 -0
  146. package/dist/core/storage/db-restore.js +3 -1
  147. package/dist/core/storage/db-restore.js.map +1 -1
  148. package/dist/core/storage/fact-batch.d.ts +9 -0
  149. package/dist/core/storage/fact-batch.js +14 -0
  150. package/dist/core/storage/fact-batch.js.map +1 -0
  151. package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
  152. package/dist/core/storage/pg-code-exemplar-store.js +7 -0
  153. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  154. package/dist/core/storage/pg-embedding-config.d.ts +29 -0
  155. package/dist/core/storage/pg-embedding-config.js +53 -0
  156. package/dist/core/storage/pg-embedding-config.js.map +1 -0
  157. package/dist/core/storage/pg-entity-store.d.ts +12 -0
  158. package/dist/core/storage/pg-entity-store.js +79 -0
  159. package/dist/core/storage/pg-entity-store.js.map +1 -0
  160. package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
  161. package/dist/core/storage/pg-fact-ingest.js +34 -0
  162. package/dist/core/storage/pg-fact-ingest.js.map +1 -0
  163. package/dist/core/storage/pg-fact-store.d.ts +3 -0
  164. package/dist/core/storage/pg-fact-store.js +26 -30
  165. package/dist/core/storage/pg-fact-store.js.map +1 -1
  166. package/dist/core/storage/pg-migrate.d.ts +18 -0
  167. package/dist/core/storage/pg-migrate.js +73 -0
  168. package/dist/core/storage/pg-migrate.js.map +1 -0
  169. package/dist/core/storage/pg-session-store.d.ts +9 -0
  170. package/dist/core/storage/pg-session-store.js +204 -76
  171. package/dist/core/storage/pg-session-store.js.map +1 -1
  172. package/dist/core/storage/pg-storage.d.ts +9 -7
  173. package/dist/core/storage/pg-storage.js +20 -50
  174. package/dist/core/storage/pg-storage.js.map +1 -1
  175. package/dist/core/storage/pg-workstream-store.d.ts +24 -0
  176. package/dist/core/storage/pg-workstream-store.js +96 -0
  177. package/dist/core/storage/pg-workstream-store.js.map +1 -0
  178. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
  179. package/dist/core/storage/sqlite-code-exemplar-store.js +11 -0
  180. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  181. package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
  182. package/dist/core/storage/sqlite-embedding-config.js +27 -0
  183. package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
  184. package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
  185. package/dist/core/storage/sqlite-entity-store.js +96 -0
  186. package/dist/core/storage/sqlite-entity-store.js.map +1 -0
  187. package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
  188. package/dist/core/storage/sqlite-fact-store.js +76 -25
  189. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  190. package/dist/core/storage/sqlite-session-store.d.ts +20 -5
  191. package/dist/core/storage/sqlite-session-store.js +183 -68
  192. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  193. package/dist/core/storage/sqlite-storage.d.ts +8 -8
  194. package/dist/core/storage/sqlite-storage.js +21 -32
  195. package/dist/core/storage/sqlite-storage.js.map +1 -1
  196. package/dist/core/storage/sqlite-workstream-store.d.ts +24 -0
  197. package/dist/core/storage/sqlite-workstream-store.js +98 -0
  198. package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
  199. package/dist/core/work-digest/active-spans.d.ts +8 -0
  200. package/dist/core/work-digest/active-spans.js +29 -0
  201. package/dist/core/work-digest/active-spans.js.map +1 -0
  202. package/dist/core/work-digest/attribute.d.ts +15 -0
  203. package/dist/core/work-digest/attribute.js +76 -0
  204. package/dist/core/work-digest/attribute.js.map +1 -0
  205. package/dist/core/work-digest/build-work-digest.d.ts +12 -0
  206. package/dist/core/work-digest/build-work-digest.js +69 -0
  207. package/dist/core/work-digest/build-work-digest.js.map +1 -0
  208. package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
  209. package/dist/core/work-digest/compose-work-digest.js +52 -0
  210. package/dist/core/work-digest/compose-work-digest.js.map +1 -0
  211. package/dist/core/work-digest/merge-active.d.ts +10 -0
  212. package/dist/core/work-digest/merge-active.js +22 -0
  213. package/dist/core/work-digest/merge-active.js.map +1 -0
  214. package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
  215. package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
  216. package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
  217. package/dist/core/work-digest/topics.d.ts +18 -0
  218. package/dist/core/work-digest/topics.js +26 -0
  219. package/dist/core/work-digest/topics.js.map +1 -0
  220. package/dist/core/work-digest/types.d.ts +45 -0
  221. package/dist/core/work-digest/types.js +2 -0
  222. package/dist/core/work-digest/types.js.map +1 -0
  223. package/dist/core/workstream/bind.d.ts +25 -0
  224. package/dist/core/workstream/bind.js +23 -0
  225. package/dist/core/workstream/bind.js.map +1 -0
  226. package/dist/core/workstream/compose-recall.d.ts +2 -0
  227. package/dist/core/workstream/compose-recall.js +39 -0
  228. package/dist/core/workstream/compose-recall.js.map +1 -0
  229. package/dist/core/workstream/merge-suggest.d.ts +19 -0
  230. package/dist/core/workstream/merge-suggest.js +61 -0
  231. package/dist/core/workstream/merge-suggest.js.map +1 -0
  232. package/dist/core/workstream/model.d.ts +20 -0
  233. package/dist/core/workstream/model.js +9 -0
  234. package/dist/core/workstream/model.js.map +1 -0
  235. package/dist/core/workstream/name-match.d.ts +8 -0
  236. package/dist/core/workstream/name-match.js +18 -0
  237. package/dist/core/workstream/name-match.js.map +1 -0
  238. package/dist/core/workstream/resolve.d.ts +10 -0
  239. package/dist/core/workstream/resolve.js +21 -0
  240. package/dist/core/workstream/resolve.js.map +1 -0
  241. package/dist/core/workstream/rollup.d.ts +12 -0
  242. package/dist/core/workstream/rollup.js +17 -0
  243. package/dist/core/workstream/rollup.js.map +1 -0
  244. package/dist/core/workstream/work-topics.d.ts +10 -0
  245. package/dist/core/workstream/work-topics.js +55 -0
  246. package/dist/core/workstream/work-topics.js.map +1 -0
  247. package/dist/hook/hook-helpers.d.ts +4 -0
  248. package/dist/hook/hook-helpers.js +36 -0
  249. package/dist/hook/hook-helpers.js.map +1 -0
  250. package/dist/hook/pre-compact-hook.d.ts +23 -0
  251. package/dist/hook/pre-compact-hook.js +66 -0
  252. package/dist/hook/pre-compact-hook.js.map +1 -0
  253. package/dist/hook/prompt-recall-hook.d.ts +37 -0
  254. package/dist/hook/prompt-recall-hook.js +120 -20
  255. package/dist/hook/prompt-recall-hook.js.map +1 -1
  256. package/dist/hook/recall-gate.d.ts +29 -0
  257. package/dist/hook/recall-gate.js +50 -0
  258. package/dist/hook/recall-gate.js.map +1 -0
  259. package/dist/hook/recall-over-http.d.ts +1 -1
  260. package/dist/hook/recall-over-http.js +11 -11
  261. package/dist/hook/recall-over-http.js.map +1 -1
  262. package/dist/hook/recent-context.d.ts +25 -0
  263. package/dist/hook/recent-context.js +104 -0
  264. package/dist/hook/recent-context.js.map +1 -0
  265. package/dist/hook/score-floor.d.ts +27 -0
  266. package/dist/hook/score-floor.js +42 -0
  267. package/dist/hook/score-floor.js.map +1 -0
  268. package/dist/hook/session-end-hook.js +8 -29
  269. package/dist/hook/session-end-hook.js.map +1 -1
  270. package/dist/hook/session-start-hook.js +24 -59
  271. package/dist/hook/session-start-hook.js.map +1 -1
  272. package/dist/hook/stop-hook.js +22 -59
  273. package/dist/hook/stop-hook.js.map +1 -1
  274. package/dist/hook/subagent-start-hook.d.ts +28 -0
  275. package/dist/hook/subagent-start-hook.js +81 -0
  276. package/dist/hook/subagent-start-hook.js.map +1 -0
  277. package/dist/http/app.js +28 -8
  278. package/dist/http/app.js.map +1 -1
  279. package/dist/install/setup.js +2 -0
  280. package/dist/install/setup.js.map +1 -1
  281. package/dist/llm/build-classifier.d.ts +22 -0
  282. package/dist/llm/build-classifier.js +42 -0
  283. package/dist/llm/build-classifier.js.map +1 -0
  284. package/dist/llm/build-embedder.d.ts +11 -0
  285. package/dist/llm/build-embedder.js +42 -0
  286. package/dist/llm/build-embedder.js.map +1 -0
  287. package/dist/llm/bundled-embedder-client.d.ts +37 -0
  288. package/dist/llm/bundled-embedder-client.js +90 -0
  289. package/dist/llm/bundled-embedder-client.js.map +1 -0
  290. package/dist/llm/classifier-box.d.ts +13 -5
  291. package/dist/llm/classifier-box.js +28 -6
  292. package/dist/llm/classifier-box.js.map +1 -1
  293. package/dist/llm/classifier-egress.d.ts +18 -0
  294. package/dist/llm/classifier-egress.js +59 -0
  295. package/dist/llm/classifier-egress.js.map +1 -0
  296. package/dist/llm/client-shared.d.ts +4 -0
  297. package/dist/llm/client-shared.js +39 -0
  298. package/dist/llm/client-shared.js.map +1 -0
  299. package/dist/llm/deepseek-client.d.ts +20 -0
  300. package/dist/llm/deepseek-client.js +59 -45
  301. package/dist/llm/deepseek-client.js.map +1 -1
  302. package/dist/llm/embedder-info.d.ts +15 -0
  303. package/dist/llm/embedder-info.js +27 -0
  304. package/dist/llm/embedder-info.js.map +1 -0
  305. package/dist/llm/model-quirks.d.ts +4 -0
  306. package/dist/llm/model-quirks.js +7 -0
  307. package/dist/llm/model-quirks.js.map +1 -0
  308. package/dist/llm/naming.d.ts +5 -0
  309. package/dist/llm/naming.js +20 -0
  310. package/dist/llm/naming.js.map +1 -0
  311. package/dist/llm/ollama-client.d.ts +7 -8
  312. package/dist/llm/ollama-client.js +59 -51
  313. package/dist/llm/ollama-client.js.map +1 -1
  314. package/dist/llm/ollama-code-embedder.d.ts +8 -3
  315. package/dist/llm/ollama-code-embedder.js +14 -24
  316. package/dist/llm/ollama-code-embedder.js.map +1 -1
  317. package/dist/llm/openai-code-embedder-client.d.ts +25 -0
  318. package/dist/llm/openai-code-embedder-client.js +52 -0
  319. package/dist/llm/openai-code-embedder-client.js.map +1 -0
  320. package/dist/llm/openai-embedder-client.d.ts +41 -0
  321. package/dist/llm/openai-embedder-client.js +83 -0
  322. package/dist/llm/openai-embedder-client.js.map +1 -0
  323. package/dist/mcp/server.d.ts +47 -2
  324. package/dist/mcp/server.js +303 -18
  325. package/dist/mcp/server.js.map +1 -1
  326. package/dist/ports/code-embedder.d.ts +6 -1
  327. package/dist/ports/code-exemplar-store.d.ts +5 -0
  328. package/dist/ports/entity-store.d.ts +35 -0
  329. package/dist/ports/entity-store.js +7 -0
  330. package/dist/ports/entity-store.js.map +1 -0
  331. package/dist/ports/fact-store.d.ts +0 -0
  332. package/dist/ports/fact-store.js.map +1 -1
  333. package/dist/ports/llm-client.d.ts +9 -1
  334. package/dist/ports/session-store.d.ts +12 -0
  335. package/dist/ports/storage.d.ts +7 -30
  336. package/dist/ports/storage.js +3 -7
  337. package/dist/ports/storage.js.map +1 -1
  338. package/dist/ports/transcript-adapter.d.ts +2 -0
  339. package/dist/ports/workstream-store.d.ts +21 -0
  340. package/dist/ports/workstream-store.js +2 -0
  341. package/dist/ports/workstream-store.js.map +1 -0
  342. package/dist/shared/types.d.ts +23 -2
  343. package/fixtures/classifier-gold/README.md +94 -0
  344. package/fixtures/classifier-gold/reference.json +318 -0
  345. package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
  346. package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
  347. package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
  348. package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
  349. package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
  350. package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
  351. package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
  352. package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
  353. package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
  354. package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
  355. package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
  356. package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
  357. package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
  358. package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
  359. package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
  360. package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
  361. package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
  362. package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
  363. package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
  364. package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
  365. package/migrations/000_initial_schema.sql +1 -1
  366. package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
  367. package/migrations/025_workstreams.sql +30 -0
  368. package/migrations/026_embedding_config.sql +12 -0
  369. package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
  370. package/migrations/028_classifier_provenance.sql +5 -0
  371. package/migrations/pg/001_initial.sql +34 -0
  372. package/migrations/pg/019_split_replaces.sql +16 -7
  373. package/migrations/pg/025_workstreams.sql +34 -0
  374. package/migrations/pg/026_embedding_config.sql +12 -0
  375. package/migrations/pg/028_classifier_provenance.sql +5 -0
  376. package/migrations/pg/029_entity_variants.sql +9 -0
  377. package/nlm/index.js +395 -37
  378. package/package.json +12 -2
  379. package/plugin/.codex-plugin/plugin.json +1 -1
  380. package/plugin/scripts/prompt-recall-hook.mjs +402 -41
  381. package/plugin/scripts/session-start-hook.mjs +286 -60
  382. package/plugin/scripts/stop-hook.mjs +85 -65
  383. package/plugin-hermes-agent/__init__.py +2 -1
  384. package/templates/agent-contract/README.md +24 -0
  385. package/templates/agent-contract/claude-code.md +40 -0
  386. package/templates/agent-contract/generic.md +38 -0
  387. package/dist/core/embedding/embed-normalize.d.ts +0 -28
  388. package/dist/core/embedding/embed-normalize.js +0 -98
  389. package/dist/core/embedding/embed-normalize.js.map +0 -1
  390. package/dist/core/storage/pg-tx-context.d.ts +0 -52
  391. package/dist/core/storage/pg-tx-context.js +0 -149
  392. package/dist/core/storage/pg-tx-context.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nlm-memory",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Local-first persistent memory for AI coding agents. Recalls relevant prior sessions on every prompt; learns which sessions actually get cited. Owner-edited timeline beats append-only memory layers.",
5
5
  "author": {
6
6
  "name": "Edward Chalupa",
@@ -6,14 +6,192 @@ import { pathToFileURL } from "node:url";
6
6
  // src/core/hook/gate.ts
7
7
  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;
8
8
  var GENERATIVE_OPENER = /^(write|draft|create|compose|generate|brainstorm|design|outline|sketch|invent|rename|come up with)\b/i;
9
+ var HARNESS_TAGS = [
10
+ "ide_selection",
11
+ "ide_opened_file",
12
+ "ide_closed_file",
13
+ "ide_diagnostics",
14
+ "ide_recently_modified_file",
15
+ "task-notification",
16
+ "system-reminder",
17
+ "command-name",
18
+ "command-message",
19
+ "command-args",
20
+ "local-command-stdout",
21
+ "local-command-stderr"
22
+ ];
23
+ var HARNESS_BLOCK = new RegExp(
24
+ `<(${HARNESS_TAGS.join("|")})\\b[^>]*>[\\s\\S]*?(?:</\\1>|$)`,
25
+ "gi"
26
+ );
27
+ 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;
28
+ function contentWordCount(s) {
29
+ const m = s.match(/[A-Za-z0-9]{2,}/g);
30
+ return m ? m.length : 0;
31
+ }
9
32
  function classifyPrompt(prompt) {
10
- let p = prompt.trim();
33
+ let p = prompt.replace(HARNESS_BLOCK, " ").replace(/\s+/g, " ").trim();
34
+ if (contentWordCount(p) === 0) return "skip";
35
+ if (ACK_ONLY.test(p)) return "skip";
11
36
  for (let i = 0; i < 3 && LEADING_FILLER.test(p); i++) {
12
37
  p = p.replace(LEADING_FILLER, "");
13
38
  }
39
+ p = p.trim();
40
+ if (contentWordCount(p) === 0) return "skip";
14
41
  return GENERATIVE_OPENER.test(p) ? "generative" : "evaluate";
15
42
  }
16
43
 
44
+ // src/hook/recent-context.ts
45
+ import { closeSync, existsSync, fstatSync, openSync, readSync } from "node:fs";
46
+ var DEFAULT_MAX_TURNS = 3;
47
+ var DEFAULT_MAX_BYTES = 64 * 1024;
48
+ var DEFAULT_PER_TURN_CHARS = 400;
49
+ var STOPWORDS = /* @__PURE__ */ new Set([
50
+ "the",
51
+ "a",
52
+ "an",
53
+ "and",
54
+ "or",
55
+ "but",
56
+ "is",
57
+ "are",
58
+ "was",
59
+ "were",
60
+ "be",
61
+ "been",
62
+ "being",
63
+ "do",
64
+ "does",
65
+ "did",
66
+ "doing",
67
+ "you",
68
+ "your",
69
+ "i",
70
+ "we",
71
+ "our",
72
+ "it",
73
+ "its",
74
+ "this",
75
+ "that",
76
+ "these",
77
+ "those",
78
+ "what",
79
+ "which",
80
+ "how",
81
+ "why",
82
+ "when",
83
+ "where",
84
+ "who",
85
+ "can",
86
+ "could",
87
+ "would",
88
+ "should",
89
+ "will",
90
+ "shall",
91
+ "may",
92
+ "might",
93
+ "must",
94
+ "please",
95
+ "to",
96
+ "of",
97
+ "in",
98
+ "on",
99
+ "for",
100
+ "with",
101
+ "so",
102
+ "now",
103
+ "then",
104
+ "here",
105
+ "there",
106
+ "just",
107
+ "also",
108
+ "as",
109
+ "at",
110
+ "by",
111
+ "if",
112
+ "my",
113
+ "me",
114
+ "us",
115
+ "ok",
116
+ "okay",
117
+ "yes",
118
+ "no",
119
+ "not",
120
+ "up",
121
+ "out",
122
+ "go",
123
+ "get",
124
+ "got",
125
+ "make",
126
+ "made",
127
+ "give",
128
+ "tell",
129
+ "want",
130
+ "need",
131
+ "think",
132
+ "know",
133
+ "let",
134
+ "lets",
135
+ "about",
136
+ "from",
137
+ "into"
138
+ ]);
139
+ function topicalWordCount(s) {
140
+ const tokens = s.toLowerCase().match(/[a-z0-9]{2,}/g) ?? [];
141
+ return tokens.filter((t) => !STOPWORDS.has(t)).length;
142
+ }
143
+ function textOf(message) {
144
+ if (typeof message !== "object" || message === null) return "";
145
+ const content = message.content;
146
+ if (typeof content === "string") return content;
147
+ if (Array.isArray(content)) {
148
+ return content.filter(
149
+ (b) => typeof b === "object" && b !== null && b.type === "text"
150
+ ).map((b) => b.text).join(" ");
151
+ }
152
+ return "";
153
+ }
154
+ function tailRead(path, maxBytes) {
155
+ const fd = openSync(path, "r");
156
+ try {
157
+ const size = fstatSync(fd).size;
158
+ const start = size > maxBytes ? size - maxBytes : 0;
159
+ const len = size - start;
160
+ if (len <= 0) return "";
161
+ const buf = Buffer.alloc(len);
162
+ readSync(fd, buf, 0, len, start);
163
+ return buf.toString("utf8");
164
+ } finally {
165
+ closeSync(fd);
166
+ }
167
+ }
168
+ function recentConversationContext(transcriptPath, opts = {}) {
169
+ try {
170
+ if (!transcriptPath || !existsSync(transcriptPath)) return "";
171
+ const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
172
+ const perTurnChars = opts.perTurnChars ?? DEFAULT_PER_TURN_CHARS;
173
+ const lines = tailRead(transcriptPath, opts.maxBytes ?? DEFAULT_MAX_BYTES).split("\n");
174
+ const turns = [];
175
+ for (let i = lines.length - 1; i >= 0 && turns.length < maxTurns; i--) {
176
+ const line = lines[i].trim();
177
+ if (!line) continue;
178
+ let evt;
179
+ try {
180
+ evt = JSON.parse(line);
181
+ } catch {
182
+ continue;
183
+ }
184
+ if (evt.type !== "user" && evt.type !== "assistant") continue;
185
+ const text = textOf(evt.message).trim();
186
+ if (!text) continue;
187
+ turns.unshift(text.slice(0, perTurnChars));
188
+ }
189
+ return turns.join(" ").trim();
190
+ } catch {
191
+ return "";
192
+ }
193
+ }
194
+
17
195
  // src/core/hook/hook-log.ts
18
196
  import { appendFileSync, mkdirSync } from "node:fs";
19
197
  import { homedir } from "node:os";
@@ -32,7 +210,15 @@ function appendHookLog(entry) {
32
210
  }
33
211
 
34
212
  // src/core/hook/memo.ts
35
- import { existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "node:fs";
213
+ import {
214
+ existsSync as existsSync2,
215
+ mkdirSync as mkdirSync2,
216
+ readdirSync,
217
+ readFileSync,
218
+ rmSync,
219
+ statSync,
220
+ writeFileSync
221
+ } from "node:fs";
36
222
  import { homedir as homedir2 } from "node:os";
37
223
  import { join as join2 } from "node:path";
38
224
  function stateDir() {
@@ -45,7 +231,7 @@ function memoPath(conversationId) {
45
231
  function loadSurfaced(conversationId) {
46
232
  try {
47
233
  const path = memoPath(conversationId);
48
- if (!existsSync(path)) return /* @__PURE__ */ new Set();
234
+ if (!existsSync2(path)) return /* @__PURE__ */ new Set();
49
235
  const parsed = JSON.parse(readFileSync(path, "utf8"));
50
236
  if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
51
237
  return new Set(parsed.filter((x) => typeof x === "string"));
@@ -64,6 +250,26 @@ function recordSurfaced(conversationId, ids) {
64
250
  }
65
251
 
66
252
  // src/core/hook/pointer-block.ts
253
+ function truncateSummary(s, max = 200) {
254
+ if (s.length <= max) return s;
255
+ const window = s.slice(0, max);
256
+ const last = window[window.length - 1];
257
+ if ((last === "." || last === "!" || last === "?") && window.length - 1 >= 60) {
258
+ return window;
259
+ }
260
+ for (let i = window.length - 2; i >= 60; i--) {
261
+ const c = window[i];
262
+ if ((c === "." || c === "!" || c === "?") && window[i + 1] === " ") {
263
+ return s.slice(0, i + 1);
264
+ }
265
+ }
266
+ for (let i = window.length - 1; i >= 60; i--) {
267
+ if (window[i] === " ") {
268
+ return s.slice(0, i) + " ...";
269
+ }
270
+ }
271
+ return window + " ...";
272
+ }
67
273
  function formatPointerBlock(hits, facts = [], exemplars = []) {
68
274
  if (hits.length === 0 && facts.length === 0 && exemplars.length === 0) return "";
69
275
  const out = [];
@@ -72,7 +278,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
72
278
  for (const h of hits) {
73
279
  const datePart = h.startedAt.slice(0, 10);
74
280
  if (h.summary) {
75
- out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${h.summary.slice(0, 120)}`);
281
+ out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${truncateSummary(h.summary)}`);
76
282
  } else {
77
283
  out.push(`- ${h.id} \xB7 ${h.label} (${datePart})`);
78
284
  }
@@ -91,7 +297,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
91
297
  out.push("## Related code exemplars (nlm-memory)");
92
298
  for (const e of exemplars) {
93
299
  const langPart = e.lang ? `${e.lang} \xB7 ` : "";
94
- out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${e.taskContext.slice(0, 120)}`);
300
+ out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${truncateSummary(e.taskContext)}`);
95
301
  }
96
302
  }
97
303
  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).";
@@ -100,10 +306,16 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
100
306
  }
101
307
 
102
308
  // src/core/hook/select.ts
309
+ function medianScore(hits) {
310
+ if (hits.length === 0) return 0;
311
+ const sorted = hits.map((h) => h.matchScore).sort((a, b) => a - b);
312
+ return sorted[Math.floor(sorted.length / 2)] ?? 0;
313
+ }
103
314
  function selectHits(params) {
104
- const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
315
+ const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap, relativeFloor = 0 } = params;
316
+ const relCut = relativeFloor > 0 ? relativeFloor * medianScore(hits) : 0;
105
317
  const eligible = hits.filter(
106
- (h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id)
318
+ (h) => h.matchScore >= scoreThreshold && h.matchScore >= relCut && !surfaced.has(h.id)
107
319
  );
108
320
  const budget = Math.max(0, perConversationCap - surfaced.size);
109
321
  const limit = Math.min(perFireCap, budget);
@@ -111,7 +323,7 @@ function selectHits(params) {
111
323
  }
112
324
 
113
325
  // src/llm/env-autoload.ts
114
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
326
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
115
327
  import { homedir as homedir3 } from "node:os";
116
328
  import { resolve } from "node:path";
117
329
  var DEFAULT_SEARCH_PATHS = [
@@ -129,7 +341,7 @@ function autoloadEnv(extraPaths = []) {
129
341
  const paths = [...DEFAULT_SEARCH_PATHS, ...extraPaths];
130
342
  for (const raw of paths) {
131
343
  const path = expandHome(raw);
132
- if (!existsSync2(path)) continue;
344
+ if (!existsSync3(path)) continue;
133
345
  try {
134
346
  const content = readFileSync2(path, "utf8");
135
347
  for (const line of content.split("\n")) {
@@ -161,7 +373,7 @@ function hookAuthHeaders(extra = {}) {
161
373
  }
162
374
 
163
375
  // src/core/hook/query-extract.ts
164
- var STOPWORDS = /* @__PURE__ */ new Set([
376
+ var STOPWORDS2 = /* @__PURE__ */ new Set([
165
377
  "a",
166
378
  "an",
167
379
  "the",
@@ -293,30 +505,55 @@ var STOPWORDS = /* @__PURE__ */ new Set([
293
505
  ]);
294
506
  var MIN_CONTENT_WORDS = 2;
295
507
  var MIN_WORD_LEN = 3;
508
+ var SYSTEM_MESSAGE_PREFIX = /^<(task-notification|command-name|command-message|command-args|local-command-stdout|local-command-caveat|output-file|system-reminder)\b/;
296
509
  function extractRecallQuery(prompt) {
510
+ if (SYSTEM_MESSAGE_PREFIX.test(prompt.trim())) return null;
297
511
  const tokens = prompt.trim().split(/\s+/).map((t) => t.replace(/^[^\w-]+|[^\w-]+$/g, "")).filter((t) => t.length >= MIN_WORD_LEN);
298
- const contentWords = tokens.filter((t) => !STOPWORDS.has(t.toLowerCase()));
512
+ const contentWords = tokens.filter((t) => !STOPWORDS2.has(t.toLowerCase()));
299
513
  if (contentWords.length < MIN_CONTENT_WORDS) return null;
300
514
  return contentWords.join(" ");
301
515
  }
302
516
 
517
+ // src/hook/hook-helpers.ts
518
+ function readStdin() {
519
+ return new Promise((resolve2) => {
520
+ let data = "";
521
+ process.stdin.setEncoding("utf8");
522
+ process.stdin.on("data", (chunk) => data += chunk);
523
+ process.stdin.on("end", () => resolve2(data));
524
+ process.stdin.on("error", () => resolve2(data));
525
+ });
526
+ }
527
+ async function fetchWithTimeout(url, init, ms) {
528
+ const controller = new AbortController();
529
+ const timer = setTimeout(() => controller.abort(), ms);
530
+ try {
531
+ return await fetch(url, { ...init, signal: controller.signal });
532
+ } finally {
533
+ clearTimeout(timer);
534
+ }
535
+ }
536
+ function hookModeFromEnv() {
537
+ return process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
538
+ }
539
+
303
540
  // src/hook/recall-over-http.ts
304
541
  var RECALL_LIMIT = 5;
305
542
  var RECALL_TIMEOUT_MS = 2e3;
306
- async function recallOverHttp(prompt, runtime, conversationId) {
543
+ async function recallOverHttp(prompt, runtime, conversationId, mode = "keyword") {
307
544
  const query = extractRecallQuery(prompt);
308
545
  if (query === null) return { hits: [], facts: [], exemplars: [] };
309
546
  const portValue = process.env["NLM_PORT"] ?? "3940";
310
- const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=keyword&limit=${RECALL_LIMIT}&withFacts=true&withExemplars=true` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "");
311
- const controller = new AbortController();
312
- const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
547
+ const url = (
548
+ // 127.0.0.1, not localhost: each hook is a fresh process with no connection
549
+ // reuse, and Node resolves localhost to IPv6 ::1 first — a measured ~50-300ms
550
+ // per-fire connect penalty vs ~3ms on the explicit IPv4 loopback.
551
+ `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)}` : "")
552
+ );
313
553
  try {
314
554
  const extra = { "x-recall-source": "hook" };
315
555
  if (runtime) extra["x-recall-runtime"] = runtime;
316
- const res = await fetch(url, {
317
- headers: hookAuthHeaders(extra),
318
- signal: controller.signal
319
- });
556
+ const res = await fetchWithTimeout(url, { headers: hookAuthHeaders(extra) }, RECALL_TIMEOUT_MS);
320
557
  if (!res.ok) return { hits: [], facts: [], exemplars: [] };
321
558
  let body;
322
559
  try {
@@ -344,20 +581,114 @@ async function recallOverHttp(prompt, runtime, conversationId) {
344
581
  taskContext: e.taskContext
345
582
  }));
346
583
  return { hits, facts, exemplars };
347
- } finally {
348
- clearTimeout(timer);
584
+ } catch {
585
+ return { hits: [], facts: [], exemplars: [] };
349
586
  }
350
587
  }
351
588
 
589
+ // src/hook/score-floor.ts
590
+ function parseScoreFloor(raw) {
591
+ if (raw === void 0) return 0;
592
+ const parsed = Number(raw);
593
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
594
+ return parsed;
595
+ }
596
+ function parseRelativeFloor(raw, fallback) {
597
+ if (raw === void 0) return fallback;
598
+ const parsed = Number(raw);
599
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
600
+ return parsed;
601
+ }
602
+
603
+ // src/hook/recall-gate.ts
604
+ var RECALL_GATE_MODEL = "qwen3.5:4b";
605
+ 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"}.`;
606
+ var GATE_FORMAT = { type: "object", properties: { gate: { type: "string", enum: ["relevant", "irrelevant"] } }, required: ["gate"] };
607
+ var GATE_OPTS = { temperature: 0, top_p: 1, top_k: 0, presence_penalty: 0, frequency_penalty: 0 };
608
+ function parseRecallGateMode(env = process.env) {
609
+ const v = env["NLM_HOOK_RECALL_GATE"]?.trim();
610
+ return v === "shadow" || v === "live" ? v : void 0;
611
+ }
612
+ var GATE_TIMEOUT_MS = 4e3;
613
+ function makeOllamaGate(url, model = RECALL_GATE_MODEL, timeoutMs = GATE_TIMEOUT_MS) {
614
+ return async (prompt, candidate) => {
615
+ try {
616
+ const r = await fetchWithTimeout(
617
+ `${url}/api/chat`,
618
+ {
619
+ method: "POST",
620
+ headers: { "Content-Type": "application/json" },
621
+ body: JSON.stringify({
622
+ model,
623
+ stream: false,
624
+ think: false,
625
+ // Keep the gate model resident between fires so only the first fire
626
+ // pays the cold-load; subsequent gates are ~1 judge call.
627
+ keep_alive: "15m",
628
+ format: GATE_FORMAT,
629
+ options: GATE_OPTS,
630
+ messages: [
631
+ { role: "system", content: RECALL_GATE_SYSTEM },
632
+ { role: "user", content: `USER PROMPT:
633
+ ${prompt}
634
+
635
+ CANDIDATE CONTEXT:
636
+ ${candidate}` }
637
+ ]
638
+ })
639
+ },
640
+ timeoutMs
641
+ );
642
+ const d = await r.json();
643
+ const v = JSON.parse(d.message?.content ?? "{}").gate;
644
+ return v === "irrelevant" ? "irrelevant" : "relevant";
645
+ } catch {
646
+ return "relevant";
647
+ }
648
+ };
649
+ }
650
+
352
651
  // src/hook/prompt-recall-hook.ts
353
- var SCORE_THRESHOLD = 0;
652
+ var SCORE_THRESHOLD = parseScoreFloor(process.env["NLM_RECALL_SCORE_FLOOR"]);
653
+ var RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
354
654
  var PER_FIRE_CAP = 3;
355
655
  var PER_CONVERSATION_CAP = 10;
356
656
  var PROMPT_PREVIEW_CHARS = 200;
657
+ function parseHookDeadline(raw) {
658
+ if (raw === void 0) return 4e3;
659
+ const n = Number(raw);
660
+ return Number.isFinite(n) && n > 0 ? n : 4e3;
661
+ }
662
+ var HOOK_DEADLINE_MS = parseHookDeadline(process.env["NLM_HOOK_DEADLINE_MS"]);
663
+ async function withDeadline(p, ms, fallback) {
664
+ if (ms <= 0) return fallback;
665
+ let timer;
666
+ const timeout = new Promise((resolve2) => {
667
+ timer = setTimeout(() => resolve2(fallback), ms);
668
+ });
669
+ try {
670
+ return await Promise.race([p, timeout]);
671
+ } finally {
672
+ clearTimeout(timer);
673
+ }
674
+ }
357
675
  function hookRuntimeFromEnv(env = process.env) {
358
676
  const raw = env["NLM_HOOK_RUNTIME"]?.trim();
359
677
  return raw ? raw : "claude-code";
360
678
  }
679
+ function promptRecallEnabled(env = process.env) {
680
+ const raw = env["NLM_HOOK_PROMPT_RECALL"]?.trim();
681
+ if (raw === void 0 || raw === "") return false;
682
+ return raw.toLowerCase() !== "off";
683
+ }
684
+ function buildRecallQuery(input, env = process.env) {
685
+ if (env["NLM_HOOK_CONTEXT_RECALL"] !== "1") return input.prompt;
686
+ if (!input.transcriptPath) return input.prompt;
687
+ const minWords = Number.parseInt(env["NLM_HOOK_CONTEXT_MIN_WORDS"] ?? "3", 10);
688
+ if (topicalWordCount(input.prompt) >= minWords) return input.prompt;
689
+ const context = recentConversationContext(input.transcriptPath);
690
+ return context ? `${context} ${input.prompt}` : input.prompt;
691
+ }
361
692
  function normalizeRecall(raw) {
362
693
  if (Array.isArray(raw)) return { hits: raw, facts: [] };
363
694
  return raw;
@@ -365,7 +696,7 @@ function normalizeRecall(raw) {
365
696
  async function runHook(input, deps) {
366
697
  const gate = classifyPrompt(input.prompt);
367
698
  const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
368
- if (gate === "generative") {
699
+ if (gate === "generative" || gate === "skip") {
369
700
  appendHookLog({
370
701
  ts: (/* @__PURE__ */ new Date()).toISOString(),
371
702
  conversationId: input.conversationId,
@@ -378,9 +709,12 @@ async function runHook(input, deps) {
378
709
  });
379
710
  return "";
380
711
  }
712
+ const deadline = Date.now() + (deps.deadlineMs ?? HOOK_DEADLINE_MS);
381
713
  let fetched = { hits: [], facts: [] };
382
714
  try {
383
- fetched = normalizeRecall(await deps.recall(input.prompt));
715
+ fetched = normalizeRecall(
716
+ await withDeadline(deps.recall(buildRecallQuery(input)), deadline - Date.now(), { hits: [], facts: [] })
717
+ );
384
718
  } catch {
385
719
  fetched = { hits: [], facts: [] };
386
720
  }
@@ -390,10 +724,33 @@ async function runHook(input, deps) {
390
724
  hits,
391
725
  surfaced,
392
726
  scoreThreshold: SCORE_THRESHOLD,
727
+ relativeFloor: RELATIVE_FLOOR,
393
728
  perFireCap: PER_FIRE_CAP,
394
729
  perConversationCap: PER_CONVERSATION_CAP
395
730
  });
396
- const block = formatPointerBlock(selected, fetched.facts, fetched.exemplars);
731
+ let gateDecisions;
732
+ let injected = selected;
733
+ if (deps.recallGate && selected.length > 0) {
734
+ const g = deps.recallGate;
735
+ const toGate = g.maxCandidates ? selected.slice(0, g.maxCandidates) : selected;
736
+ const remaining = deadline - Date.now();
737
+ const gateFallback = toGate.map((h) => ({ id: h.id, gate: "relevant" }));
738
+ if (remaining <= 0) {
739
+ gateDecisions = gateFallback;
740
+ } else {
741
+ gateDecisions = await withDeadline(
742
+ Promise.all(toGate.map(async (h) => ({ id: h.id, gate: await g.judge(input.prompt, `${h.label}
743
+ ${h.summary ?? ""}`) }))),
744
+ remaining,
745
+ gateFallback
746
+ );
747
+ }
748
+ if (g.mode === "live") {
749
+ const drop = new Set(gateDecisions.filter((d) => d.gate === "irrelevant").map((d) => d.id));
750
+ injected = selected.filter((h) => !drop.has(h.id));
751
+ }
752
+ }
753
+ const block = formatPointerBlock(injected, fetched.facts, fetched.exemplars);
397
754
  const estTokens = Math.ceil(block.length / 4);
398
755
  appendHookLog({
399
756
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -401,25 +758,17 @@ async function runHook(input, deps) {
401
758
  promptPreview: preview,
402
759
  gate,
403
760
  hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
404
- wouldInject: selected.map((h) => h.id),
761
+ wouldInject: injected.map((h) => h.id),
405
762
  estTokens,
406
- mode: deps.mode
763
+ mode: deps.mode,
764
+ ...gateDecisions ? { gateDecisions } : {}
407
765
  });
408
- if (deps.mode === "live" && selected.length > 0) {
409
- recordSurfaced(input.conversationId, selected.map((h) => h.id));
766
+ if (deps.mode === "live" && injected.length > 0) {
767
+ recordSurfaced(input.conversationId, injected.map((h) => h.id));
410
768
  return block;
411
769
  }
412
770
  return "";
413
771
  }
414
- function readStdin() {
415
- return new Promise((resolve2) => {
416
- let data = "";
417
- process.stdin.setEncoding("utf8");
418
- process.stdin.on("data", (chunk) => data += chunk);
419
- process.stdin.on("end", () => resolve2(data));
420
- process.stdin.on("error", () => resolve2(data));
421
- });
422
- }
423
772
  async function main() {
424
773
  try {
425
774
  autoloadEnv();
@@ -427,12 +776,21 @@ async function main() {
427
776
  const payload = JSON.parse(raw);
428
777
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
429
778
  const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
779
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : void 0;
430
780
  if (!prompt) return;
431
- const mode = process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
781
+ if (!promptRecallEnabled()) return;
782
+ const mode = hookModeFromEnv();
432
783
  const runtime = hookRuntimeFromEnv();
784
+ const gateMode = parseRecallGateMode();
785
+ const gateUrl = process.env["OLLAMA_URL"] ?? "http://127.0.0.1:11434";
786
+ const gateTopN = Math.max(1, Number.parseInt(process.env["NLM_HOOK_RECALL_GATE_TOPN"] ?? "1", 10) || 1);
433
787
  const out = await runHook(
434
- { prompt, conversationId },
435
- { mode, recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId) }
788
+ { prompt, conversationId, ...transcriptPath ? { transcriptPath } : {} },
789
+ {
790
+ mode,
791
+ recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId),
792
+ ...gateMode ? { recallGate: { mode: gateMode, judge: makeOllamaGate(gateUrl), maxCandidates: gateTopN } } : {}
793
+ }
436
794
  );
437
795
  if (out) process.stdout.write(out);
438
796
  } catch {
@@ -442,6 +800,9 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
442
800
  void main();
443
801
  }
444
802
  export {
803
+ buildRecallQuery,
445
804
  hookRuntimeFromEnv,
805
+ parseHookDeadline,
806
+ promptRecallEnabled,
446
807
  runHook
447
808
  };