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