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/http/app.js CHANGED
@@ -19,7 +19,6 @@
19
19
  */
20
20
  import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
21
21
  import { timingSafeEqual } from "node:crypto";
22
- import { homedir } from "node:os";
23
22
  import { dirname, extname, join, normalize, sep } from "node:path";
24
23
  import { Hono } from "hono";
25
24
  import pkg from "../../package.json" with { type: "json" };
@@ -30,7 +29,7 @@ import { logQuery, recallStats } from "../core/recall/query-log.js";
30
29
  import { recentQueryLog } from "../core/recall/recent-log.js";
31
30
  import { appendCitation, citationStats } from "../core/recall/citation-log.js";
32
31
  import { appendSupersedence } from "../core/storage/supersedence-log.js";
33
- import { getUpdateStatus } from "../core/update-check/check.js";
32
+ import { getCachedUpdateStatus, getUpdateStatus } from "../core/update-check/check.js";
34
33
  import { buildClearCookie, buildSessionCookie, deriveSessionValue, parseCookies, sanitizeNextPath, SESSION_COOKIE_NAME, verifySessionCookie, } from "./ui-auth.js";
35
34
  import { createNonceStore } from "./auth-nonce.js";
36
35
  import { clearSurfaced, loadSurfaced, recordSurfaced, resolveConversationForSession } from "../core/hook/memo.js";
@@ -39,6 +38,7 @@ import { classifyPrompt } from "../core/hook/gate.js";
39
38
  import { selectHits } from "../core/hook/select.js";
40
39
  import { formatPointerBlock } from "../core/hook/pointer-block.js";
41
40
  import { factRecallStats, logFactQuery } from "../core/recall-facts/fact-query-log.js";
41
+ import { pickRelatedConcepts } from "../core/recall/related-concepts.js";
42
42
  import { buildDataset } from "../core/dataset/build-dataset.js";
43
43
  import { ClassifierBox } from "../llm/classifier-box.js";
44
44
  import { SourceRegistry, PgSourceRegistry, } from "../core/sources/source-registry.js";
@@ -48,6 +48,8 @@ import { ingestSession, deriveSessionId } from "../core/ingest/ingest-session.js
48
48
  import { listActions, listActionsPg, undoAction, undoActionPg, writeAction, writeActionPg, writeActionsBatch, writeActionsBatchPg, } from "../core/actions/actions-log.js";
49
49
  import { PgSessionStore } from "../core/storage/pg-session-store.js";
50
50
  import { normalizeSignal } from "../core/signals/ingest-signal.js";
51
+ import { scopeStampEnabled } from "../core/scope/stamp-flag.js";
52
+ import { stampSignalScope } from "../core/signals/stamp-scope.js";
51
53
  import { normalizeExemplar } from "../core/exemplars/ingest-exemplar.js";
52
54
  import { extractExemplar } from "../core/exemplars/extract-exemplar.js";
53
55
  import { recallCode } from "../core/exemplars/recall-code.js";
@@ -59,6 +61,11 @@ import { warmupSnapshot } from "../core/health/warmup-state.js";
59
61
  import { laneHealthSnapshot } from "../core/health/embedding-lane-state.js";
60
62
  import { inflightSnapshot } from "../core/health/embed-inflight.js";
61
63
  import { corpusSnapshot } from "../core/health/corpus-state.js";
64
+ import { DEFAULT_NLM_PORT } from "../shared/net.js";
65
+ import { DEFAULT_TEAM_ID } from "../core/tenancy/default-team.js";
66
+ import { tenantStatePath } from "../core/tenancy/tenant-state-path.js";
67
+ import { isHostedMode } from "../core/tenancy/hosted-mode.js";
68
+ import { resolveTeamByToken } from "../core/tenancy/team-auth.js";
62
69
  const HERMES_RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
63
70
  const MIME_TYPES = {
64
71
  ".html": "text/html; charset=utf-8",
@@ -84,6 +91,16 @@ const DATA_STAT_TABLES = [
84
91
  "sources",
85
92
  "providers",
86
93
  ];
94
+ /**
95
+ * Tenancy (program spec §4.6 disposition LOCAL, M2 plan Wave B6): `GET
96
+ * /api/data/stats` is a LOCAL-dispositioned route (spec §4.6) — hard-403'd
97
+ * under `NLM_HOSTED=1` by Wave C's hosted-mode gate, same class as
98
+ * `/api/dataset` and `/api/data/backup`. These are per-table row counts +
99
+ * migration bookkeeping for the Settings → Data page, not corpus content;
100
+ * left whole-DB deliberately rather than fake-filtered by tenant, which
101
+ * would misrepresent what the counts mean (they're meant to show the whole
102
+ * install's state, e.g. for a local operator sanity-checking their db).
103
+ */
87
104
  function sqliteDataStats(db, dbPath) {
88
105
  const countOf = (table) => {
89
106
  try {
@@ -215,9 +232,10 @@ const VALID_KINDS = ["decision", "open"];
215
232
  const VALID_FACT_KINDS = ["decision", "open", "attribute"];
216
233
  export function createApp(deps) {
217
234
  const app = new Hono();
218
- const boundPort = process.env["NLM_PORT"] ? Number.parseInt(process.env["NLM_PORT"], 10) : 3940;
219
- installLocalOnlyMiddleware(app, boundPort);
220
- registerHealthRoute(app);
235
+ const boundPort = process.env["NLM_PORT"] ? Number.parseInt(process.env["NLM_PORT"], 10) : Number.parseInt(DEFAULT_NLM_PORT, 10);
236
+ installLocalOnlyMiddleware(app, boundPort, deps);
237
+ installHostedModeGate(app);
238
+ registerHealthRoute(app, deps);
221
239
  registerMcpRoute(app, deps);
222
240
  registerRecallRoutes(app, deps);
223
241
  registerHookRoutes(app);
@@ -233,6 +251,7 @@ export function createApp(deps) {
233
251
  registerProviderRoutes(app, deps);
234
252
  registerSessionRoute(app, deps);
235
253
  registerSignalRoutes(app, deps);
254
+ registerJobRoutes(app, deps);
236
255
  const nonceStore = createNonceStore();
237
256
  registerNonceRoute(app, nonceStore);
238
257
  if (deps.uiDist) {
@@ -267,25 +286,68 @@ function registerNonceRoute(app, nonceStore) {
267
286
  function uiAuthMode() {
268
287
  return process.env["NLM_UI_AUTH"] === "cookie" ? "cookie" : "none";
269
288
  }
270
- function installLocalOnlyMiddleware(app, boundPort) {
289
+ /**
290
+ * Tenant resolution + auth for every /api/* request (program spec §3, M3).
291
+ * Host/Origin checks are network-position defenses and stay skippable under
292
+ * VITEST/NODE_ENV=test (in-process `app.request()` calls carry no real
293
+ * socket). Tenant resolution does NOT skip under that flag — the contract's
294
+ * token-swap case (spec §6 case 8) and any other test that wires a real
295
+ * `teamTokens` store must exercise the actual resolveTeamByToken path, not a
296
+ * bypass, so a garbage token really does 401 inside a `vitest` run.
297
+ *
298
+ * Resolution order:
299
+ * 1. Hosted mode (NLM_HOSTED=1): a resolvable Bearer token is mandatory.
300
+ * No cookie fallback, no ungated fallback — absent/unresolvable token
301
+ * is 401 before any corpus read, always.
302
+ * 2. Local mode, NLM_UI_AUTH unset ("none", the default): zero UX change
303
+ * from pre-M3 — requests resolve to DEFAULT_TEAM_ID. A resolvable
304
+ * Bearer token is still honored when presented (lets local multi-team
305
+ * dev and in-process tests exercise real resolution), but nothing is
306
+ * ever rejected in this mode — that's the point of leaving auth off.
307
+ * 3. Local mode, NLM_UI_AUTH=cookie: the existing HMAC session cookie
308
+ * (still keyed off NLM_MCP_TOKEN directly — unchanged, local-only
309
+ * concern) resolves to DEFAULT_TEAM_ID. A Bearer token resolves via
310
+ * the same team_tokens lookup as every other transport instead of the
311
+ * old exact-string compare; unresolved token or no credential is 401.
312
+ */
313
+ function installLocalOnlyMiddleware(app, boundPort, deps) {
271
314
  const skipLocalGate = !!process.env["VITEST"] || process.env["NODE_ENV"] === "test";
272
315
  const extras = parseAllowedOrigins(process.env["NLM_ALLOWED_ORIGINS"] ?? "");
273
316
  app.use("/api/*", async (c, next) => {
274
- if (skipLocalGate)
275
- return next();
276
- const host = c.req.header("host");
277
- if (!isLoopbackHost(host, boundPort) && !extras.hosts.has(host?.toLowerCase() ?? "")) {
278
- return c.json({ error: "host header not allowed" }, 403);
317
+ if (!skipLocalGate) {
318
+ const host = c.req.header("host");
319
+ if (!isLoopbackHost(host, boundPort) && !extras.hosts.has(host?.toLowerCase() ?? "")) {
320
+ return c.json({ error: "host header not allowed" }, 403);
321
+ }
279
322
  }
280
323
  if (c.req.path === "/api/health") {
281
324
  return next();
282
325
  }
283
- const origin = c.req.header("origin");
284
- if (origin !== undefined && !isLoopbackOrigin(origin, boundPort) && !extras.origins.has(origin.toLowerCase())) {
285
- return c.json({ error: "origin not allowed" }, 403);
326
+ if (!skipLocalGate) {
327
+ const origin = c.req.header("origin");
328
+ if (origin !== undefined && !isLoopbackOrigin(origin, boundPort) && !extras.origins.has(origin.toLowerCase())) {
329
+ return c.json({ error: "origin not allowed" }, 403);
330
+ }
331
+ }
332
+ const auth = c.req.header("authorization") ?? "";
333
+ const match = /^Bearer\s+(\S+)$/i.exec(auth);
334
+ const presentedToken = match?.[1];
335
+ if (isHostedMode()) {
336
+ const resolved = deps.teamTokens ? await resolveTeamByToken(deps.teamTokens, presentedToken) : null;
337
+ if (!resolved)
338
+ return c.json({ error: "unauthorized" }, 401);
339
+ c.set("tenantId", resolved.teamId);
340
+ return next();
286
341
  }
287
342
  if (uiAuthMode() === "none") {
288
- // Auth disabled by user. Loopback Host + Origin checks already passed.
343
+ // Auth disabled by user (or never configured). Loopback Host + Origin
344
+ // checks already passed. A resolvable token still wins when present;
345
+ // otherwise fall back to the single local operator team exactly as
346
+ // before M3 — this mode never rejects a request.
347
+ const resolved = presentedToken && deps.teamTokens
348
+ ? await resolveTeamByToken(deps.teamTokens, presentedToken)
349
+ : null;
350
+ c.set("tenantId", resolved ? resolved.teamId : DEFAULT_TEAM_ID);
289
351
  return next();
290
352
  }
291
353
  const token = process.env["NLM_MCP_TOKEN"];
@@ -296,26 +358,55 @@ function installLocalOnlyMiddleware(app, boundPort) {
296
358
  }
297
359
  // UI session cookie (HMAC of the token, set by /ui/auth bootstrap).
298
360
  // Carries the browser's API calls and survives token-stable restarts.
361
+ // Local-only concern (program spec §3 M3): unchanged, always resolves
362
+ // to the single local operator team.
299
363
  const cookies = parseCookies(c.req.header("cookie"));
300
364
  if (verifySessionCookie(cookies[SESSION_COOKIE_NAME], token)) {
301
365
  // Rolling expiry: every authenticated hit re-issues Set-Cookie so an
302
366
  // actively-used session never sees an expiry. Only 30 days of true
303
367
  // inactivity force a re-bootstrap.
304
368
  c.header("Set-Cookie", buildSessionCookie(deriveSessionValue(token)));
369
+ c.set("tenantId", DEFAULT_TEAM_ID);
305
370
  return next();
306
371
  }
307
372
  // Bearer: programmatic clients (Hermes WebUI, agents, the MCP path).
308
- // Same secret as the cookie HMAC derives from, but transmitted directly.
309
- const auth = c.req.header("authorization") ?? "";
310
- const match = /^Bearer\s+(\S+)$/i.exec(auth);
311
- const given = Buffer.from(match?.[1] ?? "", "utf8");
312
- const want = Buffer.from(token, "utf8");
313
- if (match && given.length === want.length && timingSafeEqual(given, want)) {
373
+ // Resolved via the same team_tokens lookup every other transport uses.
374
+ const resolved = presentedToken && deps.teamTokens
375
+ ? await resolveTeamByToken(deps.teamTokens, presentedToken)
376
+ : null;
377
+ if (resolved) {
378
+ c.set("tenantId", resolved.teamId);
314
379
  return next();
315
380
  }
316
381
  return c.json({ error: "unauthorized" }, 401);
317
382
  });
318
383
  }
384
+ const HOSTED_GATED_ROUTES = [
385
+ { method: "GET", path: "/api/dataset", disposition: "LOCAL" },
386
+ { method: "GET", path: "/api/data/backup", disposition: "LOCAL" },
387
+ { method: "POST", path: "/api/data/restore", disposition: "LOCAL" },
388
+ { method: "GET", path: "/api/data/stats", disposition: "LOCAL" },
389
+ { method: "POST", path: "/api/classifier", disposition: "LOCAL" },
390
+ // Single-operator process supervision — starting/stopping the daemon's
391
+ // own reprocess child has no per-tenant meaning, same LOCAL class as the
392
+ // classifier swap above.
393
+ { method: "POST", path: "/api/jobs/reprocess", disposition: "LOCAL" },
394
+ { method: "DELETE", path: "/api/jobs/reprocess", disposition: "LOCAL" },
395
+ ];
396
+ function installHostedModeGate(app) {
397
+ const gated = new Map();
398
+ for (const r of HOSTED_GATED_ROUTES)
399
+ gated.set(`${r.method} ${r.path}`, r.disposition);
400
+ app.use("/api/*", async (c, next) => {
401
+ if (!isHostedMode())
402
+ return next();
403
+ const disposition = gated.get(`${c.req.method} ${c.req.path}`);
404
+ if (disposition) {
405
+ return c.json({ error: `route disabled in hosted mode (disposition: ${disposition})`, disposition }, 403);
406
+ }
407
+ return next();
408
+ });
409
+ }
319
410
  // ── UI session-cookie gate ───────────────────────────────────────────
320
411
  //
321
412
  // Closes the port-forward bypass: any client reaching localhost:3940
@@ -396,13 +487,30 @@ function renderAuthPage() {
396
487
  <p class="hint">Sessions roll forward on every visit, so this page only appears after ~30 days of inactivity. To turn auth off entirely, run <code>nlm config ui-auth off</code>.</p>
397
488
  </main></body></html>`;
398
489
  }
399
- function registerHealthRoute(app) {
400
- app.get("/api/health", (c) => {
490
+ function registerHealthRoute(app, deps) {
491
+ app.get("/api/health", async (c) => {
401
492
  const snap = corpusSnapshot();
402
493
  const corpus = snap
403
494
  ? { state: snap.state, dbBytes: snap.dbBytes, sessions: snap.sessions, entities: snap.entities, lastComputedAt: snap.lastComputedAt }
404
495
  : null;
405
- return c.json({ status: "ok", service: "nlm-memory", version: pkg.version, warmup: warmupSnapshot(), embedding: laneHealthSnapshot(), embedInflight: inflightSnapshot(), corpus });
496
+ // Cache-only read (see check.ts) — never fetches the registry, so this
497
+ // stays fast and never fails the health check when npm is unreachable.
498
+ const cachedUpdate = await getCachedUpdateStatus({
499
+ currentVersion: pkg.version,
500
+ ...deps.updateCheckDeps,
501
+ });
502
+ const update = {
503
+ current: cachedUpdate.current,
504
+ latest: cachedUpdate.disabled ? null : cachedUpdate.latest,
505
+ behind: cachedUpdate.disabled ? false : cachedUpdate.behind,
506
+ };
507
+ // "idle" (never started, or this daemon process's initial state — see
508
+ // job-supervisor.ts) reports as `job: null` rather than an idle block:
509
+ // an operator polling /api/health wants to know whether a run exists,
510
+ // not to distinguish "never run" from a five-field block of zeros.
511
+ const jobSnap = deps.jobSupervisor ? deps.jobSupervisor.snapshot() : null;
512
+ const job = jobSnap && jobSnap.state !== "idle" ? jobSnap : null;
513
+ return c.json({ status: "ok", service: "nlm-memory", version: pkg.version, warmup: warmupSnapshot(), embedding: laneHealthSnapshot(), embedInflight: inflightSnapshot(), corpus, update, job });
406
514
  });
407
515
  // Passive update poll for the UI. Same daily-cached check the CLI
408
516
  // startup banner uses — see src/core/update-check/check.ts for the
@@ -419,30 +527,33 @@ function registerHealthRoute(app) {
419
527
  function registerMcpRoute(app, deps) {
420
528
  if (!deps.mcpDeps)
421
529
  return;
422
- const mcpToken = process.env["NLM_MCP_TOKEN"];
423
- if (!mcpToken) {
424
- throw new Error("NLM_MCP_TOKEN must be set when mcpDeps is provided — " +
530
+ if (!deps.teamTokens) {
531
+ throw new Error("teamTokens must be wired when mcpDeps is provided — " +
425
532
  "refusing to mount an unauthenticated /mcp endpoint");
426
533
  }
427
534
  const capturedMcpDeps = deps.mcpDeps;
535
+ const teamTokens = deps.teamTokens;
428
536
  app.all("/mcp", async (c) => {
429
537
  const auth = c.req.header("authorization") ?? "";
430
538
  const match = /^Bearer\s+(\S+)$/i.exec(auth);
431
- const given = Buffer.from(match?.[1] ?? "", "utf8");
432
- const want = Buffer.from(mcpToken, "utf8");
433
- if (!match || given.length !== want.length || !timingSafeEqual(given, want)) {
539
+ const resolved = await resolveTeamByToken(teamTokens, match?.[1]);
540
+ if (!resolved) {
434
541
  return c.json({ error: "unauthorized" }, 401);
435
542
  }
436
543
  // No sessionIdGenerator = stateless mode: no session ID in responses,
437
544
  // no session validation. Correct for per-request agent calls.
438
545
  const transport = new WebStandardStreamableHTTPServerTransport({});
439
- const server = createMcpServer(capturedMcpDeps);
546
+ const server = createMcpServer(capturedMcpDeps, resolved.teamId);
440
547
  await server.connect(transport);
441
548
  return transport.handleRequest(c.req.raw);
442
549
  });
443
550
  }
444
551
  function registerRecallRoutes(app, deps) {
445
552
  app.get("/api/recall", async (c) => {
553
+ // Composition point for this request (program spec §3 M3): the tenant
554
+ // the general gate resolved from the Bearer token / cookie / local-mode
555
+ // default, stashed on the Hono context by installLocalOnlyMiddleware.
556
+ const tenantId = c.get("tenantId");
446
557
  const q = c.req.query("q") ?? "";
447
558
  const entity = c.req.query("entity");
448
559
  const kind = c.req.query("kind");
@@ -486,6 +597,18 @@ function registerRecallRoutes(app, deps) {
486
597
  else if (source === "hook") {
487
598
  withRelatedFacts = true;
488
599
  }
600
+ // Learner evidence (spec 2026-10-06): `?withConcepts=true` (or hook
601
+ // source = automatic) attaches a relatedConcepts array. Same posture
602
+ // as facts; empty when no enrollments match, so non-learner callers
603
+ // see zero behavior change.
604
+ const conceptsParam = c.req.query("withConcepts");
605
+ let withRelatedConcepts;
606
+ if (conceptsParam !== undefined) {
607
+ withRelatedConcepts = conceptsParam === "true" || conceptsParam === "1";
608
+ }
609
+ else if (source === "hook") {
610
+ withRelatedConcepts = true;
611
+ }
489
612
  // Spec §E: `?withExemplars=true` attaches relatedExemplars (passive code exemplar recall).
490
613
  const withExemplars = c.req.query("withExemplars") === "true";
491
614
  // Task #303: superseded sessions are opt-in over HTTP and DEFAULT OFF.
@@ -501,12 +624,13 @@ function registerRecallRoutes(app, deps) {
501
624
  ...(kind !== undefined ? { kind: kind } : {}),
502
625
  ...(rewrite !== undefined ? { rewrite } : {}),
503
626
  ...(withRelatedFacts !== undefined ? { withRelatedFacts } : {}),
627
+ ...(withRelatedConcepts !== undefined ? { withRelatedConcepts } : {}),
504
628
  ...(withExemplars ? { withRelatedExemplars: true } : {}),
505
629
  ...(includeSuperseded !== undefined ? { includeSuperseded } : {}),
506
630
  };
507
- const result = await deps.recall.search(query);
631
+ const result = await deps.recall.search(tenantId, query);
508
632
  // Fire-and-forget telemetry — never blocks the response.
509
- void logQuery({
633
+ void logQuery(tenantId, {
510
634
  source,
511
635
  runtime,
512
636
  query: q || null,
@@ -521,22 +645,29 @@ function registerRecallRoutes(app, deps) {
521
645
  return c.json(result);
522
646
  });
523
647
  app.get("/api/recall/stats", async (c) => {
648
+ const tenantId = c.get("tenantId");
524
649
  const daysStr = c.req.query("days") ?? "7";
525
650
  const days = Number.parseInt(daysStr, 10);
526
651
  if (!Number.isFinite(days) || days < 1 || days > 365) {
527
652
  return c.json({ error: "days must be 1..365" }, 400);
528
653
  }
529
- const stats = await recallStats(days, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
654
+ const stats = await recallStats(tenantId, days, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
530
655
  return c.json(stats);
531
656
  });
532
657
  app.get("/api/recall/recent", (c) => {
533
- const limit = parseLimit(c.req.query("limit"), 50, 200);
534
- const entries = recentQueryLog(limit, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
658
+ const tenantId = c.get("tenantId");
659
+ // Cap raised from 200: consumers strip probe traffic client-side, so the
660
+ // window has to hold a full day of RAW entries. A host emitting ~800
661
+ // probes/day filled all 200 rows with probes inside 7h, and the digest
662
+ // reported "0 queries yesterday" on a day with 27 real ones (2026-08-13).
663
+ const limit = parseLimit(c.req.query("limit"), 50, 5000);
664
+ const entries = recentQueryLog(tenantId, limit, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
535
665
  return c.json({ entries });
536
666
  });
537
667
  // Citation events from the Stop hook. One POST per surfaced ID the
538
668
  // assistant cited in its response. Feeds the recall precision metric.
539
669
  app.post("/api/recall/cite-event", async (c) => {
670
+ const tenantId = c.get("tenantId");
540
671
  let body;
541
672
  try {
542
673
  body = (await c.req.json());
@@ -554,7 +685,7 @@ function registerRecallRoutes(app, deps) {
554
685
  }
555
686
  const responsePreview = body["response_preview"];
556
687
  const kind = body["kind"];
557
- await appendCitation({
688
+ await appendCitation(tenantId, {
558
689
  conversationId,
559
690
  citedId,
560
691
  ...(kind === "tool_use" || kind === "prose" ? { kind } : {}),
@@ -565,12 +696,13 @@ function registerRecallRoutes(app, deps) {
565
696
  return c.json({ ok: true });
566
697
  });
567
698
  app.get("/api/recall/cite-stats", async (c) => {
699
+ const tenantId = c.get("tenantId");
568
700
  const daysStr = c.req.query("days") ?? "7";
569
701
  const days = Number.parseInt(daysStr, 10);
570
702
  if (!Number.isFinite(days) || days < 1 || days > 365) {
571
703
  return c.json({ error: "days must be 1..365" }, 400);
572
704
  }
573
- const stats = await citationStats(days, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
705
+ const stats = await citationStats(tenantId, days, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
574
706
  return c.json(stats);
575
707
  });
576
708
  // Explicit citation from the cite_session MCP tool. One POST per session
@@ -578,6 +710,7 @@ function registerRecallRoutes(app, deps) {
578
710
  // so the training extractor can distinguish deterministic tool citations
579
711
  // from stop-hook detected prose citations.
580
712
  app.post("/api/citation/explicit", async (c) => {
713
+ const tenantId = c.get("tenantId");
581
714
  let body;
582
715
  try {
583
716
  body = (await c.req.json());
@@ -589,10 +722,10 @@ function registerRecallRoutes(app, deps) {
589
722
  if (typeof id !== "string" || !id) {
590
723
  return c.json({ error: "id required" }, 400);
591
724
  }
592
- await appendCitation({
725
+ await appendCitation(tenantId, {
593
726
  conversationId: typeof body["conversation_id"] === "string"
594
727
  ? body["conversation_id"]
595
- : (resolveConversationForSession(id) ?? "mcp_tool"),
728
+ : (resolveConversationForSession(tenantId, id) ?? "mcp_tool"),
596
729
  citedId: id,
597
730
  kind: "tool_use",
598
731
  ...(typeof body["reason"] === "string" ? { responsePreview: body["reason"] } : {}),
@@ -607,6 +740,7 @@ function registerHookRoutes(app) {
607
740
  // suppressed by stale "already surfaced" gates.
608
741
  // Payload: { conversation_id, transcript_path?, surfaced_set?, ts? }
609
742
  app.post("/api/hook/pre-compact", async (c) => {
743
+ const tenantId = c.get("tenantId");
610
744
  let body;
611
745
  try {
612
746
  body = (await c.req.json());
@@ -618,10 +752,18 @@ function registerHookRoutes(app) {
618
752
  if (typeof conversationId !== "string" || !conversationId) {
619
753
  return c.json({ error: "conversation_id required" }, 400);
620
754
  }
621
- const flushed = loadSurfaced(conversationId).size;
622
- clearSurfaced(conversationId);
755
+ const flushed = loadSurfaced(tenantId, conversationId).size;
756
+ clearSurfaced(tenantId, conversationId);
623
757
  const compactedAt = new Date().toISOString();
624
- const logPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
758
+ // Mirrors hook-log.ts's own path derivation (core/tenancy/tenant-state-path.ts):
759
+ // the default team's log honors NLM_HOOK_LOG; every other tenant is isolated
760
+ // under ~/.nlm/tenants/<tenantId>/hook-log.jsonl. This route writes a
761
+ // differently-shaped record (`kind: "pre-compact"`) than appendHookLog's
762
+ // HookLogEntry, so it can't reuse that function directly, but must still
763
+ // land in the same tenant-scoped file.
764
+ const logPath = tenantId === DEFAULT_TEAM_ID
765
+ ? (process.env["NLM_HOOK_LOG"] ?? tenantStatePath(tenantId, "hook-log.jsonl"))
766
+ : tenantStatePath(tenantId, "hook-log.jsonl");
625
767
  try {
626
768
  mkdirSync(dirname(logPath), { recursive: true });
627
769
  appendFileSync(logPath, `${JSON.stringify({ ts: compactedAt, kind: "pre-compact", conversationId, flushed })}\n`, "utf8");
@@ -631,38 +773,6 @@ function registerHookRoutes(app) {
631
773
  }
632
774
  return c.json({ ok: true, flushed, compacted_at: compactedAt });
633
775
  });
634
- // SubagentStart hook: logging-only stub. Records the parent→subagent link
635
- // so future corpus-linking logic can correlate subagent sessions back to
636
- // their dispatching conversation.
637
- // Payload: { parent_conversation_id, subagent_session_id, subagent_description?, ts? }
638
- app.post("/api/hook/subagent-start", async (c) => {
639
- let body;
640
- try {
641
- body = (await c.req.json());
642
- }
643
- catch {
644
- return c.json({ error: "body must be JSON" }, 400);
645
- }
646
- const parentConversationId = body["parent_conversation_id"];
647
- const subagentSessionId = body["subagent_session_id"];
648
- if (typeof parentConversationId !== "string" || !parentConversationId) {
649
- return c.json({ error: "parent_conversation_id required" }, 400);
650
- }
651
- if (typeof subagentSessionId !== "string" || !subagentSessionId) {
652
- return c.json({ error: "subagent_session_id required" }, 400);
653
- }
654
- const subagentDescription = typeof body["subagent_description"] === "string" ? body["subagent_description"] : "";
655
- const ts = typeof body["ts"] === "string" ? body["ts"] : new Date().toISOString();
656
- const logPath = process.env["NLM_SUBAGENT_LOG"] ?? join(homedir(), ".nlm", "subagent-log.jsonl");
657
- try {
658
- mkdirSync(dirname(logPath), { recursive: true });
659
- appendFileSync(logPath, `${JSON.stringify({ ts, parent_conversation_id: parentConversationId, subagent_session_id: subagentSessionId, subagent_description: subagentDescription })}\n`, "utf8");
660
- }
661
- catch {
662
- // Log failure must not fail the endpoint.
663
- }
664
- return c.json({ ok: true, recorded: true });
665
- });
666
776
  }
667
777
  // ── NousResearch Hermes Agent lifecycle hooks ─────────────────────────────
668
778
  //
@@ -697,7 +807,8 @@ function registerHermesAgentHookRoutes(app, deps) {
697
807
  return c.json({ context: null });
698
808
  }
699
809
  try {
700
- const result = await deps.recall.search({
810
+ const tenantId = c.get("tenantId");
811
+ const result = await deps.recall.search(tenantId, {
701
812
  query: userMessage,
702
813
  mode: "keyword",
703
814
  limit: 5,
@@ -710,7 +821,7 @@ function registerHermesAgentHookRoutes(app, deps) {
710
821
  startedAt: r.startedAt,
711
822
  matchScore: r.matchScore,
712
823
  }));
713
- const surfaced = loadSurfaced(sessionId);
824
+ const surfaced = loadSurfaced(tenantId, sessionId);
714
825
  const selected = selectHits({ hits, surfaced, scoreThreshold: 0, relativeFloor: HERMES_RELATIVE_FLOOR, perFireCap: 3, perConversationCap: 10 });
715
826
  if (selected.length === 0 &&
716
827
  (result.relatedFacts ?? []).length === 0 &&
@@ -718,7 +829,7 @@ function registerHermesAgentHookRoutes(app, deps) {
718
829
  return c.json({ context: null });
719
830
  }
720
831
  if (selected.length > 0) {
721
- recordSurfaced(sessionId, selected.map((h) => h.id));
832
+ recordSurfaced(tenantId, sessionId, selected.map((h) => h.id));
722
833
  }
723
834
  return c.json({
724
835
  context: formatPointerBlock(selected, result.relatedFacts ?? [], (result.relatedExemplars ?? []).map((e) => ({
@@ -733,6 +844,7 @@ function registerHermesAgentHookRoutes(app, deps) {
733
844
  // post-turn: scan assistant_response for session IDs that were surfaced in
734
845
  // this conversation and log prose citation events.
735
846
  app.post("/api/hook/hermes-agent/post-turn", async (c) => {
847
+ const tenantId = c.get("tenantId");
736
848
  let body;
737
849
  try {
738
850
  body = (await c.req.json());
@@ -748,7 +860,7 @@ function registerHermesAgentHookRoutes(app, deps) {
748
860
  if (typeof assistantResponse !== "string" || !assistantResponse) {
749
861
  return c.json({ ok: true, cited: 0 });
750
862
  }
751
- const surfacedIds = [...loadSurfaced(sessionId)];
863
+ const surfacedIds = [...loadSurfaced(tenantId, sessionId)];
752
864
  const cited = [];
753
865
  for (const id of surfacedIds) {
754
866
  if (assistantResponse.includes(id))
@@ -756,13 +868,14 @@ function registerHermesAgentHookRoutes(app, deps) {
756
868
  }
757
869
  const preview = assistantResponse.slice(0, 200);
758
870
  for (const citedId of cited) {
759
- await appendCitation({ conversationId: sessionId, citedId, kind: "prose", responsePreview: preview }, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
871
+ await appendCitation(tenantId, { conversationId: sessionId, citedId, kind: "prose", responsePreview: preview }, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
760
872
  }
761
873
  return c.json({ ok: true, cited: cited.length });
762
874
  });
763
875
  // session-lifecycle: memo housekeeping for on_session_{start,end,finalize,reset}.
764
876
  // start is a no-op (memo is created lazily). end/finalize/reset clear the memo.
765
877
  app.post("/api/hook/hermes-agent/session-lifecycle", async (c) => {
878
+ const tenantId = c.get("tenantId");
766
879
  let body;
767
880
  try {
768
881
  body = (await c.req.json());
@@ -777,8 +890,8 @@ function registerHermesAgentHookRoutes(app, deps) {
777
890
  if (event !== "start") {
778
891
  const sessionId = body["session_id"];
779
892
  if (typeof sessionId === "string" && sessionId) {
780
- clearSurfaced(sessionId);
781
- clearCited(sessionId);
893
+ clearSurfaced(tenantId, sessionId);
894
+ clearCited(tenantId, sessionId);
782
895
  }
783
896
  }
784
897
  return c.json({ ok: true, event });
@@ -790,6 +903,7 @@ function registerFactRoutes(app, deps) {
790
903
  if (!deps.factRecall) {
791
904
  return c.json({ error: "fact recall not wired in this deployment" }, 503);
792
905
  }
906
+ const tenantId = c.get("tenantId");
793
907
  const q = c.req.query("q") ?? "";
794
908
  const subject = c.req.query("subject");
795
909
  const predicate = c.req.query("predicate");
@@ -825,10 +939,10 @@ function registerFactRoutes(app, deps) {
825
939
  ...(kind !== undefined ? { kind: kind } : {}),
826
940
  ...(minConfidence !== undefined ? { minConfidence } : {}),
827
941
  };
828
- const result = await deps.factRecall.search(query);
942
+ const result = await deps.factRecall.search(tenantId, query);
829
943
  const source = c.req.header("x-recall-source") ?? "http";
830
944
  const runtime = c.req.header("x-recall-runtime") ?? null;
831
- void logFactQuery({
945
+ void logFactQuery(tenantId, {
832
946
  source,
833
947
  runtime,
834
948
  query: q || null,
@@ -855,31 +969,40 @@ function registerFactRoutes(app, deps) {
855
969
  return c.json({ error: "subject is required" }, 400);
856
970
  }
857
971
  const predicate = c.req.query("predicate");
858
- const chains = await deps.factStore.getHistory(subject, predicate);
972
+ const tenantId = c.get("tenantId");
973
+ const chains = await deps.factStore.getHistory(tenantId, subject, predicate);
859
974
  return c.json({ subject, predicate: predicate ?? null, chains });
860
975
  });
861
976
  app.get("/api/recall/facts/stats", async (c) => {
977
+ const tenantId = c.get("tenantId");
862
978
  const daysStr = c.req.query("days") ?? "7";
863
979
  const days = Number.parseInt(daysStr, 10);
864
980
  if (!Number.isFinite(days) || days < 1 || days > 365) {
865
981
  return c.json({ error: "days must be 1..365" }, 400);
866
982
  }
867
- const stats = await factRecallStats(days, ...(deps.factQueryLogPath !== undefined ? [deps.factQueryLogPath] : []));
983
+ const stats = await factRecallStats(tenantId, days, ...(deps.factQueryLogPath !== undefined ? [deps.factQueryLogPath] : []));
868
984
  return c.json(stats);
869
985
  });
870
986
  }
871
987
  function registerLiveRoutes(app, deps) {
872
- app.get("/api/live/recent-writes", (c) => {
988
+ // Pre-existing bug fixed in passing (C2): PgSessionStore's recentWrites/
989
+ // recentMarkers are async (return a Promise), but SqliteSessionStore's are
990
+ // synchronous — these handlers must be async + await the call so pg
991
+ // callers get the resolved array, not an unresolved Promise serialized as
992
+ // JSON.
993
+ app.get("/api/live/recent-writes", async (c) => {
873
994
  if (!deps.liveStore)
874
995
  return c.json({ writes: [] });
996
+ const tenantId = c.get("tenantId");
875
997
  const limit = parseLimit(c.req.query("limit"), 50, 200);
876
- return c.json({ writes: deps.liveStore.recentWrites(limit) });
998
+ return c.json({ writes: await deps.liveStore.recentWrites(tenantId, limit) });
877
999
  });
878
- app.get("/api/live/recent-markers", (c) => {
1000
+ app.get("/api/live/recent-markers", async (c) => {
879
1001
  if (!deps.liveStore)
880
1002
  return c.json({ markers: [] });
1003
+ const tenantId = c.get("tenantId");
881
1004
  const limit = parseLimit(c.req.query("limit"), 50, 200);
882
- return c.json({ markers: deps.liveStore.recentMarkers(limit) });
1005
+ return c.json({ markers: await deps.liveStore.recentMarkers(tenantId, limit) });
883
1006
  });
884
1007
  }
885
1008
  function registerDatasetRoute(app, deps) {
@@ -887,7 +1010,7 @@ function registerDatasetRoute(app, deps) {
887
1010
  if (!deps.dbPath)
888
1011
  return c.json({ error: "dataset endpoint requires dbPath" }, 503);
889
1012
  const includePaths = c.req.query("include_paths") === "true";
890
- return c.json(buildDataset(deps.dbPath, { includePaths }));
1013
+ return c.json(buildDataset(deps.dbPath, DEFAULT_TEAM_ID, { includePaths }));
891
1014
  });
892
1015
  }
893
1016
  // ── Data management ─────────────────────────────────────────────
@@ -998,9 +1121,10 @@ function registerActionRoutes(app, deps) {
998
1121
  if (!parsed)
999
1122
  return c.json({ error: "invalid action payload" }, 400);
1000
1123
  const store = deps.liveStore;
1124
+ const tenantId = c.get("tenantId");
1001
1125
  const id = store instanceof PgSessionStore
1002
- ? await writeActionPg(store.pool, parsed)
1003
- : writeAction(store.rawDb(), parsed);
1126
+ ? await writeActionPg(store.pool, tenantId, parsed)
1127
+ : writeAction(store.rawDb(), tenantId, parsed);
1004
1128
  store.invalidateOverlayCache();
1005
1129
  return c.json({ id, timestamp: new Date().toISOString() });
1006
1130
  });
@@ -1016,9 +1140,10 @@ function registerActionRoutes(app, deps) {
1016
1140
  if (inputs.length === 0)
1017
1141
  return c.json({ accepted: 0, ids: [] });
1018
1142
  const store = deps.liveStore;
1143
+ const tenantId = c.get("tenantId");
1019
1144
  const ids = store instanceof PgSessionStore
1020
- ? await writeActionsBatchPg(store.pool, inputs)
1021
- : writeActionsBatch(store.rawDb(), inputs);
1145
+ ? await writeActionsBatchPg(store.pool, tenantId, inputs)
1146
+ : writeActionsBatch(store.rawDb(), tenantId, inputs);
1022
1147
  store.invalidateOverlayCache();
1023
1148
  return c.json({ accepted: ids.length, ids });
1024
1149
  });
@@ -1026,9 +1151,10 @@ function registerActionRoutes(app, deps) {
1026
1151
  if (!deps.liveStore)
1027
1152
  return c.json({ error: "actions require liveStore" }, 503);
1028
1153
  const store = deps.liveStore;
1154
+ const tenantId = c.get("tenantId");
1029
1155
  const result = store instanceof PgSessionStore
1030
- ? await undoActionPg(store.pool, c.req.param("id"))
1031
- : undoAction(store.rawDb(), c.req.param("id"));
1156
+ ? await undoActionPg(store.pool, tenantId, c.req.param("id"))
1157
+ : undoAction(store.rawDb(), tenantId, c.req.param("id"));
1032
1158
  if (!result)
1033
1159
  return c.json({ error: "action not found or already undone" }, 404);
1034
1160
  store.invalidateOverlayCache();
@@ -1047,9 +1173,10 @@ function registerActionRoutes(app, deps) {
1047
1173
  ...(kind ? { kind } : {}),
1048
1174
  };
1049
1175
  const store = deps.liveStore;
1176
+ const tenantId = c.get("tenantId");
1050
1177
  const rows = store instanceof PgSessionStore
1051
- ? await listActionsPg(store.pool, opts)
1052
- : listActions(store.rawDb(), opts);
1178
+ ? await listActionsPg(store.pool, tenantId, opts)
1179
+ : listActions(store.rawDb(), tenantId, opts);
1053
1180
  return c.json({ actions: rows });
1054
1181
  });
1055
1182
  }
@@ -1057,13 +1184,18 @@ function registerClassifierRoutes(app, deps) {
1057
1184
  app.get("/api/classifier/info", (c) => {
1058
1185
  const provider = deps.classifier?.provider ?? "ollama";
1059
1186
  const model = deps.classifier?.model ?? "qwen3.5:4b";
1187
+ const openAiSwapAvailable = deps.classifier?.openAiSwapAvailable ?? false;
1060
1188
  return c.json({
1061
1189
  provider,
1062
1190
  model,
1063
- available_providers: ["deepseek", "ollama"],
1191
+ available_providers: openAiSwapAvailable
1192
+ ? ["deepseek", "ollama", "openai"]
1193
+ : ["deepseek", "ollama"],
1194
+ openai_swap_available: openAiSwapAvailable,
1064
1195
  env_present: {
1065
1196
  deepseek: Boolean(process.env["DEEPSEEK_API_KEY"]),
1066
1197
  ollama: true,
1198
+ openai: openAiSwapAvailable,
1067
1199
  },
1068
1200
  default_models: {
1069
1201
  deepseek: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat"],
@@ -1078,8 +1210,15 @@ function registerClassifierRoutes(app, deps) {
1078
1210
  const body = (await c.req.json().catch(() => null));
1079
1211
  const provider = body?.provider;
1080
1212
  const model = body?.model;
1081
- if (provider !== "deepseek" && provider !== "ollama") {
1082
- return c.json({ error: "runtime swap supports 'deepseek' or 'ollama'; 'openai' is set via NLM_CLASSIFIER env + restart" }, 400);
1213
+ // "openai-compatible" (the Providers-registry kind for local/self-hosted
1214
+ // endpoints like LM Studio) and "openai" (the registry's real-OpenAI kind)
1215
+ // both ride ClassifierBox's single "openai" ClassifierProvider — both are
1216
+ // just DeepSeekClient against a baseUrl. That baseUrl is fixed at daemon
1217
+ // startup (NLM_CLASSIFIER_BASE_URL), so this swap is model selection
1218
+ // only, never endpoint selection.
1219
+ const isOpenAiKind = provider === "openai" || provider === "openai-compatible";
1220
+ if (provider !== "deepseek" && provider !== "ollama" && !isOpenAiKind) {
1221
+ return c.json({ error: "runtime swap supports 'deepseek', 'ollama', 'openai', or 'openai-compatible'" }, 400);
1083
1222
  }
1084
1223
  if (!model || typeof model !== "string" || model.length === 0) {
1085
1224
  return c.json({ error: "model is required" }, 400);
@@ -1087,7 +1226,14 @@ function registerClassifierRoutes(app, deps) {
1087
1226
  if (provider === "deepseek" && !process.env["DEEPSEEK_API_KEY"]) {
1088
1227
  return c.json({ error: "DEEPSEEK_API_KEY not set — cannot swap to deepseek" }, 400);
1089
1228
  }
1090
- deps.classifier.swap(provider, model);
1229
+ if (isOpenAiKind && !deps.classifier.openAiSwapAvailable) {
1230
+ return c.json({
1231
+ error: "provider 'openai' has no baseUrl configured — set NLM_CLASSIFIER=openai and " +
1232
+ "NLM_CLASSIFIER_BASE_URL, then restart the daemon before swapping models on it",
1233
+ }, 400);
1234
+ }
1235
+ const classifierProvider = isOpenAiKind ? "openai" : provider;
1236
+ deps.classifier.swap(classifierProvider, model);
1091
1237
  return c.json({ provider: deps.classifier.provider, model: deps.classifier.model });
1092
1238
  });
1093
1239
  }
@@ -1098,7 +1244,8 @@ function registerSourceRoutes(app, deps) {
1098
1244
  app.get("/api/sources", async (c) => {
1099
1245
  if (!deps.sources)
1100
1246
  return c.json({ sources: [] });
1101
- return c.json({ sources: await deps.sources.list() });
1247
+ const tenantId = c.get("tenantId");
1248
+ return c.json({ sources: await deps.sources.list(tenantId) });
1102
1249
  });
1103
1250
  app.post("/api/sources", async (c) => {
1104
1251
  if (!deps.sources)
@@ -1107,10 +1254,11 @@ function registerSourceRoutes(app, deps) {
1107
1254
  const parsed = parseSourceInsert(body);
1108
1255
  if (!parsed)
1109
1256
  return c.json({ error: "invalid source payload" }, 400);
1110
- if (await deps.sources.getByName(parsed.name)) {
1257
+ const tenantId = c.get("tenantId");
1258
+ if (await deps.sources.getByName(tenantId, parsed.name)) {
1111
1259
  return c.json({ error: `source named '${parsed.name}' already exists` }, 409);
1112
1260
  }
1113
- return c.json(await deps.sources.insert(parsed), 201);
1261
+ return c.json(await deps.sources.insert(tenantId, parsed), 201);
1114
1262
  });
1115
1263
  app.patch("/api/sources/:id", async (c) => {
1116
1264
  if (!deps.sources)
@@ -1122,7 +1270,8 @@ function registerSourceRoutes(app, deps) {
1122
1270
  const patch = parseSourceUpdate(body);
1123
1271
  if (!patch)
1124
1272
  return c.json({ error: "invalid patch payload" }, 400);
1125
- const updated = await deps.sources.update(id, patch);
1273
+ const tenantId = c.get("tenantId");
1274
+ const updated = await deps.sources.update(tenantId, id, patch);
1126
1275
  if (!updated)
1127
1276
  return c.json({ error: `source ${id} not found` }, 404);
1128
1277
  return c.json(updated);
@@ -1133,7 +1282,8 @@ function registerSourceRoutes(app, deps) {
1133
1282
  const id = Number.parseInt(c.req.param("id"), 10);
1134
1283
  if (!Number.isFinite(id))
1135
1284
  return c.json({ error: "invalid id" }, 400);
1136
- const ok = await deps.sources.delete(id);
1285
+ const tenantId = c.get("tenantId");
1286
+ const ok = await deps.sources.delete(tenantId, id);
1137
1287
  if (!ok)
1138
1288
  return c.json({ error: `source ${id} not found` }, 404);
1139
1289
  return c.json({ deleted: id });
@@ -1144,7 +1294,8 @@ function registerSourceRoutes(app, deps) {
1144
1294
  const id = Number.parseInt(c.req.param("id"), 10);
1145
1295
  if (!Number.isFinite(id))
1146
1296
  return c.json({ error: "invalid id" }, 400);
1147
- const token = await deps.sources.regenerateToken(id);
1297
+ const tenantId = c.get("tenantId");
1298
+ const token = await deps.sources.regenerateToken(tenantId, id);
1148
1299
  if (!token)
1149
1300
  return c.json({ error: "regenerate-token only applies to webhook sources" }, 400);
1150
1301
  return c.json({ token });
@@ -1185,7 +1336,11 @@ function registerIngestRoute(app, deps) {
1185
1336
  sourceId: source.id,
1186
1337
  };
1187
1338
  const ingest = deps.ingest;
1188
- void ingestSession(input, ingest).catch((e) => {
1339
+ // M4 (spec §3/§8): the ingested session is stamped with the resolved
1340
+ // source row's own tenant_id, not the caller's general-gate tenantId —
1341
+ // ingest auth is a distinct credential (the source's webhook token)
1342
+ // from the team_tokens Bearer auth the rest of /api/* uses.
1343
+ void ingestSession(input, ingest, source.tenantId).catch((e) => {
1189
1344
  const msg = e instanceof Error ? e.message : String(e);
1190
1345
  console.error(`[ingest] background failure for ${id}: ${msg}`);
1191
1346
  });
@@ -1199,7 +1354,8 @@ function registerProviderRoutes(app, deps) {
1199
1354
  app.get("/api/providers", async (c) => {
1200
1355
  if (!deps.providers)
1201
1356
  return c.json({ providers: [] });
1202
- return c.json({ providers: await deps.providers.list() });
1357
+ const tenantId = c.get("tenantId");
1358
+ return c.json({ providers: await deps.providers.list(tenantId) });
1203
1359
  });
1204
1360
  app.post("/api/providers", async (c) => {
1205
1361
  if (!deps.providers)
@@ -1208,10 +1364,11 @@ function registerProviderRoutes(app, deps) {
1208
1364
  const parsed = parseProviderInsert(body);
1209
1365
  if (!parsed)
1210
1366
  return c.json({ error: "invalid provider payload" }, 400);
1211
- if (await deps.providers.getByName(parsed.name)) {
1367
+ const tenantId = c.get("tenantId");
1368
+ if (await deps.providers.getByName(tenantId, parsed.name)) {
1212
1369
  return c.json({ error: `provider named '${parsed.name}' already exists` }, 409);
1213
1370
  }
1214
- return c.json(await deps.providers.insert(parsed), 201);
1371
+ return c.json(await deps.providers.insert(tenantId, parsed), 201);
1215
1372
  });
1216
1373
  app.patch("/api/providers/:id", async (c) => {
1217
1374
  if (!deps.providers)
@@ -1223,7 +1380,8 @@ function registerProviderRoutes(app, deps) {
1223
1380
  const patch = parseProviderUpdate(body);
1224
1381
  if (!patch)
1225
1382
  return c.json({ error: "invalid patch payload" }, 400);
1226
- const updated = await deps.providers.update(id, patch);
1383
+ const tenantId = c.get("tenantId");
1384
+ const updated = await deps.providers.update(tenantId, id, patch);
1227
1385
  if (!updated)
1228
1386
  return c.json({ error: `provider ${id} not found` }, 404);
1229
1387
  return c.json(updated);
@@ -1234,7 +1392,8 @@ function registerProviderRoutes(app, deps) {
1234
1392
  const id = Number.parseInt(c.req.param("id"), 10);
1235
1393
  if (!Number.isFinite(id))
1236
1394
  return c.json({ error: "invalid id" }, 400);
1237
- const ok = await deps.providers.delete(id);
1395
+ const tenantId = c.get("tenantId");
1396
+ const ok = await deps.providers.delete(tenantId, id);
1238
1397
  if (!ok)
1239
1398
  return c.json({ error: `provider ${id} not found` }, 404);
1240
1399
  return c.json({ deleted: id });
@@ -1245,10 +1404,11 @@ function registerProviderRoutes(app, deps) {
1245
1404
  const id = Number.parseInt(c.req.param("id"), 10);
1246
1405
  if (!Number.isFinite(id))
1247
1406
  return c.json({ error: "invalid id" }, 400);
1248
- const provider = await deps.providers.get(id);
1407
+ const tenantId = c.get("tenantId");
1408
+ const provider = await deps.providers.get(tenantId, id);
1249
1409
  if (!provider)
1250
1410
  return c.json({ error: `provider ${id} not found` }, 404);
1251
- const key = await deps.providers.getSecret(id);
1411
+ const key = await deps.providers.getSecret(tenantId, id);
1252
1412
  try {
1253
1413
  const models = await listModels(provider, { apiKey: key });
1254
1414
  return c.json({ models });
@@ -1264,10 +1424,11 @@ function registerProviderRoutes(app, deps) {
1264
1424
  const id = Number.parseInt(c.req.param("id"), 10);
1265
1425
  if (!Number.isFinite(id))
1266
1426
  return c.json({ error: "invalid id" }, 400);
1267
- const provider = await deps.providers.get(id);
1427
+ const tenantId = c.get("tenantId");
1428
+ const provider = await deps.providers.get(tenantId, id);
1268
1429
  if (!provider)
1269
1430
  return c.json({ error: `provider ${id} not found` }, 404);
1270
- const key = await deps.providers.getSecret(id);
1431
+ const key = await deps.providers.getSecret(tenantId, id);
1271
1432
  const startedAt = Date.now();
1272
1433
  try {
1273
1434
  const models = await listModels(provider, { apiKey: key });
@@ -1286,17 +1447,44 @@ function registerProviderRoutes(app, deps) {
1286
1447
  function registerSessionRoute(app, deps) {
1287
1448
  app.get("/api/session/:id", async (c) => {
1288
1449
  const id = c.req.param("id");
1289
- const session = await deps.store.getById(id);
1450
+ const tenantId = c.get("tenantId");
1451
+ const session = await deps.store.getById(tenantId, id);
1290
1452
  if (!session) {
1291
1453
  return c.json({ error: `session ${id} not found` }, 404);
1292
1454
  }
1293
1455
  return c.json(session);
1294
1456
  });
1457
+ // Learner strip data for the session drawer (spec 2026-10-06 M5): concept
1458
+ // rows for enrollments bound to this session's workstream, reusing the
1459
+ // same picker the recall hook uses. Always 200 with possibly-empty arrays
1460
+ // once the session resolves — the UI hides the section when empty.
1461
+ app.get("/api/learner/by-session/:id", async (c) => {
1462
+ const tenantId = c.get("tenantId");
1463
+ if (!deps.learnerRecall) {
1464
+ return c.json({ error: "learner recall not wired" }, 404);
1465
+ }
1466
+ const id = c.req.param("id");
1467
+ const session = await deps.store.getById(tenantId, id);
1468
+ if (!session) {
1469
+ return c.json({ error: `session ${id} not found` }, 404);
1470
+ }
1471
+ const concepts = await pickRelatedConcepts(tenantId, [{ id }], deps.learnerRecall);
1472
+ const wsMap = await deps.learnerRecall.sessions.getWorkstreamIds(tenantId, [id]);
1473
+ const wsId = wsMap.get(id);
1474
+ const enrollments = wsId
1475
+ ? await deps.learnerRecall.learners.listEnrollmentsByWorkstreams(tenantId, [wsId])
1476
+ : [];
1477
+ return c.json({
1478
+ enrollments: enrollments.map((e) => ({ learnerId: e.learnerId, courseId: e.courseId, status: e.status })),
1479
+ concepts,
1480
+ });
1481
+ });
1295
1482
  // Post-hoc supersedence write path. Mirrors the `mark_superseded` MCP tool
1296
1483
  // so UI + CLI + MCP all share one backend gesture. Path param is the
1297
1484
  // predecessor (the session being retired); body names the successor.
1298
1485
  app.post("/api/session/:id/supersede", async (c) => {
1299
1486
  const predecessorId = c.req.param("id");
1487
+ const tenantId = c.get("tenantId");
1300
1488
  let body;
1301
1489
  try {
1302
1490
  body = await c.req.json();
@@ -1317,7 +1505,7 @@ function registerSessionRoute(app, deps) {
1317
1505
  return c.json({ error: "reason must be a string if provided" }, 400);
1318
1506
  }
1319
1507
  try {
1320
- await deps.store.markSuperseded(predecessorId, successorId);
1508
+ await deps.store.markSuperseded(tenantId, predecessorId, successorId);
1321
1509
  }
1322
1510
  catch (e) {
1323
1511
  const msg = e instanceof Error ? e.message : String(e);
@@ -1326,7 +1514,7 @@ function registerSessionRoute(app, deps) {
1326
1514
  : 500;
1327
1515
  return c.json({ error: msg }, status);
1328
1516
  }
1329
- void appendSupersedence({
1517
+ void appendSupersedence(tenantId, {
1330
1518
  predecessorId,
1331
1519
  successorId,
1332
1520
  source: c.req.header("x-supersedence-source") ?? "http",
@@ -1361,7 +1549,8 @@ function parseActionInput(raw) {
1361
1549
  };
1362
1550
  }
1363
1551
  const VALID_SOURCE_KINDS = [
1364
- "claude-code", "codex", "hermes", "pi", "jsonl-generic", "webhook",
1552
+ "claude-code", "codex", "hermes", "hermes-agent", "muse", "aider", "cursor",
1553
+ "windsurf", "opencode", "pi", "jsonl-generic", "webhook",
1365
1554
  ];
1366
1555
  function parseSourceInsert(raw) {
1367
1556
  if (!raw || typeof raw !== "object")
@@ -1496,8 +1685,14 @@ function registerSignalRoutes(app, deps) {
1496
1685
  catch (e) {
1497
1686
  return c.json({ error: e instanceof Error ? e.message : "invalid signal" }, 400);
1498
1687
  }
1688
+ const rawBody = body;
1689
+ const tenantId = c.get("tenantId");
1690
+ signal = await stampSignalScope(signal, tenantId, {
1691
+ repoPath: typeof rawBody["repo_path"] === "string" ? rawBody["repo_path"] : null,
1692
+ sessionScopeReader: deps.sessionScopeReader,
1693
+ });
1499
1694
  try {
1500
- await deps.signalStore.insert(signal);
1695
+ await deps.signalStore.insert(tenantId, signal);
1501
1696
  }
1502
1697
  catch {
1503
1698
  return c.json({ error: "signal insert failed" }, 500);
@@ -1513,11 +1708,11 @@ function registerSignalRoutes(app, deps) {
1513
1708
  try {
1514
1709
  const exemplar = extractExemplar(signal, { installScope: deps.installScope });
1515
1710
  if (exemplar) {
1516
- const { id, skipped } = await exemplarStore.insert(exemplar);
1711
+ const { id, skipped } = await exemplarStore.insert(tenantId, exemplar);
1517
1712
  if (!skipped && deps.codeEmbedder) {
1518
1713
  void deps.codeEmbedder
1519
1714
  .embed(composeEmbedText(exemplar.taskContext, exemplar.code), "document")
1520
- .then((r) => exemplarStore.upsertEmbedding(id, r.vector))
1715
+ .then((r) => exemplarStore.upsertEmbedding(tenantId, id, r.vector))
1521
1716
  .catch(() => { });
1522
1717
  }
1523
1718
  }
@@ -1534,7 +1729,8 @@ function registerSignalRoutes(app, deps) {
1534
1729
  if (!repo)
1535
1730
  return c.json({ error: "repo is required" }, 400);
1536
1731
  const model = c.req.query("model");
1537
- const block = await buildFailureModeBlock(deps.signalStore, { installScope: deps.installScope, repo, ...(model ? { model } : {}) });
1732
+ const tenantId = c.get("tenantId");
1733
+ const block = await buildFailureModeBlock(tenantId, deps.signalStore, { installScope: deps.installScope, repo, ...(model ? { model } : {}) });
1538
1734
  return c.json({ repo, model: model ?? null, block });
1539
1735
  });
1540
1736
  app.get("/api/signals/stats", async (c) => {
@@ -1547,7 +1743,8 @@ function registerSignalRoutes(app, deps) {
1547
1743
  return c.json({ error: "days must be 1..365" }, 400);
1548
1744
  }
1549
1745
  const sinceTs = new Date(Date.now() - days * 86_400_000).toISOString();
1550
- const rows = await deps.signalStore.listForAggregation({ installScope: deps.installScope, sinceTs });
1746
+ const tenantId = c.get("tenantId");
1747
+ const rows = await deps.signalStore.listForAggregation(tenantId, { installScope: deps.installScope, sinceTs });
1551
1748
  const modes = aggregateFailureModes(rows);
1552
1749
  return c.json({ days, total: rows.length, modes });
1553
1750
  });
@@ -1576,11 +1773,13 @@ function registerSignalRoutes(app, deps) {
1576
1773
  catch (e) {
1577
1774
  return c.json({ error: e instanceof Error ? e.message : "invalid exemplar" }, 400);
1578
1775
  }
1579
- const { id, skipped } = await deps.exemplarStore.insert(inp);
1776
+ inp = { ...inp, scope: scopeStampEnabled() ? inp.scope : null };
1777
+ const tenantId = c.get("tenantId");
1778
+ const { id, skipped } = await deps.exemplarStore.insert(tenantId, inp);
1580
1779
  if (!skipped && deps.codeEmbedder) {
1581
1780
  // Best-effort embedding: fire-and-forget, never blocks the response.
1582
1781
  deps.codeEmbedder.embed(composeEmbedText(inp.taskContext, inp.code), "document")
1583
- .then((r) => deps.exemplarStore.upsertEmbedding(id, r.vector))
1782
+ .then((r) => deps.exemplarStore.upsertEmbedding(tenantId, id, r.vector))
1584
1783
  .catch(() => { });
1585
1784
  }
1586
1785
  return c.json({ id, skipped, status: "accepted" }, 202);
@@ -1608,7 +1807,8 @@ function registerSignalRoutes(app, deps) {
1608
1807
  if (patch.retired === undefined && patch.outcome === undefined) {
1609
1808
  return c.json({ error: "provide retire (boolean) and/or outcome" }, 400);
1610
1809
  }
1611
- const result = await deps.exemplarStore.setVerdict(c.req.param("id"), patch, "human");
1810
+ const tenantId = c.get("tenantId");
1811
+ const result = await deps.exemplarStore.setVerdict(tenantId, c.req.param("id"), patch, "human");
1612
1812
  if (result.status === "not_found")
1613
1813
  return c.json({ error: "exemplar not found" }, 404);
1614
1814
  return c.json({ id: c.req.param("id"), status: result.status }, 200);
@@ -1629,7 +1829,8 @@ function registerSignalRoutes(app, deps) {
1629
1829
  const lang = c.req.query("lang");
1630
1830
  const model = c.req.query("model");
1631
1831
  const includeNegatives = c.req.query("negatives") !== "0";
1632
- const result = await recallCode({
1832
+ const tenantId = c.get("tenantId");
1833
+ const result = await recallCode(tenantId, {
1633
1834
  query,
1634
1835
  installScope: deps.installScope,
1635
1836
  ...(repo ? { repo } : {}),
@@ -1641,6 +1842,52 @@ function registerSignalRoutes(app, deps) {
1641
1842
  return c.json(result);
1642
1843
  });
1643
1844
  }
1845
+ // ── Reprocess job supervision (single operator process, LOCAL disposition) ──
1846
+ // Thin HTTP wrapping over JobSupervisor (core/jobs/job-supervisor.ts).
1847
+ // start() is synchronous and throws BY DESIGN both when a run is already
1848
+ // active and when the injected spawnChild adapter itself fails to launch
1849
+ // the child — this route is the required catch point so neither case ever
1850
+ // crashes the daemon (binding note from Task 1's review).
1851
+ function registerJobRoutes(app, deps) {
1852
+ app.post("/api/jobs/reprocess", async (c) => {
1853
+ if (!deps.jobSupervisor)
1854
+ return c.json({ error: "job supervisor not wired in this deployment" }, 503);
1855
+ let body = null;
1856
+ try {
1857
+ const text = await c.req.text();
1858
+ body = text ? JSON.parse(text) : null;
1859
+ }
1860
+ catch {
1861
+ return c.json({ error: "body must be JSON" }, 400);
1862
+ }
1863
+ const rawArgs = body?.args;
1864
+ if (rawArgs !== undefined && (!Array.isArray(rawArgs) || !rawArgs.every((a) => typeof a === "string"))) {
1865
+ return c.json({ error: "args must be an array of strings" }, 400);
1866
+ }
1867
+ // Passed straight through as argv entries by the real spawnChild
1868
+ // adapter (never through a shell) — no interpolation risk here either
1869
+ // way, but validating shape (not content) keeps this a passthrough,
1870
+ // exactly as the reprocess CLI itself validates its own flags.
1871
+ const args = rawArgs ?? [];
1872
+ try {
1873
+ deps.jobSupervisor.start(args);
1874
+ }
1875
+ catch (e) {
1876
+ const message = e instanceof Error ? e.message : String(e);
1877
+ if (message === "reprocess job already active") {
1878
+ return c.json({ error: message, snapshot: deps.jobSupervisor.snapshot() }, 409);
1879
+ }
1880
+ return c.json({ error: message }, 500);
1881
+ }
1882
+ return c.json({ ok: true, snapshot: deps.jobSupervisor.snapshot() }, 202);
1883
+ });
1884
+ app.delete("/api/jobs/reprocess", (c) => {
1885
+ if (!deps.jobSupervisor)
1886
+ return c.json({ error: "job supervisor not wired in this deployment" }, 503);
1887
+ deps.jobSupervisor.stop();
1888
+ return c.json({ ok: true, snapshot: deps.jobSupervisor.snapshot() }, 200);
1889
+ });
1890
+ }
1644
1891
  function mountSpa(app, dist) {
1645
1892
  const indexHtml = join(dist, "index.html");
1646
1893
  if (!existsSync(indexHtml))