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