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
@@ -4,6 +4,11 @@
4
4
  * Constructor takes the Pool from PgStorage. Also exposes recentWrites()
5
5
  * and recentMarkers() for the /live HTTP endpoints, and insertSession() +
6
6
  * insertSessionForTest() for ingest and test seeding.
7
+ *
8
+ * Tenancy: mirrors SqliteSessionStore — every method takes `tenantId` as its
9
+ * non-optional first parameter and routes STAMP-table WHERE fragments
10
+ * through `tenantClausePg`. See that file's header comment for the full
11
+ * per-table rationale.
7
12
  */
8
13
  import type { Pool } from "pg";
9
14
  import type { KeywordNeighbor, SearchOptions, SemanticNeighbor, SessionStore } from "../../ports/session-store.js";
@@ -14,35 +19,34 @@ export declare class PgSessionStore implements SessionStore {
14
19
  readonly pool: Pool;
15
20
  constructor(pool: Pool);
16
21
  private overlayCache;
17
- private overlayCacheAt;
18
22
  invalidateOverlayCache(): void;
19
23
  private overlay;
20
- getById(sessionId: string): Promise<Session | null>;
21
- getByIds(ids: ReadonlyArray<string>): Promise<ReadonlyArray<Session>>;
22
- listByDateRange(fromIso: string, toIso: string): Promise<ReadonlyArray<Session>>;
23
- semanticSearch(queryVector: Float32Array, limit: number, opts?: SearchOptions): Promise<ReadonlyArray<SemanticNeighbor>>;
24
- keywordSearch(query: string, limit: number, opts?: SearchOptions): Promise<ReadonlyArray<KeywordNeighbor>>;
25
- resolveSuccessors(ids: ReadonlyArray<string>): Promise<Map<string, string>>;
26
- updateStatus(sessionId: string, status: SessionStatus): Promise<void>;
27
- markSuperseded(predecessorId: string, successorId: string): Promise<void>;
28
- getSessionScopeById(id: string): Promise<string | null>;
29
- setWorkstreamBinding(sessionId: string, workstreamId: string | null, source: import("../workstream/model.js").BindingSource | null, confidence: number | null): Promise<void>;
30
- listSessionIdsByWorkstreams(workstreamIds: ReadonlyArray<string>): Promise<ReadonlyArray<string>>;
31
- getEntities(sessionId: string): Promise<ReadonlyArray<string>>;
32
- getWorkstreamIds(sessionIds: ReadonlyArray<string>): Promise<Map<string, string | null>>;
33
- recentWrites(limit: number): Promise<RecentWrite[]>;
34
- recentMarkers(limit: number): Promise<RecentMarker[]>;
35
- insertSession(record: IngestRecord, embedder?: import("../../ports/llm-client.js").LLMClient | null, supersedes?: Supersedes | null, factSink?: {
24
+ getById(tenantId: string, sessionId: string): Promise<Session | null>;
25
+ getByIds(tenantId: string, ids: ReadonlyArray<string>): Promise<ReadonlyArray<Session>>;
26
+ listByDateRange(tenantId: string, fromIso: string, toIso: string): Promise<ReadonlyArray<Session>>;
27
+ semanticSearch(tenantId: string, queryVector: Float32Array, limit: number, opts?: SearchOptions): Promise<ReadonlyArray<SemanticNeighbor>>;
28
+ keywordSearch(tenantId: string, query: string, limit: number, opts?: SearchOptions): Promise<ReadonlyArray<KeywordNeighbor>>;
29
+ resolveSuccessors(tenantId: string, ids: ReadonlyArray<string>): Promise<Map<string, string>>;
30
+ updateStatus(tenantId: string, sessionId: string, status: SessionStatus): Promise<void>;
31
+ markSuperseded(tenantId: string, predecessorId: string, successorId: string): Promise<void>;
32
+ getSessionScopeById(tenantId: string, id: string): Promise<string | null>;
33
+ setWorkstreamBinding(tenantId: string, sessionId: string, workstreamId: string | null, source: import("../workstream/model.js").BindingSource | null, confidence: number | null): Promise<void>;
34
+ listSessionIdsByWorkstreams(tenantId: string, workstreamIds: ReadonlyArray<string>): Promise<ReadonlyArray<string>>;
35
+ getEntities(tenantId: string, sessionId: string): Promise<ReadonlyArray<string>>;
36
+ getWorkstreamIds(tenantId: string, sessionIds: ReadonlyArray<string>): Promise<Map<string, string | null>>;
37
+ recentWrites(tenantId: string, limit: number): Promise<RecentWrite[]>;
38
+ recentMarkers(tenantId: string, limit: number): Promise<RecentMarker[]>;
39
+ insertSession(tenantId: string, record: IngestRecord, embedder?: import("../../ports/llm-client.js").LLMClient | null, supersedes?: Supersedes | null, factSink?: {
36
40
  factStore: PgFactStore;
37
41
  facts: ReadonlyArray<Fact>;
38
42
  } | null): Promise<void>;
39
- insertSessionForTest(session: Session): Promise<void>;
43
+ insertSessionForTest(session: Session, tenantId?: string): Promise<void>;
40
44
  /** PG counterpart of SqliteSessionStore.listBackfillCandidates. */
41
- listBackfillCandidates(filter: BackfillCandidateFilter): Promise<BackfillCandidate[]>;
45
+ listBackfillCandidates(tenantId: string, filter: BackfillCandidateFilter): Promise<BackfillCandidate[]>;
42
46
  /** PG counterpart of SqliteSessionStore.insertFactsForSession — writes facts
43
47
  * for an EXISTING session row (deterministic supersedence + best-effort
44
48
  * embeddings) in one transaction. Used by the fact backfill. */
45
- insertFactsForSession(sessionId: string, factStore: PgFactStore, facts: ReadonlyArray<Fact>, embedder?: import("../../ports/llm-client.js").LLMClient | null): Promise<void>;
49
+ insertFactsForSession(tenantId: string, sessionId: string, factStore: PgFactStore, facts: ReadonlyArray<Fact>, embedder?: import("../../ports/llm-client.js").LLMClient | null): Promise<void>;
46
50
  private loadEntities;
47
51
  private loadMarkers;
48
52
  private loadEdges;
@@ -4,6 +4,11 @@
4
4
  * Constructor takes the Pool from PgStorage. Also exposes recentWrites()
5
5
  * and recentMarkers() for the /live HTTP endpoints, and insertSession() +
6
6
  * insertSessionForTest() for ingest and test seeding.
7
+ *
8
+ * Tenancy: mirrors SqliteSessionStore — every method takes `tenantId` as its
9
+ * non-optional first parameter and routes STAMP-table WHERE fragments
10
+ * through `tenantClausePg`. See that file's header comment for the full
11
+ * per-table rationale.
7
12
  */
8
13
  import { tokenize } from "../recall/tokenize.js";
9
14
  import { ingestSessionFactsOnClient } from "./pg-fact-ingest.js";
@@ -11,28 +16,39 @@ import { chunkSessionText } from "../embedding/chunk-body.js";
11
16
  import { batchWinners } from "./fact-batch.js";
12
17
  import { loadActionOverlayPg, openQuestionId } from "../actions/overlay.js";
13
18
  import { liveSessionStatus } from "./live-status.js";
19
+ import { tenantClausePg } from "../tenancy/tenant-clause.js";
20
+ import { DEFAULT_TEAM_ID } from "../tenancy/default-team.js";
21
+ import { recordEmbedFailure } from "../health/embed-failure-state.js";
14
22
  /**
15
23
  * PG mirror of SQLite findContinuesPredecessor: the most recent prior session
16
24
  * whose distinct entity-set is identical to the new session's. Runs on the
17
25
  * ingest transaction's own client. Returns null when there is no entity-set or
18
26
  * no exact-set match, leaving the pair unlinked for the re_derivation_rate metric.
27
+ * Scoped to the same tenant as the new session.
19
28
  */
20
- async function findContinuesPredecessorPg(client, newId, rawEntities) {
29
+ async function findContinuesPredecessorPg(client, tenantId, newId, rawEntities) {
21
30
  const entities = [...new Set(rawEntities.map((e) => e.trim()).filter(Boolean))];
22
31
  if (entities.length === 0)
23
32
  return null;
24
- const placeholders = entities.map((_, i) => `$${i + 2}`).join(",");
33
+ const placeholders = entities.map((_, i) => `$${i + 3}`).join(",");
34
+ // session_entities rows are always written with the same tenant_id as their
35
+ // owning session (every write site stamps both together), so scoping the
36
+ // outer `sessions` row by tenant is already sufficient. The `se.tenant_id =
37
+ // s.tenant_id` join condition below is defense in depth (guard test C4
38
+ // explicitly permits column-to-column tenant equality as a join form).
39
+ const tc = tenantClausePg(tenantId, 2, "s.tenant_id");
25
40
  const res = await client.query(`SELECT s.id AS id
26
41
  FROM sessions s
27
- JOIN session_entities se ON se.session_id = s.id
42
+ JOIN session_entities se ON se.session_id = s.id AND se.tenant_id = s.tenant_id
28
43
  WHERE s.id != $1
44
+ AND ${tc.sql}
29
45
  AND se.entity_canonical IN (${placeholders})
30
46
  GROUP BY s.id
31
- HAVING COUNT(DISTINCT se.entity_canonical) = $${entities.length + 2}
47
+ HAVING COUNT(DISTINCT se.entity_canonical) = $${entities.length + 3}
32
48
  AND COUNT(DISTINCT se.entity_canonical)
33
49
  = (SELECT COUNT(*) FROM session_entities x WHERE x.session_id = s.id)
34
50
  ORDER BY s.started_at DESC, s.id DESC
35
- LIMIT 1`, [newId, ...entities, entities.length]);
51
+ LIMIT 1`, [newId, tc.param, ...entities, entities.length]);
36
52
  return res.rows[0]?.id ?? null;
37
53
  }
38
54
  export class PgSessionStore {
@@ -42,74 +58,79 @@ export class PgSessionStore {
42
58
  constructor(pool) {
43
59
  this.pool = pool;
44
60
  }
45
- overlayCache = null;
46
- overlayCacheAt = 0;
61
+ // Keyed by tenantId — see SqliteSessionStore's identical cache for the
62
+ // tenancy rationale (program spec §4.6 hardening 1).
63
+ overlayCache = new Map();
47
64
  invalidateOverlayCache() {
48
- this.overlayCache = null;
65
+ this.overlayCache.clear();
49
66
  }
50
- async overlay() {
67
+ async overlay(tenantId) {
51
68
  // TTL backstop: explicit invalidation covers the daemon's own writers; the
52
69
  // 30s expiry bounds staleness if another process ever writes actions to
53
70
  // this database.
54
- if (this.overlayCache !== null && Date.now() - this.overlayCacheAt < 30_000) {
55
- return this.overlayCache;
71
+ const cached = this.overlayCache.get(tenantId);
72
+ if (cached !== undefined && Date.now() - cached.at < 30_000) {
73
+ return cached.overlay;
56
74
  }
57
- this.overlayCache = await loadActionOverlayPg(this.pool);
58
- this.overlayCacheAt = Date.now();
59
- return this.overlayCache;
75
+ const overlay = await loadActionOverlayPg(this.pool, tenantId);
76
+ this.overlayCache.set(tenantId, { overlay, at: Date.now() });
77
+ return overlay;
60
78
  }
61
- async getById(sessionId) {
79
+ async getById(tenantId, sessionId) {
80
+ const tc = tenantClausePg(tenantId, 2);
62
81
  const result = await this.pool.query(`SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
63
82
  label, summary, status, transcript_kind, transcript_path, body,
64
83
  classifier_provider, classifier_model, classifier_confidence,
65
84
  agent_persona, parent_session_id,
66
85
  primary_model, total_tokens, skill
67
- FROM sessions WHERE id = $1`, [sessionId]);
86
+ FROM sessions WHERE id = $1 AND ${tc.sql}`, [sessionId, tc.param]);
68
87
  if (!result.rows[0])
69
88
  return null;
70
89
  const [entitiesMap, markersMap, edgesMap, overlay] = await Promise.all([
71
- this.loadEntities([sessionId]),
90
+ this.loadEntities(tenantId, [sessionId]),
72
91
  this.loadMarkers([sessionId]),
73
92
  this.loadEdges([sessionId]),
74
- this.overlay(),
93
+ this.overlay(tenantId),
75
94
  ]);
76
95
  const edges = edgesMap.get(sessionId);
77
96
  return rowToSession(result.rows[0], entitiesMap, markersMap, overlay, edges);
78
97
  }
79
- async getByIds(ids) {
98
+ async getByIds(tenantId, ids) {
80
99
  if (ids.length === 0)
81
100
  return [];
82
101
  const placeholders = ids.map((_, i) => `$${i + 1}`).join(",");
102
+ const tc = tenantClausePg(tenantId, ids.length + 1);
83
103
  const result = await this.pool.query(`SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
84
104
  label, summary, status, transcript_kind, transcript_path
85
- FROM sessions WHERE id IN (${placeholders})`, [...ids]);
105
+ FROM sessions WHERE id IN (${placeholders}) AND ${tc.sql}`, [...ids, tc.param]);
86
106
  if (result.rows.length === 0)
87
107
  return [];
88
108
  const foundIds = result.rows.map((r) => r.id);
89
109
  const [entitiesMap, markersMap, overlay] = await Promise.all([
90
- this.loadEntities(foundIds),
110
+ this.loadEntities(tenantId, foundIds),
91
111
  this.loadMarkers(foundIds),
92
- this.overlay(),
112
+ this.overlay(tenantId),
93
113
  ]);
94
114
  return result.rows.map((r) => rowToSession({ ...r, body: null }, entitiesMap, markersMap, overlay));
95
115
  }
96
- async listByDateRange(fromIso, toIso) {
116
+ async listByDateRange(tenantId, fromIso, toIso) {
117
+ const tc = tenantClausePg(tenantId, 3);
97
118
  const result = await this.pool.query(`SELECT id, runtime, runtime_session_id, started_at, ended_at, duration_min,
98
119
  label, summary, status, transcript_kind, transcript_path, workstream_id
99
120
  FROM sessions
100
- WHERE started_at < $1 AND (ended_at IS NULL OR ended_at >= $2)
101
- ORDER BY started_at ASC`, [toIso, fromIso]);
121
+ WHERE started_at < $1 AND (ended_at IS NULL OR ended_at >= $2) AND ${tc.sql}
122
+ ORDER BY started_at ASC`, [toIso, fromIso, tc.param]);
102
123
  if (result.rows.length === 0)
103
124
  return [];
104
125
  const ids = result.rows.map((r) => r.id);
105
126
  const [entitiesMap, markersMap, overlay] = await Promise.all([
106
- this.loadEntities(ids),
127
+ this.loadEntities(tenantId, ids),
107
128
  this.loadMarkers(ids),
108
- this.overlay(),
129
+ this.overlay(tenantId),
109
130
  ]);
110
131
  return result.rows.map((r) => rowToSession({ ...r, body: null }, entitiesMap, markersMap, overlay));
111
132
  }
112
- async semanticSearch(queryVector, limit, opts) {
133
+ async semanticSearch(tenantId, queryVector, limit, opts) {
113
134
  const wsIds = opts?.workstreamIds;
114
135
  if (wsIds !== undefined && wsIds.length === 0)
115
136
  return [];
@@ -124,19 +145,24 @@ export class PgSessionStore {
124
145
  params.push(wsIds);
125
146
  wsClause = `AND s.workstream_id = ANY($${params.length}::text[])`;
126
147
  }
148
+ // Tenant filter is applied in the id-resolution join against `sessions`
149
+ // (program spec §4.3, vector-path rule) — the `<->` KNN scan over
150
+ // session_embedding_chunks has no tenant column of its own.
151
+ const tc = tenantClausePg(tenantId, params.length + 1, "s.tenant_id");
152
+ params.push(tc.param);
153
+ const limitParam = `$${params.length + 1}`;
127
154
  params.push(k);
128
- const limitParam = `$${params.length}`;
129
155
  const result = await this.pool.query(`SELECT sec.session_id, MIN(sec.embedding <-> $1::vector) AS distance, s.status
130
156
  FROM session_embedding_chunks sec
131
157
  JOIN sessions s ON s.id = sec.session_id
132
- WHERE TRUE ${wsClause}
158
+ WHERE ${tc.sql} ${wsClause}
133
159
  GROUP BY sec.session_id, s.status
134
160
  HAVING ${statusFilter}
135
161
  ORDER BY distance
136
162
  LIMIT ${limitParam}`, params);
137
163
  return result.rows.map((r) => ({ sessionId: r.session_id, distance: r.distance }));
138
164
  }
139
- async keywordSearch(query, limit, opts) {
165
+ async keywordSearch(tenantId, query, limit, opts) {
140
166
  const wsIds = opts?.workstreamIds;
141
167
  if (wsIds !== undefined && wsIds.length === 0)
142
168
  return [];
@@ -154,6 +180,8 @@ export class PgSessionStore {
154
180
  params.push(wsIds);
155
181
  wsClause = `AND workstream_id = ANY($${params.length}::text[])`;
156
182
  }
183
+ const tc = tenantClausePg(tenantId, params.length + 1);
184
+ params.push(tc.param);
157
185
  params.push(k);
158
186
  const limitParam = `$${params.length}`;
159
187
  const result = await this.pool.query(`SELECT id AS session_id,
@@ -162,39 +190,46 @@ export class PgSessionStore {
162
190
  WHERE fts_vector @@ websearch_to_tsquery('english', $1)
163
191
  AND ${statusFilter}
164
192
  ${wsClause}
193
+ AND ${tc.sql}
165
194
  ORDER BY score DESC
166
195
  LIMIT ${limitParam}`, params);
167
196
  return result.rows.map((r) => ({ sessionId: r.session_id, score: r.score }));
168
197
  }
169
- async resolveSuccessors(ids) {
198
+ async resolveSuccessors(tenantId, ids) {
170
199
  if (ids.length === 0)
171
200
  return new Map();
172
201
  const placeholders = ids.map((_, i) => `$${i + 1}`).join(",");
173
- const result = await this.pool.query(`SELECT from_session, to_session FROM session_edges
174
- WHERE kind = 'supersedes' AND to_session IN (${placeholders})`, [...ids]);
202
+ const tc = tenantClausePg(tenantId, ids.length + 1, "s.tenant_id");
203
+ const result = await this.pool.query(`SELECT se.from_session, se.to_session
204
+ FROM session_edges se
205
+ JOIN sessions s ON s.id = se.to_session
206
+ WHERE se.kind = 'supersedes' AND se.to_session IN (${placeholders}) AND ${tc.sql}`, [...ids, tc.param]);
175
207
  const out = new Map();
176
208
  for (const r of result.rows)
177
209
  out.set(r.to_session, r.from_session);
178
210
  return out;
179
211
  }
180
- async updateStatus(sessionId, status) {
212
+ async updateStatus(tenantId, sessionId, status) {
181
213
  if (status === "idle") {
182
214
  throw new Error("Cannot persist derived status 'idle' — only active/closed/superseded");
183
215
  }
184
- await this.pool.query("UPDATE sessions SET status = $1, updated_at = NOW() WHERE id = $2", [status, sessionId]);
216
+ const tc = tenantClausePg(tenantId, 3);
217
+ await this.pool.query(`UPDATE sessions SET status = $1, updated_at = NOW() WHERE id = $2 AND ${tc.sql}`, [status, sessionId, tc.param]);
185
218
  }
186
- async markSuperseded(predecessorId, successorId) {
219
+ async markSuperseded(tenantId, predecessorId, successorId) {
187
220
  if (predecessorId === successorId) {
188
221
  throw new Error("A session cannot supersede itself");
189
222
  }
190
223
  const client = await this.pool.connect();
191
224
  try {
192
225
  await client.query("BEGIN");
193
- const predExists = await client.query("SELECT COUNT(*) AS c FROM sessions WHERE id = $1", [predecessorId]);
226
+ const predExistsTc = tenantClausePg(tenantId, 2);
227
+ const predExists = await client.query(`SELECT COUNT(*) AS c FROM sessions WHERE id = $1 AND ${predExistsTc.sql}`, [predecessorId, predExistsTc.param]);
194
228
  if (Number(predExists.rows[0]?.c) === 0) {
195
229
  throw new Error(`predecessor session ${predecessorId} not found`);
196
230
  }
197
- const succExists = await client.query("SELECT COUNT(*) AS c FROM sessions WHERE id = $1", [successorId]);
231
+ const succExistsTc = tenantClausePg(tenantId, 2);
232
+ const succExists = await client.query(`SELECT COUNT(*) AS c FROM sessions WHERE id = $1 AND ${succExistsTc.sql}`, [successorId, succExistsTc.param]);
198
233
  if (Number(succExists.rows[0]?.c) === 0) {
199
234
  throw new Error(`successor session ${successorId} not found`);
200
235
  }
@@ -223,8 +258,12 @@ export class PgSessionStore {
223
258
  await client.query(`INSERT INTO session_edges (from_session, to_session, kind)
224
259
  VALUES ($1, $2, 'supersedes')
225
260
  ON CONFLICT DO NOTHING`, [successorId, predecessorId]);
226
- await client.query("UPDATE sessions SET status = 'superseded', updated_at = NOW() WHERE id = $1", [predecessorId]);
227
- // Cascade supersedence to facts in a single correlated UPDATE
261
+ const statusTc = tenantClausePg(tenantId, 2);
262
+ await client.query(`UPDATE sessions SET status = 'superseded', updated_at = NOW() WHERE id = $1 AND ${statusTc.sql}`, [predecessorId, statusTc.param]);
263
+ // Cascade supersedence to facts in a single correlated UPDATE, scoped to
264
+ // the caller's tenant on both sides of the join.
265
+ const sTc = tenantClausePg(tenantId, 3, "s.tenant_id");
266
+ const pTc = tenantClausePg(tenantId, 3, "p.tenant_id");
228
267
  const cascadeSQL = `
229
268
  UPDATE facts AS p
230
269
  SET superseded_by = (
@@ -233,18 +272,21 @@ export class PgSessionStore {
233
272
  AND s.subject = p.subject
234
273
  AND s.predicate = p.predicate
235
274
  AND s.superseded_by IS NULL
275
+ AND ${sTc.sql}
236
276
  LIMIT 1
237
277
  )
238
278
  WHERE p.source_session_id = $1
279
+ AND ${pTc.sql}
239
280
  AND EXISTS (
240
281
  SELECT 1 FROM facts s
241
282
  WHERE s.source_session_id = $2
242
283
  AND s.subject = p.subject
243
284
  AND s.predicate = p.predicate
244
285
  AND s.superseded_by IS NULL
286
+ AND ${sTc.sql}
245
287
  )
246
288
  `;
247
- const cascaded = await client.query(cascadeSQL + " RETURNING p.id", [predecessorId, successorId]);
289
+ const cascaded = await client.query(cascadeSQL + " RETURNING p.id", [predecessorId, successorId, sTc.param]);
248
290
  const cascadedIds = cascaded.rows.map((r) => r.id);
249
291
  if (cascadedIds.length > 0) {
250
292
  await client.query("DELETE FROM fact_embeddings WHERE fact_id = ANY($1)", [cascadedIds]);
@@ -259,43 +301,49 @@ export class PgSessionStore {
259
301
  client.release();
260
302
  }
261
303
  }
262
- async getSessionScopeById(id) {
263
- const r = await this.pool.query("SELECT scope FROM sessions WHERE id = $1", [id]);
304
+ async getSessionScopeById(tenantId, id) {
305
+ const tc = tenantClausePg(tenantId, 2);
306
+ const r = await this.pool.query(`SELECT scope FROM sessions WHERE id = $1 AND ${tc.sql}`, [id, tc.param]);
264
307
  return r.rows[0]?.scope ?? null;
265
308
  }
266
- async setWorkstreamBinding(sessionId, workstreamId, source, confidence) {
267
- await this.pool.query("UPDATE sessions SET workstream_id = $1, binding_source = $2, binding_confidence = $3, updated_at = NOW() WHERE id = $4", [workstreamId, source, confidence, sessionId]);
309
+ async setWorkstreamBinding(tenantId, sessionId, workstreamId, source, confidence) {
310
+ const tc = tenantClausePg(tenantId, 5);
311
+ await this.pool.query(`UPDATE sessions SET workstream_id = $1, binding_source = $2, binding_confidence = $3, updated_at = NOW() WHERE id = $4 AND ${tc.sql}`, [workstreamId, source, confidence, sessionId, tc.param]);
268
312
  }
269
- async listSessionIdsByWorkstreams(workstreamIds) {
313
+ async listSessionIdsByWorkstreams(tenantId, workstreamIds) {
270
314
  if (workstreamIds.length === 0)
271
315
  return [];
272
316
  const ph = workstreamIds.map((_, i) => `$${i + 1}`).join(",");
273
- const r = await this.pool.query(`SELECT id FROM sessions WHERE workstream_id IN (${ph}) ORDER BY started_at ASC`, [...workstreamIds]);
317
+ const tc = tenantClausePg(tenantId, workstreamIds.length + 1);
318
+ const r = await this.pool.query(`SELECT id FROM sessions WHERE workstream_id IN (${ph}) AND ${tc.sql} ORDER BY started_at ASC`, [...workstreamIds, tc.param]);
274
319
  return r.rows.map((row) => row.id);
275
320
  }
276
- async getEntities(sessionId) {
277
- return (await this.loadEntities([sessionId])).get(sessionId) ?? [];
321
+ async getEntities(tenantId, sessionId) {
322
+ return (await this.loadEntities(tenantId, [sessionId])).get(sessionId) ?? [];
278
323
  }
279
- async getWorkstreamIds(sessionIds) {
324
+ async getWorkstreamIds(tenantId, sessionIds) {
280
325
  const out = new Map();
281
326
  if (sessionIds.length === 0)
282
327
  return out;
283
328
  const ph = sessionIds.map((_, i) => `$${i + 1}`).join(",");
284
- const result = await this.pool.query(`SELECT id, workstream_id FROM sessions WHERE id IN (${ph})`, [...sessionIds]);
329
+ const tc = tenantClausePg(tenantId, sessionIds.length + 1);
330
+ const result = await this.pool.query(`SELECT id, workstream_id FROM sessions WHERE id IN (${ph}) AND ${tc.sql}`, [...sessionIds, tc.param]);
285
331
  for (const r of result.rows)
286
332
  out.set(r.id, r.workstream_id);
287
333
  return out;
288
334
  }
289
- async recentWrites(limit) {
335
+ async recentWrites(tenantId, limit) {
336
+ const tc = tenantClausePg(tenantId, 2);
290
337
  const result = await this.pool.query(`SELECT id, runtime, label, summary, created_at AS "createdAt"
291
- FROM sessions ORDER BY created_at DESC LIMIT $1`, [limit]);
338
+ FROM sessions WHERE ${tc.sql} ORDER BY created_at DESC LIMIT $1`, [limit, tc.param]);
292
339
  if (result.rows.length === 0)
293
340
  return [];
294
341
  const ids = result.rows.map((r) => r.id);
342
+ const entTc = tenantClausePg(tenantId, 2);
295
343
  const entityResult = await this.pool.query(`SELECT session_id, entity_canonical
296
344
  FROM session_entities
297
- WHERE session_id = ANY($1)
298
- ORDER BY entity_canonical`, [ids]);
345
+ WHERE session_id = ANY($1) AND ${entTc.sql}
346
+ ORDER BY entity_canonical`, [ids, entTc.param]);
299
347
  const byId = new Map();
300
348
  for (const e of entityResult.rows) {
301
349
  const list = byId.get(e.session_id);
@@ -306,15 +354,17 @@ export class PgSessionStore {
306
354
  }
307
355
  return result.rows.map((r) => ({ ...r, entities: byId.get(r.id) ?? [] }));
308
356
  }
309
- async recentMarkers(limit) {
357
+ async recentMarkers(tenantId, limit) {
358
+ const tc = tenantClausePg(tenantId, 2, "s.tenant_id");
310
359
  const result = await this.pool.query(`SELECT m.session_id AS "sessionId", m.kind, m.text, s.label, s.created_at AS "createdAt"
311
360
  FROM markers m
312
361
  JOIN sessions s ON s.id = m.session_id
362
+ WHERE ${tc.sql}
313
363
  ORDER BY s.created_at DESC, m.position ASC
314
- LIMIT $1`, [limit]);
364
+ LIMIT $1`, [limit, tc.param]);
315
365
  return result.rows;
316
366
  }
317
- async insertSession(record, embedder = null, supersedes = null, factSink = null) {
367
+ async insertSession(tenantId, record, embedder = null, supersedes = null, factSink = null) {
318
368
  const client = await this.pool.connect();
319
369
  try {
320
370
  await client.query("BEGIN");
@@ -324,8 +374,8 @@ export class PgSessionStore {
324
374
  transcript_offset, transcript_length,
325
375
  classifier_provider, classifier_model, classifier_confidence,
326
376
  scope, agent_persona, parent_session_id,
327
- primary_model, total_tokens, skill
328
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
377
+ primary_model, total_tokens, skill, tenant_id
378
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)
329
379
  ON CONFLICT (id) DO UPDATE SET
330
380
  ended_at = EXCLUDED.ended_at,
331
381
  duration_min = EXCLUDED.duration_min,
@@ -358,6 +408,7 @@ export class PgSessionStore {
358
408
  record.primaryModel ?? null,
359
409
  record.totalTokens ?? null,
360
410
  record.skill ?? null,
411
+ tenantId,
361
412
  ]);
362
413
  await client.query("DELETE FROM markers WHERE session_id = $1", [record.id]);
363
414
  for (let i = 0; i < record.decisions.length; i++) {
@@ -373,36 +424,43 @@ export class PgSessionStore {
373
424
  const rawNewEntities = [...new Set(record.entities.map((e) => e.trim()).filter(Boolean))];
374
425
  // Resolve each extracted entity through entity_variants so merged surface
375
426
  // forms bind to the canonical instead of resurrecting the retired source.
376
- const variantRes = await client.query(`SELECT variant, canonical FROM entity_variants WHERE variant = ANY($1)`, [rawNewEntities]);
427
+ const variantTc = tenantClausePg(tenantId, 1);
428
+ const variantRes = await client.query(`SELECT variant, canonical FROM entity_variants WHERE ${variantTc.sql} AND variant = ANY($2)`, [variantTc.param, rawNewEntities]);
377
429
  const variantMap = new Map(variantRes.rows.map((r) => [r.variant, r.canonical]));
378
430
  const newEntities = rawNewEntities.map((name) => variantMap.get(name) ?? name);
379
- const oldRes = await client.query("SELECT entity_canonical FROM session_entities WHERE session_id = $1", [record.id]);
431
+ const oldTc = tenantClausePg(tenantId, 1);
432
+ const oldRes = await client.query(`SELECT entity_canonical FROM session_entities WHERE ${oldTc.sql} AND session_id = $2`, [oldTc.param, record.id]);
380
433
  const oldEntities = new Set(oldRes.rows.map((r) => r.entity_canonical));
381
- await client.query("DELETE FROM session_entities WHERE session_id = $1", [record.id]);
434
+ const deleteTc = tenantClausePg(tenantId, 1);
435
+ await client.query(`DELETE FROM session_entities WHERE ${deleteTc.sql} AND session_id = $2`, [deleteTc.param, record.id]);
382
436
  for (const name of newEntities) {
383
- await client.query(`INSERT INTO entities (canonical, type, status, source, first_seen_session, last_seen_session, session_count)
384
- VALUES ($1, 'candidate', 'candidate', 'auto-detected', $2, $2, 0)
385
- ON CONFLICT (canonical) DO NOTHING`, [name, record.id]);
437
+ const insertTc = tenantClausePg(tenantId, 1);
438
+ await client.query(`INSERT INTO entities (tenant_id, canonical, type, status, source, first_seen_session, last_seen_session, session_count)
439
+ VALUES ($1, $2, 'candidate', 'candidate', 'auto-detected', $3, $3, 0)
440
+ ON CONFLICT (tenant_id, canonical) DO NOTHING`, [insertTc.param, name, record.id]);
386
441
  // Update last_seen for entities newly added to this session; matches prior touch semantics.
387
442
  if (!oldEntities.has(name)) {
388
- await client.query("UPDATE entities SET last_seen_session = $1, updated_at = NOW() WHERE canonical = $2", [record.id, name]);
443
+ const touchTc = tenantClausePg(tenantId, 2);
444
+ await client.query(`UPDATE entities SET last_seen_session = $1, updated_at = NOW() WHERE ${touchTc.sql} AND canonical = $3`, [record.id, touchTc.param, name]);
389
445
  }
390
- await client.query("INSERT INTO session_entities (session_id, entity_canonical) VALUES ($1, $2)", [record.id, name]);
446
+ await client.query("INSERT INTO session_entities (tenant_id, session_id, entity_canonical) VALUES ($1, $2, $3)", [tenantId, record.id, name]);
391
447
  }
392
448
  // Recompute session_count exactly for every entity in (old union new) so
393
449
  // counts reflect reality regardless of prior drift.
394
450
  const allTouched = [...new Set([...oldEntities, ...newEntities])];
395
451
  for (const name of allTouched) {
396
- await client.query("UPDATE entities SET session_count = (SELECT COUNT(*) FROM session_entities WHERE entity_canonical = $1), updated_at = NOW() WHERE canonical = $1", [name]);
452
+ const recomputeTc = tenantClausePg(tenantId, 2);
453
+ await client.query(`UPDATE entities SET session_count = (SELECT COUNT(*) FROM session_entities WHERE ${recomputeTc.sql} AND entity_canonical = $1), updated_at = NOW() WHERE ${recomputeTc.sql} AND canonical = $1`, [name, recomputeTc.param]);
397
454
  }
398
455
  if (supersedes && supersedes.priorSessionId !== record.id) {
399
456
  const predecessorStatus = supersedes.kind === "replaces" ? "replaced" : "superseded";
400
457
  await client.query(`INSERT INTO session_edges (from_session, to_session, kind)
401
458
  VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`, [record.id, supersedes.priorSessionId, supersedes.kind]);
402
- await client.query("UPDATE sessions SET status = $1, updated_at = NOW() WHERE id = $2", [predecessorStatus, supersedes.priorSessionId]);
459
+ const predStatusTc = tenantClausePg(tenantId, 3);
460
+ await client.query(`UPDATE sessions SET status = $1, updated_at = NOW() WHERE id = $2 AND ${predStatusTc.sql}`, [predecessorStatus, supersedes.priorSessionId, predStatusTc.param]);
403
461
  }
404
462
  else {
405
- const priorId = await findContinuesPredecessorPg(client, record.id, record.entities);
463
+ const priorId = await findContinuesPredecessorPg(client, tenantId, record.id, record.entities);
406
464
  if (priorId !== null) {
407
465
  await client.query(`INSERT INTO session_edges (from_session, to_session, kind)
408
466
  VALUES ($1, $2, 'continues') ON CONFLICT DO NOTHING`, [record.id, priorId]);
@@ -410,7 +468,7 @@ export class PgSessionStore {
410
468
  }
411
469
  // Atomic session+facts ingest on the session's own client. Single source of truth: pg-fact-ingest.ts.
412
470
  if (factSink !== null) {
413
- await ingestSessionFactsOnClient(client, record.id, factSink.facts, record.scope);
471
+ await ingestSessionFactsOnClient(client, tenantId, record.id, factSink.facts, record.scope);
414
472
  }
415
473
  await client.query("COMMIT");
416
474
  }
@@ -437,6 +495,7 @@ export class PgSessionStore {
437
495
  await this.pool.query("INSERT INTO session_chunk_map (chunk_id, session_id, chunk_idx) VALUES ($1, $2, $3)", [chunkId, record.id, chunkIdx]);
438
496
  }
439
497
  catch (err) {
498
+ recordEmbedFailure("chunk");
440
499
  process.stderr.write(`[nlm] embedding chunk failed session=${record.id} chunk=${chunkIdx}: ${String(err)}\n`);
441
500
  }
442
501
  }
@@ -450,27 +509,30 @@ export class PgSessionStore {
450
509
  continue;
451
510
  try {
452
511
  const { vector } = await embedder.embed(factText, "document");
453
- await factSink.factStore.upsertEmbedding(fact.id, vector);
512
+ await factSink.factStore.upsertEmbedding(tenantId, fact.id, vector);
454
513
  }
455
- catch {
456
- // Tolerated; see comment above.
514
+ catch (err) {
515
+ // Tolerated; the fact row stays current, semantic recall misses
516
+ // it until a future re-ingest.
517
+ recordEmbedFailure("fact");
518
+ process.stderr.write(`[nlm] embedding fact failed fact=${fact.id}: ${String(err)}\n`);
457
519
  }
458
520
  }
459
521
  }
460
522
  }
461
523
  }
462
- async insertSessionForTest(session) {
524
+ async insertSessionForTest(session, tenantId = DEFAULT_TEAM_ID) {
463
525
  const status = session.status === "idle" ? "active" : session.status;
464
526
  await this.pool.query(`INSERT INTO sessions (id, runtime, runtime_session_id, started_at, ended_at,
465
- duration_min, label, summary, body, status, transcript_kind, transcript_path)
466
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, [
527
+ duration_min, label, summary, body, status, transcript_kind, transcript_path, tenant_id)
528
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, [
467
529
  session.id, session.runtime, session.runtimeSessionId, session.startedAt,
468
530
  session.endedAt, session.durationMin, session.label, session.summary,
469
- session.body, status, session.transcriptKind, session.transcriptPath,
531
+ session.body, status, session.transcriptKind, session.transcriptPath, tenantId,
470
532
  ]);
471
533
  for (const e of session.entities) {
472
- await this.pool.query("INSERT INTO entities (canonical, type, status) VALUES ($1, 'candidate', 'active') ON CONFLICT DO NOTHING", [e]);
473
- await this.pool.query("INSERT INTO session_entities (session_id, entity_canonical) VALUES ($1, $2) ON CONFLICT DO NOTHING", [session.id, e]);
534
+ await this.pool.query("INSERT INTO entities (tenant_id, canonical, type, status) VALUES ($1, $2, 'candidate', 'active') ON CONFLICT DO NOTHING", [tenantId, e]);
535
+ await this.pool.query("INSERT INTO session_entities (tenant_id, session_id, entity_canonical) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", [tenantId, session.id, e]);
474
536
  }
475
537
  for (let i = 0; i < session.decisions.length; i++) {
476
538
  await this.pool.query("INSERT INTO markers (session_id, kind, text, position) VALUES ($1, 'decision', $2, $3)", [session.id, session.decisions[i], i]);
@@ -480,32 +542,41 @@ export class PgSessionStore {
480
542
  }
481
543
  }
482
544
  /** PG counterpart of SqliteSessionStore.listBackfillCandidates. */
483
- async listBackfillCandidates(filter) {
545
+ async listBackfillCandidates(tenantId, filter) {
484
546
  const params = [filter.cutoff];
485
547
  let fromClause = "";
486
548
  if (filter.from) {
487
549
  params.push(filter.from);
488
550
  fromClause = `AND s.id > $${params.length}`;
489
551
  }
552
+ // facts.tenant_id is always stamped to match its source_session_id's
553
+ // session tenant (every write site stamps both together), so scoping by
554
+ // source_session_id = s.id against an already tenant-filtered `s` is
555
+ // already sufficient. The f.tenant_id = s.tenant_id condition below is
556
+ // defense in depth (guard test C4 permits column-to-column tenant
557
+ // equality as a join form).
490
558
  const existingFactsClause = filter.reprocess
491
559
  ? ""
492
- : "AND NOT EXISTS (SELECT 1 FROM facts f WHERE f.source_session_id = s.id)";
560
+ : "AND NOT EXISTS (SELECT 1 FROM facts f WHERE f.source_session_id = s.id AND f.tenant_id = s.tenant_id)";
561
+ const tc = tenantClausePg(tenantId, params.length + 1, "s.tenant_id");
562
+ params.push(tc.param);
493
563
  const result = await this.pool.query(`SELECT s.id, s.started_at, s.body FROM sessions s
494
564
  WHERE s.started_at < $1 AND s.body IS NOT NULL AND length(s.body) > 0
495
565
  ${existingFactsClause}
496
566
  ${fromClause}
567
+ AND ${tc.sql}
497
568
  ORDER BY s.started_at ASC, s.id ASC`, params);
498
569
  return result.rows.map((r) => ({ id: r.id, startedAt: r.started_at, body: r.body }));
499
570
  }
500
571
  /** PG counterpart of SqliteSessionStore.insertFactsForSession — writes facts
501
572
  * for an EXISTING session row (deterministic supersedence + best-effort
502
573
  * embeddings) in one transaction. Used by the fact backfill. */
503
- async insertFactsForSession(sessionId, factStore, facts, embedder = null) {
504
- const sessionScope = await this.getSessionScopeById(sessionId);
574
+ async insertFactsForSession(tenantId, sessionId, factStore, facts, embedder = null) {
575
+ const sessionScope = await this.getSessionScopeById(tenantId, sessionId);
505
576
  const client = await this.pool.connect();
506
577
  try {
507
578
  await client.query("BEGIN");
508
- await ingestSessionFactsOnClient(client, sessionId, facts, sessionScope);
579
+ await ingestSessionFactsOnClient(client, tenantId, sessionId, facts, sessionScope);
509
580
  await client.query("COMMIT");
510
581
  }
511
582
  catch (err) {
@@ -522,7 +593,7 @@ export class PgSessionStore {
522
593
  continue;
523
594
  try {
524
595
  const { vector } = await embedder.embed(factText, "document");
525
- await factStore.upsertEmbedding(fact.id, vector);
596
+ await factStore.upsertEmbedding(tenantId, fact.id, vector);
526
597
  }
527
598
  catch {
528
599
  // Best-effort; a per-fact embed failure leaves the fact current but
@@ -531,12 +602,13 @@ export class PgSessionStore {
531
602
  }
532
603
  }
533
604
  }
534
- async loadEntities(ids) {
605
+ async loadEntities(tenantId, ids) {
535
606
  if (ids.length === 0)
536
607
  return new Map();
537
608
  const placeholders = ids.map((_, i) => `$${i + 1}`).join(",");
609
+ const tc = tenantClausePg(tenantId, ids.length + 1);
538
610
  const result = await this.pool.query(`SELECT session_id, entity_canonical FROM session_entities
539
- WHERE session_id IN (${placeholders}) ORDER BY session_id`, [...ids]);
611
+ WHERE session_id IN (${placeholders}) AND ${tc.sql} ORDER BY session_id`, [...ids, tc.param]);
540
612
  const out = new Map();
541
613
  for (const r of result.rows) {
542
614
  const list = out.get(r.session_id);