nlm-memory 0.21.0 → 0.21.13

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 +105 -45
  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
@@ -10,6 +10,16 @@
10
10
  * markers + session_embedding_chunks (vec0). Idle-status overlay (computed
11
11
  * from transcript mtime) is deferred to a later phase; A.2 returns the
12
12
  * persisted status verbatim.
13
+ *
14
+ * Tenancy (program spec §4, M2 plan Wave B): every method takes `tenantId`
15
+ * as its non-optional first parameter. `sessions`, `entities`,
16
+ * `entity_variants`, and `session_entities` are STAMP tables and every
17
+ * SELECT/UPDATE/DELETE against them routes its WHERE fragment through
18
+ * `tenantClause`; INSERTs stamp `tenant_id` explicitly. `markers` and
19
+ * `session_edges` carry no `tenant_id` column (DERIVE-VIA-FK) — they are
20
+ * reached only through an already tenant-filtered session id set, or (for
21
+ * `recentMarkers`/`markSuperseded`'s fact cascade) via an explicit join back
22
+ * to a tenant-filtered `sessions` row.
13
23
  */
14
24
  import { existsSync, mkdirSync } from "node:fs";
15
25
  import { dirname, resolve } from "node:path";
@@ -21,6 +31,9 @@ import { runMigrations } from "./migrate.js";
21
31
  import { tokenize } from "../recall/tokenize.js";
22
32
  import { chunkSessionText } from "../embedding/chunk-body.js";
23
33
  import { batchWinners } from "./fact-batch.js";
34
+ import { tenantClause } from "../tenancy/tenant-clause.js";
35
+ import { DEFAULT_TEAM_ID } from "../tenancy/default-team.js";
36
+ import { recordEmbedFailure } from "../health/embed-failure-state.js";
24
37
  /**
25
38
  * Find the most recent prior session whose entity-set is identical to the new
26
39
  * session's, so a `continues` edge can link the two. "Identical" means same set
@@ -28,18 +41,28 @@ import { batchWinners } from "./fact-batch.js";
28
41
  * session is not superseding/replacing anything — a continuation extends a prior
29
42
  * topic rather than overturning it. Returns null when there is no such prior
30
43
  * (no entities, or no exact-set match), which leaves the pair unlinked and thus
31
- * eligible to surface in the re_derivation_rate metric.
44
+ * eligible to surface in the re_derivation_rate metric. Scoped to the same
45
+ * tenant as the new session — a continuation can never cross tenants.
32
46
  */
33
- function findContinuesPredecessor(db, newId, rawEntities) {
47
+ function findContinuesPredecessor(db, tenantId, newId, rawEntities) {
34
48
  const entities = [...new Set(rawEntities.map((e) => e.trim()).filter(Boolean))];
35
49
  if (entities.length === 0)
36
50
  return null;
37
51
  const placeholders = entities.map(() => "?").join(",");
52
+ // session_entities rows are always written with the same tenant_id as their
53
+ // owning session (every write site stamps both together), so scoping the
54
+ // outer `sessions` row by tenant is already sufficient. The `se.tenant_id =
55
+ // s.tenant_id` join condition below is defense in depth (guard test C4
56
+ // explicitly permits column-to-column tenant equality as a join form) — it
57
+ // costs nothing and survives a future write path that stamps the two rows
58
+ // inconsistently.
59
+ const tc = tenantClause(tenantId, "s.tenant_id");
38
60
  const row = db
39
61
  .prepare(`SELECT s.id AS id
40
62
  FROM sessions s
41
- JOIN session_entities se ON se.session_id = s.id
63
+ JOIN session_entities se ON se.session_id = s.id AND se.tenant_id = s.tenant_id
42
64
  WHERE s.id != ?
65
+ AND ${tc.sql}
43
66
  AND se.entity_canonical IN (${placeholders})
44
67
  GROUP BY s.id
45
68
  HAVING COUNT(DISTINCT se.entity_canonical) = ?
@@ -47,13 +70,16 @@ function findContinuesPredecessor(db, newId, rawEntities) {
47
70
  = (SELECT COUNT(*) FROM session_entities x WHERE x.session_id = s.id)
48
71
  ORDER BY s.started_at DESC, s.id DESC
49
72
  LIMIT 1`)
50
- .get(newId, ...entities, entities.length);
73
+ .get(newId, tc.param, ...entities, entities.length);
51
74
  return row?.id ?? null;
52
75
  }
53
76
  export class SqliteSessionStore {
54
77
  db;
55
- overlayCache = null;
56
- overlayCacheAt = 0;
78
+ // Keyed by tenantId — a single-tenant Map entry today (DEFAULT_TEAM_ID),
79
+ // but the overlay's content is tenant-scoped (program spec §4.6 hardening
80
+ // 1) and must never bleed across tenants once M3 adds real multi-tenant
81
+ // traffic to one daemon instance.
82
+ overlayCache = new Map();
57
83
  /**
58
84
  * @internal. Construct via SqliteStorage.create(...) instead. Direct
59
85
  * construction is preserved for the SqliteStorage adapter only; all
@@ -90,38 +116,42 @@ export class SqliteSessionStore {
90
116
  return this.db;
91
117
  }
92
118
  invalidateOverlayCache() {
93
- this.overlayCache = null;
119
+ this.overlayCache.clear();
94
120
  }
95
- overlay() {
121
+ overlay(tenantId) {
96
122
  // TTL backstop: explicit invalidation covers the daemon's own writers; the
97
123
  // 30s expiry bounds staleness if another process ever writes actions to
98
124
  // this database file.
99
- if (this.overlayCache !== null && Date.now() - this.overlayCacheAt < 30_000) {
100
- return this.overlayCache;
125
+ const cached = this.overlayCache.get(tenantId);
126
+ if (cached !== undefined && Date.now() - cached.at < 30_000) {
127
+ return cached.overlay;
101
128
  }
102
- this.overlayCache = loadActionOverlay(this.db);
103
- this.overlayCacheAt = Date.now();
104
- return this.overlayCache;
129
+ const overlay = loadActionOverlay(this.db, tenantId);
130
+ this.overlayCache.set(tenantId, { overlay, at: Date.now() });
131
+ return overlay;
105
132
  }
106
133
  /** Recently-written sessions ordered by created_at desc. Powers /live Writes column. */
107
- recentWrites(limit) {
134
+ recentWrites(tenantId, limit) {
135
+ const tc = tenantClause(tenantId);
108
136
  const rows = this.db
109
137
  .prepare(`SELECT id, runtime, label, summary, created_at AS createdAt
110
138
  FROM sessions
139
+ WHERE ${tc.sql}
111
140
  ORDER BY created_at DESC
112
141
  LIMIT ?`)
113
- .all(limit);
142
+ .all(tc.param, limit);
114
143
  if (rows.length === 0)
115
144
  return [];
116
145
  // Pull associated entities in one shot keyed by session id; cheap because
117
146
  // limit is small (<=50). Renders as topic chips on the /live row.
118
147
  const placeholders = rows.map(() => "?").join(",");
148
+ const entTc = tenantClause(tenantId);
119
149
  const entityRows = this.db
120
150
  .prepare(`SELECT session_id, entity_canonical
121
151
  FROM session_entities
122
- WHERE session_id IN (${placeholders})
152
+ WHERE ${entTc.sql} AND session_id IN (${placeholders})
123
153
  ORDER BY entity_canonical`)
124
- .all(...rows.map((r) => r.id));
154
+ .all(entTc.param, ...rows.map((r) => r.id));
125
155
  const byId = new Map();
126
156
  for (const e of entityRows) {
127
157
  const list = byId.get(e.session_id);
@@ -133,14 +163,16 @@ export class SqliteSessionStore {
133
163
  return rows.map((r) => ({ ...r, entities: byId.get(r.id) ?? [] }));
134
164
  }
135
165
  /** Recently-extracted markers ordered by session created_at desc. Powers /live Decisions column. */
136
- recentMarkers(limit) {
166
+ recentMarkers(tenantId, limit) {
167
+ const tc = tenantClause(tenantId, "s.tenant_id");
137
168
  return this.db
138
169
  .prepare(`SELECT m.session_id AS sessionId, m.kind, m.text, s.label, s.created_at AS createdAt
139
170
  FROM markers m
140
171
  JOIN sessions s ON s.id = m.session_id
172
+ WHERE ${tc.sql}
141
173
  ORDER BY s.created_at DESC, m.position ASC
142
174
  LIMIT ?`)
143
- .all(limit);
175
+ .all(tc.param, limit);
144
176
  }
145
177
  /**
146
178
  * Atomic ingest: writes the session row, markers, entity rows + links,
@@ -153,7 +185,7 @@ export class SqliteSessionStore {
153
185
  * Mirrors Python's SQLiteStore.insert_session. Markdown projection is not
154
186
  * yet ported and skipped here.
155
187
  */
156
- async insertSession(record, embedder = null, supersedes = null, factSink = null) {
188
+ async insertSession(tenantId, record, embedder = null, supersedes = null, factSink = null) {
157
189
  const db = this.db;
158
190
  const txn = db.transaction(() => {
159
191
  db.prepare(`
@@ -163,13 +195,13 @@ export class SqliteSessionStore {
163
195
  transcript_kind, transcript_path, transcript_offset, transcript_length,
164
196
  classifier_provider, classifier_model, classifier_confidence,
165
197
  scope, agent_persona, parent_session_id,
166
- primary_model, total_tokens, skill
198
+ primary_model, total_tokens, skill, tenant_id
167
199
  ) VALUES (@id, @runtime, @runtimeSessionId, @startedAt, @endedAt, @durationMin,
168
200
  @label, @summary, @body, @status,
169
201
  @transcriptKind, @transcriptPath, @transcriptOffset, @transcriptLength,
170
202
  @classifierProvider, @classifierModel, @classifierConfidence,
171
203
  @scope, @agentPersona, @parentSessionId,
172
- @primaryModel, @totalTokens, @skill)
204
+ @primaryModel, @totalTokens, @skill, @tenantId)
173
205
  ON CONFLICT(id) DO UPDATE SET
174
206
  ended_at = excluded.ended_at,
175
207
  duration_min = excluded.duration_min,
@@ -211,6 +243,7 @@ export class SqliteSessionStore {
211
243
  primaryModel: record.primaryModel ?? null,
212
244
  totalTokens: record.totalTokens ?? null,
213
245
  skill: record.skill ?? null,
246
+ tenantId,
214
247
  });
215
248
  db.prepare("DELETE FROM markers WHERE session_id = ?").run(record.id);
216
249
  const markerStmt = db.prepare("INSERT INTO markers (session_id, kind, text, position) VALUES (?, ?, ?, ?)");
@@ -223,42 +256,49 @@ export class SqliteSessionStore {
223
256
  const rawNewEntities = [...new Set(record.entities.map((e) => e.trim()).filter(Boolean))];
224
257
  // Resolve each extracted entity through entity_variants so merged surface
225
258
  // forms bind to the canonical instead of resurrecting the retired source.
226
- const resolveVariant = db.prepare("SELECT canonical FROM entity_variants WHERE variant = ?");
227
- const newEntities = rawNewEntities.map((name) => resolveVariant.get(name)?.canonical ?? name);
259
+ const variantTc = tenantClause(tenantId);
260
+ const resolveVariant = db.prepare(`SELECT canonical FROM entity_variants WHERE ${variantTc.sql} AND variant = ?`);
261
+ const newEntities = rawNewEntities.map((name) => resolveVariant.get(variantTc.param, name)?.canonical ?? name);
262
+ const oldEntityTc = tenantClause(tenantId);
228
263
  const oldEntityRows = db
229
- .prepare("SELECT entity_canonical FROM session_entities WHERE session_id = ?")
230
- .all(record.id);
264
+ .prepare(`SELECT entity_canonical FROM session_entities WHERE ${oldEntityTc.sql} AND session_id = ?`)
265
+ .all(oldEntityTc.param, record.id);
231
266
  const oldEntities = new Set(oldEntityRows.map((r) => r.entity_canonical));
232
- db.prepare("DELETE FROM session_entities WHERE session_id = ?").run(record.id);
267
+ const deleteEntTc = tenantClause(tenantId);
268
+ db.prepare(`DELETE FROM session_entities WHERE ${deleteEntTc.sql} AND session_id = ?`).run(deleteEntTc.param, record.id);
233
269
  const insertEnt = db.prepare(`
234
270
  INSERT OR IGNORE INTO entities
235
- (canonical, type, status, source, first_seen_session, last_seen_session, session_count)
236
- VALUES (?, 'candidate', 'candidate', 'auto-detected', ?, ?, 0)
271
+ (tenant_id, canonical, type, status, source, first_seen_session, last_seen_session, session_count)
272
+ VALUES (?, ?, 'candidate', 'candidate', 'auto-detected', ?, ?, 0)
237
273
  `);
238
- const touchLastSeen = db.prepare("UPDATE entities SET last_seen_session = ?, updated_at = datetime('now') WHERE canonical = ?");
239
- const linkEnt = db.prepare("INSERT INTO session_entities (session_id, entity_canonical) VALUES (?, ?)");
274
+ const touchLastSeenTc = tenantClause(tenantId);
275
+ const touchLastSeen = db.prepare(`UPDATE entities SET last_seen_session = ?, updated_at = datetime('now') WHERE ${touchLastSeenTc.sql} AND canonical = ?`);
276
+ const linkEnt = db.prepare("INSERT INTO session_entities (tenant_id, session_id, entity_canonical) VALUES (?, ?, ?)");
240
277
  for (const name of newEntities) {
241
- insertEnt.run(name, record.id, record.id);
278
+ insertEnt.run(tenantId, name, record.id, record.id);
242
279
  // Update last_seen for entities newly added to this session; matches prior touch semantics.
243
280
  if (!oldEntities.has(name)) {
244
- touchLastSeen.run(record.id, name);
281
+ touchLastSeen.run(record.id, tenantId, name);
245
282
  }
246
- linkEnt.run(record.id, name);
283
+ linkEnt.run(tenantId, record.id, name);
247
284
  }
248
285
  // Recompute session_count exactly for every entity in (old union new) so
249
286
  // counts reflect reality regardless of prior drift.
250
- const recomputeCount = db.prepare("UPDATE entities SET session_count = (SELECT COUNT(*) FROM session_entities WHERE entity_canonical = ?), updated_at = datetime('now') WHERE canonical = ?");
287
+ const recomputeInnerTc = tenantClause(tenantId);
288
+ const recomputeOuterTc = tenantClause(tenantId);
289
+ const recomputeCount = db.prepare(`UPDATE entities SET session_count = (SELECT COUNT(*) FROM session_entities WHERE ${recomputeInnerTc.sql} AND entity_canonical = ?), updated_at = datetime('now') WHERE ${recomputeOuterTc.sql} AND canonical = ?`);
251
290
  for (const name of new Set([...oldEntities, ...newEntities])) {
252
- recomputeCount.run(name, name);
291
+ recomputeCount.run(tenantId, name, tenantId, name);
253
292
  }
254
293
  if (supersedes && supersedes.priorSessionId !== record.id) {
255
294
  const predecessorStatus = supersedes.kind === "replaces" ? "replaced" : "superseded";
256
295
  db.prepare(`INSERT OR IGNORE INTO session_edges (from_session, to_session, kind)
257
296
  VALUES (?, ?, ?)`).run(record.id, supersedes.priorSessionId, supersedes.kind);
258
- db.prepare("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?").run(predecessorStatus, supersedes.priorSessionId);
297
+ const predStatusTc = tenantClause(tenantId);
298
+ db.prepare(`UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ? AND ${predStatusTc.sql}`).run(predecessorStatus, supersedes.priorSessionId, predStatusTc.param);
259
299
  }
260
300
  else {
261
- const priorId = findContinuesPredecessor(db, record.id, record.entities);
301
+ const priorId = findContinuesPredecessor(db, tenantId, record.id, record.entities);
262
302
  if (priorId !== null) {
263
303
  db.prepare(`INSERT OR IGNORE INTO session_edges (from_session, to_session, kind)
264
304
  VALUES (?, ?, 'continues')`).run(record.id, priorId);
@@ -292,7 +332,7 @@ export class SqliteSessionStore {
292
332
  // copy drifted from the port method and missed NLM #351 bug 1 (orphan
293
333
  // embeddings on replace) and bug 2 (per-fact collapse -> supersedence
294
334
  // cycles). Runs inside this session txn.
295
- factSink.factStore.ingestSessionFactsInTxn(record.id, factSink.facts, record.scope);
335
+ factSink.factStore.ingestSessionFactsInTxn(tenantId, record.id, factSink.facts, record.scope);
296
336
  }
297
337
  });
298
338
  txn();
@@ -316,13 +356,15 @@ export class SqliteSessionStore {
316
356
  const { vector } = await embedder.embed(text, "document");
317
357
  this.insertChunkEmbedding(record.id, chunkIdx, vector);
318
358
  }
319
- catch {
359
+ catch (err) {
320
360
  // Per-chunk embedder failure must not roll the ingest back or
321
361
  // abort subsequent chunks.
362
+ recordEmbedFailure("chunk");
363
+ process.stderr.write(`[nlm] embedding chunk failed session=${record.id} chunk=${chunkIdx}: ${String(err)}\n`);
322
364
  }
323
365
  }
324
366
  if (factSink !== null) {
325
- await this.embedFacts(factSink.factStore, factSink.facts, embedder);
367
+ await this.embedFacts(tenantId, factSink.factStore, factSink.facts, embedder);
326
368
  }
327
369
  }
328
370
  }
@@ -355,22 +397,27 @@ export class SqliteSessionStore {
355
397
  * Non-empty body required (the classifier needs transcript text). When
356
398
  * `reprocess` is false, sessions that already have facts are excluded.
357
399
  */
358
- async listBackfillCandidates(filter) {
400
+ async listBackfillCandidates(tenantId, filter) {
401
+ const tc = tenantClause(tenantId, "s.tenant_id");
359
402
  const sql = filter.reprocess
360
- ? `SELECT id, started_at, body FROM sessions
361
- WHERE started_at < ? AND body IS NOT NULL AND length(body) > 0
362
- ${filter.from ? "AND id > ?" : ""}
363
- ORDER BY started_at ASC, id ASC`
403
+ ? `SELECT s.id, s.started_at, s.body FROM sessions s
404
+ WHERE ${tc.sql} AND s.started_at < ? AND s.body IS NOT NULL AND length(s.body) > 0
405
+ ${filter.from ? "AND s.id > ?" : ""}
406
+ ORDER BY s.started_at ASC, s.id ASC`
407
+ // facts.tenant_id is always stamped to match its source_session_id's
408
+ // session tenant, so this is defense in depth on top of the already
409
+ // tenant-filtered outer sessions row (guard test C4 permits
410
+ // column-to-column tenant equality as a join form).
364
411
  : `SELECT s.id, s.started_at, s.body FROM sessions s
365
- WHERE s.started_at < ? AND s.body IS NOT NULL AND length(s.body) > 0
366
- AND NOT EXISTS (SELECT 1 FROM facts f WHERE f.source_session_id = s.id)
412
+ WHERE ${tc.sql} AND s.started_at < ? AND s.body IS NOT NULL AND length(s.body) > 0
413
+ AND NOT EXISTS (SELECT 1 FROM facts f WHERE f.source_session_id = s.id AND f.tenant_id = s.tenant_id)
367
414
  ${filter.from ? "AND s.id > ?" : ""}
368
415
  ORDER BY s.started_at ASC, s.id ASC`;
369
416
  const rows = filter.from
370
417
  ? this.db.prepare(sql)
371
- .all(filter.cutoff, filter.from)
418
+ .all(tc.param, filter.cutoff, filter.from)
372
419
  : this.db.prepare(sql)
373
- .all(filter.cutoff);
420
+ .all(tc.param, filter.cutoff);
374
421
  return rows.map((r) => ({ id: r.id, startedAt: r.started_at, body: r.body }));
375
422
  }
376
423
  /**
@@ -385,17 +432,17 @@ export class SqliteSessionStore {
385
432
  * write path. The live ingest path (`insertSession`) keeps using the
386
433
  * internal helpers directly so session+facts commit together.
387
434
  */
388
- async insertFactsForSession(sessionId, factStore, facts, embedder = null) {
435
+ async insertFactsForSession(tenantId, sessionId, factStore, facts, embedder = null) {
389
436
  // Delegate to the canonical sync ingest (single source of truth for
390
437
  // replace + collapse + embedding cleanup). Sync because better-sqlite3 txn
391
438
  // callbacks must be sync; ingestSessionFactsInTxn is the sync core.
392
- const sessionScope = await this.getSessionScopeById(sessionId);
439
+ const sessionScope = await this.getSessionScopeById(tenantId, sessionId);
393
440
  const txn = this.db.transaction(() => {
394
- factStore.ingestSessionFactsInTxn(sessionId, facts, sessionScope);
441
+ factStore.ingestSessionFactsInTxn(tenantId, sessionId, facts, sessionScope);
395
442
  });
396
443
  txn();
397
444
  if (embedder) {
398
- await this.embedFacts(factStore, facts, embedder);
445
+ await this.embedFacts(tenantId, factStore, facts, embedder);
399
446
  }
400
447
  }
401
448
  /**
@@ -403,23 +450,26 @@ export class SqliteSessionStore {
403
450
  * embeddings to fact_embeddings via FactStore.upsertEmbedding. Per-fact
404
451
  * failures don't abort the batch, and never affect committed fact rows.
405
452
  */
406
- async embedFacts(factStore, facts, embedder) {
453
+ async embedFacts(tenantId, factStore, facts, embedder) {
407
454
  for (const fact of batchWinners(facts)) {
408
455
  const factText = `${fact.subject} ${fact.predicate} ${fact.value}`.trim();
409
456
  if (!factText)
410
457
  continue;
411
458
  try {
412
459
  const { vector } = await embedder.embed(factText, "document");
413
- await factStore.upsertEmbedding(fact.id, vector);
460
+ await factStore.upsertEmbedding(tenantId, fact.id, vector);
414
461
  }
415
- catch {
462
+ catch (err) {
416
463
  // Per-fact embedding failure must not abort embedding of subsequent
417
464
  // facts. The fact row stays current; semantic recall just misses it
418
465
  // until a future re-ingest.
466
+ recordEmbedFailure("fact");
467
+ process.stderr.write(`[nlm] embedding fact failed fact=${fact.id}: ${String(err)}\n`);
419
468
  }
420
469
  }
421
470
  }
422
- async getById(sessionId) {
471
+ async getById(tenantId, sessionId) {
472
+ const tc = tenantClause(tenantId);
423
473
  const row = this.db
424
474
  .prepare(`
425
475
  SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
@@ -428,15 +478,15 @@ export class SqliteSessionStore {
428
478
  agent_persona, parent_session_id,
429
479
  primary_model, total_tokens, skill
430
480
  FROM sessions
431
- WHERE id = ?
481
+ WHERE id = ? AND ${tc.sql}
432
482
  `)
433
- .get(sessionId);
483
+ .get(sessionId, tc.param);
434
484
  if (!row)
435
485
  return null;
436
- const entities = this.loadEntities([sessionId]);
486
+ const entities = this.loadEntities(tenantId, [sessionId]);
437
487
  const markers = this.loadMarkers([sessionId]);
438
488
  const edges = this.loadSessionEdges([sessionId]);
439
- const overlay = this.overlay();
489
+ const overlay = this.overlay(tenantId);
440
490
  return this.rowToSession(row, entities, markers, overlay, edges);
441
491
  }
442
492
  /**
@@ -445,45 +495,47 @@ export class SqliteSessionStore {
445
495
  * never reads, and SELECTing it for the corpus is what wedged the
446
496
  * daemon. Resolved sessions carry `body: ""`.
447
497
  */
448
- async getByIds(ids) {
498
+ async getByIds(tenantId, ids) {
449
499
  if (ids.length === 0)
450
500
  return [];
451
501
  const placeholders = ids.map(() => "?").join(",");
502
+ const tc = tenantClause(tenantId);
452
503
  const rows = this.db
453
504
  .prepare(`
454
505
  SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
455
506
  label, summary, status, transcript_kind, transcript_path
456
507
  FROM sessions
457
- WHERE id IN (${placeholders})
508
+ WHERE id IN (${placeholders}) AND ${tc.sql}
458
509
  `)
459
- .all(...ids);
510
+ .all(...ids, tc.param);
460
511
  if (rows.length === 0)
461
512
  return [];
462
513
  const foundIds = rows.map((r) => r.id);
463
- const entitiesByIdMap = this.loadEntities(foundIds);
514
+ const entitiesByIdMap = this.loadEntities(tenantId, foundIds);
464
515
  const markersByIdMap = this.loadMarkers(foundIds);
465
- const overlay = this.overlay();
516
+ const overlay = this.overlay(tenantId);
466
517
  return rows.map((r) => this.rowToSession({ ...r, body: null }, entitiesByIdMap, markersByIdMap, overlay));
467
518
  }
468
- async listByDateRange(fromIso, toIso) {
519
+ async listByDateRange(tenantId, fromIso, toIso) {
520
+ const tc = tenantClause(tenantId);
469
521
  const rows = this.db
470
522
  .prepare(`
471
523
  SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
472
524
  label, summary, status, transcript_kind, transcript_path, workstream_id
473
525
  FROM sessions
474
- WHERE started_at < ? AND (ended_at IS NULL OR ended_at >= ?)
526
+ WHERE started_at < ? AND (ended_at IS NULL OR ended_at >= ?) AND ${tc.sql}
475
527
  ORDER BY started_at ASC
476
528
  `)
477
- .all(toIso, fromIso);
529
+ .all(toIso, fromIso, tc.param);
478
530
  if (rows.length === 0)
479
531
  return [];
480
532
  const ids = rows.map((r) => r.id);
481
- const entitiesByIdMap = this.loadEntities(ids);
533
+ const entitiesByIdMap = this.loadEntities(tenantId, ids);
482
534
  const markersByIdMap = this.loadMarkers(ids);
483
- const overlay = this.overlay();
535
+ const overlay = this.overlay(tenantId);
484
536
  return rows.map((r) => this.rowToSession({ ...r, body: null }, entitiesByIdMap, markersByIdMap, overlay));
485
537
  }
486
- async semanticSearch(queryVector, limit, opts) {
538
+ async semanticSearch(tenantId, queryVector, limit, opts) {
487
539
  const k = Math.max(1, Math.trunc(limit));
488
540
  const blob = Buffer.from(queryVector.buffer, queryVector.byteOffset, queryVector.byteLength);
489
541
  // Overfetch chunks so the max-pool grouping has enough unique sessions
@@ -494,6 +546,12 @@ export class SqliteSessionStore {
494
546
  const envOverfetch = Number.parseInt(process.env["NLM_CHUNK_OVERFETCH"] ?? "", 10);
495
547
  const CHUNK_OVERFETCH = Number.isFinite(envOverfetch) && envOverfetch > 0 ? envOverfetch : 4;
496
548
  const chunkK = k * CHUNK_OVERFETCH;
549
+ // The vec0 KNN scan has no tenant awareness (session_embedding_chunks
550
+ // carries no tenant_id — DERIVE-VIA-FK), so it returns neighbors from the
551
+ // whole corpus. The tenant filter is re-applied below, in the
552
+ // id-resolution join against `sessions` (program spec §4.3, vector-path
553
+ // rule) — any candidate session id that doesn't resolve within the
554
+ // caller's tenant is excluded, not merely "not down-ranked".
497
555
  const rows = this.db
498
556
  .prepare(`
499
557
  SELECT session_id, distance
@@ -504,19 +562,29 @@ export class SqliteSessionStore {
504
562
  `)
505
563
  .all(blob, chunkK);
506
564
  // Max-pool: keep the smallest distance (highest cosine) per session,
507
- // filtering out superseded, replaced, and out-of-workstream sessions.
565
+ // filtering out superseded, replaced, out-of-workstream, and (via the
566
+ // tenant-filtered id-resolution join) out-of-tenant sessions.
508
567
  const best = new Map();
509
568
  const excludedSessionIds = new Set();
510
569
  const wsIds = opts?.workstreamIds;
511
570
  if (wsIds !== undefined && wsIds.length === 0)
512
571
  return [];
513
- // First pass: collect all unique session IDs to check their status + workstream
572
+ // First pass: resolve each unique candidate session id against the
573
+ // tenant-filtered sessions table to get its status + workstream. A
574
+ // candidate id that fails to resolve (wrong tenant or gone) is excluded
575
+ // outright, not merely left un-excluded.
514
576
  const uniqueSessionIds = [...new Set(rows.map((r) => r.session_id))];
515
577
  if (uniqueSessionIds.length > 0) {
516
578
  const placeholders = uniqueSessionIds.map(() => "?").join(",");
579
+ const tc = tenantClause(tenantId);
517
580
  const statusRows = this.db
518
- .prepare(`SELECT id, status, workstream_id FROM sessions WHERE id IN (${placeholders})`)
519
- .all(...uniqueSessionIds);
581
+ .prepare(`SELECT id, status, workstream_id FROM sessions WHERE id IN (${placeholders}) AND ${tc.sql}`)
582
+ .all(...uniqueSessionIds, tc.param);
583
+ const resolvedIds = new Set(statusRows.map((r) => r.id));
584
+ for (const id of uniqueSessionIds) {
585
+ if (!resolvedIds.has(id))
586
+ excludedSessionIds.add(id);
587
+ }
520
588
  const excludeSuperseded = opts?.includeSuperseded !== true;
521
589
  for (const sr of statusRows) {
522
590
  const statusExcluded = sr.status === "replaced" || (excludeSuperseded && sr.status === "superseded");
@@ -526,7 +594,7 @@ export class SqliteSessionStore {
526
594
  }
527
595
  }
528
596
  }
529
- // Second pass: max-pool, excluding superseded/replaced sessions
597
+ // Second pass: max-pool, excluding superseded/replaced/out-of-tenant sessions
530
598
  for (const r of rows) {
531
599
  if (excludedSessionIds.has(r.session_id))
532
600
  continue;
@@ -547,7 +615,7 @@ export class SqliteSessionStore {
547
615
  * for better matches). User input is tokenized and rebuilt into a quoted
548
616
  * OR query so FTS5 metacharacters cannot reach the MATCH parser.
549
617
  */
550
- async keywordSearch(query, limit, opts) {
618
+ async keywordSearch(tenantId, query, limit, opts) {
551
619
  const wsIds = opts?.workstreamIds;
552
620
  if (wsIds !== undefined && wsIds.length === 0)
553
621
  return [];
@@ -558,6 +626,7 @@ export class SqliteSessionStore {
558
626
  const statusFilter = opts?.includeSuperseded === true
559
627
  ? "AND s.status != 'replaced'"
560
628
  : "AND s.status NOT IN ('superseded', 'replaced')";
629
+ const tc = tenantClause(tenantId, "s.tenant_id");
561
630
  let wsFilter = "";
562
631
  const params = [matchExpr];
563
632
  if (wsIds?.length) {
@@ -565,6 +634,7 @@ export class SqliteSessionStore {
565
634
  wsFilter = `AND s.workstream_id IN (${ph})`;
566
635
  params.push(...wsIds);
567
636
  }
637
+ params.push(tc.param);
568
638
  params.push(k);
569
639
  const rows = this.db
570
640
  .prepare(`
@@ -575,48 +645,54 @@ export class SqliteSessionStore {
575
645
  WHERE sessions_fts MATCH ?
576
646
  ${statusFilter}
577
647
  ${wsFilter}
648
+ AND ${tc.sql}
578
649
  ORDER BY score DESC
579
650
  LIMIT ?
580
651
  `)
581
652
  .all(...params);
582
653
  return rows.map((r) => ({ sessionId: r.session_id, score: r.score }));
583
654
  }
584
- async resolveSuccessors(ids) {
655
+ async resolveSuccessors(tenantId, ids) {
585
656
  if (ids.length === 0)
586
657
  return new Map();
587
658
  const placeholders = ids.map(() => "?").join(",");
659
+ const tc = tenantClause(tenantId, "s.tenant_id");
588
660
  const rows = this.db
589
661
  .prepare(`
590
- SELECT from_session, to_session
591
- FROM session_edges
592
- WHERE kind = 'supersedes'
593
- AND to_session IN (${placeholders})
662
+ SELECT se.from_session, se.to_session
663
+ FROM session_edges se
664
+ JOIN sessions s ON s.id = se.to_session
665
+ WHERE se.kind = 'supersedes'
666
+ AND se.to_session IN (${placeholders})
667
+ AND ${tc.sql}
594
668
  `)
595
- .all(...ids);
669
+ .all(...ids, tc.param);
596
670
  const out = new Map();
597
671
  for (const r of rows)
598
672
  out.set(r.to_session, r.from_session);
599
673
  return out;
600
674
  }
601
- async updateStatus(sessionId, status) {
675
+ async updateStatus(tenantId, sessionId, status) {
602
676
  if (status === "idle") {
603
677
  throw new Error("Cannot persist derived status 'idle' — only active/closed/superseded");
604
678
  }
679
+ const tc = tenantClause(tenantId);
605
680
  this.db
606
- .prepare("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?")
607
- .run(status, sessionId);
681
+ .prepare(`UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ? AND ${tc.sql}`)
682
+ .run(status, sessionId, tc.param);
608
683
  }
609
- async markSuperseded(predecessorId, successorId) {
684
+ async markSuperseded(tenantId, predecessorId, successorId) {
610
685
  if (predecessorId === successorId) {
611
686
  throw new Error("A session cannot supersede itself");
612
687
  }
613
- const existsStmt = this.db.prepare("SELECT COUNT(*) AS c FROM sessions WHERE id = ?");
688
+ const existsTc = tenantClause(tenantId);
689
+ const existsStmt = this.db.prepare(`SELECT COUNT(*) AS c FROM sessions WHERE id = ? AND ${existsTc.sql}`);
614
690
  const txn = this.db.transaction(() => {
615
- const predExists = (existsStmt.get(predecessorId)?.c ?? 0) > 0;
691
+ const predExists = (existsStmt.get(predecessorId, tenantId)?.c ?? 0) > 0;
616
692
  if (!predExists) {
617
693
  throw new Error(`predecessor session ${predecessorId} not found`);
618
694
  }
619
- const succExists = (existsStmt.get(successorId)?.c ?? 0) > 0;
695
+ const succExists = (existsStmt.get(successorId, tenantId)?.c ?? 0) > 0;
620
696
  if (!succExists) {
621
697
  throw new Error(`successor session ${successorId} not found`);
622
698
  }
@@ -661,60 +737,68 @@ export class SqliteSessionStore {
661
737
  .prepare(`INSERT OR IGNORE INTO session_edges (from_session, to_session, kind)
662
738
  VALUES (?, ?, 'supersedes')`)
663
739
  .run(successorId, predecessorId);
740
+ const statusTc = tenantClause(tenantId);
664
741
  this.db
665
- .prepare("UPDATE sessions SET status = 'superseded', updated_at = datetime('now') WHERE id = ?")
666
- .run(predecessorId);
742
+ .prepare(`UPDATE sessions SET status = 'superseded', updated_at = datetime('now') WHERE id = ? AND ${statusTc.sql}`)
743
+ .run(predecessorId, statusTc.param);
667
744
  // Cascade supersedence to facts: link predecessor facts to their successors
668
- const selectPredFacts = this.db.prepare("SELECT id, subject, predicate FROM facts WHERE source_session_id = ?");
669
- const selectSuccFact = this.db.prepare("SELECT id FROM facts WHERE source_session_id = ? AND subject = ? AND predicate = ? AND superseded_by IS NULL LIMIT 1");
670
- const updateFactSuperseded = this.db.prepare("UPDATE facts SET superseded_by = ? WHERE id = ?");
745
+ const predFactsTc = tenantClause(tenantId);
746
+ const selectPredFacts = this.db.prepare(`SELECT id, subject, predicate FROM facts WHERE source_session_id = ? AND ${predFactsTc.sql}`);
747
+ const succFactTc = tenantClause(tenantId);
748
+ const selectSuccFact = this.db.prepare(`SELECT id FROM facts WHERE source_session_id = ? AND subject = ? AND predicate = ? AND superseded_by IS NULL AND ${succFactTc.sql} LIMIT 1`);
749
+ const updateFactTc = tenantClause(tenantId);
750
+ const updateFactSuperseded = this.db.prepare(`UPDATE facts SET superseded_by = ? WHERE id = ? AND ${updateFactTc.sql}`);
671
751
  const delFactEmbedding = this.db.prepare("DELETE FROM fact_embeddings WHERE fact_id = ?");
672
- const predecessorFacts = selectPredFacts.all(predecessorId);
752
+ const predecessorFacts = selectPredFacts.all(predecessorId, predFactsTc.param);
673
753
  for (const pFact of predecessorFacts) {
674
- const successor = selectSuccFact.get(successorId, pFact.subject, pFact.predicate);
754
+ const successor = selectSuccFact.get(successorId, pFact.subject, pFact.predicate, succFactTc.param);
675
755
  if (successor) {
676
- updateFactSuperseded.run(successor.id, pFact.id);
756
+ updateFactSuperseded.run(successor.id, pFact.id, updateFactTc.param);
677
757
  delFactEmbedding.run(pFact.id);
678
758
  }
679
759
  }
680
760
  });
681
761
  txn();
682
762
  }
683
- async getSessionScopeById(id) {
684
- const row = this.db.prepare("SELECT scope FROM sessions WHERE id = ?").get(id);
763
+ async getSessionScopeById(tenantId, id) {
764
+ const tc = tenantClause(tenantId);
765
+ const row = this.db.prepare(`SELECT scope FROM sessions WHERE id = ? AND ${tc.sql}`).get(id, tc.param);
685
766
  return row?.scope ?? null;
686
767
  }
687
- async setWorkstreamBinding(sessionId, workstreamId, source, confidence) {
688
- this.db.prepare("UPDATE sessions SET workstream_id = ?, binding_source = ?, binding_confidence = ?, updated_at = datetime('now') WHERE id = ?").run(workstreamId, source, confidence, sessionId);
768
+ async setWorkstreamBinding(tenantId, sessionId, workstreamId, source, confidence) {
769
+ const tc = tenantClause(tenantId);
770
+ this.db.prepare(`UPDATE sessions SET workstream_id = ?, binding_source = ?, binding_confidence = ?, updated_at = datetime('now') WHERE id = ? AND ${tc.sql}`).run(workstreamId, source, confidence, sessionId, tc.param);
689
771
  }
690
- async listSessionIdsByWorkstreams(workstreamIds) {
772
+ async listSessionIdsByWorkstreams(tenantId, workstreamIds) {
691
773
  if (workstreamIds.length === 0)
692
774
  return [];
693
775
  const ph = workstreamIds.map(() => "?").join(",");
694
- return this.db.prepare(`SELECT id FROM sessions WHERE workstream_id IN (${ph}) ORDER BY started_at ASC`).all(...workstreamIds).map((r) => r.id);
776
+ const tc = tenantClause(tenantId);
777
+ return this.db.prepare(`SELECT id FROM sessions WHERE workstream_id IN (${ph}) AND ${tc.sql} ORDER BY started_at ASC`).all(...workstreamIds, tc.param).map((r) => r.id);
695
778
  }
696
- async getEntities(sessionId) {
697
- return this.loadEntities([sessionId]).get(sessionId) ?? [];
779
+ async getEntities(tenantId, sessionId) {
780
+ return this.loadEntities(tenantId, [sessionId]).get(sessionId) ?? [];
698
781
  }
699
- async getWorkstreamIds(sessionIds) {
782
+ async getWorkstreamIds(tenantId, sessionIds) {
700
783
  const out = new Map();
701
784
  if (sessionIds.length === 0)
702
785
  return out;
703
786
  const ph = sessionIds.map(() => "?").join(",");
704
- for (const r of this.db.prepare(`SELECT id, workstream_id FROM sessions WHERE id IN (${ph})`).all(...sessionIds))
787
+ const tc = tenantClause(tenantId);
788
+ for (const r of this.db.prepare(`SELECT id, workstream_id FROM sessions WHERE id IN (${ph}) AND ${tc.sql}`).all(...sessionIds, tc.param))
705
789
  out.set(r.id, r.workstream_id);
706
790
  return out;
707
791
  }
708
792
  // ── insert helpers used by tests / future ingest path ─────────────────
709
793
  /** @internal test-only helper; production callers use insertSession(). */
710
- insertSessionForTest(session) {
794
+ insertSessionForTest(session, tenantId = DEFAULT_TEAM_ID) {
711
795
  const stmt = this.db.prepare(`
712
796
  INSERT INTO sessions (
713
797
  id, runtime, runtime_session_id, started_at, ended_at, duration_min,
714
- label, summary, body, status, transcript_kind, transcript_path
798
+ label, summary, body, status, transcript_kind, transcript_path, tenant_id
715
799
  ) VALUES (
716
800
  @id, @runtime, @runtimeSessionId, @startedAt, @endedAt, @durationMin,
717
- @label, @summary, @body, @status, @transcriptKind, @transcriptPath
801
+ @label, @summary, @body, @status, @transcriptKind, @transcriptPath, @tenantId
718
802
  )
719
803
  `);
720
804
  const status = session.status === "idle" ? "active" : session.status;
@@ -731,15 +815,16 @@ export class SqliteSessionStore {
731
815
  status,
732
816
  transcriptKind: session.transcriptKind,
733
817
  transcriptPath: session.transcriptPath,
818
+ tenantId,
734
819
  });
735
820
  const entStmt = this.db.prepare(`
736
- INSERT OR IGNORE INTO entities (canonical, type, status)
737
- VALUES (?, 'candidate', 'active')
821
+ INSERT OR IGNORE INTO entities (tenant_id, canonical, type, status)
822
+ VALUES (?, ?, 'candidate', 'active')
738
823
  `);
739
- const linkStmt = this.db.prepare("INSERT OR IGNORE INTO session_entities (session_id, entity_canonical) VALUES (?, ?)");
824
+ const linkStmt = this.db.prepare("INSERT OR IGNORE INTO session_entities (tenant_id, session_id, entity_canonical) VALUES (?, ?, ?)");
740
825
  for (const e of session.entities) {
741
- entStmt.run(e);
742
- linkStmt.run(session.id, e);
826
+ entStmt.run(tenantId, e);
827
+ linkStmt.run(tenantId, session.id, e);
743
828
  }
744
829
  const markerStmt = this.db.prepare("INSERT INTO markers (session_id, kind, text, position) VALUES (?, ?, ?, ?)");
745
830
  session.decisions.forEach((d, i) => markerStmt.run(session.id, "decision", d, i));
@@ -757,18 +842,19 @@ export class SqliteSessionStore {
757
842
  this.insertChunkEmbedding(sessionId, chunkIdx, vector);
758
843
  }
759
844
  // ── internal ──────────────────────────────────────────────────────────
760
- loadEntities(ids) {
845
+ loadEntities(tenantId, ids) {
761
846
  if (ids.length === 0)
762
847
  return new Map();
763
848
  const placeholders = ids.map(() => "?").join(",");
849
+ const tc = tenantClause(tenantId);
764
850
  const rows = this.db
765
851
  .prepare(`
766
852
  SELECT session_id, entity_canonical
767
853
  FROM session_entities
768
- WHERE session_id IN (${placeholders})
854
+ WHERE session_id IN (${placeholders}) AND ${tc.sql}
769
855
  ORDER BY session_id
770
856
  `)
771
- .all(...ids);
857
+ .all(...ids, tc.param);
772
858
  const out = new Map();
773
859
  for (const r of rows) {
774
860
  const list = out.get(r.session_id);