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
@@ -26,15 +26,28 @@ import { composeWorkstreamRecall } from "../core/workstream/compose-recall.js";
26
26
  import { normalizeLabel } from "../core/workstream/model.js";
27
27
  import { resolveWorkstreamId } from "../core/workstream/resolve.js";
28
28
  import { suggestMerges } from "../core/workstream/merge-suggest.js";
29
+ import { normalizeSignal } from "../core/signals/ingest-signal.js";
30
+ import { stampSignalScope } from "../core/signals/stamp-scope.js";
31
+ import { deriveOutcome } from "../core/outcome/rollup.js";
32
+ import { buildSqliteOutcomeDeps } from "../core/storage/sqlite-outcome-store.js";
33
+ import { completeLearnerCourse, enrollCourse, getCourseSummary, recordLearningEvent, } from "../core/learner/service.js";
34
+ import { truncateSummary } from "../core/hook/pointer-block.js";
35
+ import { SIGNAL_OUTCOMES } from "../shared/types.js";
29
36
  const CHARACTER_LIMIT = 25_000;
30
37
  const DEFAULT_LIMIT = 10;
31
38
  const SERVER_NAME = "nlm-memory-mcp-server";
32
39
  const SERVER_VERSION = "0.5.9";
33
40
  /** TOON encoding cuts token usage on large recall payloads. Opt in via
34
- * NLM_FORMAT=toon in the MCP server's env (see .mcp.json). Defaults to JSON. */
35
- const USE_TOON = process.env.NLM_FORMAT === "toon";
41
+ * NLM_FORMAT=toon. Defaults to JSON.
42
+ *
43
+ * Read at call time, not module scope: on the HTTP transport this module is
44
+ * imported by the daemon long before buildStack() calls autoloadEnv(), so an
45
+ * eager read would capture the environment before ~/.nlm/.env was loaded and
46
+ * silently ignore the setting. Only the stdio path, where the runtime passes
47
+ * env directly to the child, would have worked. */
48
+ const useToon = () => process.env.NLM_FORMAT === "toon";
36
49
  function format(data) {
37
- if (USE_TOON) {
50
+ if (useToon()) {
38
51
  try {
39
52
  return toonEncode(data);
40
53
  }
@@ -86,7 +99,7 @@ export function mcpRuntimeFromClient(client) {
86
99
  const name = client?.name?.trim().toLowerCase();
87
100
  return name ? name : null;
88
101
  }
89
- export async function recallSessionsHandler(deps, input, runtime = null) {
102
+ export async function recallSessionsHandler(deps, tenantId, input, runtime = null) {
90
103
  try {
91
104
  const rewrite = input.rewrite ?? mcpRewriteDefault();
92
105
  const query = {
@@ -102,12 +115,12 @@ export async function recallSessionsHandler(deps, input, runtime = null) {
102
115
  ...(input.kind !== undefined ? { kind: input.kind } : {}),
103
116
  ...(input.workstream !== undefined ? { workstream: input.workstream } : {}),
104
117
  };
105
- const result = await deps.recall.search(query);
118
+ const result = await deps.recall.search(tenantId, query);
106
119
  const conversationId = resolveConversationByQuery(input.query ?? "") ?? undefined;
107
120
  // Telemetry — the MCP path is the real agent-usage path; without this it
108
121
  // is invisible to query_log.jsonl and the Recall page. Fire-and-forget,
109
122
  // mirrors the HTTP /api/recall handler.
110
- void logQuery({
123
+ void logQuery(tenantId, {
111
124
  source: "mcp",
112
125
  runtime,
113
126
  query: input.query ?? null,
@@ -125,20 +138,20 @@ export async function recallSessionsHandler(deps, input, runtime = null) {
125
138
  return err(e);
126
139
  }
127
140
  }
128
- export async function workSummaryHandler(deps, input) {
141
+ export async function workSummaryHandler(deps, tenantId, input) {
129
142
  if (!deps.workDigest) {
130
143
  return okText("work_summary is not available in this deployment.");
131
144
  }
132
145
  try {
133
146
  const date = input.date ?? localToday();
134
- const digest = await buildWorkDigest(deps.workDigest, date);
147
+ const digest = await buildWorkDigest(deps.workDigest, tenantId, date);
135
148
  return okText(composeWorkDigest(digest));
136
149
  }
137
150
  catch (e) {
138
151
  return err(e);
139
152
  }
140
153
  }
141
- export async function recallWorkstreamHandler(deps, input, runtime = null) {
154
+ export async function recallWorkstreamHandler(deps, tenantId, input, runtime = null) {
142
155
  if (!deps.workstreams)
143
156
  return okText("recall_workstream is not available in this deployment.");
144
157
  try {
@@ -147,13 +160,13 @@ export async function recallWorkstreamHandler(deps, input, runtime = null) {
147
160
  return okText("Provide a workstream id or label.");
148
161
  const conversationId = resolveConversationByQuery(idOrLabel) ?? undefined;
149
162
  const ws = deps.workstreams.store;
150
- const found = (await ws.getById(idOrLabel)) ?? (await ws.findByNormalizedLabel(normalizeLabel(idOrLabel)));
163
+ const found = (await ws.getById(tenantId, idOrLabel)) ?? (await ws.findByNormalizedLabel(tenantId, normalizeLabel(idOrLabel)));
151
164
  if (!found)
152
165
  return okText(`No workstream matches "${idOrLabel}".`);
153
- const view = await rollupWorkstream({ workstreams: deps.workstreams.store, sessions: deps.workstreams.sessions, facts: deps.workstreams.facts, exemplars: deps.workstreams.exemplars }, found.id);
166
+ const view = await rollupWorkstream({ workstreams: deps.workstreams.store, sessions: deps.workstreams.sessions, facts: deps.workstreams.facts, exemplars: deps.workstreams.exemplars }, tenantId, found.id);
154
167
  if (!view)
155
168
  return okText(`No workstream matches "${idOrLabel}".`);
156
- void logQuery({
169
+ void logQuery(tenantId, {
157
170
  source: "mcp",
158
171
  runtime,
159
172
  query: idOrLabel,
@@ -171,17 +184,17 @@ export async function recallWorkstreamHandler(deps, input, runtime = null) {
171
184
  return err(e);
172
185
  }
173
186
  }
174
- /** idOrLabel -> live survivor workstream (merged_into resolved) | null. One source of truth for lifecycle handlers. */
175
- async function resolveWorkstream(store, idOrLabel) {
176
- const found = (await store.getById(idOrLabel)) ?? (await store.findByNormalizedLabel(normalizeLabel(idOrLabel)));
187
+ /** idOrLabel -> live survivor workstream (merged_into resolved) | null, within tenantId. One source of truth for lifecycle handlers. */
188
+ async function resolveWorkstream(store, tenantId, idOrLabel) {
189
+ const found = (await store.getById(tenantId, idOrLabel)) ?? (await store.findByNormalizedLabel(tenantId, normalizeLabel(idOrLabel)));
177
190
  if (!found)
178
191
  return null;
179
- const all = await store.listAll();
192
+ const all = await store.listAll(tenantId);
180
193
  const byId = new Map(all.map((w) => [w.id, { id: w.id, mergedInto: w.mergedInto }]));
181
194
  const survivorId = resolveWorkstreamId(found.id, byId);
182
- return survivorId === found.id ? found : ((await store.getById(survivorId)) ?? found);
195
+ return survivorId === found.id ? found : ((await store.getById(tenantId, survivorId)) ?? found);
183
196
  }
184
- export async function renameWorkstreamHandler(deps, input) {
197
+ export async function renameWorkstreamHandler(deps, tenantId, input) {
185
198
  if (!deps.workstreams)
186
199
  return okText("rename_workstream is not available in this deployment.");
187
200
  try {
@@ -190,38 +203,38 @@ export async function renameWorkstreamHandler(deps, input) {
190
203
  if (!idOrLabel || !label)
191
204
  return okText("Provide the workstream (id or label) and the new label.");
192
205
  const ws = deps.workstreams.store;
193
- const target = await resolveWorkstream(ws, idOrLabel);
206
+ const target = await resolveWorkstream(ws, tenantId, idOrLabel);
194
207
  if (!target)
195
208
  return okText(`No workstream matches "${idOrLabel}".`);
196
- const collision = await ws.findByNormalizedLabel(normalizeLabel(label));
209
+ const collision = await ws.findByNormalizedLabel(tenantId, normalizeLabel(label));
197
210
  if (collision && collision.id !== target.id) {
198
211
  return okText(`Label "${label}" is already used by workstream "${collision.label}" (${collision.id}).`);
199
212
  }
200
- await ws.setLabel(target.id, label);
213
+ await ws.setLabel(tenantId, target.id, label);
201
214
  return okText(`Renamed workstream ${target.id}: "${target.label}" -> "${label}".`);
202
215
  }
203
216
  catch (e) {
204
217
  return err(e);
205
218
  }
206
219
  }
207
- export async function retireWorkstreamHandler(deps, input) {
220
+ export async function retireWorkstreamHandler(deps, tenantId, input) {
208
221
  if (!deps.workstreams)
209
222
  return okText("retire_workstream is not available in this deployment.");
210
223
  try {
211
224
  const idOrLabel = (input.idOrLabel ?? "").trim();
212
225
  if (!idOrLabel)
213
226
  return okText("Provide a workstream id or label.");
214
- const target = await resolveWorkstream(deps.workstreams.store, idOrLabel);
227
+ const target = await resolveWorkstream(deps.workstreams.store, tenantId, idOrLabel);
215
228
  if (!target)
216
229
  return okText(`No workstream matches "${idOrLabel}".`);
217
- await deps.workstreams.store.setStatus(target.id, "retired");
230
+ await deps.workstreams.store.setStatus(tenantId, target.id, "retired");
218
231
  return okText(`Retired workstream "${target.label}" (${target.id}).`);
219
232
  }
220
233
  catch (e) {
221
234
  return err(e);
222
235
  }
223
236
  }
224
- export async function mergeWorkstreamsHandler(deps, input) {
237
+ export async function mergeWorkstreamsHandler(deps, tenantId, input) {
225
238
  if (!deps.workstreams)
226
239
  return okText("merge_workstreams is not available in this deployment.");
227
240
  try {
@@ -230,22 +243,22 @@ export async function mergeWorkstreamsHandler(deps, input) {
230
243
  if (!fromArg || !intoArg)
231
244
  return okText("Provide both `from` and `into` (workstream id or label).");
232
245
  const ws = deps.workstreams.store;
233
- const from = await resolveWorkstream(ws, fromArg);
234
- const into = await resolveWorkstream(ws, intoArg);
246
+ const from = await resolveWorkstream(ws, tenantId, fromArg);
247
+ const into = await resolveWorkstream(ws, tenantId, intoArg);
235
248
  if (!from)
236
249
  return okText(`No workstream matches "${fromArg}".`);
237
250
  if (!into)
238
251
  return okText(`No workstream matches "${intoArg}".`);
239
252
  if (from.id === into.id)
240
253
  return okText(`"${fromArg}" and "${intoArg}" resolve to the same workstream — nothing to merge.`);
241
- await ws.merge(from.id, into.id);
254
+ await ws.merge(tenantId, from.id, into.id);
242
255
  return okText(`Merged "${from.label}" (${from.id}) into "${into.label}" (${into.id}).`);
243
256
  }
244
257
  catch (e) {
245
258
  return err(e);
246
259
  }
247
260
  }
248
- export async function rebindSessionHandler(deps, input) {
261
+ export async function rebindSessionHandler(deps, tenantId, input) {
249
262
  if (!deps.workstreams)
250
263
  return okText("rebind_session is not available in this deployment.");
251
264
  try {
@@ -253,10 +266,10 @@ export async function rebindSessionHandler(deps, input) {
253
266
  const wsArg = (input.workstream ?? "").trim();
254
267
  if (!sessionId || !wsArg)
255
268
  return okText("Provide both a sessionId and a workstream (id or label).");
256
- const ws = await resolveWorkstream(deps.workstreams.store, wsArg);
269
+ const ws = await resolveWorkstream(deps.workstreams.store, tenantId, wsArg);
257
270
  if (!ws)
258
271
  return okText(`No workstream matches "${wsArg}".`);
259
- await deps.store.setWorkstreamBinding(sessionId, ws.id, "operator", null);
272
+ await deps.store.setWorkstreamBinding(tenantId, sessionId, ws.id, "operator", null);
260
273
  return okText(`Rebound session ${sessionId} -> workstream "${ws.label}" (${ws.id}).`);
261
274
  }
262
275
  catch (e) {
@@ -270,9 +283,9 @@ function localToday() {
270
283
  const da = String(d.getDate()).padStart(2, "0");
271
284
  return `${d.getFullYear()}-${mo}-${da}`;
272
285
  }
273
- export async function getSessionHandler(deps, input) {
286
+ export async function getSessionHandler(deps, tenantId, input) {
274
287
  try {
275
- const session = await deps.store.getById(input.id);
288
+ const session = await deps.store.getById(tenantId, input.id);
276
289
  if (!session) {
277
290
  return err(new Error(`session ${input.id} not found`));
278
291
  }
@@ -281,38 +294,58 @@ export async function getSessionHandler(deps, input) {
281
294
  ...(session.supersedes ?? []),
282
295
  ...(session.supersededBy ? [session.supersededBy] : []),
283
296
  ];
284
- const linked = linkedIds.length > 0 ? await deps.store.getByIds(linkedIds) : [];
297
+ const linked = linkedIds.length > 0 ? await deps.store.getByIds(tenantId, linkedIds) : [];
285
298
  const byId = new Map(linked.map((s) => [s.id, s]));
286
299
  // Load supersedence log once so we can join reason + recordedBy onto supersededBy
287
300
  const supersedenceLog = session.supersededBy
288
- ? await readSupersedenceLog()
301
+ ? await readSupersedenceLog(tenantId)
289
302
  : [];
290
303
  const supersedenceMap = new Map(supersedenceLog.map((e) => [e.predecessorId, e]));
291
304
  const supersedes = (session.supersedes ?? []).map((id) => {
292
305
  const s = byId.get(id);
293
306
  return s ? { id, label: s.label, summary: s.summary } : { id, label: "", summary: "" };
294
307
  });
308
+ // Supersedence-enrichment fencing (program spec §4.6 hardening 2): even
309
+ // though readSupersedenceLog now reads the caller's own tenant file, a
310
+ // log entry's `reason`/`recordedBy` may only surface for a successor id
311
+ // that resolved through the tenant-filtered getByIds above (`byId`) —
312
+ // defense in depth against any pre-M6 legacy entry that predates
313
+ // per-tenant log files. An unresolved id contributes nothing — not even
314
+ // its reason — same as the by-id not-found shape (case 4).
295
315
  const supersededBy = session.supersededBy
296
316
  ? (() => {
297
317
  const s = byId.get(session.supersededBy);
318
+ if (!s)
319
+ return { id: session.supersededBy, label: "", summary: "" };
298
320
  const logEntry = supersedenceMap.get(session.id);
299
- const base = s
300
- ? { id: session.supersededBy, label: s.label, summary: s.summary }
301
- : { id: session.supersededBy, label: "", summary: "" };
302
321
  return {
303
- ...base,
322
+ id: session.supersededBy,
323
+ label: s.label,
324
+ summary: s.summary,
304
325
  reason: logEntry?.reason,
305
326
  recordedBy: logEntry?.source,
306
327
  };
307
328
  })()
308
329
  : null;
309
- return ok({ ...session, supersedes, supersededBy });
330
+ // Isolated: the outcome rollup is enrichment, not the payload. A throw
331
+ // here (SQLITE_BUSY, stale handle, unmigrated db) must degrade to
332
+ // no-outcome, never fail the whole get_session call.
333
+ let outcome;
334
+ if (deps.outcomeDb) {
335
+ try {
336
+ outcome = await deriveOutcome(tenantId, session.id, await buildSqliteOutcomeDeps(deps.outcomeDb, tenantId));
337
+ }
338
+ catch {
339
+ outcome = undefined;
340
+ }
341
+ }
342
+ return ok({ ...session, supersedes, supersededBy, ...(outcome ? { outcome } : {}) });
310
343
  }
311
344
  catch (e) {
312
345
  return err(e);
313
346
  }
314
347
  }
315
- export async function recallFactsHandler(deps, input, runtime = null) {
348
+ export async function recallFactsHandler(deps, tenantId, input, runtime = null) {
316
349
  if (!deps.factRecall) {
317
350
  return err(new Error("fact recall not wired in this deployment"));
318
351
  }
@@ -331,10 +364,10 @@ export async function recallFactsHandler(deps, input, runtime = null) {
331
364
  ? { minConfidence: input.minConfidence }
332
365
  : {}),
333
366
  };
334
- const result = await deps.factRecall.search(query);
367
+ const result = await deps.factRecall.search(tenantId, query);
335
368
  const conversationId = resolveConversationByQuery(input.query ?? "") ?? undefined;
336
369
  // Telemetry — see recallSessionsHandler. Fire-and-forget.
337
- void logFactQuery({
370
+ void logFactQuery(tenantId, {
338
371
  source: "mcp",
339
372
  runtime,
340
373
  query: input.query ?? null,
@@ -353,12 +386,12 @@ export async function recallFactsHandler(deps, input, runtime = null) {
353
386
  return err(e);
354
387
  }
355
388
  }
356
- export async function getFactHistoryHandler(deps, input) {
389
+ export async function getFactHistoryHandler(deps, tenantId, input) {
357
390
  if (!deps.factStore) {
358
391
  return err(new Error("fact store not wired in this deployment"));
359
392
  }
360
393
  try {
361
- const chains = await deps.factStore.getHistory(input.subject, input.predicate);
394
+ const chains = await deps.factStore.getHistory(tenantId, input.subject, input.predicate);
362
395
  return ok({ subject: input.subject, predicate: input.predicate ?? null, chains });
363
396
  }
364
397
  catch (e) {
@@ -427,6 +460,10 @@ run, or any quote you intend to reference verbatim.
427
460
  The recall_sessions digest is optimized for ranking and scanning; the full body
428
461
  contains the actual conversation transcript that produced the decision.
429
462
 
463
+ When wired, the response includes an \`outcome\` object: whether the session's
464
+ decision held, was overturned, was built upon, was re-derived later, or
465
+ remains unobserved, plus the evidence tier/confidence behind that verdict.
466
+
430
467
  Args:
431
468
  - id: Canonical session ID returned by recall_sessions (e.g. "cc_abc123",
432
469
  "sess_pgvector"). Pass the id field from the recall_sessions result.`;
@@ -516,13 +553,13 @@ Args:
516
553
 
517
554
  Idempotent. Re-marking the same pair is a no-op. Returns the linked pair on
518
555
  success; errors if either id is unknown or the two ids are equal.`;
519
- export async function markSupersededHandler(deps, input) {
556
+ export async function markSupersededHandler(deps, tenantId, input) {
520
557
  if (!input.predecessor_id || !input.successor_id) {
521
558
  return err(new Error("predecessor_id and successor_id are required"));
522
559
  }
523
560
  try {
524
- await deps.store.markSuperseded(input.predecessor_id, input.successor_id);
525
- void appendSupersedence({
561
+ await deps.store.markSuperseded(tenantId, input.predecessor_id, input.successor_id);
562
+ void appendSupersedence(tenantId, {
526
563
  predecessorId: input.predecessor_id,
527
564
  successorId: input.successor_id,
528
565
  source: "mcp",
@@ -560,7 +597,7 @@ Args:
560
597
 
561
598
  Idempotent: calling it twice on the same id is a no-op. Errors if fact_id
562
599
  is unknown.`;
563
- export async function supersedeFactHandler(deps, input) {
600
+ export async function supersedeFactHandler(deps, tenantId, input) {
564
601
  if (!input.fact_id || input.fact_id.length < 4) {
565
602
  return err(new Error("fact_id is required"));
566
603
  }
@@ -568,8 +605,8 @@ export async function supersedeFactHandler(deps, input) {
568
605
  return err(new Error("fact store not available"));
569
606
  }
570
607
  try {
571
- await deps.factStore.retire(input.fact_id);
572
- void appendFactSupersedence({
608
+ await deps.factStore.retire(tenantId, input.fact_id);
609
+ void appendFactSupersedence(tenantId, {
573
610
  factId: input.fact_id,
574
611
  source: "mcp",
575
612
  ...(input.reason !== undefined ? { reason: input.reason } : {}),
@@ -584,39 +621,96 @@ export async function supersedeFactHandler(deps, input) {
584
621
  return err(e);
585
622
  }
586
623
  }
587
- export async function citeSessionHandler(input) {
624
+ export async function citeSessionHandler(tenantId, input) {
588
625
  if (!input.id || input.id.length < MIN_CITE_ID_LEN) {
589
626
  return err(new Error(`id must be at least ${MIN_CITE_ID_LEN} characters`));
590
627
  }
591
628
  try {
592
- await appendCitation({
593
- // Agents rarely pass conversation_id; resolve it server-side from the
594
- // surfaced-memo so the citation joins to its hook fire (NLM #345).
595
- conversationId: input.conversation_id ?? resolveConversationForSession(input.id) ?? "mcp_tool",
629
+ // Agents rarely pass conversation_id, so resolve it server-side. The
630
+ // surfaced-memo only knows sessions the recall HOOK injected, which is
631
+ // empty whenever ambient recall is off or failing — that gap silently
632
+ // unattributed every pull-driven citation. Fall back to the same
633
+ // transcript scan the recall handlers use: the runtime has already
634
+ // written this cite_session tool_use (carrying `id`) to the transcript
635
+ // before this handler runs, so the id is a reliable join key.
636
+ const conversationId = input.conversation_id ??
637
+ resolveConversationForSession(tenantId, input.id) ??
638
+ resolveConversationByQuery(input.id) ??
639
+ "mcp_tool";
640
+ const logged = await appendCitation(tenantId, {
641
+ conversationId,
596
642
  citedId: input.id,
597
643
  kind: "tool_use",
598
644
  ...(input.reason !== undefined ? { responsePreview: input.reason } : {}),
599
645
  });
600
- return ok({ logged: true, id: input.id });
646
+ // An unattributable citation is dropped by the write path; reporting it as
647
+ // logged would make a dead telemetry path look healthy.
648
+ return ok({ logged, id: input.id, ...(logged ? {} : { reason: "unattributable_conversation" }) });
601
649
  }
602
650
  catch (e) {
603
651
  return err(e);
604
652
  }
605
653
  }
606
- export async function listMergeSuggestionsHandler(deps, input) {
654
+ const REPORT_OUTCOME_DESCRIPTION = `Record whether a session's decision held up or was overturned. THIS IS THE CONTRACT SURFACE for outcome signals: it writes the same append-only \`signals\` row that POST /api/signal writes (producer "mcp"), just over MCP instead of HTTP.
655
+
656
+ Expected callers are deterministic producers: hooks, CI pipelines, and automation platforms confirming a build/deploy/test result against a prior session. Agents rarely self-report their own outcome; outcome should come from independent evidence, not the agent's opinion of its own work.
657
+
658
+ Args:
659
+ - session_id or correlation_key: at least one required. session_id is the NLM session this outcome applies to. correlation_key is an external identifier (e.g. a CI run id) to use when no NLM session_id is known; it is recorded on the row for traceability but is not (yet) joined into the Tier-B outcome rollup.
660
+ - outcome: one of pass | fail | fix | exhausted, the same vocabulary POST /api/signal accepts.
661
+ - source_of_record: required. Identifies the deterministic system asserting this outcome (e.g. "ci:github-actions", "hook:post-tool-use", "n8n:deploy-workflow").
662
+ - detail: optional passthrough object for additional context.`;
663
+ /** The kind stamped on rows this tool writes. Loosely, this row represents an
664
+ * outcome *review* of a prior session's decision, and "review" is the closest
665
+ * fit among the existing SignalKind vocabulary (gate|eval|review|test); no
666
+ * literal "outcome" kind exists (see rollup.ts's own note on this same
667
+ * ambiguity). Tier-A correlation in rollup.ts keys on session_id only, so
668
+ * this choice does not affect verdict derivation. */
669
+ const REPORT_OUTCOME_KIND = "review";
670
+ export async function reportOutcomeHandler(deps, tenantId, input) {
671
+ if (!input.session_id && !input.correlation_key) {
672
+ return err(new Error("session_id or correlation_key is required"));
673
+ }
674
+ if (!input.source_of_record || input.source_of_record.length === 0) {
675
+ return err(new Error("source_of_record is required"));
676
+ }
677
+ if (!deps.signalStore || deps.installScope === undefined) {
678
+ return err(new Error("signal store not available"));
679
+ }
680
+ try {
681
+ let signal = normalizeSignal({
682
+ kind: REPORT_OUTCOME_KIND,
683
+ producer: "mcp",
684
+ outcome: input.outcome,
685
+ session: input.session_id ?? null,
686
+ detail: {
687
+ ...(input.detail ?? {}),
688
+ source_of_record: input.source_of_record,
689
+ ...(input.correlation_key ? { correlation_key: input.correlation_key } : {}),
690
+ },
691
+ }, deps.installScope);
692
+ signal = await stampSignalScope(signal, tenantId, { sessionScopeReader: deps.sessionScopeReader });
693
+ await deps.signalStore.insert(tenantId, signal);
694
+ return ok({ recorded: true, id: signal.id, outcome: signal.outcome });
695
+ }
696
+ catch (e) {
697
+ return err(e);
698
+ }
699
+ }
700
+ export async function listMergeSuggestionsHandler(deps, tenantId, input) {
607
701
  if (!deps.workstreams)
608
702
  return okText("list_merge_suggestions is not available in this deployment.");
609
703
  try {
610
704
  const minScore = typeof input.minScore === "number" ? input.minScore : 0.5;
611
- const all = (await deps.workstreams.store.listAll()).filter((w) => w.status === "active");
705
+ const all = (await deps.workstreams.store.listAll(tenantId)).filter((w) => w.status === "active");
612
706
  if (all.length < 2)
613
707
  return okText("Not enough active workstreams to suggest merges.");
614
708
  const ids = all.map((w) => w.id);
615
- const entMap = await deps.workstreams.store.entitiesFor(ids);
709
+ const entMap = await deps.workstreams.store.entitiesFor(tenantId, ids);
616
710
  const items = await Promise.all(all.map(async (w) => ({
617
711
  id: w.id, label: w.label,
618
712
  entities: entMap.get(w.id) ?? [],
619
- sessionIds: await deps.workstreams.sessions.listSessionIdsByWorkstreams([w.id]),
713
+ sessionIds: await deps.workstreams.sessions.listSessionIdsByWorkstreams(tenantId, [w.id]),
620
714
  })));
621
715
  const suggestions = suggestMerges(items, minScore);
622
716
  if (suggestions.length === 0)
@@ -632,7 +726,144 @@ export async function listMergeSuggestionsHandler(deps, input) {
632
726
  return err(e);
633
727
  }
634
728
  }
635
- export function createMcpServer(deps) {
729
+ const LEARNER_ENROLL_DESCRIPTION = `\
730
+ Enroll a learner in a course, binding the enrollment to a workstream so later
731
+ sessions resolve their learning state at recall time. The course plan itself
732
+ stays with the caller — NLM stores only its reference, revision, and
733
+ fingerprint, and refuses writes when a re-enrolled plan differs.`;
734
+ const LEARNER_RECORD_DESCRIPTION = `\
735
+ Record a learning event: concepts covered plus per-concept attempts (task,
736
+ response, result/help/kind), an interpretation, and the next step. Identical
737
+ event ids are safe no-ops; conflicting ids fail rather than rewriting
738
+ history. A submitted exercise is not evidence until recorded here.`;
739
+ const LEARNER_SUMMARY_DESCRIPTION = `\
740
+ Summarize what a learner has demonstrably learned in a course: per-concept
741
+ status (exposed/practicing/demonstrated/retained), the latest next step, and
742
+ sessions bound to the enrollment's workstream since the last recorded event
743
+ (unrecorded candidates — record them with learner_record, don't assume).`;
744
+ const LEARNER_COMPLETE_DESCRIPTION = `\
745
+ Mark a learner's course completed. Preserves completion history and actual
746
+ evidence; untaught or untested topics stay explicitly marked.`;
747
+ const LEARNER_SUMMARY_DEFAULT_LIMIT = 50;
748
+ const LEARNER_UNRECORDED_CAP = 10;
749
+ export async function learnerEnrollHandler(deps, tenantId, input) {
750
+ if (!deps.learners)
751
+ return okText("learner tools are not available in this deployment.");
752
+ try {
753
+ const enrollment = await enrollCourse({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, {
754
+ learnerId: input.learner,
755
+ ...(input.label !== undefined ? { learnerLabel: input.label } : {}),
756
+ courseId: input.course,
757
+ workstream: input.workstream,
758
+ planRef: input.plan_ref,
759
+ planRevision: input.plan_revision,
760
+ planFingerprint: input.plan_fingerprint,
761
+ });
762
+ return okText(`Enrolled ${input.learner} in ${input.course} (${enrollment.status}, plan rev ${enrollment.planRevision}).`);
763
+ }
764
+ catch (e) {
765
+ return err(e);
766
+ }
767
+ }
768
+ export async function learnerRecordHandler(deps, tenantId, input) {
769
+ if (!deps.learners)
770
+ return okText("learner tools are not available in this deployment.");
771
+ try {
772
+ const result = await recordLearningEvent({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, input.learner, input.course, {
773
+ id: input.event.id,
774
+ date: input.event.date,
775
+ covered: input.event.covered,
776
+ attempts: input.event.attempts,
777
+ interpretation: input.event.interpretation,
778
+ nextStep: input.event.next_step,
779
+ });
780
+ return result === "inserted"
781
+ ? okText(`Recorded event ${input.event.id} for ${input.learner}/${input.course}.`)
782
+ : okText(`Event ${input.event.id} already recorded (identical) — no change.`);
783
+ }
784
+ catch (e) {
785
+ return err(e);
786
+ }
787
+ }
788
+ export async function learnerSummaryHandler(deps, tenantId, input) {
789
+ if (!deps.learners)
790
+ return okText("learner tools are not available in this deployment.");
791
+ try {
792
+ let course = input.course?.trim() ? input.course.trim() : undefined;
793
+ if (!course) {
794
+ const enrollments = await deps.learners.store.listEnrollments(tenantId, input.learner);
795
+ const active = enrollments.filter((e) => e.status === "active");
796
+ if (active.length === 0)
797
+ return okText(`No active enrollments for learner ${input.learner}.`);
798
+ if (active.length > 1) {
799
+ return okText(`Multiple active courses for ${input.learner}; specify course: ${active.map((e) => e.courseId).join(", ")}.`);
800
+ }
801
+ course = active[0].courseId;
802
+ }
803
+ const summary = await getCourseSummary({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, input.learner, course);
804
+ if (!summary)
805
+ return okText(`No enrollment for learner ${input.learner} in course ${course}.`);
806
+ const lines = [];
807
+ lines.push(`LEARNER ${summary.learnerId} · ${summary.courseId} (${summary.status}, rev ${summary.planRevision}, ${summary.eventCount} events)`);
808
+ const limit = input.limit ?? LEARNER_SUMMARY_DEFAULT_LIMIT;
809
+ const concepts = summary.concepts.slice(0, Math.max(0, limit));
810
+ if (concepts.length === 0) {
811
+ lines.push("(no concepts recorded yet)");
812
+ }
813
+ else {
814
+ lines.push("CONCEPTS:");
815
+ for (const c of concepts) {
816
+ const n = c.attemptCount === 1 ? "1 attempt" : `${c.attemptCount} attempts`;
817
+ lines.push(` - [${c.status}] ${c.concept} (${n})`);
818
+ }
819
+ if (summary.concepts.length > concepts.length) {
820
+ lines.push(` (+${summary.concepts.length - concepts.length} more — raise limit)`);
821
+ }
822
+ }
823
+ lines.push(`Next: ${summary.nextStep ? truncateSummary(summary.nextStep) : "(none recorded)"}`);
824
+ if (summary.workstreamId) {
825
+ const ids = await deps.learners.sessions.listSessionIdsByWorkstreams(tenantId, [summary.workstreamId]);
826
+ if (ids.length > 0) {
827
+ const sessions = await deps.learners.sessions.getByIds(tenantId, ids);
828
+ const cutoff = summary.lastEventDate ?? "";
829
+ const unrecorded = sessions
830
+ .filter((s) => s.startedAt.slice(0, 10) > cutoff)
831
+ .sort((a, b) => (a.startedAt < b.startedAt ? -1 : 1))
832
+ .slice(0, LEARNER_UNRECORDED_CAP);
833
+ if (unrecorded.length > 0) {
834
+ lines.push(`Unrecorded sessions (${unrecorded.length}):`);
835
+ for (const s of unrecorded) {
836
+ lines.push(` - ${s.id} · ${truncateSummary(s.label, 80)} (${s.startedAt.slice(0, 10)})`);
837
+ }
838
+ }
839
+ }
840
+ }
841
+ return okText(lines.join("\n"));
842
+ }
843
+ catch (e) {
844
+ return err(e);
845
+ }
846
+ }
847
+ export async function learnerCompleteHandler(deps, tenantId, input) {
848
+ if (!deps.learners)
849
+ return okText("learner tools are not available in this deployment.");
850
+ try {
851
+ await completeLearnerCourse({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, input.learner, input.course);
852
+ return okText(`Marked ${input.course} completed for ${input.learner}.`);
853
+ }
854
+ catch (e) {
855
+ return err(e);
856
+ }
857
+ }
858
+ /**
859
+ * The composition root for the MCP surface (program spec §4.6 "M2 threading
860
+ * semantics until M3"): `tenantId` is required here, not defaulted, and
861
+ * every tool handler below receives it explicitly rather than reading a
862
+ * module-level constant. The stdio caller (src/cli/nlm.ts) is local-mode
863
+ * only and always passes DEFAULT_TEAM_ID; the HTTP /mcp mount
864
+ * (src/http/app.ts) passes the token-resolved team (M3).
865
+ */
866
+ export function createMcpServer(deps, tenantId) {
636
867
  const server = new McpServer({
637
868
  name: SERVER_NAME,
638
869
  version: SERVER_VERSION,
@@ -679,7 +910,7 @@ export function createMcpServer(deps) {
679
910
  idempotentHint: true,
680
911
  openWorldHint: true,
681
912
  },
682
- }, async (args) => recallSessionsHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
913
+ }, async (args) => recallSessionsHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
683
914
  server.registerTool("get_session", {
684
915
  title: "Get Full NLM Session",
685
916
  description: GET_SESSION_DESCRIPTION,
@@ -692,7 +923,7 @@ export function createMcpServer(deps) {
692
923
  idempotentHint: true,
693
924
  openWorldHint: true,
694
925
  },
695
- }, async (args) => getSessionHandler(deps, args));
926
+ }, async (args) => getSessionHandler(deps, tenantId, args));
696
927
  if (deps.factRecall && deps.factStore) {
697
928
  server.registerTool("recall_facts", {
698
929
  title: "Recall Facts from NLM",
@@ -742,7 +973,7 @@ export function createMcpServer(deps) {
742
973
  idempotentHint: true,
743
974
  openWorldHint: true,
744
975
  },
745
- }, async (args) => recallFactsHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
976
+ }, async (args) => recallFactsHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
746
977
  server.registerTool("get_fact_history", {
747
978
  title: "Get Fact Supersedence History",
748
979
  description: GET_FACT_HISTORY_DESCRIPTION,
@@ -759,7 +990,7 @@ export function createMcpServer(deps) {
759
990
  idempotentHint: true,
760
991
  openWorldHint: true,
761
992
  },
762
- }, async (args) => getFactHistoryHandler(deps, args));
993
+ }, async (args) => getFactHistoryHandler(deps, tenantId, args));
763
994
  server.registerTool("supersede_fact", {
764
995
  title: "Supersede a Stale NLM Fact",
765
996
  description: SUPERSEDE_FACT_DESCRIPTION,
@@ -779,7 +1010,7 @@ export function createMcpServer(deps) {
779
1010
  idempotentHint: true,
780
1011
  openWorldHint: false,
781
1012
  },
782
- }, async (args) => supersedeFactHandler(deps, args));
1013
+ }, async (args) => supersedeFactHandler(deps, tenantId, args));
783
1014
  }
784
1015
  server.registerTool("cite_session", {
785
1016
  title: "Cite NLM Session",
@@ -801,7 +1032,40 @@ export function createMcpServer(deps) {
801
1032
  idempotentHint: false,
802
1033
  openWorldHint: false,
803
1034
  },
804
- }, async (args) => citeSessionHandler(args));
1035
+ }, async (args) => citeSessionHandler(tenantId, args));
1036
+ if (deps.signalStore && deps.installScope !== undefined) {
1037
+ server.registerTool("report_outcome", {
1038
+ title: "Report Session Outcome",
1039
+ description: REPORT_OUTCOME_DESCRIPTION,
1040
+ inputSchema: {
1041
+ session_id: z
1042
+ .string()
1043
+ .optional()
1044
+ .describe("NLM session ID this outcome applies to. Required if correlation_key is omitted."),
1045
+ correlation_key: z
1046
+ .string()
1047
+ .optional()
1048
+ .describe("External correlation identifier (e.g. a CI run id) when no NLM session_id is known. Required if session_id is omitted."),
1049
+ outcome: z
1050
+ .enum(SIGNAL_OUTCOMES)
1051
+ .describe("pass = held. fail | fix | exhausted = overturned."),
1052
+ source_of_record: z
1053
+ .string()
1054
+ .min(1)
1055
+ .describe("The deterministic system asserting this outcome (e.g. \"ci:github-actions\", \"hook:post-tool-use\")."),
1056
+ detail: z
1057
+ .record(z.string(), z.unknown())
1058
+ .optional()
1059
+ .describe("Optional passthrough context, stored on the signal row."),
1060
+ },
1061
+ annotations: {
1062
+ readOnlyHint: false,
1063
+ destructiveHint: false,
1064
+ idempotentHint: true,
1065
+ openWorldHint: false,
1066
+ },
1067
+ }, async (args) => reportOutcomeHandler(deps, tenantId, args));
1068
+ }
805
1069
  server.registerTool("mark_superseded", {
806
1070
  title: "Mark NLM Session Superseded",
807
1071
  description: MARK_SUPERSEDED_DESCRIPTION,
@@ -825,7 +1089,7 @@ export function createMcpServer(deps) {
825
1089
  idempotentHint: true,
826
1090
  openWorldHint: false,
827
1091
  },
828
- }, async (args) => markSupersededHandler(deps, args));
1092
+ }, async (args) => markSupersededHandler(deps, tenantId, args));
829
1093
  server.registerTool("work_summary", {
830
1094
  title: "Daily work summary",
831
1095
  description: "The operator's agent-assisted work recap for a day: where attention went (active time by topic), focus quality (context switches, longest block), and progress (decisions, open loops). Optional `date` (YYYY-MM-DD); defaults to today.",
@@ -837,7 +1101,7 @@ export function createMcpServer(deps) {
837
1101
  .describe("Day to summarize, YYYY-MM-DD. Defaults to today."),
838
1102
  },
839
1103
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
840
- }, async (args) => workSummaryHandler(deps, args));
1104
+ }, async (args) => workSummaryHandler(deps, tenantId, args));
841
1105
  server.registerTool("recall_workstream", {
842
1106
  title: "Recall a workstream's accumulated context",
843
1107
  description: "Return the coherent project view for a workstream: its member sessions, current decisions and open loops, accumulated facts, and code exemplars. Accepts a workstream id or label; merge chains resolve to the live workstream.",
@@ -845,7 +1109,7 @@ export function createMcpServer(deps) {
845
1109
  idOrLabel: z.string().describe("Workstream id (ws_...) or label (e.g. 'NLM')."),
846
1110
  },
847
1111
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
848
- }, async (args) => recallWorkstreamHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
1112
+ }, async (args) => recallWorkstreamHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
849
1113
  server.registerTool("rebind_session", {
850
1114
  title: "Rebind a session to a different workstream",
851
1115
  description: "Move a session's primary workstream binding (operator correction). Sets binding_source=operator. The session's facts and exemplars roll up under the new workstream automatically (rollup is by session binding).",
@@ -854,7 +1118,7 @@ export function createMcpServer(deps) {
854
1118
  workstream: z.string().describe("Target workstream id (ws_...) or label."),
855
1119
  },
856
1120
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
857
- }, async (args) => rebindSessionHandler(deps, args));
1121
+ }, async (args) => rebindSessionHandler(deps, tenantId, args));
858
1122
  server.registerTool("merge_workstreams", {
859
1123
  title: "Merge one workstream into another",
860
1124
  description: "Supersede a duplicate workstream into the one to keep: sets merged_into, marks it merged, and unions its entity index. Sessions, facts, and exemplars resolve to the survivor automatically (no session rewrite). Accepts ids or labels; merge chains resolve.",
@@ -863,7 +1127,7 @@ export function createMcpServer(deps) {
863
1127
  into: z.string().describe("Workstream to keep (the survivor); id or label."),
864
1128
  },
865
1129
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
866
- }, async (args) => mergeWorkstreamsHandler(deps, args));
1130
+ }, async (args) => mergeWorkstreamsHandler(deps, tenantId, args));
867
1131
  server.registerTool("rename_workstream", {
868
1132
  title: "Rename a workstream",
869
1133
  description: "Relabel a workstream. Refuses a label that collides with a different existing workstream's normalized label (prevents accidental duplicates). Accepts id or label.",
@@ -872,19 +1136,19 @@ export function createMcpServer(deps) {
872
1136
  label: z.string().describe("New label."),
873
1137
  },
874
1138
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
875
- }, async (args) => renameWorkstreamHandler(deps, args));
1139
+ }, async (args) => renameWorkstreamHandler(deps, tenantId, args));
876
1140
  server.registerTool("retire_workstream", {
877
1141
  title: "Retire a workstream",
878
1142
  description: "Mark a workstream retired (status=retired). Operator cleanup for dead one-off workstreams. Reversible by re-setting status. Accepts id or label.",
879
1143
  inputSchema: { idOrLabel: z.string().describe("Workstream id or label.") },
880
1144
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
881
- }, async (args) => retireWorkstreamHandler(deps, args));
1145
+ }, async (args) => retireWorkstreamHandler(deps, tenantId, args));
882
1146
  server.registerTool("list_merge_suggestions", {
883
1147
  title: "Suggest duplicate workstreams to merge",
884
1148
  description: "Score active workstream pairs by shared entities, co-occurring sessions, and label similarity; list likely duplicates for one-click merge_workstreams. Computed on demand; read-only.",
885
1149
  inputSchema: { minScore: z.number().optional().describe("Minimum similarity score 0..1 (default 0.5).") },
886
1150
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
887
- }, async (args) => listMergeSuggestionsHandler(deps, args));
1151
+ }, async (args) => listMergeSuggestionsHandler(deps, tenantId, args));
888
1152
  if (deps.exemplarStore &&
889
1153
  deps.installScope &&
890
1154
  process.env["NLM_CODE_EXEMPLARS_ENABLED"] === "1") {
@@ -926,7 +1190,7 @@ export function createMcpServer(deps) {
926
1190
  openWorldHint: false,
927
1191
  },
928
1192
  }, async (args) => {
929
- const result = await recallCode({
1193
+ const result = await recallCode(tenantId, {
930
1194
  query: args.query,
931
1195
  installScope,
932
1196
  ...(args.repo ? { repo: args.repo } : {}),
@@ -939,7 +1203,7 @@ export function createMcpServer(deps) {
939
1203
  ...result.positives.map((e) => e.id),
940
1204
  ...result.negatives.map((e) => e.id),
941
1205
  ];
942
- void logQuery({
1206
+ void logQuery(tenantId, {
943
1207
  source: "mcp",
944
1208
  runtime: mcpRuntimeFromClient(server.server.getClientVersion()),
945
1209
  query: args.query,
@@ -972,10 +1236,69 @@ export function createMcpServer(deps) {
972
1236
  if (patch.retired === undefined && patch.outcome === undefined) {
973
1237
  return { content: [{ type: "text", text: JSON.stringify({ error: "provide retire and/or outcome" }) }] };
974
1238
  }
975
- const res = await exemplarStore.setVerdict(a.exemplar_id, patch, "human");
1239
+ const res = await exemplarStore.setVerdict(tenantId, a.exemplar_id, patch, "human");
976
1240
  return { content: [{ type: "text", text: JSON.stringify({ exemplar_id: a.exemplar_id, status: res.status }) }] };
977
1241
  });
978
1242
  }
1243
+ if (deps.learners) {
1244
+ server.registerTool("learner_enroll", {
1245
+ title: "Enroll Learner in Course",
1246
+ description: LEARNER_ENROLL_DESCRIPTION,
1247
+ inputSchema: {
1248
+ learner: z.string().min(1).describe("Learner id (lowercase slug). Auto-created on first enroll."),
1249
+ label: z.string().optional().describe("Display label for a new learner; defaults to the id."),
1250
+ course: z.string().min(1).describe("Course id (lowercase slug)."),
1251
+ workstream: z.string().min(1).describe("Workstream id or label to bind the enrollment to."),
1252
+ plan_ref: z.string().min(1).describe("Caller-side course plan reference (NLM stores the ref, not the plan)."),
1253
+ plan_revision: z.number().int().min(1).describe("Plan revision; a changed plan must bump, never silently upsert."),
1254
+ plan_fingerprint: z.string().min(1).describe("Plan content fingerprint; mismatch blocks writes until reconciled."),
1255
+ },
1256
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1257
+ }, async (args) => learnerEnrollHandler(deps, tenantId, args));
1258
+ server.registerTool("learner_record", {
1259
+ title: "Record Learning Event",
1260
+ description: LEARNER_RECORD_DESCRIPTION,
1261
+ inputSchema: {
1262
+ learner: z.string().min(1).describe("Learner id."),
1263
+ course: z.string().min(1).describe("Course id."),
1264
+ event: z.object({
1265
+ id: z.string().min(1).describe("Event id (lowercase slug, unique per enrollment)."),
1266
+ date: z.string().describe("ISO date YYYY-MM-DD, never in the future."),
1267
+ covered: z.array(z.string()).describe("Concept ids covered (subject.concept)."),
1268
+ attempts: z.array(z.object({
1269
+ concept: z.string().describe("Concept id attempted."),
1270
+ task: z.string().min(1).describe("The task given."),
1271
+ response: z.string().min(1).describe("Short excerpt of the learner's response."),
1272
+ result: z.enum(["correct", "partial", "incorrect"]),
1273
+ help: z.enum(["none", "hint", "worked-example"]),
1274
+ kind: z.enum(["application", "retrieval", "transfer"]),
1275
+ })).describe("Per-concept attempts (may be empty for an untested lesson)."),
1276
+ interpretation: z.string().describe("Tentative explanation of the evidence (may be empty)."),
1277
+ next_step: z.string().min(1).describe("What the next teacher should try or ask."),
1278
+ }).describe("The learning event to record."),
1279
+ },
1280
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1281
+ }, async (args) => learnerRecordHandler(deps, tenantId, args));
1282
+ server.registerTool("learner_summary", {
1283
+ title: "Summarize Learner Progress",
1284
+ description: LEARNER_SUMMARY_DESCRIPTION,
1285
+ inputSchema: {
1286
+ learner: z.string().min(1).describe("Learner id."),
1287
+ course: z.string().optional().describe("Course id. Omit only when exactly one active enrollment exists."),
1288
+ limit: z.number().int().min(1).max(200).optional().describe("Max concepts to list (default 50)."),
1289
+ },
1290
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1291
+ }, async (args) => learnerSummaryHandler(deps, tenantId, args));
1292
+ server.registerTool("learner_complete_course", {
1293
+ title: "Complete Learner Course",
1294
+ description: LEARNER_COMPLETE_DESCRIPTION,
1295
+ inputSchema: {
1296
+ learner: z.string().min(1).describe("Learner id."),
1297
+ course: z.string().min(1).describe("Course id."),
1298
+ },
1299
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1300
+ }, async (args) => learnerCompleteHandler(deps, tenantId, args));
1301
+ }
979
1302
  return server;
980
1303
  }
981
1304
  const RECALL_CODE_DESCRIPTION = `\