nlm-memory 0.20.1 → 0.21.12

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 (475) hide show
  1. package/README.md +69 -12
  2. package/assets/replay-eval-dark.svg +26 -0
  3. package/assets/replay-eval-light.svg +26 -0
  4. package/dist/cli/digest.js +69 -4
  5. package/dist/cli/digest.js.map +1 -1
  6. package/dist/cli/nlm.d.ts +14 -0
  7. package/dist/cli/nlm.js +556 -71
  8. package/dist/cli/nlm.js.map +1 -1
  9. package/dist/cli/reprocess-job-adapters.d.ts +35 -0
  10. package/dist/cli/reprocess-job-adapters.js +80 -0
  11. package/dist/cli/reprocess-job-adapters.js.map +1 -0
  12. package/dist/cli/scope-backfill.d.ts +34 -0
  13. package/dist/cli/scope-backfill.js +237 -0
  14. package/dist/cli/scope-backfill.js.map +1 -0
  15. package/dist/cli/scope-coverage.d.ts +44 -0
  16. package/dist/cli/scope-coverage.js +208 -0
  17. package/dist/cli/scope-coverage.js.map +1 -0
  18. package/dist/cli/supersede.js +8 -7
  19. package/dist/cli/supersede.js.map +1 -1
  20. package/dist/core/actions/actions-log.d.ts +65 -8
  21. package/dist/core/actions/actions-log.js +134 -17
  22. package/dist/core/actions/actions-log.js.map +1 -1
  23. package/dist/core/actions/overlay.d.ts +11 -2
  24. package/dist/core/actions/overlay.js +18 -6
  25. package/dist/core/actions/overlay.js.map +1 -1
  26. package/dist/core/adapters/claude-code.d.ts +14 -0
  27. package/dist/core/adapters/claude-code.js +30 -0
  28. package/dist/core/adapters/claude-code.js.map +1 -1
  29. package/dist/core/adapters/from-source.js +11 -3
  30. package/dist/core/adapters/from-source.js.map +1 -1
  31. package/dist/core/adapters/fs-wiki-writer.d.ts +44 -0
  32. package/dist/core/adapters/fs-wiki-writer.js +132 -0
  33. package/dist/core/adapters/fs-wiki-writer.js.map +1 -0
  34. package/dist/core/adapters/hermes-agent.d.ts +15 -2
  35. package/dist/core/adapters/hermes-agent.js +57 -11
  36. package/dist/core/adapters/hermes-agent.js.map +1 -1
  37. package/dist/core/adapters/memory-wiki-writer.d.ts +8 -0
  38. package/dist/core/adapters/memory-wiki-writer.js +16 -0
  39. package/dist/core/adapters/memory-wiki-writer.js.map +1 -0
  40. package/dist/core/adapters/muse.d.ts +51 -0
  41. package/dist/core/adapters/muse.js +271 -0
  42. package/dist/core/adapters/muse.js.map +1 -0
  43. package/dist/core/adapters/opencode.d.ts +11 -4
  44. package/dist/core/adapters/opencode.js +41 -7
  45. package/dist/core/adapters/opencode.js.map +1 -1
  46. package/dist/core/alerts/alert-state.d.ts +17 -0
  47. package/dist/core/alerts/alert-state.js +45 -0
  48. package/dist/core/alerts/alert-state.js.map +1 -0
  49. package/dist/core/alerts/check-and-alert.d.ts +28 -0
  50. package/dist/core/alerts/check-and-alert.js +53 -0
  51. package/dist/core/alerts/check-and-alert.js.map +1 -0
  52. package/dist/core/alerts/drift-transition.d.ts +27 -0
  53. package/dist/core/alerts/drift-transition.js +56 -0
  54. package/dist/core/alerts/drift-transition.js.map +1 -0
  55. package/dist/core/alerts/embedder-transition.d.ts +23 -0
  56. package/dist/core/alerts/embedder-transition.js +40 -0
  57. package/dist/core/alerts/embedder-transition.js.map +1 -0
  58. package/dist/core/alerts/fire-alert.d.ts +24 -0
  59. package/dist/core/alerts/fire-alert.js +72 -0
  60. package/dist/core/alerts/fire-alert.js.map +1 -0
  61. package/dist/core/alerts/job-alert.d.ts +12 -0
  62. package/dist/core/alerts/job-alert.js +37 -0
  63. package/dist/core/alerts/job-alert.js.map +1 -0
  64. package/dist/core/alerts/types.d.ts +104 -0
  65. package/dist/core/alerts/types.js +11 -0
  66. package/dist/core/alerts/types.js.map +1 -0
  67. package/dist/core/alerts/wiki-alert.d.ts +15 -0
  68. package/dist/core/alerts/wiki-alert.js +31 -0
  69. package/dist/core/alerts/wiki-alert.js.map +1 -0
  70. package/dist/core/classifier/prompt.d.ts +13 -0
  71. package/dist/core/classifier/prompt.js +52 -2
  72. package/dist/core/classifier/prompt.js.map +1 -1
  73. package/dist/core/dataset/build-dataset.d.ts +16 -1
  74. package/dist/core/dataset/build-dataset.js +46 -13
  75. package/dist/core/dataset/build-dataset.js.map +1 -1
  76. package/dist/core/digest/compose.d.ts +11 -3
  77. package/dist/core/digest/compose.js +70 -17
  78. package/dist/core/digest/compose.js.map +1 -1
  79. package/dist/core/digest/hook-injection.d.ts +43 -0
  80. package/dist/core/digest/hook-injection.js +87 -0
  81. package/dist/core/digest/hook-injection.js.map +1 -0
  82. package/dist/core/digest/hook-version-parity.d.ts +31 -0
  83. package/dist/core/digest/hook-version-parity.js +56 -0
  84. package/dist/core/digest/hook-version-parity.js.map +1 -0
  85. package/dist/core/embedding/pg-embed-backfill.js +1 -1
  86. package/dist/core/embedding/pg-embed-backfill.js.map +1 -1
  87. package/dist/core/exemplars/capture-from-session.d.ts +2 -1
  88. package/dist/core/exemplars/capture-from-session.js +4 -4
  89. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  90. package/dist/core/exemplars/embed-backfill.d.ts +9 -0
  91. package/dist/core/exemplars/embed-backfill.js +9 -1
  92. package/dist/core/exemplars/embed-backfill.js.map +1 -1
  93. package/dist/core/exemplars/extract-exemplar.d.ts +2 -0
  94. package/dist/core/exemplars/extract-exemplar.js +7 -1
  95. package/dist/core/exemplars/extract-exemplar.js.map +1 -1
  96. package/dist/core/exemplars/ingest-exemplar.d.ts +1 -0
  97. package/dist/core/exemplars/ingest-exemplar.js +1 -0
  98. package/dist/core/exemplars/ingest-exemplar.js.map +1 -1
  99. package/dist/core/exemplars/recall-code.d.ts +1 -1
  100. package/dist/core/exemplars/recall-code.js +2 -2
  101. package/dist/core/exemplars/recall-code.js.map +1 -1
  102. package/dist/core/facts/backfill-facts.d.ts +1 -1
  103. package/dist/core/facts/backfill-facts.js +4 -4
  104. package/dist/core/facts/backfill-facts.js.map +1 -1
  105. package/dist/core/facts/embed-backfill.d.ts +54 -0
  106. package/dist/core/facts/embed-backfill.js +109 -0
  107. package/dist/core/facts/embed-backfill.js.map +1 -0
  108. package/dist/core/facts/reconcile-facts.d.ts +42 -0
  109. package/dist/core/facts/reconcile-facts.js +133 -0
  110. package/dist/core/facts/reconcile-facts.js.map +1 -0
  111. package/dist/core/health/embed-failure-state.d.ts +4 -0
  112. package/dist/core/health/embed-failure-state.js +12 -0
  113. package/dist/core/health/embed-failure-state.js.map +1 -0
  114. package/dist/core/health/warmup-retry.d.ts +27 -0
  115. package/dist/core/health/warmup-retry.js +53 -0
  116. package/dist/core/health/warmup-retry.js.map +1 -0
  117. package/dist/core/health/warmup-state.d.ts +15 -0
  118. package/dist/core/health/warmup-state.js +15 -1
  119. package/dist/core/health/warmup-state.js.map +1 -1
  120. package/dist/core/hook/cite-memo.d.ts +11 -7
  121. package/dist/core/hook/cite-memo.js +25 -17
  122. package/dist/core/hook/cite-memo.js.map +1 -1
  123. package/dist/core/hook/claude-settings.d.ts +1 -1
  124. package/dist/core/hook/claude-settings.js +0 -1
  125. package/dist/core/hook/claude-settings.js.map +1 -1
  126. package/dist/core/hook/hook-log.d.ts +13 -2
  127. package/dist/core/hook/hook-log.js +16 -8
  128. package/dist/core/hook/hook-log.js.map +1 -1
  129. package/dist/core/hook/memo.d.ts +10 -6
  130. package/dist/core/hook/memo.js +25 -17
  131. package/dist/core/hook/memo.js.map +1 -1
  132. package/dist/core/hook/pointer-block.d.ts +8 -1
  133. package/dist/core/hook/pointer-block.js +17 -4
  134. package/dist/core/hook/pointer-block.js.map +1 -1
  135. package/dist/core/hook/resolve-conversation-by-query.d.ts +6 -5
  136. package/dist/core/hook/resolve-conversation-by-query.js +24 -12
  137. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -1
  138. package/dist/core/ingest/backfill-derivables.d.ts +92 -0
  139. package/dist/core/ingest/backfill-derivables.js +175 -0
  140. package/dist/core/ingest/backfill-derivables.js.map +1 -0
  141. package/dist/core/ingest/ingest-session.d.ts +1 -1
  142. package/dist/core/ingest/ingest-session.js +23 -3
  143. package/dist/core/ingest/ingest-session.js.map +1 -1
  144. package/dist/core/ingest/reclassify-oversized.d.ts +1 -1
  145. package/dist/core/ingest/reclassify-oversized.js +8 -2
  146. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  147. package/dist/core/ingest/reprocess.d.ts +1 -1
  148. package/dist/core/ingest/reprocess.js +7 -2
  149. package/dist/core/ingest/reprocess.js.map +1 -1
  150. package/dist/core/ingest/transcript-derivables.d.ts +27 -0
  151. package/dist/core/ingest/transcript-derivables.js +138 -0
  152. package/dist/core/ingest/transcript-derivables.js.map +1 -0
  153. package/dist/core/jobs/job-supervisor.d.ts +168 -0
  154. package/dist/core/jobs/job-supervisor.js +335 -0
  155. package/dist/core/jobs/job-supervisor.js.map +1 -0
  156. package/dist/core/learner/model.d.ts +63 -0
  157. package/dist/core/learner/model.js +30 -0
  158. package/dist/core/learner/model.js.map +1 -0
  159. package/dist/core/learner/service.d.ts +42 -0
  160. package/dist/core/learner/service.js +74 -0
  161. package/dist/core/learner/service.js.map +1 -0
  162. package/dist/core/learner/summary.d.ts +28 -0
  163. package/dist/core/learner/summary.js +70 -0
  164. package/dist/core/learner/summary.js.map +1 -0
  165. package/dist/core/learner/validate.d.ts +18 -0
  166. package/dist/core/learner/validate.js +95 -0
  167. package/dist/core/learner/validate.js.map +1 -0
  168. package/dist/core/metrics/re-derivation.d.ts +31 -2
  169. package/dist/core/metrics/re-derivation.js +36 -4
  170. package/dist/core/metrics/re-derivation.js.map +1 -1
  171. package/dist/core/outcome/coverage.d.ts +33 -0
  172. package/dist/core/outcome/coverage.js +46 -0
  173. package/dist/core/outcome/coverage.js.map +1 -0
  174. package/dist/core/outcome/rollup.d.ts +33 -0
  175. package/dist/core/outcome/rollup.js +113 -0
  176. package/dist/core/outcome/rollup.js.map +1 -0
  177. package/dist/core/providers/provider-registry.d.ts +40 -23
  178. package/dist/core/providers/provider-registry.js +99 -81
  179. package/dist/core/providers/provider-registry.js.map +1 -1
  180. package/dist/core/recall/citation-log.d.ts +10 -5
  181. package/dist/core/recall/citation-log.js +21 -10
  182. package/dist/core/recall/citation-log.js.map +1 -1
  183. package/dist/core/recall/hook-recall-log.d.ts +7 -4
  184. package/dist/core/recall/hook-recall-log.js +14 -8
  185. package/dist/core/recall/hook-recall-log.js.map +1 -1
  186. package/dist/core/recall/miss-log.d.ts +7 -4
  187. package/dist/core/recall/miss-log.js +18 -11
  188. package/dist/core/recall/miss-log.js.map +1 -1
  189. package/dist/core/recall/query-log.d.ts +8 -5
  190. package/dist/core/recall/query-log.js +16 -9
  191. package/dist/core/recall/query-log.js.map +1 -1
  192. package/dist/core/recall/recall-service.d.ts +10 -1
  193. package/dist/core/recall/recall-service.js +31 -12
  194. package/dist/core/recall/recall-service.js.map +1 -1
  195. package/dist/core/recall/recent-log.d.ts +6 -1
  196. package/dist/core/recall/recent-log.js +13 -5
  197. package/dist/core/recall/recent-log.js.map +1 -1
  198. package/dist/core/recall/related-concepts.d.ts +29 -0
  199. package/dist/core/recall/related-concepts.js +67 -0
  200. package/dist/core/recall/related-concepts.js.map +1 -0
  201. package/dist/core/recall/related-exemplars.d.ts +1 -1
  202. package/dist/core/recall/related-exemplars.js +2 -2
  203. package/dist/core/recall/related-exemplars.js.map +1 -1
  204. package/dist/core/recall/related-facts.d.ts +1 -1
  205. package/dist/core/recall/related-facts.js +3 -3
  206. package/dist/core/recall/related-facts.js.map +1 -1
  207. package/dist/core/recall-facts/fact-query-log.d.ts +8 -4
  208. package/dist/core/recall-facts/fact-query-log.js +16 -8
  209. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  210. package/dist/core/recall-facts/fact-recall-service.d.ts +2 -2
  211. package/dist/core/recall-facts/fact-recall-service.js +13 -12
  212. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  213. package/dist/core/scheduler/scan-once.js +15 -12
  214. package/dist/core/scheduler/scan-once.js.map +1 -1
  215. package/dist/core/scheduler/scheduler.d.ts +3 -0
  216. package/dist/core/scheduler/scheduler.js +45 -10
  217. package/dist/core/scheduler/scheduler.js.map +1 -1
  218. package/dist/core/scope/alias-map.d.ts +10 -0
  219. package/dist/core/scope/alias-map.js +41 -0
  220. package/dist/core/scope/alias-map.js.map +1 -0
  221. package/dist/core/scope/derive-scope.d.ts +8 -0
  222. package/dist/core/scope/derive-scope.js +54 -0
  223. package/dist/core/scope/derive-scope.js.map +1 -0
  224. package/dist/core/scope/scope-clause.d.ts +16 -0
  225. package/dist/core/scope/scope-clause.js +21 -0
  226. package/dist/core/scope/scope-clause.js.map +1 -0
  227. package/dist/core/scope/stamp-flag.d.ts +1 -0
  228. package/dist/core/scope/stamp-flag.js +4 -0
  229. package/dist/core/scope/stamp-flag.js.map +1 -0
  230. package/dist/core/signals/code-signal.d.ts +1 -0
  231. package/dist/core/signals/code-signal.js +1 -0
  232. package/dist/core/signals/code-signal.js.map +1 -1
  233. package/dist/core/signals/failure-mode-recall.d.ts +1 -1
  234. package/dist/core/signals/failure-mode-recall.js +2 -2
  235. package/dist/core/signals/failure-mode-recall.js.map +1 -1
  236. package/dist/core/signals/ingest-signal.d.ts +1 -1
  237. package/dist/core/signals/ingest-signal.js +3 -1
  238. package/dist/core/signals/ingest-signal.js.map +1 -1
  239. package/dist/core/signals/stamp-scope.d.ts +18 -0
  240. package/dist/core/signals/stamp-scope.js +28 -0
  241. package/dist/core/signals/stamp-scope.js.map +1 -0
  242. package/dist/core/sources/source-registry.d.ts +49 -28
  243. package/dist/core/sources/source-registry.js +113 -70
  244. package/dist/core/sources/source-registry.js.map +1 -1
  245. package/dist/core/storage/pg-code-exemplar-store.d.ts +19 -10
  246. package/dist/core/storage/pg-code-exemplar-store.js +62 -27
  247. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  248. package/dist/core/storage/pg-entity-store.d.ts +6 -1
  249. package/dist/core/storage/pg-entity-store.js +33 -18
  250. package/dist/core/storage/pg-entity-store.js.map +1 -1
  251. package/dist/core/storage/pg-fact-ingest.d.ts +1 -1
  252. package/dist/core/storage/pg-fact-ingest.js +10 -7
  253. package/dist/core/storage/pg-fact-ingest.js.map +1 -1
  254. package/dist/core/storage/pg-fact-store.d.ts +29 -17
  255. package/dist/core/storage/pg-fact-store.js +99 -46
  256. package/dist/core/storage/pg-fact-store.js.map +1 -1
  257. package/dist/core/storage/pg-learner-store.d.ts +27 -0
  258. package/dist/core/storage/pg-learner-store.js +174 -0
  259. package/dist/core/storage/pg-learner-store.js.map +1 -0
  260. package/dist/core/storage/pg-session-store.d.ts +25 -21
  261. package/dist/core/storage/pg-session-store.js +186 -114
  262. package/dist/core/storage/pg-session-store.js.map +1 -1
  263. package/dist/core/storage/pg-signal-store.d.ts +10 -5
  264. package/dist/core/storage/pg-signal-store.js +26 -13
  265. package/dist/core/storage/pg-signal-store.js.map +1 -1
  266. package/dist/core/storage/pg-storage.d.ts +4 -0
  267. package/dist/core/storage/pg-storage.js +6 -0
  268. package/dist/core/storage/pg-storage.js.map +1 -1
  269. package/dist/core/storage/pg-workstream-store.d.ts +17 -11
  270. package/dist/core/storage/pg-workstream-store.js +47 -29
  271. package/dist/core/storage/pg-workstream-store.js.map +1 -1
  272. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +24 -10
  273. package/dist/core/storage/sqlite-code-exemplar-store.js +86 -40
  274. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  275. package/dist/core/storage/sqlite-entity-store.d.ts +8 -1
  276. package/dist/core/storage/sqlite-entity-store.js +46 -27
  277. package/dist/core/storage/sqlite-entity-store.js.map +1 -1
  278. package/dist/core/storage/sqlite-fact-store.d.ts +45 -19
  279. package/dist/core/storage/sqlite-fact-store.js +175 -64
  280. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  281. package/dist/core/storage/sqlite-learner-store.d.ts +33 -0
  282. package/dist/core/storage/sqlite-learner-store.js +178 -0
  283. package/dist/core/storage/sqlite-learner-store.js.map +1 -0
  284. package/dist/core/storage/sqlite-outcome-store.d.ts +104 -0
  285. package/dist/core/storage/sqlite-outcome-store.js +211 -0
  286. package/dist/core/storage/sqlite-outcome-store.js.map +1 -0
  287. package/dist/core/storage/sqlite-session-store.d.ts +50 -21
  288. package/dist/core/storage/sqlite-session-store.js +233 -149
  289. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  290. package/dist/core/storage/sqlite-signal-store.d.ts +12 -5
  291. package/dist/core/storage/sqlite-signal-store.js +29 -15
  292. package/dist/core/storage/sqlite-signal-store.js.map +1 -1
  293. package/dist/core/storage/sqlite-storage.d.ts +4 -0
  294. package/dist/core/storage/sqlite-storage.js +10 -2
  295. package/dist/core/storage/sqlite-storage.js.map +1 -1
  296. package/dist/core/storage/sqlite-workstream-store.d.ts +17 -11
  297. package/dist/core/storage/sqlite-workstream-store.js +53 -31
  298. package/dist/core/storage/sqlite-workstream-store.js.map +1 -1
  299. package/dist/core/storage/supersedence-log.d.ts +10 -7
  300. package/dist/core/storage/supersedence-log.js +21 -14
  301. package/dist/core/storage/supersedence-log.js.map +1 -1
  302. package/dist/core/telemetry/probe-filter.d.ts +35 -0
  303. package/dist/core/telemetry/probe-filter.js +56 -0
  304. package/dist/core/telemetry/probe-filter.js.map +1 -0
  305. package/dist/core/tenancy/default-team.d.ts +6 -0
  306. package/dist/core/tenancy/default-team.js +7 -0
  307. package/dist/core/tenancy/default-team.js.map +1 -0
  308. package/dist/core/tenancy/hosted-mode.d.ts +9 -0
  309. package/dist/core/tenancy/hosted-mode.js +12 -0
  310. package/dist/core/tenancy/hosted-mode.js.map +1 -0
  311. package/dist/core/tenancy/team-auth.d.ts +16 -0
  312. package/dist/core/tenancy/team-auth.js +22 -0
  313. package/dist/core/tenancy/team-auth.js.map +1 -0
  314. package/dist/core/tenancy/team-token-store.d.ts +45 -0
  315. package/dist/core/tenancy/team-token-store.js +52 -0
  316. package/dist/core/tenancy/team-token-store.js.map +1 -0
  317. package/dist/core/tenancy/tenant-clause.d.ts +37 -0
  318. package/dist/core/tenancy/tenant-clause.js +11 -0
  319. package/dist/core/tenancy/tenant-clause.js.map +1 -0
  320. package/dist/core/tenancy/tenant-state-path.d.ts +28 -0
  321. package/dist/core/tenancy/tenant-state-path.js +49 -0
  322. package/dist/core/tenancy/tenant-state-path.js.map +1 -0
  323. package/dist/core/update-check/check.d.ts +6 -0
  324. package/dist/core/update-check/check.js +35 -0
  325. package/dist/core/update-check/check.js.map +1 -1
  326. package/dist/core/wiki/project.d.ts +17 -0
  327. package/dist/core/wiki/project.js +37 -0
  328. package/dist/core/wiki/project.js.map +1 -0
  329. package/dist/core/wiki/reconcile.d.ts +15 -0
  330. package/dist/core/wiki/reconcile.js +14 -0
  331. package/dist/core/wiki/reconcile.js.map +1 -0
  332. package/dist/core/wiki/render.d.ts +5 -0
  333. package/dist/core/wiki/render.js +84 -0
  334. package/dist/core/wiki/render.js.map +1 -0
  335. package/dist/core/wiki/rollup.d.ts +28 -0
  336. package/dist/core/wiki/rollup.js +91 -0
  337. package/dist/core/wiki/rollup.js.map +1 -0
  338. package/dist/core/wiki/select.d.ts +8 -0
  339. package/dist/core/wiki/select.js +7 -0
  340. package/dist/core/wiki/select.js.map +1 -0
  341. package/dist/core/wiki/slug.d.ts +36 -0
  342. package/dist/core/wiki/slug.js +71 -0
  343. package/dist/core/wiki/slug.js.map +1 -0
  344. package/dist/core/wiki/types.d.ts +37 -0
  345. package/dist/core/wiki/types.js +2 -0
  346. package/dist/core/wiki/types.js.map +1 -0
  347. package/dist/core/work-digest/build-work-digest.d.ts +1 -1
  348. package/dist/core/work-digest/build-work-digest.js +3 -3
  349. package/dist/core/work-digest/build-work-digest.js.map +1 -1
  350. package/dist/core/workstream/bind.d.ts +3 -2
  351. package/dist/core/workstream/bind.js +29 -10
  352. package/dist/core/workstream/bind.js.map +1 -1
  353. package/dist/core/workstream/model.d.ts +1 -0
  354. package/dist/core/workstream/model.js.map +1 -1
  355. package/dist/core/workstream/rollup.d.ts +1 -1
  356. package/dist/core/workstream/rollup.js +6 -6
  357. package/dist/core/workstream/rollup.js.map +1 -1
  358. package/dist/hook/hook-helpers.d.ts +14 -0
  359. package/dist/hook/hook-helpers.js +29 -2
  360. package/dist/hook/hook-helpers.js.map +1 -1
  361. package/dist/hook/pre-compact-hook.js +8 -5
  362. package/dist/hook/pre-compact-hook.js.map +1 -1
  363. package/dist/hook/prompt-recall-hook.d.ts +7 -3
  364. package/dist/hook/prompt-recall-hook.js +35 -20
  365. package/dist/hook/prompt-recall-hook.js.map +1 -1
  366. package/dist/hook/recall-over-http.d.ts +28 -3
  367. package/dist/hook/recall-over-http.js +58 -12
  368. package/dist/hook/recall-over-http.js.map +1 -1
  369. package/dist/hook/session-end-hook.js +7 -5
  370. package/dist/hook/session-end-hook.js.map +1 -1
  371. package/dist/hook/session-start-hook.d.ts +14 -1
  372. package/dist/hook/session-start-hook.js +57 -16
  373. package/dist/hook/session-start-hook.js.map +1 -1
  374. package/dist/hook/stop-hook.js +11 -8
  375. package/dist/hook/stop-hook.js.map +1 -1
  376. package/dist/http/app.d.ts +47 -3
  377. package/dist/http/app.js +379 -132
  378. package/dist/http/app.js.map +1 -1
  379. package/dist/install/claude-code.d.ts +17 -0
  380. package/dist/install/claude-code.js +44 -5
  381. package/dist/install/claude-code.js.map +1 -1
  382. package/dist/install/codex.d.ts +23 -2
  383. package/dist/install/codex.js +111 -14
  384. package/dist/install/codex.js.map +1 -1
  385. package/dist/install/cursor.d.ts +2 -2
  386. package/dist/install/cursor.js +7 -7
  387. package/dist/install/cursor.js.map +1 -1
  388. package/dist/install/hermes.d.ts +8 -0
  389. package/dist/install/hermes.js +28 -5
  390. package/dist/install/hermes.js.map +1 -1
  391. package/dist/install/local-source.d.ts +20 -0
  392. package/dist/install/local-source.js +28 -0
  393. package/dist/install/local-source.js.map +1 -0
  394. package/dist/install/mcp-block.d.ts +77 -0
  395. package/dist/install/mcp-block.js +99 -0
  396. package/dist/install/mcp-block.js.map +1 -0
  397. package/dist/install/pi.d.ts +26 -0
  398. package/dist/install/pi.js +43 -0
  399. package/dist/install/pi.js.map +1 -1
  400. package/dist/install/setup.js +6 -0
  401. package/dist/install/setup.js.map +1 -1
  402. package/dist/install/windsurf.d.ts +2 -2
  403. package/dist/install/windsurf.js +7 -7
  404. package/dist/install/windsurf.js.map +1 -1
  405. package/dist/llm/classifier-box.d.ts +5 -0
  406. package/dist/llm/classifier-box.js +5 -0
  407. package/dist/llm/classifier-box.js.map +1 -1
  408. package/dist/mcp/server.d.ts +94 -15
  409. package/dist/mcp/server.js +402 -79
  410. package/dist/mcp/server.js.map +1 -1
  411. package/dist/ports/clock.d.ts +12 -0
  412. package/dist/ports/clock.js +10 -0
  413. package/dist/ports/clock.js.map +1 -0
  414. package/dist/ports/code-exemplar-store.d.ts +13 -13
  415. package/dist/ports/entity-store.d.ts +5 -1
  416. package/dist/ports/fact-store.d.ts +0 -0
  417. package/dist/ports/fact-store.js +0 -0
  418. package/dist/ports/fact-store.js.map +1 -1
  419. package/dist/ports/learner-store.d.ts +80 -0
  420. package/dist/ports/learner-store.js +19 -0
  421. package/dist/ports/learner-store.js.map +1 -0
  422. package/dist/ports/outcome.d.ts +85 -0
  423. package/dist/ports/outcome.js +11 -0
  424. package/dist/ports/outcome.js.map +1 -0
  425. package/dist/ports/session-store.d.ts +20 -20
  426. package/dist/ports/session-store.js +5 -0
  427. package/dist/ports/session-store.js.map +1 -1
  428. package/dist/ports/signal-store.d.ts +12 -8
  429. package/dist/ports/spawn-child.d.ts +14 -0
  430. package/dist/ports/spawn-child.js +10 -0
  431. package/dist/ports/spawn-child.js.map +1 -0
  432. package/dist/ports/storage.d.ts +6 -0
  433. package/dist/ports/transcript-adapter.d.ts +11 -0
  434. package/dist/ports/wiki-writer.d.ts +19 -0
  435. package/dist/ports/wiki-writer.js +12 -0
  436. package/dist/ports/wiki-writer.js.map +1 -0
  437. package/dist/ports/workstream-store.d.ts +20 -12
  438. package/dist/shared/net.d.ts +3 -0
  439. package/dist/shared/net.js +4 -0
  440. package/dist/shared/net.js.map +1 -0
  441. package/dist/shared/types.d.ts +55 -0
  442. package/dist/shared/types.js +4 -1
  443. package/dist/shared/types.js.map +1 -1
  444. package/dist/shared/version.d.ts +9 -0
  445. package/dist/shared/version.js +11 -0
  446. package/dist/shared/version.js.map +1 -0
  447. package/dist/ui/assets/index-D6F3xAis.js +11 -0
  448. package/dist/ui/index.html +1 -1
  449. package/migrations/029_project_scope.sql +21 -0
  450. package/migrations/032_session_derivables.sql +11 -0
  451. package/migrations/033_actions_kind_check.sql +58 -0
  452. package/migrations/034_tenancy_teams_and_stamps.sql +40 -0
  453. package/migrations/035_tenancy_entity_rekey.sql +75 -0
  454. package/migrations/036_tenant_name_uniqueness.sql +59 -0
  455. package/migrations/037_sessions_covering_index.sql +25 -0
  456. package/migrations/038_sources_muse.sql +33 -0
  457. package/migrations/039_learner_evidence.sql +51 -0
  458. package/migrations/pg/030_project_scope.sql +23 -0
  459. package/migrations/pg/031_widen_session_edges_kind.sql +11 -0
  460. package/migrations/pg/032_session_derivables.sql +17 -0
  461. package/migrations/pg/033_actions_kind_check.sql +17 -0
  462. package/migrations/pg/034_tenancy.sql +77 -0
  463. package/migrations/pg/035_tenant_name_uniqueness.sql +15 -0
  464. package/migrations/pg/036_sources_muse.sql +12 -0
  465. package/migrations/pg/037_learner_evidence.sql +55 -0
  466. package/nlm/index.js +258 -59
  467. package/package.json +10 -5
  468. package/plugin/.codex-plugin/plugin.json +1 -1
  469. package/plugin/scripts/prompt-recall-hook.mjs +256 -58
  470. package/plugin/scripts/session-start-hook.mjs +283 -55
  471. package/plugin/scripts/stop-hook.mjs +199 -38
  472. package/dist/hook/subagent-start-hook.d.ts +0 -28
  473. package/dist/hook/subagent-start-hook.js +0 -81
  474. package/dist/hook/subagent-start-hook.js.map +0 -1
  475. package/dist/ui/assets/index-CMpQkbQy.js +0 -11
package/dist/cli/nlm.js CHANGED
@@ -22,12 +22,13 @@
22
22
  * nlm disconnect claude-code — remove MCP block from ~/.mcp.json
23
23
  * nlm disconnect codex — remove Codex plugin
24
24
  * nlm digest — print a daily-activity digest (or --telegram to post)
25
+ * nlm reconcile-facts — retire live facts contradicting an authorities file
25
26
  * nlm init - print (or write) the agent recall contract snippet
26
27
  */
27
28
  import { fileURLToPath, pathToFileURL } from "node:url";
28
29
  import { dirname, resolve, join } from "node:path";
29
30
  import { homedir } from "node:os";
30
- import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, copyFileSync, realpathSync, appendFileSync } from "node:fs";
31
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, copyFileSync, realpathSync, appendFileSync, renameSync } from "node:fs";
31
32
  import { execFileSync, spawn, spawnSync } from "node:child_process";
32
33
  import { Command } from "commander";
33
34
  import pkg from "../../package.json" with { type: "json" };
@@ -39,6 +40,7 @@ import { ProviderRegistry } from "../core/providers/provider-registry.js";
39
40
  import { SqliteStorage } from "../core/storage/sqlite-storage.js";
40
41
  import { PgStorage } from "../core/storage/pg-storage.js";
41
42
  import { applyPendingRestore, stageRestore } from "../core/storage/db-restore.js";
43
+ import { applyRetirements, findContradictions, formatReport, loadAuthorities, } from "../core/facts/reconcile-facts.js";
42
44
  import { listBackupDates, resolveBackup, runRollingBackup } from "../core/storage/backup-rotation.js";
43
45
  import { createApp } from "../http/app.js";
44
46
  import { createMcpServer, listMergeSuggestionsHandler, mergeWorkstreamsHandler, rebindSessionHandler, recallWorkstreamHandler, renameWorkstreamHandler, retireWorkstreamHandler } from "../mcp/server.js";
@@ -50,29 +52,47 @@ import { resolveEmbedderInfo } from "../llm/embedder-info.js";
50
52
  import { autoloadEnv } from "../llm/env-autoload.js";
51
53
  import { buildEmbedder as _buildEmbedder } from "../llm/build-embedder.js";
52
54
  import { addHook, buildHookCommand, removeHook } from "../core/hook/claude-settings.js";
53
- import { codexBinaryAvailable, connectCodex, disconnectCodex, repairCodex, pluginScriptsDir, } from "../install/codex.js";
54
- import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks, mcpConfigPath } from "../install/claude-code.js";
55
+ import { codexBinaryAvailable, codexHasEffectiveMcpServer, connectCodex, disconnectCodex, repairCodex, pluginScriptsDir, } from "../install/codex.js";
56
+ import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks, mcpConfigPath, nvmPinnedHookWarning } from "../install/claude-code.js";
55
57
  import { evaluateInstallHealth, evaluateModelHealth, evaluateRecallSmoke } from "../install/health.js";
56
58
  import { codexConfigPath } from "../install/codex.js";
57
59
  import { hardenNlmDirPermissions } from "../install/nlm-dir-perms.js";
58
60
  import { embeddingModelPresent, ensureMcpToken, ollamaModelPresent } from "../install/ollama.js";
59
61
  import { connectCursor, disconnectCursor } from "../install/cursor.js";
62
+ import { connectLocalSource } from "../install/local-source.js";
63
+ import { defaultDbPath as openCodeDefaultDbPath } from "../core/adapters/opencode.js";
64
+ import { defaultSessionsPath as museDefaultSessionsPath } from "../core/adapters/muse.js";
60
65
  import { describeRemove, describeUpsert, installCursorRules, installOpencodeRules, installWindsurfRules, uninstallCursorRules, uninstallOpencodeRules, uninstallWindsurfRules, } from "../install/rules-install.js";
61
66
  import { runSupersedeCommand } from "./supersede.js";
62
67
  import { runInitCommand } from "./init.js";
68
+ import { DEFAULT_TEAM_ID } from "../core/tenancy/default-team.js";
69
+ import { hashTeamToken } from "../core/tenancy/team-auth.js";
70
+ import { runScopeBackfill, formatBackfillResult } from "./scope-backfill.js";
71
+ import { runScopeCoverage, formatCoverageResult } from "./scope-coverage.js";
72
+ import { loadAliasMap } from "../core/scope/alias-map.js";
63
73
  import { getUpdateStatus } from "../core/update-check/check.js";
74
+ import { checkDriftAndAlert, checkEmbedderAndAlert } from "../core/alerts/check-and-alert.js";
75
+ import { fireAlert } from "../core/alerts/fire-alert.js";
76
+ import { buildJobAlertEvent } from "../core/alerts/job-alert.js";
77
+ import { buildWikiDriftEvent, buildWikiFailureEvent } from "../core/alerts/wiki-alert.js";
78
+ import { projectWiki } from "../core/wiki/project.js";
79
+ import { FsWikiWriter } from "../core/adapters/fs-wiki-writer.js";
80
+ import { JobSupervisor } from "../core/jobs/job-supervisor.js";
81
+ import { realClock, createReprocessSpawnChild } from "./reprocess-job-adapters.js";
64
82
  import { connectHermes, disconnectHermes, hermesConfigPath } from "../install/hermes.js";
65
83
  import { connectHermesAgent, disconnectHermesAgent, hermesAgentPluginDir } from "../install/hermes-agent.js";
66
84
  import { connectWindsurf, disconnectWindsurf } from "../install/windsurf.js";
67
- import { connectPi, disconnectPi, piSettingsPath } from "../install/pi.js";
85
+ import { connectPi, connectPiMcp, disconnectPi, piSettingsPath } from "../install/pi.js";
68
86
  import { runSetup } from "../install/setup.js";
69
87
  import { runParity } from "./classify-parity.js";
70
88
  import { reembedCorpus } from "../core/embedding/embed-backfill.js";
71
89
  import { reembedCorpusPg } from "../core/embedding/pg-embed-backfill.js";
72
90
  import { backfillExemplarEmbeddings } from "../core/exemplars/embed-backfill.js";
91
+ import { backfillFactEmbeddings } from "../core/facts/embed-backfill.js";
73
92
  import { warmCodeEmbedder } from "../core/exemplars/warm-embedder.js";
74
- import { markWarm } from "../core/health/warmup-state.js";
93
+ import { markWarm, markWarmFailure } from "../core/health/warmup-state.js";
75
94
  import { setLaneHealth } from "../core/health/embedding-lane-state.js";
95
+ import { retryUntilWarm } from "../core/health/warmup-retry.js";
76
96
  import { setCorpusSnapshot, corpusSnapshot } from "../core/health/corpus-state.js";
77
97
  import { computeCorpusStats, sqliteCorpusStatsDeps, parseCorpusThresholds, thresholdState } from "../core/metrics/corpus-stats.js";
78
98
  import { computeReDerivationRate, sqliteReDerivationDeps } from "../core/metrics/re-derivation.js";
@@ -94,6 +114,7 @@ import { runChecksOnSqlite, runChecksOnPg, applyFixOnSqlite, applyFixOnPg } from
94
114
  import { normalizeLabel } from "../core/workstream/model.js";
95
115
  import { resolveWorkstreamId } from "../core/workstream/resolve.js";
96
116
  import { suggestMerges } from "../core/entities/dedup-suggest.js";
117
+ import { DEFAULT_NLM_PORT } from "../shared/net.js";
97
118
  const __filename = fileURLToPath(import.meta.url);
98
119
  const __dirname = dirname(__filename);
99
120
  const MIGRATIONS_DIR = resolve(__dirname, "../../migrations");
@@ -101,7 +122,7 @@ const CLASSIFIER_FIXTURES_DIR = resolve(__dirname, "../../fixtures/classifier-go
101
122
  const PG_MIGRATIONS_DIR = join(fileURLToPath(new URL(".", import.meta.url)), "../../migrations/pg");
102
123
  const UI_DIST = resolve(__dirname, "../../dist/ui");
103
124
  const DEFAULT_DB_PATH = resolve(homedir(), ".nlm/canonical.sqlite");
104
- const DEFAULT_PORT = 3940;
125
+ const DEFAULT_PORT = Number.parseInt(DEFAULT_NLM_PORT, 10);
105
126
  function dbPath() {
106
127
  return process.env["NLM_DB_PATH"] ?? DEFAULT_DB_PATH;
107
128
  }
@@ -114,6 +135,10 @@ function port() {
114
135
  return DEFAULT_PORT;
115
136
  return n;
116
137
  }
138
+ function positiveIntEnv(name, fallback) {
139
+ const parsed = Number.parseInt(process.env[name] ?? "", 10);
140
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
141
+ }
117
142
  function ollamaUrl() {
118
143
  return process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434";
119
144
  }
@@ -146,6 +171,28 @@ export function shouldAppendTrend(lastLineTs, now) {
146
171
  return true;
147
172
  return now - parsed >= TREND_INTERVAL_MS;
148
173
  }
174
+ /**
175
+ * Recomputes `computeReDerivationRate` and overwrites the pairs cache file
176
+ * (#405) that `@core/storage/sqlite-outcome-store.js` reads instead of
177
+ * recomputing the same O(n^2) scan inline. Called every 24h corpus-monitor
178
+ * cycle (not gated by `shouldAppendTrend`, which only paces the separate
179
+ * append-only trend log) so the cache stays ~24h fresh while the monitor
180
+ * runs. Exported for direct testing and smoke runs against a corpus copy.
181
+ * `tenantId` scopes both the session scan (sqliteReDerivationDeps) and the
182
+ * cache file this write targets — two tenants must never compute or read
183
+ * each other's pairs.
184
+ */
185
+ export async function persistReDerivationPairs(rawDb, tenantId, pairsPath, windowDays) {
186
+ const deps = sqliteReDerivationDeps(rawDb, tenantId);
187
+ const report = await computeReDerivationRate(deps, windowDays);
188
+ // Write-temp-then-rename: the outcome adapters read this file on every
189
+ // get_session, so a crash mid-write must never leave truncated JSON at
190
+ // the final path. rename is atomic within the same directory.
191
+ const tmpPath = `${pairsPath}.tmp-${process.pid}-${Date.now()}`;
192
+ writeFileSync(tmpPath, JSON.stringify(report.pairs), "utf8");
193
+ renameSync(tmpPath, pairsPath);
194
+ return report;
195
+ }
149
196
  function loadTopicProvider() {
150
197
  try {
151
198
  const raw = readFileSync(join(homedir(), ".nlm", "work-topics.json"), "utf8");
@@ -233,14 +280,14 @@ async function reconcileEmbeddingLanes(storage, embedder, codeEmbedder) {
233
280
  // probe failure; code lane stays unknown
234
281
  }
235
282
  }
236
- async function buildAdapters(sources) {
283
+ async function buildAdapters(sources, tenantId) {
237
284
  // Sources table is the source of truth. Each enabled row maps to one
238
285
  // adapter via adapterFromSource(). Detection still gates registration —
239
286
  // a row pointing at a missing dir won't poll. NLM_ADAPTERS keeps working
240
287
  // as a name-based filter for forcing a subset during dev.
241
288
  const explicit = process.env["NLM_ADAPTERS"];
242
289
  const allowed = explicit ? new Set(explicit.split(",").map((s) => s.trim())) : null;
243
- const rows = await sources.list();
290
+ const rows = await sources.list(tenantId);
244
291
  const out = [];
245
292
  for (const row of rows) {
246
293
  if (!row.enabled)
@@ -286,13 +333,13 @@ async function buildStack() {
286
333
  const signals = storage.signals;
287
334
  const scope = installScope();
288
335
  const sources = storage.sources;
289
- await sources.seedDefaults();
336
+ await sources.seedDefaults(DEFAULT_TEAM_ID);
290
337
  const providers = storage.providers;
291
338
  // Provider seeding is SQLite-only: it bridges from the local DEEPSEEK_API_KEY
292
339
  // env, which is wrong for a hosted multi-tenant PG. PgProviderRegistry has no
293
340
  // seedDefaults (not on ProviderRegistryPort), so this narrows to the SQLite case.
294
341
  if (providers instanceof ProviderRegistry)
295
- await providers.seedDefaults();
342
+ await providers.seedDefaults(DEFAULT_TEAM_ID);
296
343
  // Recall only uses embed(). Default embedder is local Ollama; NLM_EMBED_*
297
344
  // can point it at any OpenAI-compatible endpoint. Classifier is wired
298
345
  // separately for Phase D ingest.
@@ -303,11 +350,12 @@ async function buildStack() {
303
350
  store,
304
351
  llm: embedder,
305
352
  factStore: facts,
353
+ learnerDeps: { learners: storage.learners, sessions: store },
306
354
  exemplarStore: storage.exemplars,
307
355
  codeEmbedder: buildCodeEmbedder(),
308
356
  installScope: scope,
309
357
  resolveWorkstreamMembers: async (idOrLabel) => {
310
- const all = await wsStore.listAll();
358
+ const all = await wsStore.listAll(DEFAULT_TEAM_ID);
311
359
  const byId = new Map(all.map((w) => [w.id, { id: w.id, mergedInto: w.mergedInto }]));
312
360
  const target = all.find((w) => w.id === idOrLabel)
313
361
  ?? all.find((w) => normalizeLabel(w.label) === normalizeLabel(idOrLabel));
@@ -360,11 +408,32 @@ program
360
408
  // Generate NLM_MCP_TOKEN if missing so /api/* gets Bearer-protected for
361
409
  // non-browser callers. Idempotent: re-reads persisted token first.
362
410
  autoloadEnv();
363
- ensureMcpToken();
411
+ const mcpToken = ensureMcpToken();
364
412
  const { storage, store, facts, signals, scope, sources, providers, recall, factRecall, embedder, classifier } = await buildStack();
413
+ // Local-mode continuity (program spec §3 M3): idempotently seed the
414
+ // existing NLM_MCP_TOKEN's hash for the default team, so an install that
415
+ // already has a token keeps authenticating through the same
416
+ // resolveTeamByToken path as any future per-team token. No-op after the
417
+ // first boot (ensureActive is insert-if-absent).
418
+ await storage.teamTokens.ensureActive(hashTeamToken(mcpToken), DEFAULT_TEAM_ID);
365
419
  const { existsSync } = await import("node:fs");
366
420
  const hasMcpToken = Boolean(process.env["NLM_MCP_TOKEN"]);
367
421
  const codeEmb = buildCodeEmbedder();
422
+ // Job supervision: owns the daemon's own reprocess child, restarting it
423
+ // on a non-clean exit while work remains and reporting stalls — see
424
+ // core/jobs/job-supervisor.ts for why this exists (a detached
425
+ // `nlm reprocess` died on OOM and sat unobserved).
426
+ // onEvent -> fireAlert is fire-and-forget on purpose: fireAlert already
427
+ // never throws (best-effort delivery, see fire-alert.ts) and JobSupervisor
428
+ // itself catches/logs anything an onEvent callback throws, so this can't
429
+ // crash the daemon either way.
430
+ const jobSupervisor = new JobSupervisor({
431
+ spawnChild: createReprocessSpawnChild({ execPath: process.execPath, nlmScriptPath: __filename }),
432
+ clock: realClock,
433
+ onEvent: (event) => {
434
+ void fireAlert(buildJobAlertEvent(event));
435
+ },
436
+ });
368
437
  const app = createApp({
369
438
  recall,
370
439
  store,
@@ -374,7 +443,9 @@ program
374
443
  dbPath: dbPath(),
375
444
  classifier,
376
445
  sources,
446
+ jobSupervisor,
377
447
  providers,
448
+ teamTokens: storage.teamTokens,
378
449
  ingest: {
379
450
  classifier,
380
451
  embedder,
@@ -383,7 +454,11 @@ program
383
454
  classifierDescriptor: { provider: classifier.provider, model: classifier.model },
384
455
  },
385
456
  signalStore: signals,
457
+ learnerRecall: { learners: storage.learners, sessions: store },
386
458
  installScope: scope,
459
+ // Signal scope inheritance (NLM_SCOPE_STAMP): a signal with only a
460
+ // session_id inherits that session's stamped scope at ingest.
461
+ sessionScopeReader: store,
387
462
  // Code-exemplar lane. The store + code embedder are always wired; the
388
463
  // NLM_CODE_EXEMPLARS_ENABLED flag gates capture (POST /api/signal) and
389
464
  // the /api/exemplar + /api/recall-code routes at request time.
@@ -406,21 +481,33 @@ program
406
481
  exemplarStore: storage.exemplars,
407
482
  codeEmbedder: codeEmb,
408
483
  installScope: scope,
484
+ signalStore: signals,
485
+ sessionScopeReader: store,
409
486
  workDigest: { store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() },
410
487
  workstreams: { store: storage.workstreams, sessions: store, facts: facts, exemplars: storage.exemplars },
488
+ learners: { store: storage.learners, workstreams: storage.workstreams, sessions: store },
411
489
  },
412
490
  }
413
491
  : {}),
414
492
  });
415
493
  warmCodeEmbedder(codeEmb);
416
494
  void reconcileEmbeddingLanes(storage, embedder, codeEmb).catch(() => { });
417
- void embedder
418
- .embed("warmup init", "query")
419
- .then(() => markWarm("textEmbedder"))
420
- .catch(() => { });
495
+ // Retried, not one-shot. A single attempt with a swallowed catch latched
496
+ // this stage cold for the whole uptime whenever the backend was not yet
497
+ // servable, which is the normal case when the embedder host is started by
498
+ // hand after a reboot.
499
+ void retryUntilWarm({
500
+ attempt: () => embedder.embed("warmup init", "query"),
501
+ onSuccess: () => {
502
+ markWarm("textEmbedder");
503
+ setLaneHealth("prose", "ok");
504
+ },
505
+ onFailure: (reason, attempt) => markWarmFailure("textEmbedder", reason, attempt),
506
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms).unref?.()),
507
+ });
421
508
  setImmediate(() => {
422
509
  void recall
423
- .search({ query: "warmup init", mode: "keyword", limit: 1 })
510
+ .search(DEFAULT_TEAM_ID, { query: "warmup init", mode: "keyword", limit: 1 })
424
511
  .then(() => markWarm("fts5"))
425
512
  .catch(() => { });
426
513
  });
@@ -483,17 +570,36 @@ program
483
570
  const SIGNAL_PRUNE_INTERVAL_MS = 6 * 60 * 60_000;
484
571
  const signalPruneTimer = setInterval(() => {
485
572
  const cutoff = new Date(Date.now() - SIGNAL_RETENTION_DAYS * 86_400_000).toISOString();
486
- void signals.pruneOlderThan(cutoff).catch(() => { });
573
+ void signals.pruneOlderThan(DEFAULT_TEAM_ID, cutoff).catch(() => { });
487
574
  }, SIGNAL_PRUNE_INTERVAL_MS);
488
575
  signalPruneTimer.unref();
489
- // Corpus monitor: 24h stats job + weekly re-derivation trend. SQLite only.
490
- // First run is delayed ~60s so warmup finishes first; then repeats every 24h.
576
+ // NLM_ALERT_WEBHOOK self-reporting: version-drift + embedder-cold
577
+ // transitions POST to an operator-owned webhook. No-op (zero network
578
+ // calls) when the env var is unset — see core/alerts/fire-alert.ts.
579
+ const ALERT_DRIFT_CHECK_INTERVAL_MS = 60 * 60_000;
580
+ const alertDriftTimer = setInterval(() => {
581
+ void checkDriftAndAlert({ currentVersion: pkg.version }).catch(() => {
582
+ // Best-effort self-reporting — a failed check never blocks the daemon.
583
+ });
584
+ }, ALERT_DRIFT_CHECK_INTERVAL_MS);
585
+ alertDriftTimer.unref();
586
+ const ALERT_EMBEDDER_CHECK_INTERVAL_MS = 5 * 60_000;
587
+ const alertEmbedderTimer = setInterval(() => {
588
+ void checkEmbedderAndAlert().catch(() => {
589
+ // Best-effort self-reporting — a failed check never blocks the daemon.
590
+ });
591
+ }, ALERT_EMBEDDER_CHECK_INTERVAL_MS);
592
+ alertEmbedderTimer.unref();
593
+ // Corpus monitor: 24h stats job + pairs-cache refresh + weekly trend log.
594
+ // SQLite only. First run is delayed ~60s so warmup finishes first; then
595
+ // repeats every 24h.
491
596
  if (!(storage instanceof PgStorage)) {
492
597
  const CORPUS_MONITOR_INTERVAL_MS = 24 * 60 * 60 * 1000;
493
598
  const CORPUS_MONITOR_INITIAL_DELAY_MS = 60 * 1000;
494
599
  const nlmDataDir = join(homedir(), ".nlm");
495
600
  const corpusStatsPath = join(nlmDataDir, "corpus-stats.jsonl");
496
601
  const rederivTrendPath = join(nlmDataDir, "re-derivation-trend.jsonl");
602
+ const rederivPairsPath = join(nlmDataDir, "re-derivation-pairs.json");
497
603
  const runCorpusMonitor = async () => {
498
604
  try {
499
605
  const sqliteStorage = storage;
@@ -527,9 +633,14 @@ program
527
633
  catch {
528
634
  // file absent or unreadable; treat as no prior entry
529
635
  }
636
+ // Pairs cache is refreshed every 24h cycle regardless of the trend
637
+ // gate below - the trend JSONL is an append-only history and stays
638
+ // on its existing weekly cadence, but the pairs cache is a single
639
+ // overwritten file so refreshing it daily costs nothing extra and
640
+ // keeps sqlite-outcome-store.ts's reads within a ~24h staleness
641
+ // bound (see #405).
642
+ const report = await persistReDerivationPairs(rawDb, DEFAULT_TEAM_ID, rederivPairsPath, 42);
530
643
  if (shouldAppendTrend(lastTrendTs, now)) {
531
- const rdDeps = sqliteReDerivationDeps(rawDb);
532
- const report = await computeReDerivationRate(rdDeps, 42);
533
644
  appendFileSync(rederivTrendPath, JSON.stringify({ ts, windowDays: 42, rate: report.rate, pairCount: report.pairs.length, eligible: report.eligible }) + "\n", "utf8");
534
645
  }
535
646
  }
@@ -540,6 +651,78 @@ program
540
651
  const corpusMonitorTimer = setInterval(() => { void runCorpusMonitor(); }, CORPUS_MONITOR_INTERVAL_MS);
541
652
  corpusMonitorTimer.unref();
542
653
  setTimeout(() => { void runCorpusMonitor(); }, CORPUS_MONITOR_INITIAL_DELAY_MS).unref();
654
+ // Wiki projection: regenerate the generated markdown tree. SQLite only,
655
+ // same as the corpus monitor. Idempotent, so a run with no corpus change
656
+ // writes zero bytes. Failure leaves the previous tree untouched and
657
+ // self-reports on the operator's webhook if one is configured. Opt-in:
658
+ // nlm-memory is a published public package, so the projection only
659
+ // registers (timer, directory, log line) when an operator explicitly
660
+ // sets NLM_WIKI_DIR — an upgrade must never start writing ~500 markdown
661
+ // files under a stranger's home directory on its own.
662
+ const wikiDir = process.env["NLM_WIKI_DIR"];
663
+ if (wikiDir) {
664
+ const parsedWikiHours = Number.parseInt(process.env["NLM_WIKI_INTERVAL_HOURS"] ?? "6", 10);
665
+ const WIKI_INTERVAL_MS = (Number.isFinite(parsedWikiHours) && parsedWikiHours > 0 ? parsedWikiHours : 6) * 60 * 60_000;
666
+ const WIKI_INITIAL_DELAY_MS = 90 * 1000;
667
+ const wikiConfig = {
668
+ minFacts: positiveIntEnv("NLM_WIKI_MIN_FACTS", 3),
669
+ minSessions: positiveIntEnv("NLM_WIKI_MIN_SESSIONS", 3),
670
+ linkBase: process.env["NLM_WIKI_LINK_BASE"] ?? `http://127.0.0.1:${p}`,
671
+ };
672
+ const wikiTarget = join(wikiDir, "Memory");
673
+ const runWikiProjection = async () => {
674
+ try {
675
+ const writer = new FsWikiWriter(wikiTarget);
676
+ const today = new Date().toISOString().slice(0, 10);
677
+ const result = await projectWiki({ facts, writer }, DEFAULT_TEAM_ID, wikiConfig, today);
678
+ console.error(`[wiki] ${result.written} written, ${result.unchanged} unchanged, ${result.removed} removed`);
679
+ const drift = buildWikiDriftEvent(result);
680
+ if (drift)
681
+ await fireAlert(drift);
682
+ }
683
+ catch (e) {
684
+ console.error(`[wiki] error: ${e instanceof Error ? e.message : String(e)}`);
685
+ await fireAlert(buildWikiFailureEvent(e)).catch(() => {
686
+ // Self-reporting is best-effort; a webhook outage never escalates.
687
+ });
688
+ }
689
+ };
690
+ console.error(` wiki: enabled, projecting to ${wikiTarget}`);
691
+ const wikiTimer = setInterval(() => { void runWikiProjection(); }, WIKI_INTERVAL_MS);
692
+ wikiTimer.unref();
693
+ setTimeout(() => { void runWikiProjection(); }, WIKI_INITIAL_DELAY_MS).unref();
694
+ }
695
+ }
696
+ // Exemplar embed-backfill: repairs code_exemplars rows whose vector was
697
+ // dropped by a cold CodeRankEmbed on first capture (see
698
+ // core/exemplars/embed-backfill.ts). SQLite only, same constraint as the
699
+ // corpus monitor above — discovery reads code_exemplars_vec directly via
700
+ // dbPath(). Daily cadence: the drop is a rare cold-embedder race, not a
701
+ // steady leak, so this just keeps recall_code from silently degrading
702
+ // between operator-run `nlm embed-backfill --exemplars` passes.
703
+ if (!(storage instanceof PgStorage) && process.env["NLM_CODE_EXEMPLARS_ENABLED"] === "1") {
704
+ const EXEMPLAR_BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1000;
705
+ const EXEMPLAR_BACKFILL_INITIAL_DELAY_MS = 120 * 1000;
706
+ const runExemplarBackfill = async () => {
707
+ try {
708
+ const report = await backfillExemplarEmbeddings({
709
+ tenantId: DEFAULT_TEAM_ID,
710
+ dbPath: dbPath(),
711
+ embedder: buildCodeEmbedder(),
712
+ store: storage.exemplars,
713
+ });
714
+ if (report.succeeded > 0 || report.failed > 0) {
715
+ console.error(`[exemplar-backfill] ${report.succeeded}/${report.total} repaired (${report.failed} failed)`);
716
+ }
717
+ }
718
+ catch (e) {
719
+ console.error(`[exemplar-backfill] error: ${e instanceof Error ? e.message : String(e)}`);
720
+ }
721
+ };
722
+ const exemplarBackfillTimer = setInterval(() => { void runExemplarBackfill(); }, EXEMPLAR_BACKFILL_INTERVAL_MS);
723
+ exemplarBackfillTimer.unref();
724
+ setTimeout(() => { void runExemplarBackfill(); }, EXEMPLAR_BACKFILL_INITIAL_DELAY_MS).unref();
725
+ console.error(" exemplar-backfill: enabled, repair sweep every 24h");
543
726
  }
544
727
  // Memo sweep runs independently of the transcript scheduler — it's the
545
728
  // backstop for SessionEnd hook unreliability (crashes, kill -9, IDE
@@ -549,7 +732,7 @@ program
549
732
  memoSweep.start();
550
733
  console.error(" memo sweep: dormant cleanup every 5m (threshold 24h)");
551
734
  if (opts.scheduler !== false) {
552
- const adapters = await buildAdapters(sources);
735
+ const adapters = await buildAdapters(sources, DEFAULT_TEAM_ID);
553
736
  if (adapters.length === 0) {
554
737
  console.error(" scheduler: no adapters detected (set NLM_ADAPTERS to force-enable)");
555
738
  }
@@ -575,6 +758,10 @@ program
575
758
  clearInterval(checkpointTimer);
576
759
  scheduler.stop();
577
760
  memoSweep.stop();
761
+ // Reaps an active reprocess child instead of leaving it orphaned
762
+ // when the daemon exits. stop() is idempotent and a no-op when no
763
+ // run is active.
764
+ jobSupervisor.stop();
578
765
  await storage.close();
579
766
  process.exit(0);
580
767
  };
@@ -610,7 +797,7 @@ program
610
797
  .action(async (query, opts) => {
611
798
  const { storage, recall } = await buildStack();
612
799
  try {
613
- const result = await recall.search({
800
+ const result = await recall.search(DEFAULT_TEAM_ID, {
614
801
  query,
615
802
  mode: opts.mode,
616
803
  limit: opts.limit,
@@ -644,7 +831,7 @@ program
644
831
  .action(async (idOrLabel) => {
645
832
  const { storage, store } = await buildStack();
646
833
  try {
647
- const r = await recallWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel });
834
+ const r = await recallWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, DEFAULT_TEAM_ID, { idOrLabel });
648
835
  process.stdout.write(r.content[0].text + "\n");
649
836
  }
650
837
  finally {
@@ -659,7 +846,7 @@ program
659
846
  .action(async (sessionId, workstream) => {
660
847
  const { storage, store } = await buildStack();
661
848
  try {
662
- const r = await rebindSessionHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { sessionId, workstream });
849
+ const r = await rebindSessionHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, DEFAULT_TEAM_ID, { sessionId, workstream });
663
850
  process.stdout.write(r.content[0].text + "\n");
664
851
  }
665
852
  finally {
@@ -674,7 +861,7 @@ program
674
861
  .action(async (from, into) => {
675
862
  const { storage, store } = await buildStack();
676
863
  try {
677
- const r = await mergeWorkstreamsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { from, into });
864
+ const r = await mergeWorkstreamsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, DEFAULT_TEAM_ID, { from, into });
678
865
  process.stdout.write(r.content[0].text + "\n");
679
866
  }
680
867
  finally {
@@ -689,7 +876,7 @@ program
689
876
  .action(async (idOrLabel, label) => {
690
877
  const { storage, store } = await buildStack();
691
878
  try {
692
- const r = await renameWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel, label });
879
+ const r = await renameWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, DEFAULT_TEAM_ID, { idOrLabel, label });
693
880
  process.stdout.write(r.content[0].text + "\n");
694
881
  }
695
882
  finally {
@@ -703,7 +890,7 @@ program
703
890
  .action(async (idOrLabel) => {
704
891
  const { storage, store } = await buildStack();
705
892
  try {
706
- const r = await retireWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { idOrLabel });
893
+ const r = await retireWorkstreamHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, DEFAULT_TEAM_ID, { idOrLabel });
707
894
  process.stdout.write(r.content[0].text + "\n");
708
895
  }
709
896
  finally {
@@ -723,7 +910,7 @@ program
723
910
  }
724
911
  const { storage, store } = await buildStack();
725
912
  try {
726
- const r = await listMergeSuggestionsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, { minScore });
913
+ const r = await listMergeSuggestionsHandler({ recall: {}, store, workstreams: { store: storage.workstreams, sessions: store, facts: storage.facts, exemplars: storage.exemplars } }, DEFAULT_TEAM_ID, { minScore });
727
914
  process.stdout.write(r.content[0].text + "\n");
728
915
  }
729
916
  finally {
@@ -768,7 +955,7 @@ program
768
955
  const { runEval } = await import("../core/eval/run-eval.js");
769
956
  const { storage, recall } = await buildStack();
770
957
  try {
771
- const report = await runEval({ recall }, queries, { mode: opts.mode, k: 5 });
958
+ const report = await runEval({ recall: { search: (q) => recall.search(DEFAULT_TEAM_ID, q) } }, queries, { mode: opts.mode, k: 5 });
772
959
  if (opts.json) {
773
960
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
774
961
  return;
@@ -799,7 +986,7 @@ program
799
986
  const { installScope: getScope } = await import("../core/signals/install-scope.js");
800
987
  try {
801
988
  const embedder = buildCodeEmbedder();
802
- const result = await recallCode({
989
+ const result = await recallCode(DEFAULT_TEAM_ID, {
803
990
  query,
804
991
  installScope: getScope(),
805
992
  ...(opts.repo ? { repo: opts.repo } : {}),
@@ -880,7 +1067,7 @@ program
880
1067
  .option("--json", "emit JSON instead of a human-readable table")
881
1068
  .action(async (opts) => {
882
1069
  const { missStats } = await import("../core/recall/miss-log.js");
883
- const stats = await missStats(opts.days);
1070
+ const stats = await missStats(DEFAULT_TEAM_ID, opts.days);
884
1071
  if (opts.json) {
885
1072
  process.stdout.write(`${JSON.stringify(stats, null, 2)}\n`);
886
1073
  return;
@@ -915,9 +1102,9 @@ program
915
1102
  const { readQueryLog } = await import("../core/recall/query-log.js");
916
1103
  const { readCitationLog } = await import("../core/recall/citation-log.js");
917
1104
  const [recallEntries, queryEntries, citationEntries] = await Promise.all([
918
- readHookRecallLog(opts.days),
919
- readQueryLog(opts.days),
920
- readCitationLog(opts.days),
1105
+ readHookRecallLog(DEFAULT_TEAM_ID, opts.days),
1106
+ readQueryLog(DEFAULT_TEAM_ID, opts.days),
1107
+ readCitationLog(DEFAULT_TEAM_ID, opts.days),
921
1108
  ]);
922
1109
  const result = computePrecision(recallEntries, citationEntries);
923
1110
  const { perSource, unmeasurable } = computePerSourcePrecision(queryEntries, citationEntries);
@@ -975,7 +1162,7 @@ program
975
1162
  process.exitCode = 1;
976
1163
  return;
977
1164
  }
978
- const deps = sqliteReDerivationDeps(store.rawDb());
1165
+ const deps = sqliteReDerivationDeps(store.rawDb(), DEFAULT_TEAM_ID);
979
1166
  const report = await computeReDerivationRate(deps, opts.window);
980
1167
  if (opts.json) {
981
1168
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
@@ -1007,6 +1194,46 @@ program
1007
1194
  yes: Boolean(opts.yes),
1008
1195
  });
1009
1196
  });
1197
+ program
1198
+ .command("reconcile-facts")
1199
+ .description("Find live facts that contradict an authorities file (dry run unless --apply)")
1200
+ .requiredOption("--authorities <file>", "JSON file of authoritative values and deleted files")
1201
+ .option("--apply", "retire the findings (backs up the DB first)")
1202
+ .option("-l, --limit <n>", "max facts changed per run", (v) => Number.parseInt(v, 10), 100)
1203
+ .option("--json", "emit the report as JSON")
1204
+ .action(async (opts) => {
1205
+ if (process.env["NLM_PG_URL"]) {
1206
+ console.error("nlm reconcile-facts: the Postgres backend is not supported yet");
1207
+ process.exit(1);
1208
+ }
1209
+ const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
1210
+ try {
1211
+ const db = storage.rawDb();
1212
+ const findings = findContradictions(db, loadAuthorities(opts.authorities));
1213
+ const limit = opts.limit;
1214
+ const applied = opts.apply ? await applyRetirements(db, dbPath(), findings, limit) : null;
1215
+ if (opts.json) {
1216
+ process.stdout.write(JSON.stringify({
1217
+ applied: Boolean(opts.apply),
1218
+ total: findings.length,
1219
+ findings,
1220
+ retired: applied?.retired.map((f) => f.factId) ?? [],
1221
+ backupPath: applied?.backupPath ?? null,
1222
+ }, null, 2) + "\n");
1223
+ return;
1224
+ }
1225
+ console.log(formatReport(findings, limit));
1226
+ if (applied) {
1227
+ console.log(`retired ${applied.retired.length} fact(s); backup: ${applied.backupPath ?? "none"}`);
1228
+ }
1229
+ else {
1230
+ console.log("dry run: nothing written (pass --apply to retire)");
1231
+ }
1232
+ }
1233
+ finally {
1234
+ await storage.close();
1235
+ }
1236
+ });
1010
1237
  program
1011
1238
  .command("classify-parity")
1012
1239
  .description("Run TS classifier against ~/.nlm/canonical.sqlite and diff vs persisted Python output")
@@ -1033,13 +1260,39 @@ program
1033
1260
  .option("-l, --limit <n>", "session cap (default: all)", (v) => Number.parseInt(v, 10))
1034
1261
  .option("--state <path>", "resume state file (default ~/.nlm/embed_reembed.state)")
1035
1262
  .option("--exemplars", "instead: embed code_exemplars rows missing a vector (repairs dropped capture embeds)")
1263
+ .option("--facts", "instead: embed active facts missing a vector (repairs dropped ingest embeds)")
1036
1264
  .option("--dry-run", "report dim mismatch and what would be dropped without writing")
1037
1265
  .option("-v, --verbose", "per-session progress on stderr")
1038
1266
  .action(async (opts) => {
1267
+ if (opts.facts) {
1268
+ const { storage } = await buildStack();
1269
+ try {
1270
+ const report = await backfillFactEmbeddings({
1271
+ tenantId: DEFAULT_TEAM_ID,
1272
+ dbPath: dbPath(),
1273
+ embedder: buildEmbedder(),
1274
+ store: storage.facts,
1275
+ ...(opts.limit ? { limit: opts.limit } : {}),
1276
+ ...(opts.verbose
1277
+ ? {
1278
+ onProgress: (i, n, id, status) => {
1279
+ process.stderr.write(` [${i}/${n}] ${id} ${status}\n`);
1280
+ },
1281
+ }
1282
+ : {}),
1283
+ });
1284
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
1285
+ }
1286
+ finally {
1287
+ await storage.close();
1288
+ }
1289
+ return;
1290
+ }
1039
1291
  if (opts.exemplars) {
1040
1292
  const { storage } = await buildStack();
1041
1293
  try {
1042
1294
  const report = await backfillExemplarEmbeddings({
1295
+ tenantId: DEFAULT_TEAM_ID,
1043
1296
  dbPath: dbPath(),
1044
1297
  embedder: buildCodeEmbedder(),
1045
1298
  store: storage.exemplars,
@@ -1111,7 +1364,7 @@ program
1111
1364
  },
1112
1365
  }
1113
1366
  : {}),
1114
- });
1367
+ }, DEFAULT_TEAM_ID);
1115
1368
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
1116
1369
  }
1117
1370
  finally {
@@ -1132,7 +1385,7 @@ program
1132
1385
  await stack.storage.close();
1133
1386
  process.exit(1);
1134
1387
  }
1135
- const adapters = await buildAdapters(stack.sources);
1388
+ const adapters = await buildAdapters(stack.sources, DEFAULT_TEAM_ID);
1136
1389
  const r = await reclassifyOversized({
1137
1390
  db: stack.storage.rawDb(),
1138
1391
  store: stack.storage.sessions,
@@ -1141,7 +1394,7 @@ program
1141
1394
  classifier: stack.classifier,
1142
1395
  classifierDescriptor: { provider: stack.classifier.provider, model: stack.classifier.model },
1143
1396
  adapters,
1144
- }, { ...(opts.limit ? { limit: opts.limit } : {}), dryRun: Boolean(opts.dryRun) });
1397
+ }, DEFAULT_TEAM_ID, { ...(opts.limit ? { limit: opts.limit } : {}), dryRun: Boolean(opts.dryRun) });
1145
1398
  console.log(`reclassify-oversized: attempted=${r.attempted} ingested=${r.ingested} ` +
1146
1399
  `lowConfidence=${r.skippedLowConfidence} missingFile=${r.missingFile} failed=${r.failed} ` +
1147
1400
  `| gained entities=${r.entities} decisions=${r.decisions} facts=${r.facts}`);
@@ -1163,7 +1416,59 @@ program
1163
1416
  .option("--dry-run", "print cohort report without writing")
1164
1417
  .option("-v, --verbose", "per-session progress on stderr")
1165
1418
  .option("--force-embed", "proceed even when the stored prose embedding lane mismatches the runtime embedder")
1419
+ .option("--daemon", "submit to the running daemon's job supervisor instead of running in this process (restarts on OOM/crash, reports stalls)")
1166
1420
  .action(async (opts) => {
1421
+ if (opts.daemon) {
1422
+ // Same daemon-call auth pattern as `nlm ui`'s bootstrap-nonce POST:
1423
+ // autoload ~/.nlm/.env so a fresh shell still has NLM_MCP_TOKEN, then
1424
+ // send it as Authorization: Bearer (absent token = local-mode
1425
+ // fallback on the daemon side, never a hard requirement).
1426
+ autoloadEnv();
1427
+ const p = port();
1428
+ const token = process.env["NLM_MCP_TOKEN"];
1429
+ const args = [];
1430
+ if (opts.limit !== undefined)
1431
+ args.push("--limit", String(opts.limit));
1432
+ if (opts.minConfidence !== undefined)
1433
+ args.push("--min-confidence", String(opts.minConfidence));
1434
+ if (opts.onlyNull)
1435
+ args.push("--only-null");
1436
+ for (const tag of opts.excludeModel ?? [])
1437
+ args.push("--exclude-model", tag);
1438
+ if (opts.state)
1439
+ args.push("--state", opts.state);
1440
+ if (opts.dryRun)
1441
+ args.push("--dry-run");
1442
+ if (opts.forceEmbed)
1443
+ args.push("--force-embed");
1444
+ // -v is always added by the daemon's spawn adapter (job-supervisor.ts
1445
+ // parses reprocess's [i/n] progress lines from it) — a local
1446
+ // --verbose here would only affect this short-lived CLI invocation's
1447
+ // own stdout, which prints nothing itself, so it's not forwarded.
1448
+ try {
1449
+ const res = await fetch(`http://localhost:${p}/api/jobs/reprocess`, {
1450
+ method: "POST",
1451
+ headers: {
1452
+ "content-type": "application/json",
1453
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
1454
+ },
1455
+ body: JSON.stringify({ args }),
1456
+ });
1457
+ const body = await res.json().catch(() => ({}));
1458
+ if (res.status === 202) {
1459
+ console.log(`reprocess --daemon: submitted (${JSON.stringify(body)})`);
1460
+ process.exit(0);
1461
+ }
1462
+ console.error(`reprocess --daemon: daemon returned ${res.status}: ${JSON.stringify(body)}`);
1463
+ process.exit(1);
1464
+ }
1465
+ catch (e) {
1466
+ console.error(`reprocess --daemon: could not reach the daemon at localhost:${p}. Is it running?`);
1467
+ console.error(` ${e instanceof Error ? e.message : String(e)}`);
1468
+ process.exit(1);
1469
+ }
1470
+ return;
1471
+ }
1167
1472
  const { reprocess: runReprocess } = await import("../core/ingest/reprocess.js");
1168
1473
  const stack = await buildStack();
1169
1474
  try {
@@ -1182,7 +1487,7 @@ program
1182
1487
  classifierDescriptor: { provider: stack.classifier.provider, model: stack.classifier.model },
1183
1488
  embeddingConfig: stack.storage.embeddingConfig,
1184
1489
  embedderDescriptor: { provider: embedderInfo.provider, model: embedderInfo.model },
1185
- }, {
1490
+ }, DEFAULT_TEAM_ID, {
1186
1491
  ...(opts.limit ? { limit: opts.limit } : {}),
1187
1492
  ...(opts.minConfidence !== undefined ? { minConfidence: opts.minConfidence } : {}),
1188
1493
  ...(opts.onlyNull ? { onlyNull: true } : {}),
@@ -1205,11 +1510,39 @@ program
1205
1510
  await stack.storage.close();
1206
1511
  }
1207
1512
  });
1513
+ program
1514
+ .command("backfill-derivables")
1515
+ .description("Retroactive: stamp agent_persona + parent_session_id (#352 phase 2) onto sessions ingested\n" +
1516
+ "before the scheduler started stamping them at classify time. Idempotent (WHERE agent_persona IS NULL).")
1517
+ .option("--dry-run", "report counts without writing")
1518
+ .option("--with-transcript-scan", "also scan each candidate's transcript for primary_model/total_tokens/skill (slower)")
1519
+ .action(async (opts) => {
1520
+ const { backfillDerivables } = await import("../core/ingest/backfill-derivables.js");
1521
+ const stack = await buildStack();
1522
+ try {
1523
+ if (!(stack.storage instanceof SqliteStorage)) {
1524
+ console.error("backfill-derivables: only supported with SQLite storage (NLM_PG_URL must not be set)");
1525
+ await stack.storage.close();
1526
+ process.exit(1);
1527
+ }
1528
+ const report = await backfillDerivables(stack.storage.rawDb(), {
1529
+ dryRun: Boolean(opts.dryRun),
1530
+ withTranscriptScan: Boolean(opts.withTranscriptScan),
1531
+ });
1532
+ console.log(`backfill-derivables${opts.dryRun ? " (dry-run)" : ""}: ` +
1533
+ `updated=${report.updated} skipped(already-stamped)=${report.skippedAlreadyStamped} ` +
1534
+ `skipped(no-op)=${report.skippedNoop} subagent-candidates=${report.subagentCandidates} ` +
1535
+ `unknown-parent=${report.unknownParent} transcript-scanned=${report.transcriptScanned} total=${report.total}`);
1536
+ }
1537
+ finally {
1538
+ await stack.storage.close();
1539
+ }
1540
+ });
1208
1541
  program
1209
1542
  .command("mcp")
1210
1543
  .description("Run as an MCP stdio server (for ~/.mcp.json)")
1211
1544
  .action(async () => {
1212
- const { recall, store, facts, factRecall, storage, scope } = await buildStack();
1545
+ const { recall, store, facts, factRecall, storage, scope, signals } = await buildStack();
1213
1546
  const server = createMcpServer({
1214
1547
  recall,
1215
1548
  store,
@@ -1218,9 +1551,18 @@ program
1218
1551
  exemplarStore: storage.exemplars,
1219
1552
  codeEmbedder: buildCodeEmbedder(),
1220
1553
  installScope: scope,
1554
+ signalStore: signals,
1555
+ sessionScopeReader: store,
1221
1556
  workDigest: { store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() },
1222
1557
  workstreams: { store: storage.workstreams, sessions: store, facts: facts, exemplars: storage.exemplars },
1223
- });
1558
+ learners: { store: storage.learners, workstreams: storage.workstreams, sessions: store },
1559
+ // Tier-B outcome rollup (#352 phase 2) only has a SQLite adapter today;
1560
+ // PgStorage deployments skip get_session's `outcome` field until parity lands.
1561
+ ...(storage instanceof SqliteStorage ? { outcomeDb: storage.rawDb() } : {}),
1562
+ },
1563
+ // stdio MCP transport carries no bearer token — it is local-mode only
1564
+ // by construction (program spec §3), bound to the default team.
1565
+ DEFAULT_TEAM_ID);
1224
1566
  const transport = new StdioServerTransport();
1225
1567
  await server.connect(transport);
1226
1568
  });
@@ -1607,14 +1949,12 @@ const SESSION_START_HOOK_JS = resolve(__dirname, "../hook/session-start-hook.js"
1607
1949
  const SESSION_END_HOOK_JS = resolve(__dirname, "../hook/session-end-hook.js");
1608
1950
  const STOP_HOOK_JS = resolve(__dirname, "../hook/stop-hook.js");
1609
1951
  const PRE_COMPACT_HOOK_JS = resolve(__dirname, "../hook/pre-compact-hook.js");
1610
- const SUBAGENT_START_HOOK_JS = resolve(__dirname, "../hook/subagent-start-hook.js");
1611
1952
  const ALL_HOOKS = [
1612
1953
  { event: "UserPromptSubmit", script: HOOK_JS, label: "recall" },
1613
1954
  { event: "SessionStart", script: SESSION_START_HOOK_JS, label: "session-start" },
1614
1955
  { event: "SessionEnd", script: SESSION_END_HOOK_JS, label: "session-end" },
1615
1956
  { event: "Stop", script: STOP_HOOK_JS, label: "stop" },
1616
1957
  { event: "PreCompact", script: PRE_COMPACT_HOOK_JS, label: "pre-compact" },
1617
- { event: "SubagentStart", script: SUBAGENT_START_HOOK_JS, label: "subagent-start" },
1618
1958
  ];
1619
1959
  function claudeSettingsPath() {
1620
1960
  return process.env["NLM_CLAUDE_SETTINGS"] ?? join(homedir(), ".claude", "settings.json");
@@ -1624,7 +1964,7 @@ const hook = program
1624
1964
  .description("Manage the Claude Code NLM hooks");
1625
1965
  hook
1626
1966
  .command("install")
1627
- .description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (live mode)")
1967
+ .description("Add the NLM hooks (recall + session-start + session-end + stop + pre-compact) to ~/.claude/settings.json (live mode)")
1628
1968
  .action(() => {
1629
1969
  const path = claudeSettingsPath();
1630
1970
  const installed = [];
@@ -1650,6 +1990,9 @@ hook
1650
1990
  console.error(" Session-end hook cleans up ~/.nlm/hook-state/<session>.json on session close.");
1651
1991
  console.error(" To run silently for calibration (no injection): set NLM_HOOK_MODE=shadow in the command.");
1652
1992
  console.error(" To remove: nlm hook uninstall");
1993
+ const nvmWarning = nvmPinnedHookWarning(process.execPath, homedir());
1994
+ if (nvmWarning)
1995
+ console.error(`nlm: ${nvmWarning}`);
1653
1996
  });
1654
1997
  hook
1655
1998
  .command("uninstall")
@@ -1674,13 +2017,19 @@ connect
1674
2017
  .option("--with-hooks", "additionally write absolute paths to ~/.codex/hooks.json (Codex Desktop fallback for openai/codex#16430)")
1675
2018
  .option("--repair", "first strip a stale pre-rename nlm-memory-ts install (config block + plugin + marketplace), then connect")
1676
2019
  .option("--dry-run", "print what would happen without invoking codex")
2020
+ .option("--stdio", "use the stdio transport instead of the shared HTTP endpoint (works with the daemon stopped)")
1677
2021
  .action((opts) => {
1678
2022
  if (!opts.dryRun && !codexBinaryAvailable()) {
1679
2023
  console.error("nlm connect codex: `codex` binary not on PATH. Install via `npm i -g @openai/codex` or `brew install codex`.");
1680
2024
  process.exit(1);
1681
2025
  }
1682
2026
  const source = opts.local ? REPO_ROOT : opts.source;
1683
- const connectOpts = { source, withHooks: Boolean(opts.withHooks), dryRun: Boolean(opts.dryRun) };
2027
+ const connectOpts = {
2028
+ source,
2029
+ withHooks: Boolean(opts.withHooks),
2030
+ dryRun: Boolean(opts.dryRun),
2031
+ transport: opts.stdio ? "stdio" : "http",
2032
+ };
1684
2033
  const repair = opts.repair
1685
2034
  ? repairCodex(connectOpts, pluginScriptsDir(REPO_ROOT))
1686
2035
  : null;
@@ -1733,17 +2082,25 @@ connect
1733
2082
  .description("Write the nlm-memory MCP server block into ~/.mcp.json")
1734
2083
  .option("--with-hooks", "also install Claude Code session hooks")
1735
2084
  .option("--dry-run", "print what would happen without changing files")
2085
+ .option("--stdio", "use the stdio transport instead of the shared HTTP endpoint (works with the daemon stopped)")
1736
2086
  .action((opts) => {
2087
+ // A fresh shell has no NLM_MCP_TOKEN exported even though ~/.nlm/.env
2088
+ // holds one; the HTTP block needs it. Mirror `nlm ui`'s lookup.
2089
+ autoloadEnv();
2090
+ const transport = opts.stdio ? "stdio" : "http";
1737
2091
  if (opts.dryRun) {
1738
2092
  console.error("nlm connect claude-code (dry run):");
1739
- console.error(` write [mcpServers.nlm-memory] to ${mcpConfigPath()}`);
2093
+ console.error(` write [mcpServers.nlm-memory] (${transport}) to ${mcpConfigPath()}`);
1740
2094
  if (opts.withHooks)
1741
2095
  console.error(" install 6 Claude Code hooks");
1742
2096
  return;
1743
2097
  }
1744
- const report = connectClaudeCode({ nlmBinPath: __filename, nodeExecPath: process.execPath });
2098
+ const report = connectClaudeCode({ nlmBinPath: __filename, nodeExecPath: process.execPath, transport });
1745
2099
  const action = report.alreadyPresent ? "updated" : "written";
1746
- console.error(`nlm: [mcpServers.nlm-memory] ${action} → ${report.mcpConfigPath}`);
2100
+ console.error(`nlm: [mcpServers.nlm-memory] ${action} (${report.transport}) → ${report.mcpConfigPath}`);
2101
+ if (report.migratedFromStdio) {
2102
+ console.error(" migrated from stdio → shared HTTP endpoint (one daemon-backed server for every session).");
2103
+ }
1747
2104
  console.error(" Restart Claude Code to activate the MCP server.");
1748
2105
  if (opts.withHooks) {
1749
2106
  const path = claudeSettingsPath();
@@ -1760,20 +2117,31 @@ connect
1760
2117
  process.exit(1);
1761
2118
  }
1762
2119
  console.error(`nlm: ${result.count} hooks installed → ${path}`);
2120
+ const nvmWarning = nvmPinnedHookWarning(process.execPath, homedir());
2121
+ if (nvmWarning)
2122
+ console.error(`nlm: ${nvmWarning}`);
1763
2123
  }
1764
2124
  });
1765
2125
  connect
1766
2126
  .command("hermes")
1767
2127
  .description("Write the nlm-memory MCP server entry into ~/.hermes/config.yaml")
1768
2128
  .option("--dry-run", "print what would happen without changing files")
2129
+ .option("--stdio", "use the stdio transport instead of the shared HTTP endpoint (works with the daemon stopped)")
1769
2130
  .action((opts) => {
2131
+ // A fresh shell has no NLM_MCP_TOKEN exported even though ~/.nlm/.env
2132
+ // holds one; the HTTP block needs it. Mirror `nlm ui`'s lookup.
2133
+ autoloadEnv();
2134
+ const transport = opts.stdio ? "stdio" : "http";
1770
2135
  if (opts.dryRun) {
1771
- console.error(`nlm connect hermes (dry run): write [mcp_servers.nlm-memory] to ${hermesConfigPath()}`);
2136
+ console.error(`nlm connect hermes (dry run): write [mcp_servers.nlm-memory] (${transport}) to ${hermesConfigPath()}`);
1772
2137
  return;
1773
2138
  }
1774
- const report = connectHermes({ nlmBinPath: __filename, nodeExecPath: process.execPath, dryRun: false });
2139
+ const report = connectHermes({ nlmBinPath: __filename, nodeExecPath: process.execPath, dryRun: false, transport });
1775
2140
  const action = report.alreadyPresent ? "updated" : "written";
1776
- console.error(`nlm: [mcp_servers.nlm-memory] ${action} → ${report.configPath}`);
2141
+ console.error(`nlm: [mcp_servers.nlm-memory] ${action} (${report.transport}) → ${report.configPath}`);
2142
+ if (report.migratedFromStdio) {
2143
+ console.error(" migrated from stdio → shared HTTP endpoint (one daemon-backed server for every session).");
2144
+ }
1777
2145
  console.error(" Restart Hermes to activate the MCP server.");
1778
2146
  });
1779
2147
  connect
@@ -1808,7 +2176,7 @@ connect
1808
2176
  const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
1809
2177
  await storage.init();
1810
2178
  try {
1811
- const report = await connectCursor(storage.sources, {
2179
+ const report = await connectCursor(storage.sources, DEFAULT_TEAM_ID, {
1812
2180
  ...(opts.dbPath ? { dbPath: opts.dbPath } : {}),
1813
2181
  dryRun: Boolean(opts.dryRun),
1814
2182
  });
@@ -1840,7 +2208,7 @@ connect
1840
2208
  const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
1841
2209
  await storage.init();
1842
2210
  try {
1843
- const report = await connectWindsurf(storage.sources, {
2211
+ const report = await connectWindsurf(storage.sources, DEFAULT_TEAM_ID, {
1844
2212
  ...(opts.userDir ? { userDir: opts.userDir } : {}),
1845
2213
  dryRun: Boolean(opts.dryRun),
1846
2214
  });
@@ -1864,30 +2232,66 @@ connect
1864
2232
  connect
1865
2233
  .command("opencode")
1866
2234
  .description("Register OpenCode as an nlm source (reads opencode.db directly) and optionally install rules nudge")
2235
+ .option("--db-path <path>", "override path to opencode.db")
1867
2236
  .option("--with-rules", "also install global rules nudge at ~/.config/opencode/AGENTS.md")
1868
2237
  .option("--dry-run", "print what would happen without changing files")
1869
- .action((opts) => {
2238
+ .action(async (opts) => {
2239
+ const spec = {
2240
+ kind: "opencode",
2241
+ name: "OpenCode",
2242
+ runtimeLabel: "opencode/1.0",
2243
+ path: opts.dbPath ?? openCodeDefaultDbPath(),
2244
+ };
2245
+ await connectLocalSourceCommand(spec, Boolean(opts.dryRun));
1870
2246
  if (opts.dryRun) {
1871
- console.error("nlm connect opencode (dry run):");
1872
- console.error(" OpenCode adapter is already wired via migrations/010_sources_opencode.sql — no source-registry mutation required");
1873
2247
  if (opts.withRules)
1874
- console.error(" install global rules nudge at ~/.config/opencode/AGENTS.md");
2248
+ console.error(" also install global rules nudge at ~/.config/opencode/AGENTS.md");
1875
2249
  return;
1876
2250
  }
1877
- console.error("nlm: OpenCode source already registered (see migration 010). No source-registry changes needed.");
1878
2251
  if (opts.withRules) {
1879
2252
  const rules = installOpencodeRules();
1880
2253
  console.error(` ${describeUpsert("OpenCode", rules)}`);
1881
2254
  }
1882
- else {
1883
- console.error(" Pass --with-rules to install the recall nudge at ~/.config/opencode/AGENTS.md");
1884
- }
1885
2255
  });
2256
+ connect
2257
+ .command("muse")
2258
+ .description("Register Muse as an nlm source (reads its session store directly — no files installed)")
2259
+ .option("--sessions-path <path>", "override path to the Muse sessions directory")
2260
+ .option("--dry-run", "print what would happen without changing files")
2261
+ .action(async (opts) => {
2262
+ await connectLocalSourceCommand({
2263
+ kind: "muse",
2264
+ name: "Muse",
2265
+ runtimeLabel: "muse/1.0",
2266
+ path: opts.sessionsPath ?? museDefaultSessionsPath(),
2267
+ }, Boolean(opts.dryRun));
2268
+ });
2269
+ async function connectLocalSourceCommand(spec, dryRun) {
2270
+ const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
2271
+ await storage.init();
2272
+ try {
2273
+ const report = await connectLocalSource(storage.sources, DEFAULT_TEAM_ID, spec, { dryRun });
2274
+ const missing = report.exists ? "" : " (not found yet)";
2275
+ if (dryRun) {
2276
+ console.error(`nlm connect ${spec.kind} (dry run): register source at ${report.path}${missing}`);
2277
+ return;
2278
+ }
2279
+ console.error(`nlm: ${spec.name} source ${report.action} → ${report.path}${missing}`);
2280
+ console.error(" restart the daemon to start scanning it");
2281
+ }
2282
+ finally {
2283
+ await storage.close();
2284
+ }
2285
+ }
1886
2286
  connect
1887
2287
  .command("pi")
1888
- .description("Register the nlm-memory prompt-recall extension in ~/.pi/agent/settings.json")
2288
+ .description("Register the nlm-memory prompt-recall extension + MCP server for pi")
1889
2289
  .option("--dry-run", "print what would happen without changing files")
2290
+ .option("--stdio", "use the stdio transport instead of the shared HTTP endpoint (works with the daemon stopped)")
1890
2291
  .action((opts) => {
2292
+ // The HTTP block needs NLM_MCP_TOKEN, which a fresh shell has not exported.
2293
+ autoloadEnv();
2294
+ const transport = opts.stdio ? "stdio" : "http";
1891
2295
  const pluginDir = join(REPO_ROOT, "nlm");
1892
2296
  const report = connectPi({ pluginDir, dryRun: Boolean(opts.dryRun) });
1893
2297
  if (opts.dryRun) {
@@ -1902,6 +2306,19 @@ connect
1902
2306
  console.error(`nlm: pi extension registered → ${report.settingsPath}`);
1903
2307
  console.error(` Packages entry: ${report.pluginDir}`);
1904
2308
  }
2309
+ // Two different files: the extension supplies the hook, mcp.json supplies
2310
+ // the callable tools. pi core has no MCP concept; pi-mcp-adapter reads it.
2311
+ const mcp = connectPiMcp({
2312
+ transport,
2313
+ nlmBinPath: __filename,
2314
+ nodeExecPath: process.execPath,
2315
+ dryRun: Boolean(opts.dryRun),
2316
+ });
2317
+ const mcpVerb = opts.dryRun ? "would write" : (mcp.alreadyPresent ? "updated" : "written");
2318
+ console.error(`nlm: [mcpServers.nlm-memory] ${mcpVerb} (${mcp.transport}) → ${mcp.mcpConfigPath}`);
2319
+ if (mcp.migratedFromStdio) {
2320
+ console.error(" migrated from stdio → shared HTTP endpoint (one daemon-backed server for every session).");
2321
+ }
1905
2322
  console.error(" Restart pi to activate the prompt-recall hook.");
1906
2323
  console.error(" Set NLM_HOOK_MODE=live in ~/.nlm/.env to flip from shadow → live.");
1907
2324
  });
@@ -1913,6 +2330,7 @@ disconnect
1913
2330
  .description("Remove the nlm-memory plugin + marketplace from Codex")
1914
2331
  .option("--with-hooks", "also strip our entries from ~/.codex/hooks.json")
1915
2332
  .option("--dry-run", "print what would happen without invoking codex")
2333
+ .option("--stdio", "use the stdio transport instead of the shared HTTP endpoint (works with the daemon stopped)")
1916
2334
  .action((opts) => {
1917
2335
  if (!opts.dryRun && !codexBinaryAvailable()) {
1918
2336
  console.error("nlm disconnect codex: `codex` binary not on PATH.");
@@ -2003,7 +2421,7 @@ disconnect
2003
2421
  const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
2004
2422
  await storage.init();
2005
2423
  try {
2006
- const report = await disconnectCursor(storage.sources, { dryRun: Boolean(opts.dryRun) });
2424
+ const report = await disconnectCursor(storage.sources, DEFAULT_TEAM_ID, { dryRun: Boolean(opts.dryRun) });
2007
2425
  if (opts.dryRun) {
2008
2426
  console.error("nlm disconnect cursor (dry run): disable Cursor source in registry");
2009
2427
  if (opts.withRules)
@@ -2031,7 +2449,7 @@ disconnect
2031
2449
  const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
2032
2450
  await storage.init();
2033
2451
  try {
2034
- const report = await disconnectWindsurf(storage.sources, { dryRun: Boolean(opts.dryRun) });
2452
+ const report = await disconnectWindsurf(storage.sources, DEFAULT_TEAM_ID, { dryRun: Boolean(opts.dryRun) });
2035
2453
  if (opts.dryRun) {
2036
2454
  console.error("nlm disconnect windsurf (dry run): disable Windsurf source in registry");
2037
2455
  if (opts.withRules)
@@ -2109,7 +2527,7 @@ program
2109
2527
  const storage = await buildStorage(dbPath());
2110
2528
  const scope = installScope();
2111
2529
  const sinceTs = new Date(Date.now() - opts.days * 86_400_000).toISOString();
2112
- const rows = await storage.signals.listForAggregation({ installScope: scope, sinceTs });
2530
+ const rows = await storage.signals.listForAggregation(DEFAULT_TEAM_ID, { installScope: scope, sinceTs });
2113
2531
  const { aggregateFailureModes } = await import("../core/signals/aggregate.js");
2114
2532
  const { recommendActions } = await import("../core/signals/recommend.js");
2115
2533
  const modes = aggregateFailureModes(rows);
@@ -2130,7 +2548,7 @@ program
2130
2548
  program
2131
2549
  .command("digest")
2132
2550
  .description("Compose a daily-activity digest from the running daemon (optionally post to Telegram)")
2133
- .option("-p, --port <n>", "daemon port", (v) => Number.parseInt(v, 10), Number.parseInt(process.env["NLM_PORT"] ?? "3940", 10))
2551
+ .option("-p, --port <n>", "daemon port", (v) => Number.parseInt(v, 10), Number.parseInt(process.env["NLM_PORT"] ?? DEFAULT_NLM_PORT, 10))
2134
2552
  .option("--telegram", "post to Telegram instead of printing to stdout (requires TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID)")
2135
2553
  .action(async (opts) => {
2136
2554
  autoloadEnv();
@@ -2156,7 +2574,7 @@ program
2156
2574
  const date = resolveDigestDate(opts.date);
2157
2575
  const { storage, store } = await buildStack();
2158
2576
  try {
2159
- const digest = await buildWorkDigest({ store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() }, date);
2577
+ const digest = await buildWorkDigest({ store, topicProvider: loadTopicProvider(), workstreams: storage.workstreams, ...workDigestEnv() }, DEFAULT_TEAM_ID, date);
2160
2578
  console.log(composeWorkDigest(digest));
2161
2579
  }
2162
2580
  finally {
@@ -2206,6 +2624,13 @@ async function gatherInstallProbe() {
2206
2624
  // trust entry is Codex's registry (not nlm's), so it must not trip this check.
2207
2625
  codexStale = txt.includes("@nlm-memory-ts");
2208
2626
  }
2627
+ // Codex plugins contribute MCP servers to the effective configuration
2628
+ // without writing a table into config.toml. Prefer Codex's own resolved
2629
+ // view so `nlm verify` does not report a healthy plugin install as unwired.
2630
+ if (!codexMcp && codexBinaryAvailable()) {
2631
+ codexPresent = true;
2632
+ codexMcp = codexHasEffectiveMcpServer("nlm-memory");
2633
+ }
2209
2634
  const envPath = join(homedir(), ".nlm", ".env");
2210
2635
  return {
2211
2636
  daemon: { reachable, version, expectedVersion: pkg.version },
@@ -2518,7 +2943,7 @@ entitiesCmd
2518
2943
  if (opts.applySafe) {
2519
2944
  for (const s of safe) {
2520
2945
  try {
2521
- await storage.entities.merge(s.source, s.target);
2946
+ await storage.entities.merge(DEFAULT_TEAM_ID, s.source, s.target);
2522
2947
  console.log(`merged: ${s.source} -> ${s.target}`);
2523
2948
  merged++;
2524
2949
  }
@@ -2542,7 +2967,7 @@ entitiesCmd
2542
2967
  const answer = await ask(`merge ${s.source} -> ${s.target}? [y/N] `);
2543
2968
  if (answer.trim().toLowerCase() === "y") {
2544
2969
  try {
2545
- await storage.entities.merge(s.source, s.target);
2970
+ await storage.entities.merge(DEFAULT_TEAM_ID, s.source, s.target);
2546
2971
  console.log(`merged: ${s.source} -> ${s.target}`);
2547
2972
  merged++;
2548
2973
  }
@@ -2566,6 +2991,66 @@ entitiesCmd
2566
2991
  if (failed > 0)
2567
2992
  process.exit(1);
2568
2993
  });
2994
+ const scopeCmd = program
2995
+ .command("scope")
2996
+ .description("Scope management commands");
2997
+ scopeCmd
2998
+ .command("backfill")
2999
+ .description("Derive and stamp scope on legacy sessions/facts/exemplars/signals/workstreams from stored transcript evidence")
3000
+ .option("--apply", "write changes (default: dry run, no writes)")
3001
+ .option("--db <path>", "SQLite DB file to backfill (default: canonical NLM DB)")
3002
+ .option("--json", "emit machine-readable JSON summary instead of human-readable output")
3003
+ .action(async (opts) => {
3004
+ const targetPath = opts.db ?? dbPath();
3005
+ const storage = SqliteStorage.create({ dbPath: targetPath, migrationsDir: MIGRATIONS_DIR });
3006
+ const aliasMap = loadAliasMap();
3007
+ try {
3008
+ const result = await runScopeBackfill({
3009
+ db: storage.rawDb(),
3010
+ apply: Boolean(opts.apply),
3011
+ aliasMap,
3012
+ });
3013
+ if (opts.json) {
3014
+ const out = {
3015
+ dryRun: result.dryRun,
3016
+ sessions: { total: result.sessions.total, byScope: result.sessions.byScope },
3017
+ facts: { total: result.facts.total, byScope: result.facts.byScope },
3018
+ exemplars: { total: result.exemplars.total, byScope: result.exemplars.byScope },
3019
+ signals: { total: result.signals.total, byScope: result.signals.byScope },
3020
+ workstreams: { total: result.workstreams.total, byScope: result.workstreams.byScope },
3021
+ skipped: result.skipped,
3022
+ };
3023
+ process.stdout.write(JSON.stringify(out, null, 2) + "\n");
3024
+ }
3025
+ else {
3026
+ formatBackfillResult(result, (s) => process.stdout.write(s));
3027
+ }
3028
+ }
3029
+ finally {
3030
+ await storage.close();
3031
+ }
3032
+ });
3033
+ scopeCmd
3034
+ .command("coverage")
3035
+ .description("Report per-table and recall-weighted scope coverage fractions")
3036
+ .option("--db <path>", "SQLite DB file (default: canonical NLM DB)")
3037
+ .option("--query-log <path>", "query log path for recall-weighted coverage")
3038
+ .option("--window <n>", "last N log entries for recall-weighted coverage (default 200)", (v) => Number.parseInt(v, 10), 200)
3039
+ .option("--json", "emit machine-readable JSON instead of human-readable output")
3040
+ .action(async (opts) => {
3041
+ const targetPath = opts.db ?? dbPath();
3042
+ const result = await runScopeCoverage({
3043
+ dbPath: targetPath,
3044
+ ...(opts.queryLog ? { queryLogPath: opts.queryLog } : {}),
3045
+ window: opts.window,
3046
+ });
3047
+ if (opts.json) {
3048
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
3049
+ }
3050
+ else {
3051
+ formatCoverageResult(result, (s) => process.stdout.write(s));
3052
+ }
3053
+ });
2569
3054
  // Entrypoint guard: only parse argv when this file IS the executed script.
2570
3055
  // realpathSync resolves the npm bin symlink so the global `nlm` shim still
2571
3056
  // matches; without the guard, importing any helper from this module inside