nlm-memory 0.20.1 → 0.21.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. package/README.md +69 -12
  2. package/assets/replay-eval-dark.svg +26 -0
  3. package/assets/replay-eval-light.svg +26 -0
  4. package/dist/cli/digest.js +69 -4
  5. package/dist/cli/digest.js.map +1 -1
  6. package/dist/cli/nlm.d.ts +14 -0
  7. package/dist/cli/nlm.js +556 -71
  8. package/dist/cli/nlm.js.map +1 -1
  9. package/dist/cli/reprocess-job-adapters.d.ts +35 -0
  10. package/dist/cli/reprocess-job-adapters.js +80 -0
  11. package/dist/cli/reprocess-job-adapters.js.map +1 -0
  12. package/dist/cli/scope-backfill.d.ts +34 -0
  13. package/dist/cli/scope-backfill.js +237 -0
  14. package/dist/cli/scope-backfill.js.map +1 -0
  15. package/dist/cli/scope-coverage.d.ts +44 -0
  16. package/dist/cli/scope-coverage.js +208 -0
  17. package/dist/cli/scope-coverage.js.map +1 -0
  18. package/dist/cli/supersede.js +8 -7
  19. package/dist/cli/supersede.js.map +1 -1
  20. package/dist/core/actions/actions-log.d.ts +65 -8
  21. package/dist/core/actions/actions-log.js +134 -17
  22. package/dist/core/actions/actions-log.js.map +1 -1
  23. package/dist/core/actions/overlay.d.ts +11 -2
  24. package/dist/core/actions/overlay.js +18 -6
  25. package/dist/core/actions/overlay.js.map +1 -1
  26. package/dist/core/adapters/claude-code.d.ts +14 -0
  27. package/dist/core/adapters/claude-code.js +30 -0
  28. package/dist/core/adapters/claude-code.js.map +1 -1
  29. package/dist/core/adapters/from-source.js +11 -3
  30. package/dist/core/adapters/from-source.js.map +1 -1
  31. package/dist/core/adapters/fs-wiki-writer.d.ts +44 -0
  32. package/dist/core/adapters/fs-wiki-writer.js +132 -0
  33. package/dist/core/adapters/fs-wiki-writer.js.map +1 -0
  34. package/dist/core/adapters/hermes-agent.d.ts +15 -2
  35. package/dist/core/adapters/hermes-agent.js +57 -11
  36. package/dist/core/adapters/hermes-agent.js.map +1 -1
  37. package/dist/core/adapters/memory-wiki-writer.d.ts +8 -0
  38. package/dist/core/adapters/memory-wiki-writer.js +16 -0
  39. package/dist/core/adapters/memory-wiki-writer.js.map +1 -0
  40. package/dist/core/adapters/muse.d.ts +51 -0
  41. package/dist/core/adapters/muse.js +271 -0
  42. package/dist/core/adapters/muse.js.map +1 -0
  43. package/dist/core/adapters/opencode.d.ts +11 -4
  44. package/dist/core/adapters/opencode.js +41 -7
  45. package/dist/core/adapters/opencode.js.map +1 -1
  46. package/dist/core/alerts/alert-state.d.ts +17 -0
  47. package/dist/core/alerts/alert-state.js +45 -0
  48. package/dist/core/alerts/alert-state.js.map +1 -0
  49. package/dist/core/alerts/check-and-alert.d.ts +28 -0
  50. package/dist/core/alerts/check-and-alert.js +53 -0
  51. package/dist/core/alerts/check-and-alert.js.map +1 -0
  52. package/dist/core/alerts/drift-transition.d.ts +27 -0
  53. package/dist/core/alerts/drift-transition.js +56 -0
  54. package/dist/core/alerts/drift-transition.js.map +1 -0
  55. package/dist/core/alerts/embedder-transition.d.ts +23 -0
  56. package/dist/core/alerts/embedder-transition.js +40 -0
  57. package/dist/core/alerts/embedder-transition.js.map +1 -0
  58. package/dist/core/alerts/fire-alert.d.ts +24 -0
  59. package/dist/core/alerts/fire-alert.js +72 -0
  60. package/dist/core/alerts/fire-alert.js.map +1 -0
  61. package/dist/core/alerts/job-alert.d.ts +12 -0
  62. package/dist/core/alerts/job-alert.js +37 -0
  63. package/dist/core/alerts/job-alert.js.map +1 -0
  64. package/dist/core/alerts/types.d.ts +104 -0
  65. package/dist/core/alerts/types.js +11 -0
  66. package/dist/core/alerts/types.js.map +1 -0
  67. package/dist/core/alerts/wiki-alert.d.ts +15 -0
  68. package/dist/core/alerts/wiki-alert.js +31 -0
  69. package/dist/core/alerts/wiki-alert.js.map +1 -0
  70. package/dist/core/classifier/prompt.d.ts +13 -0
  71. package/dist/core/classifier/prompt.js +52 -2
  72. package/dist/core/classifier/prompt.js.map +1 -1
  73. package/dist/core/dataset/build-dataset.d.ts +16 -1
  74. package/dist/core/dataset/build-dataset.js +46 -13
  75. package/dist/core/dataset/build-dataset.js.map +1 -1
  76. package/dist/core/digest/compose.d.ts +11 -3
  77. package/dist/core/digest/compose.js +70 -17
  78. package/dist/core/digest/compose.js.map +1 -1
  79. package/dist/core/digest/hook-injection.d.ts +43 -0
  80. package/dist/core/digest/hook-injection.js +87 -0
  81. package/dist/core/digest/hook-injection.js.map +1 -0
  82. package/dist/core/digest/hook-version-parity.d.ts +31 -0
  83. package/dist/core/digest/hook-version-parity.js +56 -0
  84. package/dist/core/digest/hook-version-parity.js.map +1 -0
  85. package/dist/core/embedding/pg-embed-backfill.js +1 -1
  86. package/dist/core/embedding/pg-embed-backfill.js.map +1 -1
  87. package/dist/core/exemplars/capture-from-session.d.ts +2 -1
  88. package/dist/core/exemplars/capture-from-session.js +4 -4
  89. package/dist/core/exemplars/capture-from-session.js.map +1 -1
  90. package/dist/core/exemplars/embed-backfill.d.ts +9 -0
  91. package/dist/core/exemplars/embed-backfill.js +9 -1
  92. package/dist/core/exemplars/embed-backfill.js.map +1 -1
  93. package/dist/core/exemplars/extract-exemplar.d.ts +2 -0
  94. package/dist/core/exemplars/extract-exemplar.js +7 -1
  95. package/dist/core/exemplars/extract-exemplar.js.map +1 -1
  96. package/dist/core/exemplars/ingest-exemplar.d.ts +1 -0
  97. package/dist/core/exemplars/ingest-exemplar.js +1 -0
  98. package/dist/core/exemplars/ingest-exemplar.js.map +1 -1
  99. package/dist/core/exemplars/recall-code.d.ts +1 -1
  100. package/dist/core/exemplars/recall-code.js +2 -2
  101. package/dist/core/exemplars/recall-code.js.map +1 -1
  102. package/dist/core/facts/backfill-facts.d.ts +1 -1
  103. package/dist/core/facts/backfill-facts.js +4 -4
  104. package/dist/core/facts/backfill-facts.js.map +1 -1
  105. package/dist/core/facts/embed-backfill.d.ts +54 -0
  106. package/dist/core/facts/embed-backfill.js +109 -0
  107. package/dist/core/facts/embed-backfill.js.map +1 -0
  108. package/dist/core/facts/reconcile-facts.d.ts +42 -0
  109. package/dist/core/facts/reconcile-facts.js +133 -0
  110. package/dist/core/facts/reconcile-facts.js.map +1 -0
  111. package/dist/core/health/embed-failure-state.d.ts +4 -0
  112. package/dist/core/health/embed-failure-state.js +12 -0
  113. package/dist/core/health/embed-failure-state.js.map +1 -0
  114. package/dist/core/health/warmup-retry.d.ts +27 -0
  115. package/dist/core/health/warmup-retry.js +53 -0
  116. package/dist/core/health/warmup-retry.js.map +1 -0
  117. package/dist/core/health/warmup-state.d.ts +15 -0
  118. package/dist/core/health/warmup-state.js +15 -1
  119. package/dist/core/health/warmup-state.js.map +1 -1
  120. package/dist/core/hook/cite-memo.d.ts +11 -7
  121. package/dist/core/hook/cite-memo.js +25 -17
  122. package/dist/core/hook/cite-memo.js.map +1 -1
  123. package/dist/core/hook/claude-settings.d.ts +1 -1
  124. package/dist/core/hook/claude-settings.js +0 -1
  125. package/dist/core/hook/claude-settings.js.map +1 -1
  126. package/dist/core/hook/hook-log.d.ts +13 -2
  127. package/dist/core/hook/hook-log.js +16 -8
  128. package/dist/core/hook/hook-log.js.map +1 -1
  129. package/dist/core/hook/memo.d.ts +10 -6
  130. package/dist/core/hook/memo.js +25 -17
  131. package/dist/core/hook/memo.js.map +1 -1
  132. package/dist/core/hook/pointer-block.d.ts +8 -1
  133. package/dist/core/hook/pointer-block.js +17 -4
  134. package/dist/core/hook/pointer-block.js.map +1 -1
  135. package/dist/core/hook/resolve-conversation-by-query.d.ts +6 -5
  136. package/dist/core/hook/resolve-conversation-by-query.js +24 -12
  137. package/dist/core/hook/resolve-conversation-by-query.js.map +1 -1
  138. package/dist/core/ingest/backfill-derivables.d.ts +92 -0
  139. package/dist/core/ingest/backfill-derivables.js +175 -0
  140. package/dist/core/ingest/backfill-derivables.js.map +1 -0
  141. package/dist/core/ingest/ingest-session.d.ts +1 -1
  142. package/dist/core/ingest/ingest-session.js +23 -3
  143. package/dist/core/ingest/ingest-session.js.map +1 -1
  144. package/dist/core/ingest/reclassify-oversized.d.ts +1 -1
  145. package/dist/core/ingest/reclassify-oversized.js +8 -2
  146. package/dist/core/ingest/reclassify-oversized.js.map +1 -1
  147. package/dist/core/ingest/reprocess.d.ts +1 -1
  148. package/dist/core/ingest/reprocess.js +7 -2
  149. package/dist/core/ingest/reprocess.js.map +1 -1
  150. package/dist/core/ingest/transcript-derivables.d.ts +27 -0
  151. package/dist/core/ingest/transcript-derivables.js +138 -0
  152. package/dist/core/ingest/transcript-derivables.js.map +1 -0
  153. package/dist/core/jobs/job-supervisor.d.ts +168 -0
  154. package/dist/core/jobs/job-supervisor.js +335 -0
  155. package/dist/core/jobs/job-supervisor.js.map +1 -0
  156. package/dist/core/learner/model.d.ts +63 -0
  157. package/dist/core/learner/model.js +30 -0
  158. package/dist/core/learner/model.js.map +1 -0
  159. package/dist/core/learner/service.d.ts +42 -0
  160. package/dist/core/learner/service.js +74 -0
  161. package/dist/core/learner/service.js.map +1 -0
  162. package/dist/core/learner/summary.d.ts +28 -0
  163. package/dist/core/learner/summary.js +70 -0
  164. package/dist/core/learner/summary.js.map +1 -0
  165. package/dist/core/learner/validate.d.ts +18 -0
  166. package/dist/core/learner/validate.js +95 -0
  167. package/dist/core/learner/validate.js.map +1 -0
  168. package/dist/core/metrics/re-derivation.d.ts +31 -2
  169. package/dist/core/metrics/re-derivation.js +36 -4
  170. package/dist/core/metrics/re-derivation.js.map +1 -1
  171. package/dist/core/outcome/coverage.d.ts +33 -0
  172. package/dist/core/outcome/coverage.js +46 -0
  173. package/dist/core/outcome/coverage.js.map +1 -0
  174. package/dist/core/outcome/rollup.d.ts +33 -0
  175. package/dist/core/outcome/rollup.js +113 -0
  176. package/dist/core/outcome/rollup.js.map +1 -0
  177. package/dist/core/providers/provider-registry.d.ts +40 -23
  178. package/dist/core/providers/provider-registry.js +99 -81
  179. package/dist/core/providers/provider-registry.js.map +1 -1
  180. package/dist/core/recall/citation-log.d.ts +10 -5
  181. package/dist/core/recall/citation-log.js +21 -10
  182. package/dist/core/recall/citation-log.js.map +1 -1
  183. package/dist/core/recall/hook-recall-log.d.ts +7 -4
  184. package/dist/core/recall/hook-recall-log.js +14 -8
  185. package/dist/core/recall/hook-recall-log.js.map +1 -1
  186. package/dist/core/recall/miss-log.d.ts +7 -4
  187. package/dist/core/recall/miss-log.js +18 -11
  188. package/dist/core/recall/miss-log.js.map +1 -1
  189. package/dist/core/recall/query-log.d.ts +8 -5
  190. package/dist/core/recall/query-log.js +16 -9
  191. package/dist/core/recall/query-log.js.map +1 -1
  192. package/dist/core/recall/recall-service.d.ts +10 -1
  193. package/dist/core/recall/recall-service.js +31 -12
  194. package/dist/core/recall/recall-service.js.map +1 -1
  195. package/dist/core/recall/recent-log.d.ts +6 -1
  196. package/dist/core/recall/recent-log.js +13 -5
  197. package/dist/core/recall/recent-log.js.map +1 -1
  198. package/dist/core/recall/related-concepts.d.ts +29 -0
  199. package/dist/core/recall/related-concepts.js +67 -0
  200. package/dist/core/recall/related-concepts.js.map +1 -0
  201. package/dist/core/recall/related-exemplars.d.ts +1 -1
  202. package/dist/core/recall/related-exemplars.js +2 -2
  203. package/dist/core/recall/related-exemplars.js.map +1 -1
  204. package/dist/core/recall/related-facts.d.ts +1 -1
  205. package/dist/core/recall/related-facts.js +3 -3
  206. package/dist/core/recall/related-facts.js.map +1 -1
  207. package/dist/core/recall-facts/fact-query-log.d.ts +8 -4
  208. package/dist/core/recall-facts/fact-query-log.js +16 -8
  209. package/dist/core/recall-facts/fact-query-log.js.map +1 -1
  210. package/dist/core/recall-facts/fact-recall-service.d.ts +2 -2
  211. package/dist/core/recall-facts/fact-recall-service.js +13 -12
  212. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  213. package/dist/core/scheduler/scan-once.js +15 -12
  214. package/dist/core/scheduler/scan-once.js.map +1 -1
  215. package/dist/core/scheduler/scheduler.d.ts +3 -0
  216. package/dist/core/scheduler/scheduler.js +45 -10
  217. package/dist/core/scheduler/scheduler.js.map +1 -1
  218. package/dist/core/scope/alias-map.d.ts +10 -0
  219. package/dist/core/scope/alias-map.js +41 -0
  220. package/dist/core/scope/alias-map.js.map +1 -0
  221. package/dist/core/scope/derive-scope.d.ts +8 -0
  222. package/dist/core/scope/derive-scope.js +54 -0
  223. package/dist/core/scope/derive-scope.js.map +1 -0
  224. package/dist/core/scope/scope-clause.d.ts +16 -0
  225. package/dist/core/scope/scope-clause.js +21 -0
  226. package/dist/core/scope/scope-clause.js.map +1 -0
  227. package/dist/core/scope/stamp-flag.d.ts +1 -0
  228. package/dist/core/scope/stamp-flag.js +4 -0
  229. package/dist/core/scope/stamp-flag.js.map +1 -0
  230. package/dist/core/signals/code-signal.d.ts +1 -0
  231. package/dist/core/signals/code-signal.js +1 -0
  232. package/dist/core/signals/code-signal.js.map +1 -1
  233. package/dist/core/signals/failure-mode-recall.d.ts +1 -1
  234. package/dist/core/signals/failure-mode-recall.js +2 -2
  235. package/dist/core/signals/failure-mode-recall.js.map +1 -1
  236. package/dist/core/signals/ingest-signal.d.ts +1 -1
  237. package/dist/core/signals/ingest-signal.js +3 -1
  238. package/dist/core/signals/ingest-signal.js.map +1 -1
  239. package/dist/core/signals/stamp-scope.d.ts +18 -0
  240. package/dist/core/signals/stamp-scope.js +28 -0
  241. package/dist/core/signals/stamp-scope.js.map +1 -0
  242. package/dist/core/sources/source-registry.d.ts +49 -28
  243. package/dist/core/sources/source-registry.js +113 -70
  244. package/dist/core/sources/source-registry.js.map +1 -1
  245. package/dist/core/storage/pg-code-exemplar-store.d.ts +19 -10
  246. package/dist/core/storage/pg-code-exemplar-store.js +62 -27
  247. package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
  248. package/dist/core/storage/pg-entity-store.d.ts +6 -1
  249. package/dist/core/storage/pg-entity-store.js +33 -18
  250. package/dist/core/storage/pg-entity-store.js.map +1 -1
  251. package/dist/core/storage/pg-fact-ingest.d.ts +1 -1
  252. package/dist/core/storage/pg-fact-ingest.js +10 -7
  253. package/dist/core/storage/pg-fact-ingest.js.map +1 -1
  254. package/dist/core/storage/pg-fact-store.d.ts +29 -17
  255. package/dist/core/storage/pg-fact-store.js +99 -46
  256. package/dist/core/storage/pg-fact-store.js.map +1 -1
  257. package/dist/core/storage/pg-learner-store.d.ts +27 -0
  258. package/dist/core/storage/pg-learner-store.js +174 -0
  259. package/dist/core/storage/pg-learner-store.js.map +1 -0
  260. package/dist/core/storage/pg-session-store.d.ts +25 -21
  261. package/dist/core/storage/pg-session-store.js +186 -114
  262. package/dist/core/storage/pg-session-store.js.map +1 -1
  263. package/dist/core/storage/pg-signal-store.d.ts +10 -5
  264. package/dist/core/storage/pg-signal-store.js +26 -13
  265. package/dist/core/storage/pg-signal-store.js.map +1 -1
  266. package/dist/core/storage/pg-storage.d.ts +4 -0
  267. package/dist/core/storage/pg-storage.js +6 -0
  268. package/dist/core/storage/pg-storage.js.map +1 -1
  269. package/dist/core/storage/pg-workstream-store.d.ts +17 -11
  270. package/dist/core/storage/pg-workstream-store.js +47 -29
  271. package/dist/core/storage/pg-workstream-store.js.map +1 -1
  272. package/dist/core/storage/sqlite-code-exemplar-store.d.ts +24 -10
  273. package/dist/core/storage/sqlite-code-exemplar-store.js +86 -40
  274. package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
  275. package/dist/core/storage/sqlite-entity-store.d.ts +8 -1
  276. package/dist/core/storage/sqlite-entity-store.js +46 -27
  277. package/dist/core/storage/sqlite-entity-store.js.map +1 -1
  278. package/dist/core/storage/sqlite-fact-store.d.ts +45 -19
  279. package/dist/core/storage/sqlite-fact-store.js +175 -64
  280. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  281. package/dist/core/storage/sqlite-learner-store.d.ts +33 -0
  282. package/dist/core/storage/sqlite-learner-store.js +178 -0
  283. package/dist/core/storage/sqlite-learner-store.js.map +1 -0
  284. package/dist/core/storage/sqlite-outcome-store.d.ts +104 -0
  285. package/dist/core/storage/sqlite-outcome-store.js +211 -0
  286. package/dist/core/storage/sqlite-outcome-store.js.map +1 -0
  287. package/dist/core/storage/sqlite-session-store.d.ts +50 -21
  288. package/dist/core/storage/sqlite-session-store.js +233 -149
  289. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  290. package/dist/core/storage/sqlite-signal-store.d.ts +12 -5
  291. package/dist/core/storage/sqlite-signal-store.js +29 -15
  292. package/dist/core/storage/sqlite-signal-store.js.map +1 -1
  293. package/dist/core/storage/sqlite-storage.d.ts +4 -0
  294. package/dist/core/storage/sqlite-storage.js +10 -2
  295. package/dist/core/storage/sqlite-storage.js.map +1 -1
  296. package/dist/core/storage/sqlite-workstream-store.d.ts +17 -11
  297. package/dist/core/storage/sqlite-workstream-store.js +53 -31
  298. package/dist/core/storage/sqlite-workstream-store.js.map +1 -1
  299. package/dist/core/storage/supersedence-log.d.ts +10 -7
  300. package/dist/core/storage/supersedence-log.js +21 -14
  301. package/dist/core/storage/supersedence-log.js.map +1 -1
  302. package/dist/core/telemetry/probe-filter.d.ts +35 -0
  303. package/dist/core/telemetry/probe-filter.js +56 -0
  304. package/dist/core/telemetry/probe-filter.js.map +1 -0
  305. package/dist/core/tenancy/default-team.d.ts +6 -0
  306. package/dist/core/tenancy/default-team.js +7 -0
  307. package/dist/core/tenancy/default-team.js.map +1 -0
  308. package/dist/core/tenancy/hosted-mode.d.ts +9 -0
  309. package/dist/core/tenancy/hosted-mode.js +12 -0
  310. package/dist/core/tenancy/hosted-mode.js.map +1 -0
  311. package/dist/core/tenancy/team-auth.d.ts +16 -0
  312. package/dist/core/tenancy/team-auth.js +22 -0
  313. package/dist/core/tenancy/team-auth.js.map +1 -0
  314. package/dist/core/tenancy/team-token-store.d.ts +45 -0
  315. package/dist/core/tenancy/team-token-store.js +52 -0
  316. package/dist/core/tenancy/team-token-store.js.map +1 -0
  317. package/dist/core/tenancy/tenant-clause.d.ts +37 -0
  318. package/dist/core/tenancy/tenant-clause.js +11 -0
  319. package/dist/core/tenancy/tenant-clause.js.map +1 -0
  320. package/dist/core/tenancy/tenant-state-path.d.ts +28 -0
  321. package/dist/core/tenancy/tenant-state-path.js +49 -0
  322. package/dist/core/tenancy/tenant-state-path.js.map +1 -0
  323. package/dist/core/update-check/check.d.ts +6 -0
  324. package/dist/core/update-check/check.js +35 -0
  325. package/dist/core/update-check/check.js.map +1 -1
  326. package/dist/core/wiki/project.d.ts +17 -0
  327. package/dist/core/wiki/project.js +37 -0
  328. package/dist/core/wiki/project.js.map +1 -0
  329. package/dist/core/wiki/reconcile.d.ts +15 -0
  330. package/dist/core/wiki/reconcile.js +14 -0
  331. package/dist/core/wiki/reconcile.js.map +1 -0
  332. package/dist/core/wiki/render.d.ts +5 -0
  333. package/dist/core/wiki/render.js +84 -0
  334. package/dist/core/wiki/render.js.map +1 -0
  335. package/dist/core/wiki/rollup.d.ts +28 -0
  336. package/dist/core/wiki/rollup.js +91 -0
  337. package/dist/core/wiki/rollup.js.map +1 -0
  338. package/dist/core/wiki/select.d.ts +8 -0
  339. package/dist/core/wiki/select.js +7 -0
  340. package/dist/core/wiki/select.js.map +1 -0
  341. package/dist/core/wiki/slug.d.ts +36 -0
  342. package/dist/core/wiki/slug.js +71 -0
  343. package/dist/core/wiki/slug.js.map +1 -0
  344. package/dist/core/wiki/types.d.ts +37 -0
  345. package/dist/core/wiki/types.js +2 -0
  346. package/dist/core/wiki/types.js.map +1 -0
  347. package/dist/core/work-digest/build-work-digest.d.ts +1 -1
  348. package/dist/core/work-digest/build-work-digest.js +3 -3
  349. package/dist/core/work-digest/build-work-digest.js.map +1 -1
  350. package/dist/core/workstream/bind.d.ts +3 -2
  351. package/dist/core/workstream/bind.js +29 -10
  352. package/dist/core/workstream/bind.js.map +1 -1
  353. package/dist/core/workstream/model.d.ts +1 -0
  354. package/dist/core/workstream/model.js.map +1 -1
  355. package/dist/core/workstream/rollup.d.ts +1 -1
  356. package/dist/core/workstream/rollup.js +6 -6
  357. package/dist/core/workstream/rollup.js.map +1 -1
  358. package/dist/hook/hook-helpers.d.ts +14 -0
  359. package/dist/hook/hook-helpers.js +29 -2
  360. package/dist/hook/hook-helpers.js.map +1 -1
  361. package/dist/hook/pre-compact-hook.js +8 -5
  362. package/dist/hook/pre-compact-hook.js.map +1 -1
  363. package/dist/hook/prompt-recall-hook.d.ts +7 -3
  364. package/dist/hook/prompt-recall-hook.js +35 -20
  365. package/dist/hook/prompt-recall-hook.js.map +1 -1
  366. package/dist/hook/recall-over-http.d.ts +28 -3
  367. package/dist/hook/recall-over-http.js +58 -12
  368. package/dist/hook/recall-over-http.js.map +1 -1
  369. package/dist/hook/session-end-hook.js +7 -5
  370. package/dist/hook/session-end-hook.js.map +1 -1
  371. package/dist/hook/session-start-hook.d.ts +14 -1
  372. package/dist/hook/session-start-hook.js +57 -16
  373. package/dist/hook/session-start-hook.js.map +1 -1
  374. package/dist/hook/stop-hook.js +11 -8
  375. package/dist/hook/stop-hook.js.map +1 -1
  376. package/dist/http/app.d.ts +47 -3
  377. package/dist/http/app.js +379 -132
  378. package/dist/http/app.js.map +1 -1
  379. package/dist/install/claude-code.d.ts +17 -0
  380. package/dist/install/claude-code.js +44 -5
  381. package/dist/install/claude-code.js.map +1 -1
  382. package/dist/install/codex.d.ts +23 -2
  383. package/dist/install/codex.js +111 -14
  384. package/dist/install/codex.js.map +1 -1
  385. package/dist/install/cursor.d.ts +2 -2
  386. package/dist/install/cursor.js +7 -7
  387. package/dist/install/cursor.js.map +1 -1
  388. package/dist/install/hermes.d.ts +8 -0
  389. package/dist/install/hermes.js +28 -5
  390. package/dist/install/hermes.js.map +1 -1
  391. package/dist/install/local-source.d.ts +20 -0
  392. package/dist/install/local-source.js +28 -0
  393. package/dist/install/local-source.js.map +1 -0
  394. package/dist/install/mcp-block.d.ts +77 -0
  395. package/dist/install/mcp-block.js +99 -0
  396. package/dist/install/mcp-block.js.map +1 -0
  397. package/dist/install/pi.d.ts +26 -0
  398. package/dist/install/pi.js +43 -0
  399. package/dist/install/pi.js.map +1 -1
  400. package/dist/install/setup.js +6 -0
  401. package/dist/install/setup.js.map +1 -1
  402. package/dist/install/windsurf.d.ts +2 -2
  403. package/dist/install/windsurf.js +7 -7
  404. package/dist/install/windsurf.js.map +1 -1
  405. package/dist/llm/classifier-box.d.ts +5 -0
  406. package/dist/llm/classifier-box.js +5 -0
  407. package/dist/llm/classifier-box.js.map +1 -1
  408. package/dist/mcp/server.d.ts +94 -15
  409. package/dist/mcp/server.js +402 -79
  410. package/dist/mcp/server.js.map +1 -1
  411. package/dist/ports/clock.d.ts +12 -0
  412. package/dist/ports/clock.js +10 -0
  413. package/dist/ports/clock.js.map +1 -0
  414. package/dist/ports/code-exemplar-store.d.ts +13 -13
  415. package/dist/ports/entity-store.d.ts +5 -1
  416. package/dist/ports/fact-store.d.ts +0 -0
  417. package/dist/ports/fact-store.js +0 -0
  418. package/dist/ports/fact-store.js.map +1 -1
  419. package/dist/ports/learner-store.d.ts +80 -0
  420. package/dist/ports/learner-store.js +19 -0
  421. package/dist/ports/learner-store.js.map +1 -0
  422. package/dist/ports/outcome.d.ts +85 -0
  423. package/dist/ports/outcome.js +11 -0
  424. package/dist/ports/outcome.js.map +1 -0
  425. package/dist/ports/session-store.d.ts +20 -20
  426. package/dist/ports/session-store.js +5 -0
  427. package/dist/ports/session-store.js.map +1 -1
  428. package/dist/ports/signal-store.d.ts +12 -8
  429. package/dist/ports/spawn-child.d.ts +14 -0
  430. package/dist/ports/spawn-child.js +10 -0
  431. package/dist/ports/spawn-child.js.map +1 -0
  432. package/dist/ports/storage.d.ts +6 -0
  433. package/dist/ports/transcript-adapter.d.ts +11 -0
  434. package/dist/ports/wiki-writer.d.ts +19 -0
  435. package/dist/ports/wiki-writer.js +12 -0
  436. package/dist/ports/wiki-writer.js.map +1 -0
  437. package/dist/ports/workstream-store.d.ts +20 -12
  438. package/dist/shared/net.d.ts +3 -0
  439. package/dist/shared/net.js +4 -0
  440. package/dist/shared/net.js.map +1 -0
  441. package/dist/shared/types.d.ts +55 -0
  442. package/dist/shared/types.js +4 -1
  443. package/dist/shared/types.js.map +1 -1
  444. package/dist/shared/version.d.ts +9 -0
  445. package/dist/shared/version.js +11 -0
  446. package/dist/shared/version.js.map +1 -0
  447. package/dist/ui/assets/index-D6F3xAis.js +11 -0
  448. package/dist/ui/index.html +1 -1
  449. package/migrations/029_project_scope.sql +21 -0
  450. package/migrations/032_session_derivables.sql +11 -0
  451. package/migrations/033_actions_kind_check.sql +58 -0
  452. package/migrations/034_tenancy_teams_and_stamps.sql +40 -0
  453. package/migrations/035_tenancy_entity_rekey.sql +75 -0
  454. package/migrations/036_tenant_name_uniqueness.sql +59 -0
  455. package/migrations/037_sessions_covering_index.sql +25 -0
  456. package/migrations/038_sources_muse.sql +33 -0
  457. package/migrations/039_learner_evidence.sql +51 -0
  458. package/migrations/pg/030_project_scope.sql +23 -0
  459. package/migrations/pg/031_widen_session_edges_kind.sql +11 -0
  460. package/migrations/pg/032_session_derivables.sql +17 -0
  461. package/migrations/pg/033_actions_kind_check.sql +17 -0
  462. package/migrations/pg/034_tenancy.sql +77 -0
  463. package/migrations/pg/035_tenant_name_uniqueness.sql +15 -0
  464. package/migrations/pg/036_sources_muse.sql +12 -0
  465. package/migrations/pg/037_learner_evidence.sql +55 -0
  466. package/nlm/index.js +258 -59
  467. package/package.json +10 -5
  468. package/plugin/.codex-plugin/plugin.json +1 -1
  469. package/plugin/scripts/prompt-recall-hook.mjs +256 -58
  470. package/plugin/scripts/session-start-hook.mjs +283 -55
  471. package/plugin/scripts/stop-hook.mjs +199 -38
  472. package/dist/hook/subagent-start-hook.d.ts +0 -28
  473. package/dist/hook/subagent-start-hook.js +0 -81
  474. package/dist/hook/subagent-start-hook.js.map +0 -1
  475. package/dist/ui/assets/index-CMpQkbQy.js +0 -11
@@ -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(`
@@ -161,11 +193,15 @@ export class SqliteSessionStore {
161
193
  id, runtime, runtime_session_id, started_at, ended_at, duration_min,
162
194
  label, summary, body, status,
163
195
  transcript_kind, transcript_path, transcript_offset, transcript_length,
164
- classifier_provider, classifier_model, classifier_confidence
196
+ classifier_provider, classifier_model, classifier_confidence,
197
+ scope, agent_persona, parent_session_id,
198
+ primary_model, total_tokens, skill, tenant_id
165
199
  ) VALUES (@id, @runtime, @runtimeSessionId, @startedAt, @endedAt, @durationMin,
166
200
  @label, @summary, @body, @status,
167
201
  @transcriptKind, @transcriptPath, @transcriptOffset, @transcriptLength,
168
- @classifierProvider, @classifierModel, @classifierConfidence)
202
+ @classifierProvider, @classifierModel, @classifierConfidence,
203
+ @scope, @agentPersona, @parentSessionId,
204
+ @primaryModel, @totalTokens, @skill, @tenantId)
169
205
  ON CONFLICT(id) DO UPDATE SET
170
206
  ended_at = excluded.ended_at,
171
207
  duration_min = excluded.duration_min,
@@ -176,6 +212,12 @@ export class SqliteSessionStore {
176
212
  classifier_provider = excluded.classifier_provider,
177
213
  classifier_model = excluded.classifier_model,
178
214
  classifier_confidence = excluded.classifier_confidence,
215
+ scope = COALESCE(excluded.scope, scope),
216
+ agent_persona = COALESCE(excluded.agent_persona, agent_persona),
217
+ parent_session_id = COALESCE(excluded.parent_session_id, parent_session_id),
218
+ primary_model = COALESCE(excluded.primary_model, primary_model),
219
+ total_tokens = COALESCE(excluded.total_tokens, total_tokens),
220
+ skill = COALESCE(excluded.skill, skill),
179
221
  updated_at = datetime('now')
180
222
  `).run({
181
223
  id: record.id,
@@ -195,6 +237,13 @@ export class SqliteSessionStore {
195
237
  classifierProvider: record.classifier?.provider ?? null,
196
238
  classifierModel: record.classifier?.model ?? null,
197
239
  classifierConfidence: record.classifier?.confidence ?? null,
240
+ scope: record.scope,
241
+ agentPersona: record.agentPersona ?? null,
242
+ parentSessionId: record.parentSessionId ?? null,
243
+ primaryModel: record.primaryModel ?? null,
244
+ totalTokens: record.totalTokens ?? null,
245
+ skill: record.skill ?? null,
246
+ tenantId,
198
247
  });
199
248
  db.prepare("DELETE FROM markers WHERE session_id = ?").run(record.id);
200
249
  const markerStmt = db.prepare("INSERT INTO markers (session_id, kind, text, position) VALUES (?, ?, ?, ?)");
@@ -207,42 +256,49 @@ export class SqliteSessionStore {
207
256
  const rawNewEntities = [...new Set(record.entities.map((e) => e.trim()).filter(Boolean))];
208
257
  // Resolve each extracted entity through entity_variants so merged surface
209
258
  // forms bind to the canonical instead of resurrecting the retired source.
210
- const resolveVariant = db.prepare("SELECT canonical FROM entity_variants WHERE variant = ?");
211
- 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);
212
263
  const oldEntityRows = db
213
- .prepare("SELECT entity_canonical FROM session_entities WHERE session_id = ?")
214
- .all(record.id);
264
+ .prepare(`SELECT entity_canonical FROM session_entities WHERE ${oldEntityTc.sql} AND session_id = ?`)
265
+ .all(oldEntityTc.param, record.id);
215
266
  const oldEntities = new Set(oldEntityRows.map((r) => r.entity_canonical));
216
- 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);
217
269
  const insertEnt = db.prepare(`
218
270
  INSERT OR IGNORE INTO entities
219
- (canonical, type, status, source, first_seen_session, last_seen_session, session_count)
220
- 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)
221
273
  `);
222
- const touchLastSeen = db.prepare("UPDATE entities SET last_seen_session = ?, updated_at = datetime('now') WHERE canonical = ?");
223
- 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 (?, ?, ?)");
224
277
  for (const name of newEntities) {
225
- insertEnt.run(name, record.id, record.id);
278
+ insertEnt.run(tenantId, name, record.id, record.id);
226
279
  // Update last_seen for entities newly added to this session; matches prior touch semantics.
227
280
  if (!oldEntities.has(name)) {
228
- touchLastSeen.run(record.id, name);
281
+ touchLastSeen.run(record.id, tenantId, name);
229
282
  }
230
- linkEnt.run(record.id, name);
283
+ linkEnt.run(tenantId, record.id, name);
231
284
  }
232
285
  // Recompute session_count exactly for every entity in (old union new) so
233
286
  // counts reflect reality regardless of prior drift.
234
- 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 = ?`);
235
290
  for (const name of new Set([...oldEntities, ...newEntities])) {
236
- recomputeCount.run(name, name);
291
+ recomputeCount.run(tenantId, name, tenantId, name);
237
292
  }
238
293
  if (supersedes && supersedes.priorSessionId !== record.id) {
239
294
  const predecessorStatus = supersedes.kind === "replaces" ? "replaced" : "superseded";
240
295
  db.prepare(`INSERT OR IGNORE INTO session_edges (from_session, to_session, kind)
241
296
  VALUES (?, ?, ?)`).run(record.id, supersedes.priorSessionId, supersedes.kind);
242
- 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);
243
299
  }
244
300
  else {
245
- const priorId = findContinuesPredecessor(db, record.id, record.entities);
301
+ const priorId = findContinuesPredecessor(db, tenantId, record.id, record.entities);
246
302
  if (priorId !== null) {
247
303
  db.prepare(`INSERT OR IGNORE INTO session_edges (from_session, to_session, kind)
248
304
  VALUES (?, ?, 'continues')`).run(record.id, priorId);
@@ -276,7 +332,7 @@ export class SqliteSessionStore {
276
332
  // copy drifted from the port method and missed NLM #351 bug 1 (orphan
277
333
  // embeddings on replace) and bug 2 (per-fact collapse -> supersedence
278
334
  // cycles). Runs inside this session txn.
279
- factSink.factStore.ingestSessionFactsInTxn(record.id, factSink.facts);
335
+ factSink.factStore.ingestSessionFactsInTxn(tenantId, record.id, factSink.facts, record.scope);
280
336
  }
281
337
  });
282
338
  txn();
@@ -300,13 +356,15 @@ export class SqliteSessionStore {
300
356
  const { vector } = await embedder.embed(text, "document");
301
357
  this.insertChunkEmbedding(record.id, chunkIdx, vector);
302
358
  }
303
- catch {
359
+ catch (err) {
304
360
  // Per-chunk embedder failure must not roll the ingest back or
305
361
  // abort subsequent chunks.
362
+ recordEmbedFailure("chunk");
363
+ process.stderr.write(`[nlm] embedding chunk failed session=${record.id} chunk=${chunkIdx}: ${String(err)}\n`);
306
364
  }
307
365
  }
308
366
  if (factSink !== null) {
309
- await this.embedFacts(factSink.factStore, factSink.facts, embedder);
367
+ await this.embedFacts(tenantId, factSink.factStore, factSink.facts, embedder);
310
368
  }
311
369
  }
312
370
  }
@@ -339,22 +397,27 @@ export class SqliteSessionStore {
339
397
  * Non-empty body required (the classifier needs transcript text). When
340
398
  * `reprocess` is false, sessions that already have facts are excluded.
341
399
  */
342
- async listBackfillCandidates(filter) {
400
+ async listBackfillCandidates(tenantId, filter) {
401
+ const tc = tenantClause(tenantId, "s.tenant_id");
343
402
  const sql = filter.reprocess
344
- ? `SELECT id, started_at, body FROM sessions
345
- WHERE started_at < ? AND body IS NOT NULL AND length(body) > 0
346
- ${filter.from ? "AND id > ?" : ""}
347
- 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).
348
411
  : `SELECT s.id, s.started_at, s.body FROM sessions s
349
- WHERE s.started_at < ? AND s.body IS NOT NULL AND length(s.body) > 0
350
- 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)
351
414
  ${filter.from ? "AND s.id > ?" : ""}
352
415
  ORDER BY s.started_at ASC, s.id ASC`;
353
416
  const rows = filter.from
354
417
  ? this.db.prepare(sql)
355
- .all(filter.cutoff, filter.from)
418
+ .all(tc.param, filter.cutoff, filter.from)
356
419
  : this.db.prepare(sql)
357
- .all(filter.cutoff);
420
+ .all(tc.param, filter.cutoff);
358
421
  return rows.map((r) => ({ id: r.id, startedAt: r.started_at, body: r.body }));
359
422
  }
360
423
  /**
@@ -369,16 +432,17 @@ export class SqliteSessionStore {
369
432
  * write path. The live ingest path (`insertSession`) keeps using the
370
433
  * internal helpers directly so session+facts commit together.
371
434
  */
372
- async insertFactsForSession(sessionId, factStore, facts, embedder = null) {
435
+ async insertFactsForSession(tenantId, sessionId, factStore, facts, embedder = null) {
373
436
  // Delegate to the canonical sync ingest (single source of truth for
374
437
  // replace + collapse + embedding cleanup). Sync because better-sqlite3 txn
375
438
  // callbacks must be sync; ingestSessionFactsInTxn is the sync core.
439
+ const sessionScope = await this.getSessionScopeById(tenantId, sessionId);
376
440
  const txn = this.db.transaction(() => {
377
- factStore.ingestSessionFactsInTxn(sessionId, facts);
441
+ factStore.ingestSessionFactsInTxn(tenantId, sessionId, facts, sessionScope);
378
442
  });
379
443
  txn();
380
444
  if (embedder) {
381
- await this.embedFacts(factStore, facts, embedder);
445
+ await this.embedFacts(tenantId, factStore, facts, embedder);
382
446
  }
383
447
  }
384
448
  /**
@@ -386,68 +450,43 @@ export class SqliteSessionStore {
386
450
  * embeddings to fact_embeddings via FactStore.upsertEmbedding. Per-fact
387
451
  * failures don't abort the batch, and never affect committed fact rows.
388
452
  */
389
- async embedFacts(factStore, facts, embedder) {
453
+ async embedFacts(tenantId, factStore, facts, embedder) {
390
454
  for (const fact of batchWinners(facts)) {
391
455
  const factText = `${fact.subject} ${fact.predicate} ${fact.value}`.trim();
392
456
  if (!factText)
393
457
  continue;
394
458
  try {
395
459
  const { vector } = await embedder.embed(factText, "document");
396
- await factStore.upsertEmbedding(fact.id, vector);
460
+ await factStore.upsertEmbedding(tenantId, fact.id, vector);
397
461
  }
398
- catch {
462
+ catch (err) {
399
463
  // Per-fact embedding failure must not abort embedding of subsequent
400
464
  // facts. The fact row stays current; semantic recall just misses it
401
465
  // until a future re-ingest.
466
+ recordEmbedFailure("fact");
467
+ process.stderr.write(`[nlm] embedding fact failed fact=${fact.id}: ${String(err)}\n`);
402
468
  }
403
469
  }
404
470
  }
405
- async list(filter) {
406
- const rows = this.db
407
- .prepare(`
408
- SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
409
- label, summary, status, transcript_kind, transcript_path, body,
410
- classifier_provider, classifier_model, classifier_confidence
411
- FROM sessions
412
- ORDER BY started_at ASC
413
- `)
414
- .all();
415
- if (rows.length === 0)
416
- return [];
417
- const ids = rows.map((r) => r.id);
418
- const entitiesByIdMap = this.loadEntities(ids);
419
- const markersByIdMap = this.loadMarkers(ids);
420
- const overlay = this.overlay();
421
- const sessions = rows.map((r) => this.rowToSession(r, entitiesByIdMap, markersByIdMap, overlay));
422
- if (!filter)
423
- return sessions;
424
- return sessions.filter((s) => {
425
- if (filter.entity !== undefined && !s.entities.includes(filter.entity)) {
426
- return false;
427
- }
428
- if (filter.hasDecisions === true && s.decisions.length === 0)
429
- return false;
430
- if (filter.hasOpenQuestions === true && s.open.length === 0)
431
- return false;
432
- return true;
433
- });
434
- }
435
- async getById(sessionId) {
471
+ async getById(tenantId, sessionId) {
472
+ const tc = tenantClause(tenantId);
436
473
  const row = this.db
437
474
  .prepare(`
438
475
  SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
439
476
  label, summary, status, transcript_kind, transcript_path, body,
440
- classifier_provider, classifier_model, classifier_confidence
477
+ classifier_provider, classifier_model, classifier_confidence,
478
+ agent_persona, parent_session_id,
479
+ primary_model, total_tokens, skill
441
480
  FROM sessions
442
- WHERE id = ?
481
+ WHERE id = ? AND ${tc.sql}
443
482
  `)
444
- .get(sessionId);
483
+ .get(sessionId, tc.param);
445
484
  if (!row)
446
485
  return null;
447
- const entities = this.loadEntities([sessionId]);
486
+ const entities = this.loadEntities(tenantId, [sessionId]);
448
487
  const markers = this.loadMarkers([sessionId]);
449
488
  const edges = this.loadSessionEdges([sessionId]);
450
- const overlay = this.overlay();
489
+ const overlay = this.overlay(tenantId);
451
490
  return this.rowToSession(row, entities, markers, overlay, edges);
452
491
  }
453
492
  /**
@@ -456,45 +495,47 @@ export class SqliteSessionStore {
456
495
  * never reads, and SELECTing it for the corpus is what wedged the
457
496
  * daemon. Resolved sessions carry `body: ""`.
458
497
  */
459
- async getByIds(ids) {
498
+ async getByIds(tenantId, ids) {
460
499
  if (ids.length === 0)
461
500
  return [];
462
501
  const placeholders = ids.map(() => "?").join(",");
502
+ const tc = tenantClause(tenantId);
463
503
  const rows = this.db
464
504
  .prepare(`
465
505
  SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
466
506
  label, summary, status, transcript_kind, transcript_path
467
507
  FROM sessions
468
- WHERE id IN (${placeholders})
508
+ WHERE id IN (${placeholders}) AND ${tc.sql}
469
509
  `)
470
- .all(...ids);
510
+ .all(...ids, tc.param);
471
511
  if (rows.length === 0)
472
512
  return [];
473
513
  const foundIds = rows.map((r) => r.id);
474
- const entitiesByIdMap = this.loadEntities(foundIds);
514
+ const entitiesByIdMap = this.loadEntities(tenantId, foundIds);
475
515
  const markersByIdMap = this.loadMarkers(foundIds);
476
- const overlay = this.overlay();
516
+ const overlay = this.overlay(tenantId);
477
517
  return rows.map((r) => this.rowToSession({ ...r, body: null }, entitiesByIdMap, markersByIdMap, overlay));
478
518
  }
479
- async listByDateRange(fromIso, toIso) {
519
+ async listByDateRange(tenantId, fromIso, toIso) {
520
+ const tc = tenantClause(tenantId);
480
521
  const rows = this.db
481
522
  .prepare(`
482
523
  SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
483
524
  label, summary, status, transcript_kind, transcript_path, workstream_id
484
525
  FROM sessions
485
- 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}
486
527
  ORDER BY started_at ASC
487
528
  `)
488
- .all(toIso, fromIso);
529
+ .all(toIso, fromIso, tc.param);
489
530
  if (rows.length === 0)
490
531
  return [];
491
532
  const ids = rows.map((r) => r.id);
492
- const entitiesByIdMap = this.loadEntities(ids);
533
+ const entitiesByIdMap = this.loadEntities(tenantId, ids);
493
534
  const markersByIdMap = this.loadMarkers(ids);
494
- const overlay = this.overlay();
535
+ const overlay = this.overlay(tenantId);
495
536
  return rows.map((r) => this.rowToSession({ ...r, body: null }, entitiesByIdMap, markersByIdMap, overlay));
496
537
  }
497
- async semanticSearch(queryVector, limit, opts) {
538
+ async semanticSearch(tenantId, queryVector, limit, opts) {
498
539
  const k = Math.max(1, Math.trunc(limit));
499
540
  const blob = Buffer.from(queryVector.buffer, queryVector.byteOffset, queryVector.byteLength);
500
541
  // Overfetch chunks so the max-pool grouping has enough unique sessions
@@ -505,6 +546,12 @@ export class SqliteSessionStore {
505
546
  const envOverfetch = Number.parseInt(process.env["NLM_CHUNK_OVERFETCH"] ?? "", 10);
506
547
  const CHUNK_OVERFETCH = Number.isFinite(envOverfetch) && envOverfetch > 0 ? envOverfetch : 4;
507
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".
508
555
  const rows = this.db
509
556
  .prepare(`
510
557
  SELECT session_id, distance
@@ -515,19 +562,29 @@ export class SqliteSessionStore {
515
562
  `)
516
563
  .all(blob, chunkK);
517
564
  // Max-pool: keep the smallest distance (highest cosine) per session,
518
- // 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.
519
567
  const best = new Map();
520
568
  const excludedSessionIds = new Set();
521
569
  const wsIds = opts?.workstreamIds;
522
570
  if (wsIds !== undefined && wsIds.length === 0)
523
571
  return [];
524
- // 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.
525
576
  const uniqueSessionIds = [...new Set(rows.map((r) => r.session_id))];
526
577
  if (uniqueSessionIds.length > 0) {
527
578
  const placeholders = uniqueSessionIds.map(() => "?").join(",");
579
+ const tc = tenantClause(tenantId);
528
580
  const statusRows = this.db
529
- .prepare(`SELECT id, status, workstream_id FROM sessions WHERE id IN (${placeholders})`)
530
- .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
+ }
531
588
  const excludeSuperseded = opts?.includeSuperseded !== true;
532
589
  for (const sr of statusRows) {
533
590
  const statusExcluded = sr.status === "replaced" || (excludeSuperseded && sr.status === "superseded");
@@ -537,7 +594,7 @@ export class SqliteSessionStore {
537
594
  }
538
595
  }
539
596
  }
540
- // Second pass: max-pool, excluding superseded/replaced sessions
597
+ // Second pass: max-pool, excluding superseded/replaced/out-of-tenant sessions
541
598
  for (const r of rows) {
542
599
  if (excludedSessionIds.has(r.session_id))
543
600
  continue;
@@ -558,7 +615,7 @@ export class SqliteSessionStore {
558
615
  * for better matches). User input is tokenized and rebuilt into a quoted
559
616
  * OR query so FTS5 metacharacters cannot reach the MATCH parser.
560
617
  */
561
- async keywordSearch(query, limit, opts) {
618
+ async keywordSearch(tenantId, query, limit, opts) {
562
619
  const wsIds = opts?.workstreamIds;
563
620
  if (wsIds !== undefined && wsIds.length === 0)
564
621
  return [];
@@ -569,6 +626,7 @@ export class SqliteSessionStore {
569
626
  const statusFilter = opts?.includeSuperseded === true
570
627
  ? "AND s.status != 'replaced'"
571
628
  : "AND s.status NOT IN ('superseded', 'replaced')";
629
+ const tc = tenantClause(tenantId, "s.tenant_id");
572
630
  let wsFilter = "";
573
631
  const params = [matchExpr];
574
632
  if (wsIds?.length) {
@@ -576,6 +634,7 @@ export class SqliteSessionStore {
576
634
  wsFilter = `AND s.workstream_id IN (${ph})`;
577
635
  params.push(...wsIds);
578
636
  }
637
+ params.push(tc.param);
579
638
  params.push(k);
580
639
  const rows = this.db
581
640
  .prepare(`
@@ -586,48 +645,54 @@ export class SqliteSessionStore {
586
645
  WHERE sessions_fts MATCH ?
587
646
  ${statusFilter}
588
647
  ${wsFilter}
648
+ AND ${tc.sql}
589
649
  ORDER BY score DESC
590
650
  LIMIT ?
591
651
  `)
592
652
  .all(...params);
593
653
  return rows.map((r) => ({ sessionId: r.session_id, score: r.score }));
594
654
  }
595
- async resolveSuccessors(ids) {
655
+ async resolveSuccessors(tenantId, ids) {
596
656
  if (ids.length === 0)
597
657
  return new Map();
598
658
  const placeholders = ids.map(() => "?").join(",");
659
+ const tc = tenantClause(tenantId, "s.tenant_id");
599
660
  const rows = this.db
600
661
  .prepare(`
601
- SELECT from_session, to_session
602
- FROM session_edges
603
- WHERE kind = 'supersedes'
604
- 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}
605
668
  `)
606
- .all(...ids);
669
+ .all(...ids, tc.param);
607
670
  const out = new Map();
608
671
  for (const r of rows)
609
672
  out.set(r.to_session, r.from_session);
610
673
  return out;
611
674
  }
612
- async updateStatus(sessionId, status) {
675
+ async updateStatus(tenantId, sessionId, status) {
613
676
  if (status === "idle") {
614
677
  throw new Error("Cannot persist derived status 'idle' — only active/closed/superseded");
615
678
  }
679
+ const tc = tenantClause(tenantId);
616
680
  this.db
617
- .prepare("UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?")
618
- .run(status, sessionId);
681
+ .prepare(`UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ? AND ${tc.sql}`)
682
+ .run(status, sessionId, tc.param);
619
683
  }
620
- async markSuperseded(predecessorId, successorId) {
684
+ async markSuperseded(tenantId, predecessorId, successorId) {
621
685
  if (predecessorId === successorId) {
622
686
  throw new Error("A session cannot supersede itself");
623
687
  }
624
- 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}`);
625
690
  const txn = this.db.transaction(() => {
626
- const predExists = (existsStmt.get(predecessorId)?.c ?? 0) > 0;
691
+ const predExists = (existsStmt.get(predecessorId, tenantId)?.c ?? 0) > 0;
627
692
  if (!predExists) {
628
693
  throw new Error(`predecessor session ${predecessorId} not found`);
629
694
  }
630
- const succExists = (existsStmt.get(successorId)?.c ?? 0) > 0;
695
+ const succExists = (existsStmt.get(successorId, tenantId)?.c ?? 0) > 0;
631
696
  if (!succExists) {
632
697
  throw new Error(`successor session ${successorId} not found`);
633
698
  }
@@ -672,56 +737,68 @@ export class SqliteSessionStore {
672
737
  .prepare(`INSERT OR IGNORE INTO session_edges (from_session, to_session, kind)
673
738
  VALUES (?, ?, 'supersedes')`)
674
739
  .run(successorId, predecessorId);
740
+ const statusTc = tenantClause(tenantId);
675
741
  this.db
676
- .prepare("UPDATE sessions SET status = 'superseded', updated_at = datetime('now') WHERE id = ?")
677
- .run(predecessorId);
742
+ .prepare(`UPDATE sessions SET status = 'superseded', updated_at = datetime('now') WHERE id = ? AND ${statusTc.sql}`)
743
+ .run(predecessorId, statusTc.param);
678
744
  // Cascade supersedence to facts: link predecessor facts to their successors
679
- const selectPredFacts = this.db.prepare("SELECT id, subject, predicate FROM facts WHERE source_session_id = ?");
680
- const selectSuccFact = this.db.prepare("SELECT id FROM facts WHERE source_session_id = ? AND subject = ? AND predicate = ? AND superseded_by IS NULL LIMIT 1");
681
- 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}`);
682
751
  const delFactEmbedding = this.db.prepare("DELETE FROM fact_embeddings WHERE fact_id = ?");
683
- const predecessorFacts = selectPredFacts.all(predecessorId);
752
+ const predecessorFacts = selectPredFacts.all(predecessorId, predFactsTc.param);
684
753
  for (const pFact of predecessorFacts) {
685
- const successor = selectSuccFact.get(successorId, pFact.subject, pFact.predicate);
754
+ const successor = selectSuccFact.get(successorId, pFact.subject, pFact.predicate, succFactTc.param);
686
755
  if (successor) {
687
- updateFactSuperseded.run(successor.id, pFact.id);
756
+ updateFactSuperseded.run(successor.id, pFact.id, updateFactTc.param);
688
757
  delFactEmbedding.run(pFact.id);
689
758
  }
690
759
  }
691
760
  });
692
761
  txn();
693
762
  }
694
- async setWorkstreamBinding(sessionId, workstreamId, source, confidence) {
695
- this.db.prepare("UPDATE sessions SET workstream_id = ?, binding_source = ?, binding_confidence = ?, updated_at = datetime('now') WHERE id = ?").run(workstreamId, source, confidence, sessionId);
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);
766
+ return row?.scope ?? null;
696
767
  }
697
- async listSessionIdsByWorkstreams(workstreamIds) {
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);
771
+ }
772
+ async listSessionIdsByWorkstreams(tenantId, workstreamIds) {
698
773
  if (workstreamIds.length === 0)
699
774
  return [];
700
775
  const ph = workstreamIds.map(() => "?").join(",");
701
- 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);
702
778
  }
703
- async getEntities(sessionId) {
704
- return this.loadEntities([sessionId]).get(sessionId) ?? [];
779
+ async getEntities(tenantId, sessionId) {
780
+ return this.loadEntities(tenantId, [sessionId]).get(sessionId) ?? [];
705
781
  }
706
- async getWorkstreamIds(sessionIds) {
782
+ async getWorkstreamIds(tenantId, sessionIds) {
707
783
  const out = new Map();
708
784
  if (sessionIds.length === 0)
709
785
  return out;
710
786
  const ph = sessionIds.map(() => "?").join(",");
711
- 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))
712
789
  out.set(r.id, r.workstream_id);
713
790
  return out;
714
791
  }
715
792
  // ── insert helpers used by tests / future ingest path ─────────────────
716
793
  /** @internal test-only helper; production callers use insertSession(). */
717
- insertSessionForTest(session) {
794
+ insertSessionForTest(session, tenantId = DEFAULT_TEAM_ID) {
718
795
  const stmt = this.db.prepare(`
719
796
  INSERT INTO sessions (
720
797
  id, runtime, runtime_session_id, started_at, ended_at, duration_min,
721
- label, summary, body, status, transcript_kind, transcript_path
798
+ label, summary, body, status, transcript_kind, transcript_path, tenant_id
722
799
  ) VALUES (
723
800
  @id, @runtime, @runtimeSessionId, @startedAt, @endedAt, @durationMin,
724
- @label, @summary, @body, @status, @transcriptKind, @transcriptPath
801
+ @label, @summary, @body, @status, @transcriptKind, @transcriptPath, @tenantId
725
802
  )
726
803
  `);
727
804
  const status = session.status === "idle" ? "active" : session.status;
@@ -738,15 +815,16 @@ export class SqliteSessionStore {
738
815
  status,
739
816
  transcriptKind: session.transcriptKind,
740
817
  transcriptPath: session.transcriptPath,
818
+ tenantId,
741
819
  });
742
820
  const entStmt = this.db.prepare(`
743
- INSERT OR IGNORE INTO entities (canonical, type, status)
744
- VALUES (?, 'candidate', 'active')
821
+ INSERT OR IGNORE INTO entities (tenant_id, canonical, type, status)
822
+ VALUES (?, ?, 'candidate', 'active')
745
823
  `);
746
- 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 (?, ?, ?)");
747
825
  for (const e of session.entities) {
748
- entStmt.run(e);
749
- linkStmt.run(session.id, e);
826
+ entStmt.run(tenantId, e);
827
+ linkStmt.run(tenantId, session.id, e);
750
828
  }
751
829
  const markerStmt = this.db.prepare("INSERT INTO markers (session_id, kind, text, position) VALUES (?, ?, ?, ?)");
752
830
  session.decisions.forEach((d, i) => markerStmt.run(session.id, "decision", d, i));
@@ -764,18 +842,19 @@ export class SqliteSessionStore {
764
842
  this.insertChunkEmbedding(sessionId, chunkIdx, vector);
765
843
  }
766
844
  // ── internal ──────────────────────────────────────────────────────────
767
- loadEntities(ids) {
845
+ loadEntities(tenantId, ids) {
768
846
  if (ids.length === 0)
769
847
  return new Map();
770
848
  const placeholders = ids.map(() => "?").join(",");
849
+ const tc = tenantClause(tenantId);
771
850
  const rows = this.db
772
851
  .prepare(`
773
852
  SELECT session_id, entity_canonical
774
853
  FROM session_entities
775
- WHERE session_id IN (${placeholders})
854
+ WHERE session_id IN (${placeholders}) AND ${tc.sql}
776
855
  ORDER BY session_id
777
856
  `)
778
- .all(...ids);
857
+ .all(...ids, tc.param);
779
858
  const out = new Map();
780
859
  for (const r of rows) {
781
860
  const list = out.get(r.session_id);
@@ -879,6 +958,11 @@ export class SqliteSessionStore {
879
958
  classifierProvider: row.classifier_provider ?? null,
880
959
  classifierModel: row.classifier_model ?? null,
881
960
  classifierConfidence: row.classifier_confidence ?? null,
961
+ agentPersona: row.agent_persona ?? null,
962
+ parentSessionId: row.parent_session_id ?? null,
963
+ primaryModel: row.primary_model ?? null,
964
+ totalTokens: row.total_tokens ?? null,
965
+ skill: row.skill ?? null,
882
966
  };
883
967
  }
884
968
  }