nlm-memory 0.21.0 → 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 (413) hide show
  1. package/README.md +67 -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 +34 -7
  5. package/dist/cli/digest.js.map +1 -1
  6. package/dist/cli/nlm.d.ts +14 -0
  7. package/dist/cli/nlm.js +450 -65
  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-coverage.js +4 -1
  13. package/dist/cli/scope-coverage.js.map +1 -1
  14. package/dist/cli/supersede.js +8 -7
  15. package/dist/cli/supersede.js.map +1 -1
  16. package/dist/core/actions/actions-log.d.ts +65 -8
  17. package/dist/core/actions/actions-log.js +134 -17
  18. package/dist/core/actions/actions-log.js.map +1 -1
  19. package/dist/core/actions/overlay.d.ts +11 -2
  20. package/dist/core/actions/overlay.js +18 -6
  21. package/dist/core/actions/overlay.js.map +1 -1
  22. package/dist/core/adapters/from-source.js +11 -3
  23. package/dist/core/adapters/from-source.js.map +1 -1
  24. package/dist/core/adapters/fs-wiki-writer.d.ts +44 -0
  25. package/dist/core/adapters/fs-wiki-writer.js +132 -0
  26. package/dist/core/adapters/fs-wiki-writer.js.map +1 -0
  27. package/dist/core/adapters/hermes-agent.d.ts +15 -2
  28. package/dist/core/adapters/hermes-agent.js +57 -11
  29. package/dist/core/adapters/hermes-agent.js.map +1 -1
  30. package/dist/core/adapters/memory-wiki-writer.d.ts +8 -0
  31. package/dist/core/adapters/memory-wiki-writer.js +16 -0
  32. package/dist/core/adapters/memory-wiki-writer.js.map +1 -0
  33. package/dist/core/adapters/muse.d.ts +51 -0
  34. package/dist/core/adapters/muse.js +271 -0
  35. package/dist/core/adapters/muse.js.map +1 -0
  36. package/dist/core/adapters/opencode.d.ts +11 -4
  37. package/dist/core/adapters/opencode.js +41 -7
  38. package/dist/core/adapters/opencode.js.map +1 -1
  39. package/dist/core/alerts/alert-state.d.ts +17 -0
  40. package/dist/core/alerts/alert-state.js +45 -0
  41. package/dist/core/alerts/alert-state.js.map +1 -0
  42. package/dist/core/alerts/check-and-alert.d.ts +28 -0
  43. package/dist/core/alerts/check-and-alert.js +53 -0
  44. package/dist/core/alerts/check-and-alert.js.map +1 -0
  45. package/dist/core/alerts/drift-transition.d.ts +27 -0
  46. package/dist/core/alerts/drift-transition.js +56 -0
  47. package/dist/core/alerts/drift-transition.js.map +1 -0
  48. package/dist/core/alerts/embedder-transition.d.ts +23 -0
  49. package/dist/core/alerts/embedder-transition.js +40 -0
  50. package/dist/core/alerts/embedder-transition.js.map +1 -0
  51. package/dist/core/alerts/fire-alert.d.ts +24 -0
  52. package/dist/core/alerts/fire-alert.js +72 -0
  53. package/dist/core/alerts/fire-alert.js.map +1 -0
  54. package/dist/core/alerts/job-alert.d.ts +12 -0
  55. package/dist/core/alerts/job-alert.js +37 -0
  56. package/dist/core/alerts/job-alert.js.map +1 -0
  57. package/dist/core/alerts/types.d.ts +104 -0
  58. package/dist/core/alerts/types.js +11 -0
  59. package/dist/core/alerts/types.js.map +1 -0
  60. package/dist/core/alerts/wiki-alert.d.ts +15 -0
  61. package/dist/core/alerts/wiki-alert.js +31 -0
  62. package/dist/core/alerts/wiki-alert.js.map +1 -0
  63. package/dist/core/classifier/prompt.d.ts +13 -0
  64. package/dist/core/classifier/prompt.js +52 -2
  65. package/dist/core/classifier/prompt.js.map +1 -1
  66. package/dist/core/dataset/build-dataset.d.ts +16 -1
  67. package/dist/core/dataset/build-dataset.js +45 -13
  68. package/dist/core/dataset/build-dataset.js.map +1 -1
  69. package/dist/core/digest/compose.d.ts +8 -0
  70. package/dist/core/digest/compose.js +54 -0
  71. package/dist/core/digest/compose.js.map +1 -1
  72. package/dist/core/digest/hook-injection.d.ts +1 -1
  73. package/dist/core/digest/hook-injection.js +9 -1
  74. package/dist/core/digest/hook-injection.js.map +1 -1
  75. package/dist/core/digest/hook-version-parity.d.ts +31 -0
  76. package/dist/core/digest/hook-version-parity.js +56 -0
  77. package/dist/core/digest/hook-version-parity.js.map +1 -0
  78. package/dist/core/embedding/pg-embed-backfill.js +1 -1
  79. package/dist/core/embedding/pg-embed-backfill.js.map +1 -1
  80. package/dist/core/exemplars/capture-from-session.d.ts +1 -1
  81. package/dist/core/exemplars/capture-from-session.js +3 -3
  82. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  83. package/dist/core/exemplars/embed-backfill.d.ts +9 -0
  84. package/dist/core/exemplars/embed-backfill.js +9 -1
  85. package/dist/core/exemplars/embed-backfill.js.map +1 -1
  86. package/dist/core/exemplars/recall-code.d.ts +1 -1
  87. package/dist/core/exemplars/recall-code.js +2 -2
  88. package/dist/core/exemplars/recall-code.js.map +1 -1
  89. package/dist/core/facts/backfill-facts.d.ts +1 -1
  90. package/dist/core/facts/backfill-facts.js +4 -4
  91. package/dist/core/facts/backfill-facts.js.map +1 -1
  92. package/dist/core/facts/embed-backfill.d.ts +54 -0
  93. package/dist/core/facts/embed-backfill.js +109 -0
  94. package/dist/core/facts/embed-backfill.js.map +1 -0
  95. package/dist/core/facts/reconcile-facts.d.ts +42 -0
  96. package/dist/core/facts/reconcile-facts.js +133 -0
  97. package/dist/core/facts/reconcile-facts.js.map +1 -0
  98. package/dist/core/health/embed-failure-state.d.ts +4 -0
  99. package/dist/core/health/embed-failure-state.js +12 -0
  100. package/dist/core/health/embed-failure-state.js.map +1 -0
  101. package/dist/core/health/warmup-retry.d.ts +27 -0
  102. package/dist/core/health/warmup-retry.js +53 -0
  103. package/dist/core/health/warmup-retry.js.map +1 -0
  104. package/dist/core/health/warmup-state.d.ts +15 -0
  105. package/dist/core/health/warmup-state.js +15 -1
  106. package/dist/core/health/warmup-state.js.map +1 -1
  107. package/dist/core/hook/cite-memo.d.ts +11 -7
  108. package/dist/core/hook/cite-memo.js +25 -17
  109. package/dist/core/hook/cite-memo.js.map +1 -1
  110. package/dist/core/hook/hook-log.d.ts +13 -2
  111. package/dist/core/hook/hook-log.js +16 -8
  112. package/dist/core/hook/hook-log.js.map +1 -1
  113. package/dist/core/hook/memo.d.ts +10 -6
  114. package/dist/core/hook/memo.js +25 -17
  115. package/dist/core/hook/memo.js.map +1 -1
  116. package/dist/core/hook/pointer-block.d.ts +8 -1
  117. package/dist/core/hook/pointer-block.js +17 -4
  118. package/dist/core/hook/pointer-block.js.map +1 -1
  119. package/dist/core/hook/resolve-conversation-by-query.d.ts +6 -5
  120. package/dist/core/hook/resolve-conversation-by-query.js +24 -12
  121. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -1
  122. package/dist/core/ingest/ingest-session.d.ts +1 -1
  123. package/dist/core/ingest/ingest-session.js +3 -3
  124. package/dist/core/ingest/ingest-session.js.map +1 -1
  125. package/dist/core/ingest/reclassify-oversized.d.ts +1 -1
  126. package/dist/core/ingest/reclassify-oversized.js +2 -2
  127. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  128. package/dist/core/ingest/reprocess.d.ts +1 -1
  129. package/dist/core/ingest/reprocess.js +2 -2
  130. package/dist/core/ingest/reprocess.js.map +1 -1
  131. package/dist/core/jobs/job-supervisor.d.ts +168 -0
  132. package/dist/core/jobs/job-supervisor.js +335 -0
  133. package/dist/core/jobs/job-supervisor.js.map +1 -0
  134. package/dist/core/learner/model.d.ts +63 -0
  135. package/dist/core/learner/model.js +30 -0
  136. package/dist/core/learner/model.js.map +1 -0
  137. package/dist/core/learner/service.d.ts +42 -0
  138. package/dist/core/learner/service.js +74 -0
  139. package/dist/core/learner/service.js.map +1 -0
  140. package/dist/core/learner/summary.d.ts +28 -0
  141. package/dist/core/learner/summary.js +70 -0
  142. package/dist/core/learner/summary.js.map +1 -0
  143. package/dist/core/learner/validate.d.ts +18 -0
  144. package/dist/core/learner/validate.js +95 -0
  145. package/dist/core/learner/validate.js.map +1 -0
  146. package/dist/core/metrics/re-derivation.d.ts +31 -2
  147. package/dist/core/metrics/re-derivation.js +36 -4
  148. package/dist/core/metrics/re-derivation.js.map +1 -1
  149. package/dist/core/outcome/coverage.d.ts +1 -1
  150. package/dist/core/outcome/coverage.js +2 -2
  151. package/dist/core/outcome/coverage.js.map +1 -1
  152. package/dist/core/outcome/rollup.d.ts +1 -1
  153. package/dist/core/outcome/rollup.js +4 -4
  154. package/dist/core/outcome/rollup.js.map +1 -1
  155. package/dist/core/providers/provider-registry.d.ts +40 -23
  156. package/dist/core/providers/provider-registry.js +99 -81
  157. package/dist/core/providers/provider-registry.js.map +1 -1
  158. package/dist/core/recall/citation-log.d.ts +10 -5
  159. package/dist/core/recall/citation-log.js +21 -10
  160. package/dist/core/recall/citation-log.js.map +1 -1
  161. package/dist/core/recall/hook-recall-log.d.ts +5 -2
  162. package/dist/core/recall/hook-recall-log.js +12 -6
  163. package/dist/core/recall/hook-recall-log.js.map +1 -1
  164. package/dist/core/recall/miss-log.d.ts +7 -4
  165. package/dist/core/recall/miss-log.js +18 -11
  166. package/dist/core/recall/miss-log.js.map +1 -1
  167. package/dist/core/recall/query-log.d.ts +8 -5
  168. package/dist/core/recall/query-log.js +16 -9
  169. package/dist/core/recall/query-log.js.map +1 -1
  170. package/dist/core/recall/recall-service.d.ts +10 -1
  171. package/dist/core/recall/recall-service.js +31 -12
  172. package/dist/core/recall/recall-service.js.map +1 -1
  173. package/dist/core/recall/recent-log.d.ts +6 -1
  174. package/dist/core/recall/recent-log.js +13 -5
  175. package/dist/core/recall/recent-log.js.map +1 -1
  176. package/dist/core/recall/related-concepts.d.ts +29 -0
  177. package/dist/core/recall/related-concepts.js +67 -0
  178. package/dist/core/recall/related-concepts.js.map +1 -0
  179. package/dist/core/recall/related-exemplars.d.ts +1 -1
  180. package/dist/core/recall/related-exemplars.js +2 -2
  181. package/dist/core/recall/related-exemplars.js.map +1 -1
  182. package/dist/core/recall/related-facts.d.ts +1 -1
  183. package/dist/core/recall/related-facts.js +3 -3
  184. package/dist/core/recall/related-facts.js.map +1 -1
  185. package/dist/core/recall-facts/fact-query-log.d.ts +8 -4
  186. package/dist/core/recall-facts/fact-query-log.js +16 -8
  187. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  188. package/dist/core/recall-facts/fact-recall-service.d.ts +2 -2
  189. package/dist/core/recall-facts/fact-recall-service.js +12 -12
  190. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  191. package/dist/core/scheduler/scan-once.js +15 -14
  192. package/dist/core/scheduler/scan-once.js.map +1 -1
  193. package/dist/core/scheduler/scheduler.d.ts +3 -0
  194. package/dist/core/scheduler/scheduler.js +15 -11
  195. package/dist/core/scheduler/scheduler.js.map +1 -1
  196. package/dist/core/signals/failure-mode-recall.d.ts +1 -1
  197. package/dist/core/signals/failure-mode-recall.js +2 -2
  198. package/dist/core/signals/failure-mode-recall.js.map +1 -1
  199. package/dist/core/signals/stamp-scope.d.ts +2 -2
  200. package/dist/core/signals/stamp-scope.js +2 -2
  201. package/dist/core/signals/stamp-scope.js.map +1 -1
  202. package/dist/core/sources/source-registry.d.ts +49 -28
  203. package/dist/core/sources/source-registry.js +113 -70
  204. package/dist/core/sources/source-registry.js.map +1 -1
  205. package/dist/core/storage/pg-code-exemplar-store.d.ts +19 -10
  206. package/dist/core/storage/pg-code-exemplar-store.js +58 -27
  207. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  208. package/dist/core/storage/pg-entity-store.d.ts +6 -1
  209. package/dist/core/storage/pg-entity-store.js +33 -18
  210. package/dist/core/storage/pg-entity-store.js.map +1 -1
  211. package/dist/core/storage/pg-fact-ingest.d.ts +1 -1
  212. package/dist/core/storage/pg-fact-ingest.js +10 -7
  213. package/dist/core/storage/pg-fact-ingest.js.map +1 -1
  214. package/dist/core/storage/pg-fact-store.d.ts +29 -17
  215. package/dist/core/storage/pg-fact-store.js +99 -46
  216. package/dist/core/storage/pg-fact-store.js.map +1 -1
  217. package/dist/core/storage/pg-learner-store.d.ts +27 -0
  218. package/dist/core/storage/pg-learner-store.js +174 -0
  219. package/dist/core/storage/pg-learner-store.js.map +1 -0
  220. package/dist/core/storage/pg-session-store.d.ts +24 -20
  221. package/dist/core/storage/pg-session-store.js +162 -90
  222. package/dist/core/storage/pg-session-store.js.map +1 -1
  223. package/dist/core/storage/pg-signal-store.d.ts +10 -5
  224. package/dist/core/storage/pg-signal-store.js +24 -12
  225. package/dist/core/storage/pg-signal-store.js.map +1 -1
  226. package/dist/core/storage/pg-storage.d.ts +4 -0
  227. package/dist/core/storage/pg-storage.js +6 -0
  228. package/dist/core/storage/pg-storage.js.map +1 -1
  229. package/dist/core/storage/pg-workstream-store.d.ts +16 -11
  230. package/dist/core/storage/pg-workstream-store.js +46 -29
  231. package/dist/core/storage/pg-workstream-store.js.map +1 -1
  232. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +24 -10
  233. package/dist/core/storage/sqlite-code-exemplar-store.js +84 -40
  234. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  235. package/dist/core/storage/sqlite-entity-store.d.ts +8 -1
  236. package/dist/core/storage/sqlite-entity-store.js +46 -27
  237. package/dist/core/storage/sqlite-entity-store.js.map +1 -1
  238. package/dist/core/storage/sqlite-fact-store.d.ts +45 -19
  239. package/dist/core/storage/sqlite-fact-store.js +172 -64
  240. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  241. package/dist/core/storage/sqlite-learner-store.d.ts +33 -0
  242. package/dist/core/storage/sqlite-learner-store.js +178 -0
  243. package/dist/core/storage/sqlite-learner-store.js.map +1 -0
  244. package/dist/core/storage/sqlite-outcome-store.d.ts +37 -22
  245. package/dist/core/storage/sqlite-outcome-store.js +90 -37
  246. package/dist/core/storage/sqlite-outcome-store.js.map +1 -1
  247. package/dist/core/storage/sqlite-session-store.d.ts +29 -20
  248. package/dist/core/storage/sqlite-session-store.js +207 -121
  249. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  250. package/dist/core/storage/sqlite-signal-store.d.ts +12 -5
  251. package/dist/core/storage/sqlite-signal-store.js +27 -15
  252. package/dist/core/storage/sqlite-signal-store.js.map +1 -1
  253. package/dist/core/storage/sqlite-storage.d.ts +4 -0
  254. package/dist/core/storage/sqlite-storage.js +10 -2
  255. package/dist/core/storage/sqlite-storage.js.map +1 -1
  256. package/dist/core/storage/sqlite-workstream-store.d.ts +16 -11
  257. package/dist/core/storage/sqlite-workstream-store.js +52 -31
  258. package/dist/core/storage/sqlite-workstream-store.js.map +1 -1
  259. package/dist/core/storage/supersedence-log.d.ts +10 -7
  260. package/dist/core/storage/supersedence-log.js +19 -14
  261. package/dist/core/storage/supersedence-log.js.map +1 -1
  262. package/dist/core/tenancy/default-team.d.ts +6 -0
  263. package/dist/core/tenancy/default-team.js +7 -0
  264. package/dist/core/tenancy/default-team.js.map +1 -0
  265. package/dist/core/tenancy/hosted-mode.d.ts +9 -0
  266. package/dist/core/tenancy/hosted-mode.js +12 -0
  267. package/dist/core/tenancy/hosted-mode.js.map +1 -0
  268. package/dist/core/tenancy/team-auth.d.ts +16 -0
  269. package/dist/core/tenancy/team-auth.js +22 -0
  270. package/dist/core/tenancy/team-auth.js.map +1 -0
  271. package/dist/core/tenancy/team-token-store.d.ts +45 -0
  272. package/dist/core/tenancy/team-token-store.js +52 -0
  273. package/dist/core/tenancy/team-token-store.js.map +1 -0
  274. package/dist/core/tenancy/tenant-clause.d.ts +37 -0
  275. package/dist/core/tenancy/tenant-clause.js +11 -0
  276. package/dist/core/tenancy/tenant-clause.js.map +1 -0
  277. package/dist/core/tenancy/tenant-state-path.d.ts +28 -0
  278. package/dist/core/tenancy/tenant-state-path.js +49 -0
  279. package/dist/core/tenancy/tenant-state-path.js.map +1 -0
  280. package/dist/core/wiki/project.d.ts +17 -0
  281. package/dist/core/wiki/project.js +37 -0
  282. package/dist/core/wiki/project.js.map +1 -0
  283. package/dist/core/wiki/reconcile.d.ts +15 -0
  284. package/dist/core/wiki/reconcile.js +14 -0
  285. package/dist/core/wiki/reconcile.js.map +1 -0
  286. package/dist/core/wiki/render.d.ts +5 -0
  287. package/dist/core/wiki/render.js +84 -0
  288. package/dist/core/wiki/render.js.map +1 -0
  289. package/dist/core/wiki/rollup.d.ts +28 -0
  290. package/dist/core/wiki/rollup.js +91 -0
  291. package/dist/core/wiki/rollup.js.map +1 -0
  292. package/dist/core/wiki/select.d.ts +8 -0
  293. package/dist/core/wiki/select.js +7 -0
  294. package/dist/core/wiki/select.js.map +1 -0
  295. package/dist/core/wiki/slug.d.ts +36 -0
  296. package/dist/core/wiki/slug.js +71 -0
  297. package/dist/core/wiki/slug.js.map +1 -0
  298. package/dist/core/wiki/types.d.ts +37 -0
  299. package/dist/core/wiki/types.js +2 -0
  300. package/dist/core/wiki/types.js.map +1 -0
  301. package/dist/core/work-digest/build-work-digest.d.ts +1 -1
  302. package/dist/core/work-digest/build-work-digest.js +3 -3
  303. package/dist/core/work-digest/build-work-digest.js.map +1 -1
  304. package/dist/core/workstream/bind.d.ts +1 -1
  305. package/dist/core/workstream/bind.js +10 -10
  306. package/dist/core/workstream/bind.js.map +1 -1
  307. package/dist/core/workstream/rollup.d.ts +1 -1
  308. package/dist/core/workstream/rollup.js +6 -6
  309. package/dist/core/workstream/rollup.js.map +1 -1
  310. package/dist/hook/hook-helpers.d.ts +14 -0
  311. package/dist/hook/hook-helpers.js +29 -2
  312. package/dist/hook/hook-helpers.js.map +1 -1
  313. package/dist/hook/pre-compact-hook.js +6 -4
  314. package/dist/hook/pre-compact-hook.js.map +1 -1
  315. package/dist/hook/prompt-recall-hook.d.ts +5 -2
  316. package/dist/hook/prompt-recall-hook.js +28 -17
  317. package/dist/hook/prompt-recall-hook.js.map +1 -1
  318. package/dist/hook/recall-over-http.d.ts +27 -3
  319. package/dist/hook/recall-over-http.js +44 -11
  320. package/dist/hook/recall-over-http.js.map +1 -1
  321. package/dist/hook/session-end-hook.js +7 -5
  322. package/dist/hook/session-end-hook.js.map +1 -1
  323. package/dist/hook/session-start-hook.d.ts +14 -1
  324. package/dist/hook/session-start-hook.js +55 -15
  325. package/dist/hook/session-start-hook.js.map +1 -1
  326. package/dist/hook/stop-hook.js +9 -7
  327. package/dist/hook/stop-hook.js.map +1 -1
  328. package/dist/http/app.d.ts +41 -3
  329. package/dist/http/app.js +355 -96
  330. package/dist/http/app.js.map +1 -1
  331. package/dist/install/claude-code.d.ts +17 -0
  332. package/dist/install/claude-code.js +44 -5
  333. package/dist/install/claude-code.js.map +1 -1
  334. package/dist/install/codex.d.ts +23 -2
  335. package/dist/install/codex.js +111 -14
  336. package/dist/install/codex.js.map +1 -1
  337. package/dist/install/cursor.d.ts +2 -2
  338. package/dist/install/cursor.js +7 -7
  339. package/dist/install/cursor.js.map +1 -1
  340. package/dist/install/hermes.d.ts +8 -0
  341. package/dist/install/hermes.js +28 -5
  342. package/dist/install/hermes.js.map +1 -1
  343. package/dist/install/local-source.d.ts +20 -0
  344. package/dist/install/local-source.js +28 -0
  345. package/dist/install/local-source.js.map +1 -0
  346. package/dist/install/mcp-block.d.ts +77 -0
  347. package/dist/install/mcp-block.js +99 -0
  348. package/dist/install/mcp-block.js.map +1 -0
  349. package/dist/install/pi.d.ts +26 -0
  350. package/dist/install/pi.js +43 -0
  351. package/dist/install/pi.js.map +1 -1
  352. package/dist/install/setup.js +6 -0
  353. package/dist/install/setup.js.map +1 -1
  354. package/dist/install/windsurf.d.ts +2 -2
  355. package/dist/install/windsurf.js +7 -7
  356. package/dist/install/windsurf.js.map +1 -1
  357. package/dist/llm/classifier-box.d.ts +5 -0
  358. package/dist/llm/classifier-box.js +5 -0
  359. package/dist/llm/classifier-box.js.map +1 -1
  360. package/dist/mcp/server.d.ts +73 -16
  361. package/dist/mcp/server.js +306 -83
  362. package/dist/mcp/server.js.map +1 -1
  363. package/dist/ports/clock.d.ts +12 -0
  364. package/dist/ports/clock.js +10 -0
  365. package/dist/ports/clock.js.map +1 -0
  366. package/dist/ports/code-exemplar-store.d.ts +13 -13
  367. package/dist/ports/entity-store.d.ts +5 -1
  368. package/dist/ports/fact-store.d.ts +0 -0
  369. package/dist/ports/fact-store.js +0 -0
  370. package/dist/ports/fact-store.js.map +1 -1
  371. package/dist/ports/learner-store.d.ts +80 -0
  372. package/dist/ports/learner-store.js +19 -0
  373. package/dist/ports/learner-store.js.map +1 -0
  374. package/dist/ports/outcome.d.ts +3 -3
  375. package/dist/ports/session-store.d.ts +20 -14
  376. package/dist/ports/session-store.js +5 -0
  377. package/dist/ports/session-store.js.map +1 -1
  378. package/dist/ports/signal-store.d.ts +12 -8
  379. package/dist/ports/spawn-child.d.ts +14 -0
  380. package/dist/ports/spawn-child.js +10 -0
  381. package/dist/ports/spawn-child.js.map +1 -0
  382. package/dist/ports/storage.d.ts +6 -0
  383. package/dist/ports/transcript-adapter.d.ts +11 -0
  384. package/dist/ports/wiki-writer.d.ts +19 -0
  385. package/dist/ports/wiki-writer.js +12 -0
  386. package/dist/ports/wiki-writer.js.map +1 -0
  387. package/dist/ports/workstream-store.d.ts +19 -12
  388. package/dist/shared/types.d.ts +22 -0
  389. package/dist/shared/types.js.map +1 -1
  390. package/dist/shared/version.d.ts +9 -0
  391. package/dist/shared/version.js +11 -0
  392. package/dist/shared/version.js.map +1 -0
  393. package/dist/ui/assets/index-D6F3xAis.js +11 -0
  394. package/dist/ui/index.html +1 -1
  395. package/migrations/033_actions_kind_check.sql +58 -0
  396. package/migrations/034_tenancy_teams_and_stamps.sql +40 -0
  397. package/migrations/035_tenancy_entity_rekey.sql +75 -0
  398. package/migrations/036_tenant_name_uniqueness.sql +59 -0
  399. package/migrations/037_sessions_covering_index.sql +25 -0
  400. package/migrations/038_sources_muse.sql +33 -0
  401. package/migrations/039_learner_evidence.sql +51 -0
  402. package/migrations/pg/033_actions_kind_check.sql +17 -0
  403. package/migrations/pg/034_tenancy.sql +77 -0
  404. package/migrations/pg/035_tenant_name_uniqueness.sql +15 -0
  405. package/migrations/pg/036_sources_muse.sql +12 -0
  406. package/migrations/pg/037_learner_evidence.sql +55 -0
  407. package/nlm/index.js +246 -55
  408. package/package.json +10 -5
  409. package/plugin/.codex-plugin/plugin.json +2 -2
  410. package/plugin/scripts/prompt-recall-hook.mjs +244 -55
  411. package/plugin/scripts/session-start-hook.mjs +273 -53
  412. package/plugin/scripts/stop-hook.mjs +195 -37
  413. package/dist/ui/assets/index-CSaBtgXD.js +0 -11
@@ -30,16 +30,24 @@ import { normalizeSignal } from "../core/signals/ingest-signal.js";
30
30
  import { stampSignalScope } from "../core/signals/stamp-scope.js";
31
31
  import { deriveOutcome } from "../core/outcome/rollup.js";
32
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";
33
35
  import { SIGNAL_OUTCOMES } from "../shared/types.js";
34
36
  const CHARACTER_LIMIT = 25_000;
35
37
  const DEFAULT_LIMIT = 10;
36
38
  const SERVER_NAME = "nlm-memory-mcp-server";
37
39
  const SERVER_VERSION = "0.5.9";
38
40
  /** TOON encoding cuts token usage on large recall payloads. Opt in via
39
- * NLM_FORMAT=toon in the MCP server's env (see .mcp.json). Defaults to JSON. */
40
- 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";
41
49
  function format(data) {
42
- if (USE_TOON) {
50
+ if (useToon()) {
43
51
  try {
44
52
  return toonEncode(data);
45
53
  }
@@ -91,7 +99,7 @@ export function mcpRuntimeFromClient(client) {
91
99
  const name = client?.name?.trim().toLowerCase();
92
100
  return name ? name : null;
93
101
  }
94
- export async function recallSessionsHandler(deps, input, runtime = null) {
102
+ export async function recallSessionsHandler(deps, tenantId, input, runtime = null) {
95
103
  try {
96
104
  const rewrite = input.rewrite ?? mcpRewriteDefault();
97
105
  const query = {
@@ -107,12 +115,12 @@ export async function recallSessionsHandler(deps, input, runtime = null) {
107
115
  ...(input.kind !== undefined ? { kind: input.kind } : {}),
108
116
  ...(input.workstream !== undefined ? { workstream: input.workstream } : {}),
109
117
  };
110
- const result = await deps.recall.search(query);
118
+ const result = await deps.recall.search(tenantId, query);
111
119
  const conversationId = resolveConversationByQuery(input.query ?? "") ?? undefined;
112
120
  // Telemetry — the MCP path is the real agent-usage path; without this it
113
121
  // is invisible to query_log.jsonl and the Recall page. Fire-and-forget,
114
122
  // mirrors the HTTP /api/recall handler.
115
- void logQuery({
123
+ void logQuery(tenantId, {
116
124
  source: "mcp",
117
125
  runtime,
118
126
  query: input.query ?? null,
@@ -130,20 +138,20 @@ export async function recallSessionsHandler(deps, input, runtime = null) {
130
138
  return err(e);
131
139
  }
132
140
  }
133
- export async function workSummaryHandler(deps, input) {
141
+ export async function workSummaryHandler(deps, tenantId, input) {
134
142
  if (!deps.workDigest) {
135
143
  return okText("work_summary is not available in this deployment.");
136
144
  }
137
145
  try {
138
146
  const date = input.date ?? localToday();
139
- const digest = await buildWorkDigest(deps.workDigest, date);
147
+ const digest = await buildWorkDigest(deps.workDigest, tenantId, date);
140
148
  return okText(composeWorkDigest(digest));
141
149
  }
142
150
  catch (e) {
143
151
  return err(e);
144
152
  }
145
153
  }
146
- export async function recallWorkstreamHandler(deps, input, runtime = null) {
154
+ export async function recallWorkstreamHandler(deps, tenantId, input, runtime = null) {
147
155
  if (!deps.workstreams)
148
156
  return okText("recall_workstream is not available in this deployment.");
149
157
  try {
@@ -152,13 +160,13 @@ export async function recallWorkstreamHandler(deps, input, runtime = null) {
152
160
  return okText("Provide a workstream id or label.");
153
161
  const conversationId = resolveConversationByQuery(idOrLabel) ?? undefined;
154
162
  const ws = deps.workstreams.store;
155
- 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)));
156
164
  if (!found)
157
165
  return okText(`No workstream matches "${idOrLabel}".`);
158
- 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);
159
167
  if (!view)
160
168
  return okText(`No workstream matches "${idOrLabel}".`);
161
- void logQuery({
169
+ void logQuery(tenantId, {
162
170
  source: "mcp",
163
171
  runtime,
164
172
  query: idOrLabel,
@@ -176,17 +184,17 @@ export async function recallWorkstreamHandler(deps, input, runtime = null) {
176
184
  return err(e);
177
185
  }
178
186
  }
179
- /** idOrLabel -> live survivor workstream (merged_into resolved) | null. One source of truth for lifecycle handlers. */
180
- async function resolveWorkstream(store, idOrLabel) {
181
- 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)));
182
190
  if (!found)
183
191
  return null;
184
- const all = await store.listAll();
192
+ const all = await store.listAll(tenantId);
185
193
  const byId = new Map(all.map((w) => [w.id, { id: w.id, mergedInto: w.mergedInto }]));
186
194
  const survivorId = resolveWorkstreamId(found.id, byId);
187
- return survivorId === found.id ? found : ((await store.getById(survivorId)) ?? found);
195
+ return survivorId === found.id ? found : ((await store.getById(tenantId, survivorId)) ?? found);
188
196
  }
189
- export async function renameWorkstreamHandler(deps, input) {
197
+ export async function renameWorkstreamHandler(deps, tenantId, input) {
190
198
  if (!deps.workstreams)
191
199
  return okText("rename_workstream is not available in this deployment.");
192
200
  try {
@@ -195,38 +203,38 @@ export async function renameWorkstreamHandler(deps, input) {
195
203
  if (!idOrLabel || !label)
196
204
  return okText("Provide the workstream (id or label) and the new label.");
197
205
  const ws = deps.workstreams.store;
198
- const target = await resolveWorkstream(ws, idOrLabel);
206
+ const target = await resolveWorkstream(ws, tenantId, idOrLabel);
199
207
  if (!target)
200
208
  return okText(`No workstream matches "${idOrLabel}".`);
201
- const collision = await ws.findByNormalizedLabel(normalizeLabel(label));
209
+ const collision = await ws.findByNormalizedLabel(tenantId, normalizeLabel(label));
202
210
  if (collision && collision.id !== target.id) {
203
211
  return okText(`Label "${label}" is already used by workstream "${collision.label}" (${collision.id}).`);
204
212
  }
205
- await ws.setLabel(target.id, label);
213
+ await ws.setLabel(tenantId, target.id, label);
206
214
  return okText(`Renamed workstream ${target.id}: "${target.label}" -> "${label}".`);
207
215
  }
208
216
  catch (e) {
209
217
  return err(e);
210
218
  }
211
219
  }
212
- export async function retireWorkstreamHandler(deps, input) {
220
+ export async function retireWorkstreamHandler(deps, tenantId, input) {
213
221
  if (!deps.workstreams)
214
222
  return okText("retire_workstream is not available in this deployment.");
215
223
  try {
216
224
  const idOrLabel = (input.idOrLabel ?? "").trim();
217
225
  if (!idOrLabel)
218
226
  return okText("Provide a workstream id or label.");
219
- const target = await resolveWorkstream(deps.workstreams.store, idOrLabel);
227
+ const target = await resolveWorkstream(deps.workstreams.store, tenantId, idOrLabel);
220
228
  if (!target)
221
229
  return okText(`No workstream matches "${idOrLabel}".`);
222
- await deps.workstreams.store.setStatus(target.id, "retired");
230
+ await deps.workstreams.store.setStatus(tenantId, target.id, "retired");
223
231
  return okText(`Retired workstream "${target.label}" (${target.id}).`);
224
232
  }
225
233
  catch (e) {
226
234
  return err(e);
227
235
  }
228
236
  }
229
- export async function mergeWorkstreamsHandler(deps, input) {
237
+ export async function mergeWorkstreamsHandler(deps, tenantId, input) {
230
238
  if (!deps.workstreams)
231
239
  return okText("merge_workstreams is not available in this deployment.");
232
240
  try {
@@ -235,22 +243,22 @@ export async function mergeWorkstreamsHandler(deps, input) {
235
243
  if (!fromArg || !intoArg)
236
244
  return okText("Provide both `from` and `into` (workstream id or label).");
237
245
  const ws = deps.workstreams.store;
238
- const from = await resolveWorkstream(ws, fromArg);
239
- const into = await resolveWorkstream(ws, intoArg);
246
+ const from = await resolveWorkstream(ws, tenantId, fromArg);
247
+ const into = await resolveWorkstream(ws, tenantId, intoArg);
240
248
  if (!from)
241
249
  return okText(`No workstream matches "${fromArg}".`);
242
250
  if (!into)
243
251
  return okText(`No workstream matches "${intoArg}".`);
244
252
  if (from.id === into.id)
245
253
  return okText(`"${fromArg}" and "${intoArg}" resolve to the same workstream — nothing to merge.`);
246
- await ws.merge(from.id, into.id);
254
+ await ws.merge(tenantId, from.id, into.id);
247
255
  return okText(`Merged "${from.label}" (${from.id}) into "${into.label}" (${into.id}).`);
248
256
  }
249
257
  catch (e) {
250
258
  return err(e);
251
259
  }
252
260
  }
253
- export async function rebindSessionHandler(deps, input) {
261
+ export async function rebindSessionHandler(deps, tenantId, input) {
254
262
  if (!deps.workstreams)
255
263
  return okText("rebind_session is not available in this deployment.");
256
264
  try {
@@ -258,10 +266,10 @@ export async function rebindSessionHandler(deps, input) {
258
266
  const wsArg = (input.workstream ?? "").trim();
259
267
  if (!sessionId || !wsArg)
260
268
  return okText("Provide both a sessionId and a workstream (id or label).");
261
- const ws = await resolveWorkstream(deps.workstreams.store, wsArg);
269
+ const ws = await resolveWorkstream(deps.workstreams.store, tenantId, wsArg);
262
270
  if (!ws)
263
271
  return okText(`No workstream matches "${wsArg}".`);
264
- await deps.store.setWorkstreamBinding(sessionId, ws.id, "operator", null);
272
+ await deps.store.setWorkstreamBinding(tenantId, sessionId, ws.id, "operator", null);
265
273
  return okText(`Rebound session ${sessionId} -> workstream "${ws.label}" (${ws.id}).`);
266
274
  }
267
275
  catch (e) {
@@ -275,9 +283,9 @@ function localToday() {
275
283
  const da = String(d.getDate()).padStart(2, "0");
276
284
  return `${d.getFullYear()}-${mo}-${da}`;
277
285
  }
278
- export async function getSessionHandler(deps, input) {
286
+ export async function getSessionHandler(deps, tenantId, input) {
279
287
  try {
280
- const session = await deps.store.getById(input.id);
288
+ const session = await deps.store.getById(tenantId, input.id);
281
289
  if (!session) {
282
290
  return err(new Error(`session ${input.id} not found`));
283
291
  }
@@ -286,26 +294,34 @@ export async function getSessionHandler(deps, input) {
286
294
  ...(session.supersedes ?? []),
287
295
  ...(session.supersededBy ? [session.supersededBy] : []),
288
296
  ];
289
- const linked = linkedIds.length > 0 ? await deps.store.getByIds(linkedIds) : [];
297
+ const linked = linkedIds.length > 0 ? await deps.store.getByIds(tenantId, linkedIds) : [];
290
298
  const byId = new Map(linked.map((s) => [s.id, s]));
291
299
  // Load supersedence log once so we can join reason + recordedBy onto supersededBy
292
300
  const supersedenceLog = session.supersededBy
293
- ? await readSupersedenceLog()
301
+ ? await readSupersedenceLog(tenantId)
294
302
  : [];
295
303
  const supersedenceMap = new Map(supersedenceLog.map((e) => [e.predecessorId, e]));
296
304
  const supersedes = (session.supersedes ?? []).map((id) => {
297
305
  const s = byId.get(id);
298
306
  return s ? { id, label: s.label, summary: s.summary } : { id, label: "", summary: "" };
299
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).
300
315
  const supersededBy = session.supersededBy
301
316
  ? (() => {
302
317
  const s = byId.get(session.supersededBy);
318
+ if (!s)
319
+ return { id: session.supersededBy, label: "", summary: "" };
303
320
  const logEntry = supersedenceMap.get(session.id);
304
- const base = s
305
- ? { id: session.supersededBy, label: s.label, summary: s.summary }
306
- : { id: session.supersededBy, label: "", summary: "" };
307
321
  return {
308
- ...base,
322
+ id: session.supersededBy,
323
+ label: s.label,
324
+ summary: s.summary,
309
325
  reason: logEntry?.reason,
310
326
  recordedBy: logEntry?.source,
311
327
  };
@@ -317,7 +333,7 @@ export async function getSessionHandler(deps, input) {
317
333
  let outcome;
318
334
  if (deps.outcomeDb) {
319
335
  try {
320
- outcome = await deriveOutcome(session.id, await buildSqliteOutcomeDeps(deps.outcomeDb));
336
+ outcome = await deriveOutcome(tenantId, session.id, await buildSqliteOutcomeDeps(deps.outcomeDb, tenantId));
321
337
  }
322
338
  catch {
323
339
  outcome = undefined;
@@ -329,7 +345,7 @@ export async function getSessionHandler(deps, input) {
329
345
  return err(e);
330
346
  }
331
347
  }
332
- export async function recallFactsHandler(deps, input, runtime = null) {
348
+ export async function recallFactsHandler(deps, tenantId, input, runtime = null) {
333
349
  if (!deps.factRecall) {
334
350
  return err(new Error("fact recall not wired in this deployment"));
335
351
  }
@@ -348,10 +364,10 @@ export async function recallFactsHandler(deps, input, runtime = null) {
348
364
  ? { minConfidence: input.minConfidence }
349
365
  : {}),
350
366
  };
351
- const result = await deps.factRecall.search(query);
367
+ const result = await deps.factRecall.search(tenantId, query);
352
368
  const conversationId = resolveConversationByQuery(input.query ?? "") ?? undefined;
353
369
  // Telemetry — see recallSessionsHandler. Fire-and-forget.
354
- void logFactQuery({
370
+ void logFactQuery(tenantId, {
355
371
  source: "mcp",
356
372
  runtime,
357
373
  query: input.query ?? null,
@@ -370,12 +386,12 @@ export async function recallFactsHandler(deps, input, runtime = null) {
370
386
  return err(e);
371
387
  }
372
388
  }
373
- export async function getFactHistoryHandler(deps, input) {
389
+ export async function getFactHistoryHandler(deps, tenantId, input) {
374
390
  if (!deps.factStore) {
375
391
  return err(new Error("fact store not wired in this deployment"));
376
392
  }
377
393
  try {
378
- const chains = await deps.factStore.getHistory(input.subject, input.predicate);
394
+ const chains = await deps.factStore.getHistory(tenantId, input.subject, input.predicate);
379
395
  return ok({ subject: input.subject, predicate: input.predicate ?? null, chains });
380
396
  }
381
397
  catch (e) {
@@ -537,13 +553,13 @@ Args:
537
553
 
538
554
  Idempotent. Re-marking the same pair is a no-op. Returns the linked pair on
539
555
  success; errors if either id is unknown or the two ids are equal.`;
540
- export async function markSupersededHandler(deps, input) {
556
+ export async function markSupersededHandler(deps, tenantId, input) {
541
557
  if (!input.predecessor_id || !input.successor_id) {
542
558
  return err(new Error("predecessor_id and successor_id are required"));
543
559
  }
544
560
  try {
545
- await deps.store.markSuperseded(input.predecessor_id, input.successor_id);
546
- void appendSupersedence({
561
+ await deps.store.markSuperseded(tenantId, input.predecessor_id, input.successor_id);
562
+ void appendSupersedence(tenantId, {
547
563
  predecessorId: input.predecessor_id,
548
564
  successorId: input.successor_id,
549
565
  source: "mcp",
@@ -581,7 +597,7 @@ Args:
581
597
 
582
598
  Idempotent: calling it twice on the same id is a no-op. Errors if fact_id
583
599
  is unknown.`;
584
- export async function supersedeFactHandler(deps, input) {
600
+ export async function supersedeFactHandler(deps, tenantId, input) {
585
601
  if (!input.fact_id || input.fact_id.length < 4) {
586
602
  return err(new Error("fact_id is required"));
587
603
  }
@@ -589,8 +605,8 @@ export async function supersedeFactHandler(deps, input) {
589
605
  return err(new Error("fact store not available"));
590
606
  }
591
607
  try {
592
- await deps.factStore.retire(input.fact_id);
593
- void appendFactSupersedence({
608
+ await deps.factStore.retire(tenantId, input.fact_id);
609
+ void appendFactSupersedence(tenantId, {
594
610
  factId: input.fact_id,
595
611
  source: "mcp",
596
612
  ...(input.reason !== undefined ? { reason: input.reason } : {}),
@@ -605,20 +621,31 @@ export async function supersedeFactHandler(deps, input) {
605
621
  return err(e);
606
622
  }
607
623
  }
608
- export async function citeSessionHandler(input) {
624
+ export async function citeSessionHandler(tenantId, input) {
609
625
  if (!input.id || input.id.length < MIN_CITE_ID_LEN) {
610
626
  return err(new Error(`id must be at least ${MIN_CITE_ID_LEN} characters`));
611
627
  }
612
628
  try {
613
- await appendCitation({
614
- // Agents rarely pass conversation_id; resolve it server-side from the
615
- // surfaced-memo so the citation joins to its hook fire (NLM #345).
616
- 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,
617
642
  citedId: input.id,
618
643
  kind: "tool_use",
619
644
  ...(input.reason !== undefined ? { responsePreview: input.reason } : {}),
620
645
  });
621
- 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" }) });
622
649
  }
623
650
  catch (e) {
624
651
  return err(e);
@@ -640,7 +667,7 @@ Args:
640
667
  * ambiguity). Tier-A correlation in rollup.ts keys on session_id only, so
641
668
  * this choice does not affect verdict derivation. */
642
669
  const REPORT_OUTCOME_KIND = "review";
643
- export async function reportOutcomeHandler(deps, input) {
670
+ export async function reportOutcomeHandler(deps, tenantId, input) {
644
671
  if (!input.session_id && !input.correlation_key) {
645
672
  return err(new Error("session_id or correlation_key is required"));
646
673
  }
@@ -662,28 +689,28 @@ export async function reportOutcomeHandler(deps, input) {
662
689
  ...(input.correlation_key ? { correlation_key: input.correlation_key } : {}),
663
690
  },
664
691
  }, deps.installScope);
665
- signal = await stampSignalScope(signal, { sessionScopeReader: deps.sessionScopeReader });
666
- await deps.signalStore.insert(signal);
692
+ signal = await stampSignalScope(signal, tenantId, { sessionScopeReader: deps.sessionScopeReader });
693
+ await deps.signalStore.insert(tenantId, signal);
667
694
  return ok({ recorded: true, id: signal.id, outcome: signal.outcome });
668
695
  }
669
696
  catch (e) {
670
697
  return err(e);
671
698
  }
672
699
  }
673
- export async function listMergeSuggestionsHandler(deps, input) {
700
+ export async function listMergeSuggestionsHandler(deps, tenantId, input) {
674
701
  if (!deps.workstreams)
675
702
  return okText("list_merge_suggestions is not available in this deployment.");
676
703
  try {
677
704
  const minScore = typeof input.minScore === "number" ? input.minScore : 0.5;
678
- 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");
679
706
  if (all.length < 2)
680
707
  return okText("Not enough active workstreams to suggest merges.");
681
708
  const ids = all.map((w) => w.id);
682
- const entMap = await deps.workstreams.store.entitiesFor(ids);
709
+ const entMap = await deps.workstreams.store.entitiesFor(tenantId, ids);
683
710
  const items = await Promise.all(all.map(async (w) => ({
684
711
  id: w.id, label: w.label,
685
712
  entities: entMap.get(w.id) ?? [],
686
- sessionIds: await deps.workstreams.sessions.listSessionIdsByWorkstreams([w.id]),
713
+ sessionIds: await deps.workstreams.sessions.listSessionIdsByWorkstreams(tenantId, [w.id]),
687
714
  })));
688
715
  const suggestions = suggestMerges(items, minScore);
689
716
  if (suggestions.length === 0)
@@ -699,7 +726,144 @@ export async function listMergeSuggestionsHandler(deps, input) {
699
726
  return err(e);
700
727
  }
701
728
  }
702
- 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) {
703
867
  const server = new McpServer({
704
868
  name: SERVER_NAME,
705
869
  version: SERVER_VERSION,
@@ -746,7 +910,7 @@ export function createMcpServer(deps) {
746
910
  idempotentHint: true,
747
911
  openWorldHint: true,
748
912
  },
749
- }, async (args) => recallSessionsHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
913
+ }, async (args) => recallSessionsHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
750
914
  server.registerTool("get_session", {
751
915
  title: "Get Full NLM Session",
752
916
  description: GET_SESSION_DESCRIPTION,
@@ -759,7 +923,7 @@ export function createMcpServer(deps) {
759
923
  idempotentHint: true,
760
924
  openWorldHint: true,
761
925
  },
762
- }, async (args) => getSessionHandler(deps, args));
926
+ }, async (args) => getSessionHandler(deps, tenantId, args));
763
927
  if (deps.factRecall && deps.factStore) {
764
928
  server.registerTool("recall_facts", {
765
929
  title: "Recall Facts from NLM",
@@ -809,7 +973,7 @@ export function createMcpServer(deps) {
809
973
  idempotentHint: true,
810
974
  openWorldHint: true,
811
975
  },
812
- }, async (args) => recallFactsHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
976
+ }, async (args) => recallFactsHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
813
977
  server.registerTool("get_fact_history", {
814
978
  title: "Get Fact Supersedence History",
815
979
  description: GET_FACT_HISTORY_DESCRIPTION,
@@ -826,7 +990,7 @@ export function createMcpServer(deps) {
826
990
  idempotentHint: true,
827
991
  openWorldHint: true,
828
992
  },
829
- }, async (args) => getFactHistoryHandler(deps, args));
993
+ }, async (args) => getFactHistoryHandler(deps, tenantId, args));
830
994
  server.registerTool("supersede_fact", {
831
995
  title: "Supersede a Stale NLM Fact",
832
996
  description: SUPERSEDE_FACT_DESCRIPTION,
@@ -846,7 +1010,7 @@ export function createMcpServer(deps) {
846
1010
  idempotentHint: true,
847
1011
  openWorldHint: false,
848
1012
  },
849
- }, async (args) => supersedeFactHandler(deps, args));
1013
+ }, async (args) => supersedeFactHandler(deps, tenantId, args));
850
1014
  }
851
1015
  server.registerTool("cite_session", {
852
1016
  title: "Cite NLM Session",
@@ -868,7 +1032,7 @@ export function createMcpServer(deps) {
868
1032
  idempotentHint: false,
869
1033
  openWorldHint: false,
870
1034
  },
871
- }, async (args) => citeSessionHandler(args));
1035
+ }, async (args) => citeSessionHandler(tenantId, args));
872
1036
  if (deps.signalStore && deps.installScope !== undefined) {
873
1037
  server.registerTool("report_outcome", {
874
1038
  title: "Report Session Outcome",
@@ -900,7 +1064,7 @@ export function createMcpServer(deps) {
900
1064
  idempotentHint: true,
901
1065
  openWorldHint: false,
902
1066
  },
903
- }, async (args) => reportOutcomeHandler(deps, args));
1067
+ }, async (args) => reportOutcomeHandler(deps, tenantId, args));
904
1068
  }
905
1069
  server.registerTool("mark_superseded", {
906
1070
  title: "Mark NLM Session Superseded",
@@ -925,7 +1089,7 @@ export function createMcpServer(deps) {
925
1089
  idempotentHint: true,
926
1090
  openWorldHint: false,
927
1091
  },
928
- }, async (args) => markSupersededHandler(deps, args));
1092
+ }, async (args) => markSupersededHandler(deps, tenantId, args));
929
1093
  server.registerTool("work_summary", {
930
1094
  title: "Daily work summary",
931
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.",
@@ -937,7 +1101,7 @@ export function createMcpServer(deps) {
937
1101
  .describe("Day to summarize, YYYY-MM-DD. Defaults to today."),
938
1102
  },
939
1103
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
940
- }, async (args) => workSummaryHandler(deps, args));
1104
+ }, async (args) => workSummaryHandler(deps, tenantId, args));
941
1105
  server.registerTool("recall_workstream", {
942
1106
  title: "Recall a workstream's accumulated context",
943
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.",
@@ -945,7 +1109,7 @@ export function createMcpServer(deps) {
945
1109
  idOrLabel: z.string().describe("Workstream id (ws_...) or label (e.g. 'NLM')."),
946
1110
  },
947
1111
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
948
- }, async (args) => recallWorkstreamHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
1112
+ }, async (args) => recallWorkstreamHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
949
1113
  server.registerTool("rebind_session", {
950
1114
  title: "Rebind a session to a different workstream",
951
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).",
@@ -954,7 +1118,7 @@ export function createMcpServer(deps) {
954
1118
  workstream: z.string().describe("Target workstream id (ws_...) or label."),
955
1119
  },
956
1120
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
957
- }, async (args) => rebindSessionHandler(deps, args));
1121
+ }, async (args) => rebindSessionHandler(deps, tenantId, args));
958
1122
  server.registerTool("merge_workstreams", {
959
1123
  title: "Merge one workstream into another",
960
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.",
@@ -963,7 +1127,7 @@ export function createMcpServer(deps) {
963
1127
  into: z.string().describe("Workstream to keep (the survivor); id or label."),
964
1128
  },
965
1129
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
966
- }, async (args) => mergeWorkstreamsHandler(deps, args));
1130
+ }, async (args) => mergeWorkstreamsHandler(deps, tenantId, args));
967
1131
  server.registerTool("rename_workstream", {
968
1132
  title: "Rename a workstream",
969
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.",
@@ -972,19 +1136,19 @@ export function createMcpServer(deps) {
972
1136
  label: z.string().describe("New label."),
973
1137
  },
974
1138
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
975
- }, async (args) => renameWorkstreamHandler(deps, args));
1139
+ }, async (args) => renameWorkstreamHandler(deps, tenantId, args));
976
1140
  server.registerTool("retire_workstream", {
977
1141
  title: "Retire a workstream",
978
1142
  description: "Mark a workstream retired (status=retired). Operator cleanup for dead one-off workstreams. Reversible by re-setting status. Accepts id or label.",
979
1143
  inputSchema: { idOrLabel: z.string().describe("Workstream id or label.") },
980
1144
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
981
- }, async (args) => retireWorkstreamHandler(deps, args));
1145
+ }, async (args) => retireWorkstreamHandler(deps, tenantId, args));
982
1146
  server.registerTool("list_merge_suggestions", {
983
1147
  title: "Suggest duplicate workstreams to merge",
984
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.",
985
1149
  inputSchema: { minScore: z.number().optional().describe("Minimum similarity score 0..1 (default 0.5).") },
986
1150
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
987
- }, async (args) => listMergeSuggestionsHandler(deps, args));
1151
+ }, async (args) => listMergeSuggestionsHandler(deps, tenantId, args));
988
1152
  if (deps.exemplarStore &&
989
1153
  deps.installScope &&
990
1154
  process.env["NLM_CODE_EXEMPLARS_ENABLED"] === "1") {
@@ -1026,7 +1190,7 @@ export function createMcpServer(deps) {
1026
1190
  openWorldHint: false,
1027
1191
  },
1028
1192
  }, async (args) => {
1029
- const result = await recallCode({
1193
+ const result = await recallCode(tenantId, {
1030
1194
  query: args.query,
1031
1195
  installScope,
1032
1196
  ...(args.repo ? { repo: args.repo } : {}),
@@ -1039,7 +1203,7 @@ export function createMcpServer(deps) {
1039
1203
  ...result.positives.map((e) => e.id),
1040
1204
  ...result.negatives.map((e) => e.id),
1041
1205
  ];
1042
- void logQuery({
1206
+ void logQuery(tenantId, {
1043
1207
  source: "mcp",
1044
1208
  runtime: mcpRuntimeFromClient(server.server.getClientVersion()),
1045
1209
  query: args.query,
@@ -1072,10 +1236,69 @@ export function createMcpServer(deps) {
1072
1236
  if (patch.retired === undefined && patch.outcome === undefined) {
1073
1237
  return { content: [{ type: "text", text: JSON.stringify({ error: "provide retire and/or outcome" }) }] };
1074
1238
  }
1075
- const res = await exemplarStore.setVerdict(a.exemplar_id, patch, "human");
1239
+ const res = await exemplarStore.setVerdict(tenantId, a.exemplar_id, patch, "human");
1076
1240
  return { content: [{ type: "text", text: JSON.stringify({ exemplar_id: a.exemplar_id, status: res.status }) }] };
1077
1241
  });
1078
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
+ }
1079
1302
  return server;
1080
1303
  }
1081
1304
  const RECALL_CODE_DESCRIPTION = `\