nlm-memory 0.20.0 → 0.21.0

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 (471) hide show
  1. package/README.md +19 -10
  2. package/dist/cli/digest.js +60 -1
  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 +872 -61
  9. package/dist/cli/nlm.js.map +1 -1
  10. package/dist/cli/scope-backfill.d.ts +34 -0
  11. package/dist/cli/scope-backfill.js +237 -0
  12. package/dist/cli/scope-backfill.js.map +1 -0
  13. package/dist/cli/scope-coverage.d.ts +44 -0
  14. package/dist/cli/scope-coverage.js +205 -0
  15. package/dist/cli/scope-coverage.js.map +1 -0
  16. package/dist/core/actions/overlay.d.ts +20 -6
  17. package/dist/core/actions/overlay.js +32 -18
  18. package/dist/core/actions/overlay.js.map +1 -1
  19. package/dist/core/adapters/claude-code.d.ts +14 -0
  20. package/dist/core/adapters/claude-code.js +36 -0
  21. package/dist/core/adapters/claude-code.js.map +1 -1
  22. package/dist/core/classifier/prompt.js +3 -3
  23. package/dist/core/dataset/build-dataset.js +1 -0
  24. package/dist/core/dataset/build-dataset.js.map +1 -1
  25. package/dist/core/digest/compose.d.ts +13 -3
  26. package/dist/core/digest/compose.js +23 -18
  27. package/dist/core/digest/compose.js.map +1 -1
  28. package/dist/core/digest/hook-injection.d.ts +43 -0
  29. package/dist/core/digest/hook-injection.js +79 -0
  30. package/dist/core/digest/hook-injection.js.map +1 -0
  31. package/dist/core/embedding/embed-backfill.d.ts +13 -2
  32. package/dist/core/embedding/embed-backfill.js +181 -43
  33. package/dist/core/embedding/embed-backfill.js.map +1 -1
  34. package/dist/core/embedding/embedding-config.d.ts +17 -0
  35. package/dist/core/embedding/embedding-config.js +12 -0
  36. package/dist/core/embedding/embedding-config.js.map +1 -0
  37. package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
  38. package/dist/core/embedding/pg-embed-backfill.js +264 -0
  39. package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
  40. package/dist/core/entities/dedup-suggest.d.ts +25 -0
  41. package/dist/core/entities/dedup-suggest.js +107 -0
  42. package/dist/core/entities/dedup-suggest.js.map +1 -0
  43. package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
  44. package/dist/core/eval/classifier-fixture-eval.js +217 -0
  45. package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
  46. package/dist/core/eval/extraction-scoring.d.ts +63 -0
  47. package/dist/core/eval/extraction-scoring.js +79 -0
  48. package/dist/core/eval/extraction-scoring.js.map +1 -0
  49. package/dist/core/eval/run-eval.d.ts +40 -0
  50. package/dist/core/eval/run-eval.js +34 -0
  51. package/dist/core/eval/run-eval.js.map +1 -0
  52. package/dist/core/exemplars/capture-from-session.d.ts +2 -0
  53. package/dist/core/exemplars/capture-from-session.js +7 -3
  54. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  55. package/dist/core/exemplars/diff-parse.d.ts +23 -0
  56. package/dist/core/exemplars/diff-parse.js +73 -0
  57. package/dist/core/exemplars/diff-parse.js.map +1 -0
  58. package/dist/core/exemplars/embed-backfill.d.ts +36 -0
  59. package/dist/core/exemplars/embed-backfill.js +85 -0
  60. package/dist/core/exemplars/embed-backfill.js.map +1 -0
  61. package/dist/core/exemplars/embed-text.d.ts +15 -0
  62. package/dist/core/exemplars/embed-text.js +18 -0
  63. package/dist/core/exemplars/embed-text.js.map +1 -0
  64. package/dist/core/exemplars/extract-exemplar.d.ts +2 -0
  65. package/dist/core/exemplars/extract-exemplar.js +9 -66
  66. package/dist/core/exemplars/extract-exemplar.js.map +1 -1
  67. package/dist/core/exemplars/ingest-exemplar.d.ts +1 -0
  68. package/dist/core/exemplars/ingest-exemplar.js +1 -0
  69. package/dist/core/exemplars/ingest-exemplar.js.map +1 -1
  70. package/dist/core/exemplars/recall-code.js +4 -0
  71. package/dist/core/exemplars/recall-code.js.map +1 -1
  72. package/dist/core/exemplars/warm-embedder.d.ts +14 -0
  73. package/dist/core/exemplars/warm-embedder.js +18 -0
  74. package/dist/core/exemplars/warm-embedder.js.map +1 -0
  75. package/dist/core/health/corpus-state.d.ts +8 -0
  76. package/dist/core/health/corpus-state.js +11 -0
  77. package/dist/core/health/corpus-state.js.map +1 -0
  78. package/dist/core/health/embed-inflight.d.ts +9 -0
  79. package/dist/core/health/embed-inflight.js +30 -0
  80. package/dist/core/health/embed-inflight.js.map +1 -0
  81. package/dist/core/health/embedding-lane-state.d.ts +6 -0
  82. package/dist/core/health/embedding-lane-state.js +18 -0
  83. package/dist/core/health/embedding-lane-state.js.map +1 -0
  84. package/dist/core/health/warmup-state.d.ts +12 -0
  85. package/dist/core/health/warmup-state.js +12 -0
  86. package/dist/core/health/warmup-state.js.map +1 -0
  87. package/dist/core/hook/claude-settings.d.ts +1 -1
  88. package/dist/core/hook/claude-settings.js +0 -1
  89. package/dist/core/hook/claude-settings.js.map +1 -1
  90. package/dist/core/hook/gate.d.ts +13 -6
  91. package/dist/core/hook/gate.js +47 -6
  92. package/dist/core/hook/gate.js.map +1 -1
  93. package/dist/core/hook/hook-log.d.ts +5 -0
  94. package/dist/core/hook/hook-log.js.map +1 -1
  95. package/dist/core/hook/memo.d.ts +12 -0
  96. package/dist/core/hook/memo.js +41 -1
  97. package/dist/core/hook/memo.js.map +1 -1
  98. package/dist/core/hook/pointer-block.d.ts +1 -0
  99. package/dist/core/hook/pointer-block.js +23 -2
  100. package/dist/core/hook/pointer-block.js.map +1 -1
  101. package/dist/core/hook/query-extract.d.ts +2 -1
  102. package/dist/core/hook/query-extract.js +10 -1
  103. package/dist/core/hook/query-extract.js.map +1 -1
  104. package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
  105. package/dist/core/hook/resolve-conversation-by-query.js +98 -0
  106. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
  107. package/dist/core/hook/select.d.ts +8 -0
  108. package/dist/core/hook/select.js +9 -2
  109. package/dist/core/hook/select.js.map +1 -1
  110. package/dist/core/hook/transcript.js +16 -2
  111. package/dist/core/hook/transcript.js.map +1 -1
  112. package/dist/core/ingest/backfill-derivables.d.ts +92 -0
  113. package/dist/core/ingest/backfill-derivables.js +175 -0
  114. package/dist/core/ingest/backfill-derivables.js.map +1 -0
  115. package/dist/core/ingest/ingest-session.d.ts +5 -0
  116. package/dist/core/ingest/ingest-session.js +23 -0
  117. package/dist/core/ingest/ingest-session.js.map +1 -1
  118. package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
  119. package/dist/core/ingest/reclassify-oversized.js +9 -0
  120. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  121. package/dist/core/ingest/reprocess.d.ts +96 -0
  122. package/dist/core/ingest/reprocess.js +280 -0
  123. package/dist/core/ingest/reprocess.js.map +1 -0
  124. package/dist/core/ingest/transcript-derivables.d.ts +27 -0
  125. package/dist/core/ingest/transcript-derivables.js +138 -0
  126. package/dist/core/ingest/transcript-derivables.js.map +1 -0
  127. package/dist/core/integrity/check-invariants.d.ts +6 -1
  128. package/dist/core/integrity/check-invariants.js +209 -3
  129. package/dist/core/integrity/check-invariants.js.map +1 -1
  130. package/dist/core/metrics/corpus-stats.d.ts +49 -0
  131. package/dist/core/metrics/corpus-stats.js +111 -0
  132. package/dist/core/metrics/corpus-stats.js.map +1 -0
  133. package/dist/core/metrics/re-derivation.d.ts +48 -0
  134. package/dist/core/metrics/re-derivation.js +87 -0
  135. package/dist/core/metrics/re-derivation.js.map +1 -0
  136. package/dist/core/outcome/coverage.d.ts +33 -0
  137. package/dist/core/outcome/coverage.js +46 -0
  138. package/dist/core/outcome/coverage.js.map +1 -0
  139. package/dist/core/outcome/rollup.d.ts +33 -0
  140. package/dist/core/outcome/rollup.js +113 -0
  141. package/dist/core/outcome/rollup.js.map +1 -0
  142. package/dist/core/recall/citation-log.d.ts +8 -0
  143. package/dist/core/recall/citation-log.js +17 -0
  144. package/dist/core/recall/citation-log.js.map +1 -1
  145. package/dist/core/recall/hook-recall-log.d.ts +2 -2
  146. package/dist/core/recall/hook-recall-log.js +2 -2
  147. package/dist/core/recall/query-intent.d.ts +7 -0
  148. package/dist/core/recall/query-intent.js +25 -0
  149. package/dist/core/recall/query-intent.js.map +1 -0
  150. package/dist/core/recall/query-log.d.ts +4 -0
  151. package/dist/core/recall/query-log.js +4 -0
  152. package/dist/core/recall/query-log.js.map +1 -1
  153. package/dist/core/recall/recall-service.d.ts +37 -1
  154. package/dist/core/recall/recall-service.js +106 -63
  155. package/dist/core/recall/recall-service.js.map +1 -1
  156. package/dist/core/recall/related-exemplars.d.ts +1 -0
  157. package/dist/core/recall/related-exemplars.js +4 -1
  158. package/dist/core/recall/related-exemplars.js.map +1 -1
  159. package/dist/core/recall/related-facts.js +2 -2
  160. package/dist/core/recall/related-facts.js.map +1 -1
  161. package/dist/core/recall/reranker.d.ts +2 -1
  162. package/dist/core/recall/reranker.js +3 -3
  163. package/dist/core/recall/reranker.js.map +1 -1
  164. package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
  165. package/dist/core/recall-facts/fact-query-log.js +2 -0
  166. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  167. package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
  168. package/dist/core/recall-facts/fact-recall-service.js +22 -8
  169. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  170. package/dist/core/scheduler/scan-once.d.ts +6 -6
  171. package/dist/core/scheduler/scan-once.js +15 -49
  172. package/dist/core/scheduler/scan-once.js.map +1 -1
  173. package/dist/core/scheduler/scheduler.d.ts +10 -0
  174. package/dist/core/scheduler/scheduler.js +109 -13
  175. package/dist/core/scheduler/scheduler.js.map +1 -1
  176. package/dist/core/scope/alias-map.d.ts +10 -0
  177. package/dist/core/scope/alias-map.js +41 -0
  178. package/dist/core/scope/alias-map.js.map +1 -0
  179. package/dist/core/scope/derive-scope.d.ts +8 -0
  180. package/dist/core/scope/derive-scope.js +54 -0
  181. package/dist/core/scope/derive-scope.js.map +1 -0
  182. package/dist/core/scope/scope-clause.d.ts +16 -0
  183. package/dist/core/scope/scope-clause.js +21 -0
  184. package/dist/core/scope/scope-clause.js.map +1 -0
  185. package/dist/core/scope/stamp-flag.d.ts +1 -0
  186. package/dist/core/scope/stamp-flag.js +4 -0
  187. package/dist/core/scope/stamp-flag.js.map +1 -0
  188. package/dist/core/signals/code-signal.d.ts +50 -0
  189. package/dist/core/signals/code-signal.js +54 -0
  190. package/dist/core/signals/code-signal.js.map +1 -0
  191. package/dist/core/signals/ingest-signal.d.ts +1 -1
  192. package/dist/core/signals/ingest-signal.js +3 -1
  193. package/dist/core/signals/ingest-signal.js.map +1 -1
  194. package/dist/core/signals/stamp-scope.d.ts +18 -0
  195. package/dist/core/signals/stamp-scope.js +28 -0
  196. package/dist/core/signals/stamp-scope.js.map +1 -0
  197. package/dist/core/storage/db-restore.js +3 -1
  198. package/dist/core/storage/db-restore.js.map +1 -1
  199. package/dist/core/storage/fact-batch.d.ts +9 -0
  200. package/dist/core/storage/fact-batch.js +14 -0
  201. package/dist/core/storage/fact-batch.js.map +1 -0
  202. package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
  203. package/dist/core/storage/pg-code-exemplar-store.js +13 -2
  204. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  205. package/dist/core/storage/pg-embedding-config.d.ts +29 -0
  206. package/dist/core/storage/pg-embedding-config.js +53 -0
  207. package/dist/core/storage/pg-embedding-config.js.map +1 -0
  208. package/dist/core/storage/pg-entity-store.d.ts +12 -0
  209. package/dist/core/storage/pg-entity-store.js +79 -0
  210. package/dist/core/storage/pg-entity-store.js.map +1 -0
  211. package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
  212. package/dist/core/storage/pg-fact-ingest.js +34 -0
  213. package/dist/core/storage/pg-fact-ingest.js.map +1 -0
  214. package/dist/core/storage/pg-fact-store.d.ts +3 -0
  215. package/dist/core/storage/pg-fact-store.js +26 -30
  216. package/dist/core/storage/pg-fact-store.js.map +1 -1
  217. package/dist/core/storage/pg-migrate.d.ts +18 -0
  218. package/dist/core/storage/pg-migrate.js +73 -0
  219. package/dist/core/storage/pg-migrate.js.map +1 -0
  220. package/dist/core/storage/pg-session-store.d.ts +11 -2
  221. package/dist/core/storage/pg-session-store.js +224 -96
  222. package/dist/core/storage/pg-session-store.js.map +1 -1
  223. package/dist/core/storage/pg-signal-store.js +4 -3
  224. package/dist/core/storage/pg-signal-store.js.map +1 -1
  225. package/dist/core/storage/pg-storage.d.ts +9 -7
  226. package/dist/core/storage/pg-storage.js +20 -50
  227. package/dist/core/storage/pg-storage.js.map +1 -1
  228. package/dist/core/storage/pg-workstream-store.d.ts +25 -0
  229. package/dist/core/storage/pg-workstream-store.js +97 -0
  230. package/dist/core/storage/pg-workstream-store.js.map +1 -0
  231. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
  232. package/dist/core/storage/sqlite-code-exemplar-store.js +15 -2
  233. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  234. package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
  235. package/dist/core/storage/sqlite-embedding-config.js +27 -0
  236. package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
  237. package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
  238. package/dist/core/storage/sqlite-entity-store.js +96 -0
  239. package/dist/core/storage/sqlite-entity-store.js.map +1 -0
  240. package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
  241. package/dist/core/storage/sqlite-fact-store.js +81 -27
  242. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  243. package/dist/core/storage/sqlite-outcome-store.d.ts +89 -0
  244. package/dist/core/storage/sqlite-outcome-store.js +158 -0
  245. package/dist/core/storage/sqlite-outcome-store.js.map +1 -0
  246. package/dist/core/storage/sqlite-session-store.d.ts +42 -7
  247. package/dist/core/storage/sqlite-session-store.js +208 -95
  248. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  249. package/dist/core/storage/sqlite-signal-store.js +4 -2
  250. package/dist/core/storage/sqlite-signal-store.js.map +1 -1
  251. package/dist/core/storage/sqlite-storage.d.ts +8 -8
  252. package/dist/core/storage/sqlite-storage.js +21 -32
  253. package/dist/core/storage/sqlite-storage.js.map +1 -1
  254. package/dist/core/storage/sqlite-workstream-store.d.ts +25 -0
  255. package/dist/core/storage/sqlite-workstream-store.js +99 -0
  256. package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
  257. package/dist/core/storage/supersedence-log.js +2 -0
  258. package/dist/core/storage/supersedence-log.js.map +1 -1
  259. package/dist/core/telemetry/probe-filter.d.ts +35 -0
  260. package/dist/core/telemetry/probe-filter.js +56 -0
  261. package/dist/core/telemetry/probe-filter.js.map +1 -0
  262. package/dist/core/update-check/check.d.ts +6 -0
  263. package/dist/core/update-check/check.js +35 -0
  264. package/dist/core/update-check/check.js.map +1 -1
  265. package/dist/core/work-digest/active-spans.d.ts +8 -0
  266. package/dist/core/work-digest/active-spans.js +29 -0
  267. package/dist/core/work-digest/active-spans.js.map +1 -0
  268. package/dist/core/work-digest/attribute.d.ts +15 -0
  269. package/dist/core/work-digest/attribute.js +76 -0
  270. package/dist/core/work-digest/attribute.js.map +1 -0
  271. package/dist/core/work-digest/build-work-digest.d.ts +12 -0
  272. package/dist/core/work-digest/build-work-digest.js +69 -0
  273. package/dist/core/work-digest/build-work-digest.js.map +1 -0
  274. package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
  275. package/dist/core/work-digest/compose-work-digest.js +52 -0
  276. package/dist/core/work-digest/compose-work-digest.js.map +1 -0
  277. package/dist/core/work-digest/merge-active.d.ts +10 -0
  278. package/dist/core/work-digest/merge-active.js +22 -0
  279. package/dist/core/work-digest/merge-active.js.map +1 -0
  280. package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
  281. package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
  282. package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
  283. package/dist/core/work-digest/topics.d.ts +18 -0
  284. package/dist/core/work-digest/topics.js +26 -0
  285. package/dist/core/work-digest/topics.js.map +1 -0
  286. package/dist/core/work-digest/types.d.ts +45 -0
  287. package/dist/core/work-digest/types.js +2 -0
  288. package/dist/core/work-digest/types.js.map +1 -0
  289. package/dist/core/workstream/bind.d.ts +26 -0
  290. package/dist/core/workstream/bind.js +42 -0
  291. package/dist/core/workstream/bind.js.map +1 -0
  292. package/dist/core/workstream/compose-recall.d.ts +2 -0
  293. package/dist/core/workstream/compose-recall.js +39 -0
  294. package/dist/core/workstream/compose-recall.js.map +1 -0
  295. package/dist/core/workstream/merge-suggest.d.ts +19 -0
  296. package/dist/core/workstream/merge-suggest.js +61 -0
  297. package/dist/core/workstream/merge-suggest.js.map +1 -0
  298. package/dist/core/workstream/model.d.ts +21 -0
  299. package/dist/core/workstream/model.js +9 -0
  300. package/dist/core/workstream/model.js.map +1 -0
  301. package/dist/core/workstream/name-match.d.ts +8 -0
  302. package/dist/core/workstream/name-match.js +18 -0
  303. package/dist/core/workstream/name-match.js.map +1 -0
  304. package/dist/core/workstream/resolve.d.ts +10 -0
  305. package/dist/core/workstream/resolve.js +21 -0
  306. package/dist/core/workstream/resolve.js.map +1 -0
  307. package/dist/core/workstream/rollup.d.ts +12 -0
  308. package/dist/core/workstream/rollup.js +17 -0
  309. package/dist/core/workstream/rollup.js.map +1 -0
  310. package/dist/core/workstream/work-topics.d.ts +10 -0
  311. package/dist/core/workstream/work-topics.js +55 -0
  312. package/dist/core/workstream/work-topics.js.map +1 -0
  313. package/dist/hook/hook-helpers.d.ts +4 -0
  314. package/dist/hook/hook-helpers.js +36 -0
  315. package/dist/hook/hook-helpers.js.map +1 -0
  316. package/dist/hook/pre-compact-hook.d.ts +23 -0
  317. package/dist/hook/pre-compact-hook.js +67 -0
  318. package/dist/hook/pre-compact-hook.js.map +1 -0
  319. package/dist/hook/prompt-recall-hook.d.ts +38 -0
  320. package/dist/hook/prompt-recall-hook.js +124 -20
  321. package/dist/hook/prompt-recall-hook.js.map +1 -1
  322. package/dist/hook/recall-gate.d.ts +29 -0
  323. package/dist/hook/recall-gate.js +50 -0
  324. package/dist/hook/recall-gate.js.map +1 -0
  325. package/dist/hook/recall-over-http.d.ts +3 -2
  326. package/dist/hook/recall-over-http.js +26 -13
  327. package/dist/hook/recall-over-http.js.map +1 -1
  328. package/dist/hook/recent-context.d.ts +25 -0
  329. package/dist/hook/recent-context.js +104 -0
  330. package/dist/hook/recent-context.js.map +1 -0
  331. package/dist/hook/score-floor.d.ts +27 -0
  332. package/dist/hook/score-floor.js +42 -0
  333. package/dist/hook/score-floor.js.map +1 -0
  334. package/dist/hook/session-end-hook.js +8 -29
  335. package/dist/hook/session-end-hook.js.map +1 -1
  336. package/dist/hook/session-start-hook.js +26 -60
  337. package/dist/hook/session-start-hook.js.map +1 -1
  338. package/dist/hook/stop-hook.js +24 -60
  339. package/dist/hook/stop-hook.js.map +1 -1
  340. package/dist/http/app.d.ts +6 -0
  341. package/dist/http/app.js +52 -44
  342. package/dist/http/app.js.map +1 -1
  343. package/dist/install/setup.js +2 -0
  344. package/dist/install/setup.js.map +1 -1
  345. package/dist/llm/build-classifier.d.ts +22 -0
  346. package/dist/llm/build-classifier.js +42 -0
  347. package/dist/llm/build-classifier.js.map +1 -0
  348. package/dist/llm/build-embedder.d.ts +11 -0
  349. package/dist/llm/build-embedder.js +42 -0
  350. package/dist/llm/build-embedder.js.map +1 -0
  351. package/dist/llm/bundled-embedder-client.d.ts +37 -0
  352. package/dist/llm/bundled-embedder-client.js +90 -0
  353. package/dist/llm/bundled-embedder-client.js.map +1 -0
  354. package/dist/llm/classifier-box.d.ts +13 -5
  355. package/dist/llm/classifier-box.js +28 -6
  356. package/dist/llm/classifier-box.js.map +1 -1
  357. package/dist/llm/classifier-egress.d.ts +18 -0
  358. package/dist/llm/classifier-egress.js +59 -0
  359. package/dist/llm/classifier-egress.js.map +1 -0
  360. package/dist/llm/client-shared.d.ts +4 -0
  361. package/dist/llm/client-shared.js +39 -0
  362. package/dist/llm/client-shared.js.map +1 -0
  363. package/dist/llm/deepseek-client.d.ts +20 -0
  364. package/dist/llm/deepseek-client.js +59 -45
  365. package/dist/llm/deepseek-client.js.map +1 -1
  366. package/dist/llm/embedder-info.d.ts +15 -0
  367. package/dist/llm/embedder-info.js +27 -0
  368. package/dist/llm/embedder-info.js.map +1 -0
  369. package/dist/llm/model-quirks.d.ts +4 -0
  370. package/dist/llm/model-quirks.js +7 -0
  371. package/dist/llm/model-quirks.js.map +1 -0
  372. package/dist/llm/naming.d.ts +5 -0
  373. package/dist/llm/naming.js +20 -0
  374. package/dist/llm/naming.js.map +1 -0
  375. package/dist/llm/ollama-client.d.ts +7 -8
  376. package/dist/llm/ollama-client.js +59 -51
  377. package/dist/llm/ollama-client.js.map +1 -1
  378. package/dist/llm/ollama-code-embedder.d.ts +8 -3
  379. package/dist/llm/ollama-code-embedder.js +14 -24
  380. package/dist/llm/ollama-code-embedder.js.map +1 -1
  381. package/dist/llm/openai-code-embedder-client.d.ts +25 -0
  382. package/dist/llm/openai-code-embedder-client.js +52 -0
  383. package/dist/llm/openai-code-embedder-client.js.map +1 -0
  384. package/dist/llm/openai-embedder-client.d.ts +41 -0
  385. package/dist/llm/openai-embedder-client.js +83 -0
  386. package/dist/llm/openai-embedder-client.js.map +1 -0
  387. package/dist/mcp/server.d.ts +69 -2
  388. package/dist/mcp/server.js +404 -19
  389. package/dist/mcp/server.js.map +1 -1
  390. package/dist/ports/code-embedder.d.ts +6 -1
  391. package/dist/ports/code-exemplar-store.d.ts +5 -0
  392. package/dist/ports/entity-store.d.ts +35 -0
  393. package/dist/ports/entity-store.js +7 -0
  394. package/dist/ports/entity-store.js.map +1 -0
  395. package/dist/ports/fact-store.d.ts +0 -0
  396. package/dist/ports/fact-store.js.map +1 -1
  397. package/dist/ports/llm-client.d.ts +9 -1
  398. package/dist/ports/outcome.d.ts +85 -0
  399. package/dist/ports/outcome.js +11 -0
  400. package/dist/ports/outcome.js.map +1 -0
  401. package/dist/ports/session-store.d.ts +12 -6
  402. package/dist/ports/storage.d.ts +7 -30
  403. package/dist/ports/storage.js +3 -7
  404. package/dist/ports/storage.js.map +1 -1
  405. package/dist/ports/transcript-adapter.d.ts +2 -0
  406. package/dist/ports/workstream-store.d.ts +22 -0
  407. package/dist/ports/workstream-store.js +2 -0
  408. package/dist/ports/workstream-store.js.map +1 -0
  409. package/dist/shared/net.d.ts +3 -0
  410. package/dist/shared/net.js +4 -0
  411. package/dist/shared/net.js.map +1 -0
  412. package/dist/shared/types.d.ts +56 -2
  413. package/dist/shared/types.js +4 -1
  414. package/dist/shared/types.js.map +1 -1
  415. package/dist/ui/assets/{index-CMpQkbQy.js → index-CSaBtgXD.js} +1 -1
  416. package/dist/ui/index.html +1 -1
  417. package/fixtures/classifier-gold/README.md +94 -0
  418. package/fixtures/classifier-gold/reference.json +318 -0
  419. package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
  420. package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
  421. package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
  422. package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
  423. package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
  424. package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
  425. package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
  426. package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
  427. package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
  428. package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
  429. package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
  430. package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
  431. package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
  432. package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
  433. package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
  434. package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
  435. package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
  436. package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
  437. package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
  438. package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
  439. package/migrations/000_initial_schema.sql +1 -1
  440. package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
  441. package/migrations/025_workstreams.sql +30 -0
  442. package/migrations/026_embedding_config.sql +12 -0
  443. package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
  444. package/migrations/028_classifier_provenance.sql +5 -0
  445. package/migrations/029_project_scope.sql +21 -0
  446. package/migrations/032_session_derivables.sql +11 -0
  447. package/migrations/pg/001_initial.sql +34 -0
  448. package/migrations/pg/019_split_replaces.sql +16 -7
  449. package/migrations/pg/025_workstreams.sql +34 -0
  450. package/migrations/pg/026_embedding_config.sql +12 -0
  451. package/migrations/pg/028_classifier_provenance.sql +5 -0
  452. package/migrations/pg/029_entity_variants.sql +9 -0
  453. package/migrations/pg/030_project_scope.sql +23 -0
  454. package/migrations/pg/031_widen_session_edges_kind.sql +11 -0
  455. package/migrations/pg/032_session_derivables.sql +17 -0
  456. package/nlm/index.js +405 -39
  457. package/package.json +12 -2
  458. package/plugin/.codex-plugin/plugin.json +2 -2
  459. package/plugin/scripts/prompt-recall-hook.mjs +413 -43
  460. package/plugin/scripts/session-start-hook.mjs +295 -61
  461. package/plugin/scripts/stop-hook.mjs +89 -66
  462. package/plugin-hermes-agent/__init__.py +2 -1
  463. package/templates/agent-contract/README.md +24 -0
  464. package/templates/agent-contract/claude-code.md +40 -0
  465. package/templates/agent-contract/generic.md +38 -0
  466. package/dist/core/embedding/embed-normalize.d.ts +0 -28
  467. package/dist/core/embedding/embed-normalize.js +0 -98
  468. package/dist/core/embedding/embed-normalize.js.map +0 -1
  469. package/dist/core/storage/pg-tx-context.d.ts +0 -52
  470. package/dist/core/storage/pg-tx-context.js +0 -149
  471. package/dist/core/storage/pg-tx-context.js.map +0 -1
@@ -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,63 @@ 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
+
540
+ // src/shared/net.ts
541
+ var DEFAULT_NLM_PORT = "3940";
542
+
303
543
  // src/hook/recall-over-http.ts
304
544
  var RECALL_LIMIT = 5;
305
- var RECALL_TIMEOUT_MS = 2e3;
306
- async function recallOverHttp(prompt, runtime, conversationId) {
545
+ function parseRecallTimeout(raw) {
546
+ if (raw === void 0) return 4e3;
547
+ const n = Number(raw);
548
+ return Number.isFinite(n) && n > 0 ? n : 4e3;
549
+ }
550
+ var RECALL_TIMEOUT_MS = parseRecallTimeout(process.env["NLM_HOOK_RECALL_TIMEOUT_MS"]);
551
+ async function recallOverHttp(prompt, runtime, conversationId, mode = "keyword") {
307
552
  const query = extractRecallQuery(prompt);
308
553
  if (query === null) return { hits: [], facts: [], exemplars: [] };
309
- 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);
554
+ const portValue = process.env["NLM_PORT"] ?? DEFAULT_NLM_PORT;
555
+ const url = (
556
+ // 127.0.0.1, not localhost: each hook is a fresh process with no connection
557
+ // reuse, and Node resolves localhost to IPv6 ::1 first — a measured ~50-300ms
558
+ // per-fire connect penalty vs ~3ms on the explicit IPv4 loopback.
559
+ `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)}` : "")
560
+ );
313
561
  try {
314
562
  const extra = { "x-recall-source": "hook" };
315
563
  if (runtime) extra["x-recall-runtime"] = runtime;
316
- const res = await fetch(url, {
317
- headers: hookAuthHeaders(extra),
318
- signal: controller.signal
319
- });
564
+ const res = await fetchWithTimeout(url, { headers: hookAuthHeaders(extra) }, RECALL_TIMEOUT_MS);
320
565
  if (!res.ok) return { hits: [], facts: [], exemplars: [] };
321
566
  let body;
322
567
  try {
@@ -344,20 +589,114 @@ async function recallOverHttp(prompt, runtime, conversationId) {
344
589
  taskContext: e.taskContext
345
590
  }));
346
591
  return { hits, facts, exemplars };
347
- } finally {
348
- clearTimeout(timer);
592
+ } catch {
593
+ return { hits: [], facts: [], exemplars: [] };
349
594
  }
350
595
  }
351
596
 
597
+ // src/hook/score-floor.ts
598
+ function parseScoreFloor(raw) {
599
+ if (raw === void 0) return 0;
600
+ const parsed = Number(raw);
601
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
602
+ return parsed;
603
+ }
604
+ function parseRelativeFloor(raw, fallback) {
605
+ if (raw === void 0) return fallback;
606
+ const parsed = Number(raw);
607
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
608
+ return parsed;
609
+ }
610
+
611
+ // src/hook/recall-gate.ts
612
+ var RECALL_GATE_MODEL = "qwen3.5:4b";
613
+ 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"}.`;
614
+ var GATE_FORMAT = { type: "object", properties: { gate: { type: "string", enum: ["relevant", "irrelevant"] } }, required: ["gate"] };
615
+ var GATE_OPTS = { temperature: 0, top_p: 1, top_k: 0, presence_penalty: 0, frequency_penalty: 0 };
616
+ function parseRecallGateMode(env = process.env) {
617
+ const v = env["NLM_HOOK_RECALL_GATE"]?.trim();
618
+ return v === "shadow" || v === "live" ? v : void 0;
619
+ }
620
+ var GATE_TIMEOUT_MS = 4e3;
621
+ function makeOllamaGate(url, model = RECALL_GATE_MODEL, timeoutMs = GATE_TIMEOUT_MS) {
622
+ return async (prompt, candidate) => {
623
+ try {
624
+ const r = await fetchWithTimeout(
625
+ `${url}/api/chat`,
626
+ {
627
+ method: "POST",
628
+ headers: { "Content-Type": "application/json" },
629
+ body: JSON.stringify({
630
+ model,
631
+ stream: false,
632
+ think: false,
633
+ // Keep the gate model resident between fires so only the first fire
634
+ // pays the cold-load; subsequent gates are ~1 judge call.
635
+ keep_alive: "15m",
636
+ format: GATE_FORMAT,
637
+ options: GATE_OPTS,
638
+ messages: [
639
+ { role: "system", content: RECALL_GATE_SYSTEM },
640
+ { role: "user", content: `USER PROMPT:
641
+ ${prompt}
642
+
643
+ CANDIDATE CONTEXT:
644
+ ${candidate}` }
645
+ ]
646
+ })
647
+ },
648
+ timeoutMs
649
+ );
650
+ const d = await r.json();
651
+ const v = JSON.parse(d.message?.content ?? "{}").gate;
652
+ return v === "irrelevant" ? "irrelevant" : "relevant";
653
+ } catch {
654
+ return "relevant";
655
+ }
656
+ };
657
+ }
658
+
352
659
  // src/hook/prompt-recall-hook.ts
353
- var SCORE_THRESHOLD = 0;
660
+ var SCORE_THRESHOLD = parseScoreFloor(process.env["NLM_RECALL_SCORE_FLOOR"]);
661
+ var RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
354
662
  var PER_FIRE_CAP = 3;
355
663
  var PER_CONVERSATION_CAP = 10;
356
664
  var PROMPT_PREVIEW_CHARS = 200;
665
+ function parseHookDeadline(raw, defaultMs = 4e3) {
666
+ if (raw === void 0) return defaultMs;
667
+ const n = Number(raw);
668
+ return Number.isFinite(n) && n > 0 ? n : defaultMs;
669
+ }
670
+ var HOOK_DEADLINE_MS = parseHookDeadline(process.env["NLM_HOOK_DEADLINE_MS"], 6e3);
671
+ async function withDeadline(p, ms, fallback) {
672
+ if (ms <= 0) return fallback;
673
+ let timer;
674
+ const timeout = new Promise((resolve2) => {
675
+ timer = setTimeout(() => resolve2(fallback), ms);
676
+ });
677
+ try {
678
+ return await Promise.race([p, timeout]);
679
+ } finally {
680
+ clearTimeout(timer);
681
+ }
682
+ }
357
683
  function hookRuntimeFromEnv(env = process.env) {
358
684
  const raw = env["NLM_HOOK_RUNTIME"]?.trim();
359
685
  return raw ? raw : "claude-code";
360
686
  }
687
+ function promptRecallEnabled(env = process.env) {
688
+ const raw = env["NLM_HOOK_PROMPT_RECALL"]?.trim();
689
+ if (raw === void 0 || raw === "") return false;
690
+ return raw.toLowerCase() !== "off";
691
+ }
692
+ function buildRecallQuery(input, env = process.env) {
693
+ if (env["NLM_HOOK_CONTEXT_RECALL"] !== "1") return input.prompt;
694
+ if (!input.transcriptPath) return input.prompt;
695
+ const minWords = Number.parseInt(env["NLM_HOOK_CONTEXT_MIN_WORDS"] ?? "3", 10);
696
+ if (topicalWordCount(input.prompt) >= minWords) return input.prompt;
697
+ const context = recentConversationContext(input.transcriptPath);
698
+ return context ? `${context} ${input.prompt}` : input.prompt;
699
+ }
361
700
  function normalizeRecall(raw) {
362
701
  if (Array.isArray(raw)) return { hits: raw, facts: [] };
363
702
  return raw;
@@ -365,7 +704,7 @@ function normalizeRecall(raw) {
365
704
  async function runHook(input, deps) {
366
705
  const gate = classifyPrompt(input.prompt);
367
706
  const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
368
- if (gate === "generative") {
707
+ if (gate === "generative" || gate === "skip") {
369
708
  appendHookLog({
370
709
  ts: (/* @__PURE__ */ new Date()).toISOString(),
371
710
  conversationId: input.conversationId,
@@ -378,9 +717,12 @@ async function runHook(input, deps) {
378
717
  });
379
718
  return "";
380
719
  }
720
+ const deadline = Date.now() + (deps.deadlineMs ?? HOOK_DEADLINE_MS);
381
721
  let fetched = { hits: [], facts: [] };
382
722
  try {
383
- fetched = normalizeRecall(await deps.recall(input.prompt));
723
+ fetched = normalizeRecall(
724
+ await withDeadline(deps.recall(buildRecallQuery(input)), deadline - Date.now(), { hits: [], facts: [] })
725
+ );
384
726
  } catch {
385
727
  fetched = { hits: [], facts: [] };
386
728
  }
@@ -390,10 +732,33 @@ async function runHook(input, deps) {
390
732
  hits,
391
733
  surfaced,
392
734
  scoreThreshold: SCORE_THRESHOLD,
735
+ relativeFloor: RELATIVE_FLOOR,
393
736
  perFireCap: PER_FIRE_CAP,
394
737
  perConversationCap: PER_CONVERSATION_CAP
395
738
  });
396
- const block = formatPointerBlock(selected, fetched.facts, fetched.exemplars);
739
+ let gateDecisions;
740
+ let injected = selected;
741
+ if (deps.recallGate && selected.length > 0) {
742
+ const g = deps.recallGate;
743
+ const toGate = g.maxCandidates ? selected.slice(0, g.maxCandidates) : selected;
744
+ const remaining = deadline - Date.now();
745
+ const gateFallback = toGate.map((h) => ({ id: h.id, gate: "relevant" }));
746
+ if (remaining <= 0) {
747
+ gateDecisions = gateFallback;
748
+ } else {
749
+ gateDecisions = await withDeadline(
750
+ Promise.all(toGate.map(async (h) => ({ id: h.id, gate: await g.judge(input.prompt, `${h.label}
751
+ ${h.summary ?? ""}`) }))),
752
+ remaining,
753
+ gateFallback
754
+ );
755
+ }
756
+ if (g.mode === "live") {
757
+ const drop = new Set(gateDecisions.filter((d) => d.gate === "irrelevant").map((d) => d.id));
758
+ injected = selected.filter((h) => !drop.has(h.id));
759
+ }
760
+ }
761
+ const block = formatPointerBlock(injected, fetched.facts, fetched.exemplars);
397
762
  const estTokens = Math.ceil(block.length / 4);
398
763
  appendHookLog({
399
764
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -401,25 +766,17 @@ async function runHook(input, deps) {
401
766
  promptPreview: preview,
402
767
  gate,
403
768
  hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
404
- wouldInject: selected.map((h) => h.id),
769
+ wouldInject: injected.map((h) => h.id),
405
770
  estTokens,
406
- mode: deps.mode
771
+ mode: deps.mode,
772
+ ...gateDecisions ? { gateDecisions } : {}
407
773
  });
408
- if (deps.mode === "live" && selected.length > 0) {
409
- recordSurfaced(input.conversationId, selected.map((h) => h.id));
774
+ if (deps.mode === "live" && injected.length > 0) {
775
+ recordSurfaced(input.conversationId, injected.map((h) => h.id));
410
776
  return block;
411
777
  }
412
778
  return "";
413
779
  }
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
780
  async function main() {
424
781
  try {
425
782
  autoloadEnv();
@@ -427,12 +784,21 @@ async function main() {
427
784
  const payload = JSON.parse(raw);
428
785
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
429
786
  const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
787
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : void 0;
430
788
  if (!prompt) return;
431
- const mode = process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
789
+ if (!promptRecallEnabled()) return;
790
+ const mode = hookModeFromEnv();
432
791
  const runtime = hookRuntimeFromEnv();
792
+ const gateMode = parseRecallGateMode();
793
+ const gateUrl = process.env["OLLAMA_URL"] ?? "http://127.0.0.1:11434";
794
+ const gateTopN = Math.max(1, Number.parseInt(process.env["NLM_HOOK_RECALL_GATE_TOPN"] ?? "1", 10) || 1);
433
795
  const out = await runHook(
434
- { prompt, conversationId },
435
- { mode, recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId) }
796
+ { prompt, conversationId, ...transcriptPath ? { transcriptPath } : {} },
797
+ {
798
+ mode,
799
+ recall: (q) => recallOverHttp(q, runtime, conversationId === "unknown" ? void 0 : conversationId),
800
+ ...gateMode ? { recallGate: { mode: gateMode, judge: makeOllamaGate(gateUrl), maxCandidates: gateTopN } } : {}
801
+ }
436
802
  );
437
803
  if (out) process.stdout.write(out);
438
804
  } catch {
@@ -442,6 +808,10 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
442
808
  void main();
443
809
  }
444
810
  export {
811
+ HOOK_DEADLINE_MS,
812
+ buildRecallQuery,
445
813
  hookRuntimeFromEnv,
814
+ parseHookDeadline,
815
+ promptRecallEnabled,
446
816
  runHook
447
817
  };