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
package/dist/http/app.js
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { timingSafeEqual } from "node:crypto";
|
|
22
|
-
import { homedir } from "node:os";
|
|
23
22
|
import { dirname, extname, join, normalize, sep } from "node:path";
|
|
24
23
|
import { Hono } from "hono";
|
|
25
24
|
import pkg from "../../package.json" with { type: "json" };
|
|
@@ -39,6 +38,7 @@ import { classifyPrompt } from "../core/hook/gate.js";
|
|
|
39
38
|
import { selectHits } from "../core/hook/select.js";
|
|
40
39
|
import { formatPointerBlock } from "../core/hook/pointer-block.js";
|
|
41
40
|
import { factRecallStats, logFactQuery } from "../core/recall-facts/fact-query-log.js";
|
|
41
|
+
import { pickRelatedConcepts } from "../core/recall/related-concepts.js";
|
|
42
42
|
import { buildDataset } from "../core/dataset/build-dataset.js";
|
|
43
43
|
import { ClassifierBox } from "../llm/classifier-box.js";
|
|
44
44
|
import { SourceRegistry, PgSourceRegistry, } from "../core/sources/source-registry.js";
|
|
@@ -62,6 +62,10 @@ import { laneHealthSnapshot } from "../core/health/embedding-lane-state.js";
|
|
|
62
62
|
import { inflightSnapshot } from "../core/health/embed-inflight.js";
|
|
63
63
|
import { corpusSnapshot } from "../core/health/corpus-state.js";
|
|
64
64
|
import { DEFAULT_NLM_PORT } from "../shared/net.js";
|
|
65
|
+
import { DEFAULT_TEAM_ID } from "../core/tenancy/default-team.js";
|
|
66
|
+
import { tenantStatePath } from "../core/tenancy/tenant-state-path.js";
|
|
67
|
+
import { isHostedMode } from "../core/tenancy/hosted-mode.js";
|
|
68
|
+
import { resolveTeamByToken } from "../core/tenancy/team-auth.js";
|
|
65
69
|
const HERMES_RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
|
|
66
70
|
const MIME_TYPES = {
|
|
67
71
|
".html": "text/html; charset=utf-8",
|
|
@@ -87,6 +91,16 @@ const DATA_STAT_TABLES = [
|
|
|
87
91
|
"sources",
|
|
88
92
|
"providers",
|
|
89
93
|
];
|
|
94
|
+
/**
|
|
95
|
+
* Tenancy (program spec §4.6 disposition LOCAL, M2 plan Wave B6): `GET
|
|
96
|
+
* /api/data/stats` is a LOCAL-dispositioned route (spec §4.6) — hard-403'd
|
|
97
|
+
* under `NLM_HOSTED=1` by Wave C's hosted-mode gate, same class as
|
|
98
|
+
* `/api/dataset` and `/api/data/backup`. These are per-table row counts +
|
|
99
|
+
* migration bookkeeping for the Settings → Data page, not corpus content;
|
|
100
|
+
* left whole-DB deliberately rather than fake-filtered by tenant, which
|
|
101
|
+
* would misrepresent what the counts mean (they're meant to show the whole
|
|
102
|
+
* install's state, e.g. for a local operator sanity-checking their db).
|
|
103
|
+
*/
|
|
90
104
|
function sqliteDataStats(db, dbPath) {
|
|
91
105
|
const countOf = (table) => {
|
|
92
106
|
try {
|
|
@@ -219,7 +233,8 @@ const VALID_FACT_KINDS = ["decision", "open", "attribute"];
|
|
|
219
233
|
export function createApp(deps) {
|
|
220
234
|
const app = new Hono();
|
|
221
235
|
const boundPort = process.env["NLM_PORT"] ? Number.parseInt(process.env["NLM_PORT"], 10) : Number.parseInt(DEFAULT_NLM_PORT, 10);
|
|
222
|
-
installLocalOnlyMiddleware(app, boundPort);
|
|
236
|
+
installLocalOnlyMiddleware(app, boundPort, deps);
|
|
237
|
+
installHostedModeGate(app);
|
|
223
238
|
registerHealthRoute(app, deps);
|
|
224
239
|
registerMcpRoute(app, deps);
|
|
225
240
|
registerRecallRoutes(app, deps);
|
|
@@ -236,6 +251,7 @@ export function createApp(deps) {
|
|
|
236
251
|
registerProviderRoutes(app, deps);
|
|
237
252
|
registerSessionRoute(app, deps);
|
|
238
253
|
registerSignalRoutes(app, deps);
|
|
254
|
+
registerJobRoutes(app, deps);
|
|
239
255
|
const nonceStore = createNonceStore();
|
|
240
256
|
registerNonceRoute(app, nonceStore);
|
|
241
257
|
if (deps.uiDist) {
|
|
@@ -270,25 +286,68 @@ function registerNonceRoute(app, nonceStore) {
|
|
|
270
286
|
function uiAuthMode() {
|
|
271
287
|
return process.env["NLM_UI_AUTH"] === "cookie" ? "cookie" : "none";
|
|
272
288
|
}
|
|
273
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Tenant resolution + auth for every /api/* request (program spec §3, M3).
|
|
291
|
+
* Host/Origin checks are network-position defenses and stay skippable under
|
|
292
|
+
* VITEST/NODE_ENV=test (in-process `app.request()` calls carry no real
|
|
293
|
+
* socket). Tenant resolution does NOT skip under that flag — the contract's
|
|
294
|
+
* token-swap case (spec §6 case 8) and any other test that wires a real
|
|
295
|
+
* `teamTokens` store must exercise the actual resolveTeamByToken path, not a
|
|
296
|
+
* bypass, so a garbage token really does 401 inside a `vitest` run.
|
|
297
|
+
*
|
|
298
|
+
* Resolution order:
|
|
299
|
+
* 1. Hosted mode (NLM_HOSTED=1): a resolvable Bearer token is mandatory.
|
|
300
|
+
* No cookie fallback, no ungated fallback — absent/unresolvable token
|
|
301
|
+
* is 401 before any corpus read, always.
|
|
302
|
+
* 2. Local mode, NLM_UI_AUTH unset ("none", the default): zero UX change
|
|
303
|
+
* from pre-M3 — requests resolve to DEFAULT_TEAM_ID. A resolvable
|
|
304
|
+
* Bearer token is still honored when presented (lets local multi-team
|
|
305
|
+
* dev and in-process tests exercise real resolution), but nothing is
|
|
306
|
+
* ever rejected in this mode — that's the point of leaving auth off.
|
|
307
|
+
* 3. Local mode, NLM_UI_AUTH=cookie: the existing HMAC session cookie
|
|
308
|
+
* (still keyed off NLM_MCP_TOKEN directly — unchanged, local-only
|
|
309
|
+
* concern) resolves to DEFAULT_TEAM_ID. A Bearer token resolves via
|
|
310
|
+
* the same team_tokens lookup as every other transport instead of the
|
|
311
|
+
* old exact-string compare; unresolved token or no credential is 401.
|
|
312
|
+
*/
|
|
313
|
+
function installLocalOnlyMiddleware(app, boundPort, deps) {
|
|
274
314
|
const skipLocalGate = !!process.env["VITEST"] || process.env["NODE_ENV"] === "test";
|
|
275
315
|
const extras = parseAllowedOrigins(process.env["NLM_ALLOWED_ORIGINS"] ?? "");
|
|
276
316
|
app.use("/api/*", async (c, next) => {
|
|
277
|
-
if (skipLocalGate)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
317
|
+
if (!skipLocalGate) {
|
|
318
|
+
const host = c.req.header("host");
|
|
319
|
+
if (!isLoopbackHost(host, boundPort) && !extras.hosts.has(host?.toLowerCase() ?? "")) {
|
|
320
|
+
return c.json({ error: "host header not allowed" }, 403);
|
|
321
|
+
}
|
|
282
322
|
}
|
|
283
323
|
if (c.req.path === "/api/health") {
|
|
284
324
|
return next();
|
|
285
325
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
326
|
+
if (!skipLocalGate) {
|
|
327
|
+
const origin = c.req.header("origin");
|
|
328
|
+
if (origin !== undefined && !isLoopbackOrigin(origin, boundPort) && !extras.origins.has(origin.toLowerCase())) {
|
|
329
|
+
return c.json({ error: "origin not allowed" }, 403);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const auth = c.req.header("authorization") ?? "";
|
|
333
|
+
const match = /^Bearer\s+(\S+)$/i.exec(auth);
|
|
334
|
+
const presentedToken = match?.[1];
|
|
335
|
+
if (isHostedMode()) {
|
|
336
|
+
const resolved = deps.teamTokens ? await resolveTeamByToken(deps.teamTokens, presentedToken) : null;
|
|
337
|
+
if (!resolved)
|
|
338
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
339
|
+
c.set("tenantId", resolved.teamId);
|
|
340
|
+
return next();
|
|
289
341
|
}
|
|
290
342
|
if (uiAuthMode() === "none") {
|
|
291
|
-
// Auth disabled by user. Loopback Host + Origin
|
|
343
|
+
// Auth disabled by user (or never configured). Loopback Host + Origin
|
|
344
|
+
// checks already passed. A resolvable token still wins when present;
|
|
345
|
+
// otherwise fall back to the single local operator team exactly as
|
|
346
|
+
// before M3 — this mode never rejects a request.
|
|
347
|
+
const resolved = presentedToken && deps.teamTokens
|
|
348
|
+
? await resolveTeamByToken(deps.teamTokens, presentedToken)
|
|
349
|
+
: null;
|
|
350
|
+
c.set("tenantId", resolved ? resolved.teamId : DEFAULT_TEAM_ID);
|
|
292
351
|
return next();
|
|
293
352
|
}
|
|
294
353
|
const token = process.env["NLM_MCP_TOKEN"];
|
|
@@ -299,26 +358,55 @@ function installLocalOnlyMiddleware(app, boundPort) {
|
|
|
299
358
|
}
|
|
300
359
|
// UI session cookie (HMAC of the token, set by /ui/auth bootstrap).
|
|
301
360
|
// Carries the browser's API calls and survives token-stable restarts.
|
|
361
|
+
// Local-only concern (program spec §3 M3): unchanged, always resolves
|
|
362
|
+
// to the single local operator team.
|
|
302
363
|
const cookies = parseCookies(c.req.header("cookie"));
|
|
303
364
|
if (verifySessionCookie(cookies[SESSION_COOKIE_NAME], token)) {
|
|
304
365
|
// Rolling expiry: every authenticated hit re-issues Set-Cookie so an
|
|
305
366
|
// actively-used session never sees an expiry. Only 30 days of true
|
|
306
367
|
// inactivity force a re-bootstrap.
|
|
307
368
|
c.header("Set-Cookie", buildSessionCookie(deriveSessionValue(token)));
|
|
369
|
+
c.set("tenantId", DEFAULT_TEAM_ID);
|
|
308
370
|
return next();
|
|
309
371
|
}
|
|
310
372
|
// Bearer: programmatic clients (Hermes WebUI, agents, the MCP path).
|
|
311
|
-
//
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
373
|
+
// Resolved via the same team_tokens lookup every other transport uses.
|
|
374
|
+
const resolved = presentedToken && deps.teamTokens
|
|
375
|
+
? await resolveTeamByToken(deps.teamTokens, presentedToken)
|
|
376
|
+
: null;
|
|
377
|
+
if (resolved) {
|
|
378
|
+
c.set("tenantId", resolved.teamId);
|
|
317
379
|
return next();
|
|
318
380
|
}
|
|
319
381
|
return c.json({ error: "unauthorized" }, 401);
|
|
320
382
|
});
|
|
321
383
|
}
|
|
384
|
+
const HOSTED_GATED_ROUTES = [
|
|
385
|
+
{ method: "GET", path: "/api/dataset", disposition: "LOCAL" },
|
|
386
|
+
{ method: "GET", path: "/api/data/backup", disposition: "LOCAL" },
|
|
387
|
+
{ method: "POST", path: "/api/data/restore", disposition: "LOCAL" },
|
|
388
|
+
{ method: "GET", path: "/api/data/stats", disposition: "LOCAL" },
|
|
389
|
+
{ method: "POST", path: "/api/classifier", disposition: "LOCAL" },
|
|
390
|
+
// Single-operator process supervision — starting/stopping the daemon's
|
|
391
|
+
// own reprocess child has no per-tenant meaning, same LOCAL class as the
|
|
392
|
+
// classifier swap above.
|
|
393
|
+
{ method: "POST", path: "/api/jobs/reprocess", disposition: "LOCAL" },
|
|
394
|
+
{ method: "DELETE", path: "/api/jobs/reprocess", disposition: "LOCAL" },
|
|
395
|
+
];
|
|
396
|
+
function installHostedModeGate(app) {
|
|
397
|
+
const gated = new Map();
|
|
398
|
+
for (const r of HOSTED_GATED_ROUTES)
|
|
399
|
+
gated.set(`${r.method} ${r.path}`, r.disposition);
|
|
400
|
+
app.use("/api/*", async (c, next) => {
|
|
401
|
+
if (!isHostedMode())
|
|
402
|
+
return next();
|
|
403
|
+
const disposition = gated.get(`${c.req.method} ${c.req.path}`);
|
|
404
|
+
if (disposition) {
|
|
405
|
+
return c.json({ error: `route disabled in hosted mode (disposition: ${disposition})`, disposition }, 403);
|
|
406
|
+
}
|
|
407
|
+
return next();
|
|
408
|
+
});
|
|
409
|
+
}
|
|
322
410
|
// ── UI session-cookie gate ───────────────────────────────────────────
|
|
323
411
|
//
|
|
324
412
|
// Closes the port-forward bypass: any client reaching localhost:3940
|
|
@@ -416,7 +504,13 @@ function registerHealthRoute(app, deps) {
|
|
|
416
504
|
latest: cachedUpdate.disabled ? null : cachedUpdate.latest,
|
|
417
505
|
behind: cachedUpdate.disabled ? false : cachedUpdate.behind,
|
|
418
506
|
};
|
|
419
|
-
|
|
507
|
+
// "idle" (never started, or this daemon process's initial state — see
|
|
508
|
+
// job-supervisor.ts) reports as `job: null` rather than an idle block:
|
|
509
|
+
// an operator polling /api/health wants to know whether a run exists,
|
|
510
|
+
// not to distinguish "never run" from a five-field block of zeros.
|
|
511
|
+
const jobSnap = deps.jobSupervisor ? deps.jobSupervisor.snapshot() : null;
|
|
512
|
+
const job = jobSnap && jobSnap.state !== "idle" ? jobSnap : null;
|
|
513
|
+
return c.json({ status: "ok", service: "nlm-memory", version: pkg.version, warmup: warmupSnapshot(), embedding: laneHealthSnapshot(), embedInflight: inflightSnapshot(), corpus, update, job });
|
|
420
514
|
});
|
|
421
515
|
// Passive update poll for the UI. Same daily-cached check the CLI
|
|
422
516
|
// startup banner uses — see src/core/update-check/check.ts for the
|
|
@@ -433,30 +527,33 @@ function registerHealthRoute(app, deps) {
|
|
|
433
527
|
function registerMcpRoute(app, deps) {
|
|
434
528
|
if (!deps.mcpDeps)
|
|
435
529
|
return;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
throw new Error("NLM_MCP_TOKEN must be set when mcpDeps is provided — " +
|
|
530
|
+
if (!deps.teamTokens) {
|
|
531
|
+
throw new Error("teamTokens must be wired when mcpDeps is provided — " +
|
|
439
532
|
"refusing to mount an unauthenticated /mcp endpoint");
|
|
440
533
|
}
|
|
441
534
|
const capturedMcpDeps = deps.mcpDeps;
|
|
535
|
+
const teamTokens = deps.teamTokens;
|
|
442
536
|
app.all("/mcp", async (c) => {
|
|
443
537
|
const auth = c.req.header("authorization") ?? "";
|
|
444
538
|
const match = /^Bearer\s+(\S+)$/i.exec(auth);
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
if (!match || given.length !== want.length || !timingSafeEqual(given, want)) {
|
|
539
|
+
const resolved = await resolveTeamByToken(teamTokens, match?.[1]);
|
|
540
|
+
if (!resolved) {
|
|
448
541
|
return c.json({ error: "unauthorized" }, 401);
|
|
449
542
|
}
|
|
450
543
|
// No sessionIdGenerator = stateless mode: no session ID in responses,
|
|
451
544
|
// no session validation. Correct for per-request agent calls.
|
|
452
545
|
const transport = new WebStandardStreamableHTTPServerTransport({});
|
|
453
|
-
const server = createMcpServer(capturedMcpDeps);
|
|
546
|
+
const server = createMcpServer(capturedMcpDeps, resolved.teamId);
|
|
454
547
|
await server.connect(transport);
|
|
455
548
|
return transport.handleRequest(c.req.raw);
|
|
456
549
|
});
|
|
457
550
|
}
|
|
458
551
|
function registerRecallRoutes(app, deps) {
|
|
459
552
|
app.get("/api/recall", async (c) => {
|
|
553
|
+
// Composition point for this request (program spec §3 M3): the tenant
|
|
554
|
+
// the general gate resolved from the Bearer token / cookie / local-mode
|
|
555
|
+
// default, stashed on the Hono context by installLocalOnlyMiddleware.
|
|
556
|
+
const tenantId = c.get("tenantId");
|
|
460
557
|
const q = c.req.query("q") ?? "";
|
|
461
558
|
const entity = c.req.query("entity");
|
|
462
559
|
const kind = c.req.query("kind");
|
|
@@ -500,6 +597,18 @@ function registerRecallRoutes(app, deps) {
|
|
|
500
597
|
else if (source === "hook") {
|
|
501
598
|
withRelatedFacts = true;
|
|
502
599
|
}
|
|
600
|
+
// Learner evidence (spec 2026-10-06): `?withConcepts=true` (or hook
|
|
601
|
+
// source = automatic) attaches a relatedConcepts array. Same posture
|
|
602
|
+
// as facts; empty when no enrollments match, so non-learner callers
|
|
603
|
+
// see zero behavior change.
|
|
604
|
+
const conceptsParam = c.req.query("withConcepts");
|
|
605
|
+
let withRelatedConcepts;
|
|
606
|
+
if (conceptsParam !== undefined) {
|
|
607
|
+
withRelatedConcepts = conceptsParam === "true" || conceptsParam === "1";
|
|
608
|
+
}
|
|
609
|
+
else if (source === "hook") {
|
|
610
|
+
withRelatedConcepts = true;
|
|
611
|
+
}
|
|
503
612
|
// Spec §E: `?withExemplars=true` attaches relatedExemplars (passive code exemplar recall).
|
|
504
613
|
const withExemplars = c.req.query("withExemplars") === "true";
|
|
505
614
|
// Task #303: superseded sessions are opt-in over HTTP and DEFAULT OFF.
|
|
@@ -515,12 +624,13 @@ function registerRecallRoutes(app, deps) {
|
|
|
515
624
|
...(kind !== undefined ? { kind: kind } : {}),
|
|
516
625
|
...(rewrite !== undefined ? { rewrite } : {}),
|
|
517
626
|
...(withRelatedFacts !== undefined ? { withRelatedFacts } : {}),
|
|
627
|
+
...(withRelatedConcepts !== undefined ? { withRelatedConcepts } : {}),
|
|
518
628
|
...(withExemplars ? { withRelatedExemplars: true } : {}),
|
|
519
629
|
...(includeSuperseded !== undefined ? { includeSuperseded } : {}),
|
|
520
630
|
};
|
|
521
|
-
const result = await deps.recall.search(query);
|
|
631
|
+
const result = await deps.recall.search(tenantId, query);
|
|
522
632
|
// Fire-and-forget telemetry — never blocks the response.
|
|
523
|
-
void logQuery({
|
|
633
|
+
void logQuery(tenantId, {
|
|
524
634
|
source,
|
|
525
635
|
runtime,
|
|
526
636
|
query: q || null,
|
|
@@ -535,22 +645,29 @@ function registerRecallRoutes(app, deps) {
|
|
|
535
645
|
return c.json(result);
|
|
536
646
|
});
|
|
537
647
|
app.get("/api/recall/stats", async (c) => {
|
|
648
|
+
const tenantId = c.get("tenantId");
|
|
538
649
|
const daysStr = c.req.query("days") ?? "7";
|
|
539
650
|
const days = Number.parseInt(daysStr, 10);
|
|
540
651
|
if (!Number.isFinite(days) || days < 1 || days > 365) {
|
|
541
652
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
542
653
|
}
|
|
543
|
-
const stats = await recallStats(days, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
|
|
654
|
+
const stats = await recallStats(tenantId, days, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
|
|
544
655
|
return c.json(stats);
|
|
545
656
|
});
|
|
546
657
|
app.get("/api/recall/recent", (c) => {
|
|
547
|
-
const
|
|
548
|
-
|
|
658
|
+
const tenantId = c.get("tenantId");
|
|
659
|
+
// Cap raised from 200: consumers strip probe traffic client-side, so the
|
|
660
|
+
// window has to hold a full day of RAW entries. A host emitting ~800
|
|
661
|
+
// probes/day filled all 200 rows with probes inside 7h, and the digest
|
|
662
|
+
// reported "0 queries yesterday" on a day with 27 real ones (2026-08-13).
|
|
663
|
+
const limit = parseLimit(c.req.query("limit"), 50, 5000);
|
|
664
|
+
const entries = recentQueryLog(tenantId, limit, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
|
|
549
665
|
return c.json({ entries });
|
|
550
666
|
});
|
|
551
667
|
// Citation events from the Stop hook. One POST per surfaced ID the
|
|
552
668
|
// assistant cited in its response. Feeds the recall precision metric.
|
|
553
669
|
app.post("/api/recall/cite-event", async (c) => {
|
|
670
|
+
const tenantId = c.get("tenantId");
|
|
554
671
|
let body;
|
|
555
672
|
try {
|
|
556
673
|
body = (await c.req.json());
|
|
@@ -568,7 +685,7 @@ function registerRecallRoutes(app, deps) {
|
|
|
568
685
|
}
|
|
569
686
|
const responsePreview = body["response_preview"];
|
|
570
687
|
const kind = body["kind"];
|
|
571
|
-
await appendCitation({
|
|
688
|
+
await appendCitation(tenantId, {
|
|
572
689
|
conversationId,
|
|
573
690
|
citedId,
|
|
574
691
|
...(kind === "tool_use" || kind === "prose" ? { kind } : {}),
|
|
@@ -579,12 +696,13 @@ function registerRecallRoutes(app, deps) {
|
|
|
579
696
|
return c.json({ ok: true });
|
|
580
697
|
});
|
|
581
698
|
app.get("/api/recall/cite-stats", async (c) => {
|
|
699
|
+
const tenantId = c.get("tenantId");
|
|
582
700
|
const daysStr = c.req.query("days") ?? "7";
|
|
583
701
|
const days = Number.parseInt(daysStr, 10);
|
|
584
702
|
if (!Number.isFinite(days) || days < 1 || days > 365) {
|
|
585
703
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
586
704
|
}
|
|
587
|
-
const stats = await citationStats(days, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
|
|
705
|
+
const stats = await citationStats(tenantId, days, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
|
|
588
706
|
return c.json(stats);
|
|
589
707
|
});
|
|
590
708
|
// Explicit citation from the cite_session MCP tool. One POST per session
|
|
@@ -592,6 +710,7 @@ function registerRecallRoutes(app, deps) {
|
|
|
592
710
|
// so the training extractor can distinguish deterministic tool citations
|
|
593
711
|
// from stop-hook detected prose citations.
|
|
594
712
|
app.post("/api/citation/explicit", async (c) => {
|
|
713
|
+
const tenantId = c.get("tenantId");
|
|
595
714
|
let body;
|
|
596
715
|
try {
|
|
597
716
|
body = (await c.req.json());
|
|
@@ -603,10 +722,10 @@ function registerRecallRoutes(app, deps) {
|
|
|
603
722
|
if (typeof id !== "string" || !id) {
|
|
604
723
|
return c.json({ error: "id required" }, 400);
|
|
605
724
|
}
|
|
606
|
-
await appendCitation({
|
|
725
|
+
await appendCitation(tenantId, {
|
|
607
726
|
conversationId: typeof body["conversation_id"] === "string"
|
|
608
727
|
? body["conversation_id"]
|
|
609
|
-
: (resolveConversationForSession(id) ?? "mcp_tool"),
|
|
728
|
+
: (resolveConversationForSession(tenantId, id) ?? "mcp_tool"),
|
|
610
729
|
citedId: id,
|
|
611
730
|
kind: "tool_use",
|
|
612
731
|
...(typeof body["reason"] === "string" ? { responsePreview: body["reason"] } : {}),
|
|
@@ -621,6 +740,7 @@ function registerHookRoutes(app) {
|
|
|
621
740
|
// suppressed by stale "already surfaced" gates.
|
|
622
741
|
// Payload: { conversation_id, transcript_path?, surfaced_set?, ts? }
|
|
623
742
|
app.post("/api/hook/pre-compact", async (c) => {
|
|
743
|
+
const tenantId = c.get("tenantId");
|
|
624
744
|
let body;
|
|
625
745
|
try {
|
|
626
746
|
body = (await c.req.json());
|
|
@@ -632,10 +752,18 @@ function registerHookRoutes(app) {
|
|
|
632
752
|
if (typeof conversationId !== "string" || !conversationId) {
|
|
633
753
|
return c.json({ error: "conversation_id required" }, 400);
|
|
634
754
|
}
|
|
635
|
-
const flushed = loadSurfaced(conversationId).size;
|
|
636
|
-
clearSurfaced(conversationId);
|
|
755
|
+
const flushed = loadSurfaced(tenantId, conversationId).size;
|
|
756
|
+
clearSurfaced(tenantId, conversationId);
|
|
637
757
|
const compactedAt = new Date().toISOString();
|
|
638
|
-
|
|
758
|
+
// Mirrors hook-log.ts's own path derivation (core/tenancy/tenant-state-path.ts):
|
|
759
|
+
// the default team's log honors NLM_HOOK_LOG; every other tenant is isolated
|
|
760
|
+
// under ~/.nlm/tenants/<tenantId>/hook-log.jsonl. This route writes a
|
|
761
|
+
// differently-shaped record (`kind: "pre-compact"`) than appendHookLog's
|
|
762
|
+
// HookLogEntry, so it can't reuse that function directly, but must still
|
|
763
|
+
// land in the same tenant-scoped file.
|
|
764
|
+
const logPath = tenantId === DEFAULT_TEAM_ID
|
|
765
|
+
? (process.env["NLM_HOOK_LOG"] ?? tenantStatePath(tenantId, "hook-log.jsonl"))
|
|
766
|
+
: tenantStatePath(tenantId, "hook-log.jsonl");
|
|
639
767
|
try {
|
|
640
768
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
641
769
|
appendFileSync(logPath, `${JSON.stringify({ ts: compactedAt, kind: "pre-compact", conversationId, flushed })}\n`, "utf8");
|
|
@@ -679,7 +807,8 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
679
807
|
return c.json({ context: null });
|
|
680
808
|
}
|
|
681
809
|
try {
|
|
682
|
-
const
|
|
810
|
+
const tenantId = c.get("tenantId");
|
|
811
|
+
const result = await deps.recall.search(tenantId, {
|
|
683
812
|
query: userMessage,
|
|
684
813
|
mode: "keyword",
|
|
685
814
|
limit: 5,
|
|
@@ -692,7 +821,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
692
821
|
startedAt: r.startedAt,
|
|
693
822
|
matchScore: r.matchScore,
|
|
694
823
|
}));
|
|
695
|
-
const surfaced = loadSurfaced(sessionId);
|
|
824
|
+
const surfaced = loadSurfaced(tenantId, sessionId);
|
|
696
825
|
const selected = selectHits({ hits, surfaced, scoreThreshold: 0, relativeFloor: HERMES_RELATIVE_FLOOR, perFireCap: 3, perConversationCap: 10 });
|
|
697
826
|
if (selected.length === 0 &&
|
|
698
827
|
(result.relatedFacts ?? []).length === 0 &&
|
|
@@ -700,7 +829,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
700
829
|
return c.json({ context: null });
|
|
701
830
|
}
|
|
702
831
|
if (selected.length > 0) {
|
|
703
|
-
recordSurfaced(sessionId, selected.map((h) => h.id));
|
|
832
|
+
recordSurfaced(tenantId, sessionId, selected.map((h) => h.id));
|
|
704
833
|
}
|
|
705
834
|
return c.json({
|
|
706
835
|
context: formatPointerBlock(selected, result.relatedFacts ?? [], (result.relatedExemplars ?? []).map((e) => ({
|
|
@@ -715,6 +844,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
715
844
|
// post-turn: scan assistant_response for session IDs that were surfaced in
|
|
716
845
|
// this conversation and log prose citation events.
|
|
717
846
|
app.post("/api/hook/hermes-agent/post-turn", async (c) => {
|
|
847
|
+
const tenantId = c.get("tenantId");
|
|
718
848
|
let body;
|
|
719
849
|
try {
|
|
720
850
|
body = (await c.req.json());
|
|
@@ -730,7 +860,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
730
860
|
if (typeof assistantResponse !== "string" || !assistantResponse) {
|
|
731
861
|
return c.json({ ok: true, cited: 0 });
|
|
732
862
|
}
|
|
733
|
-
const surfacedIds = [...loadSurfaced(sessionId)];
|
|
863
|
+
const surfacedIds = [...loadSurfaced(tenantId, sessionId)];
|
|
734
864
|
const cited = [];
|
|
735
865
|
for (const id of surfacedIds) {
|
|
736
866
|
if (assistantResponse.includes(id))
|
|
@@ -738,13 +868,14 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
738
868
|
}
|
|
739
869
|
const preview = assistantResponse.slice(0, 200);
|
|
740
870
|
for (const citedId of cited) {
|
|
741
|
-
await appendCitation({ conversationId: sessionId, citedId, kind: "prose", responsePreview: preview }, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
|
|
871
|
+
await appendCitation(tenantId, { conversationId: sessionId, citedId, kind: "prose", responsePreview: preview }, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
|
|
742
872
|
}
|
|
743
873
|
return c.json({ ok: true, cited: cited.length });
|
|
744
874
|
});
|
|
745
875
|
// session-lifecycle: memo housekeeping for on_session_{start,end,finalize,reset}.
|
|
746
876
|
// start is a no-op (memo is created lazily). end/finalize/reset clear the memo.
|
|
747
877
|
app.post("/api/hook/hermes-agent/session-lifecycle", async (c) => {
|
|
878
|
+
const tenantId = c.get("tenantId");
|
|
748
879
|
let body;
|
|
749
880
|
try {
|
|
750
881
|
body = (await c.req.json());
|
|
@@ -759,8 +890,8 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
759
890
|
if (event !== "start") {
|
|
760
891
|
const sessionId = body["session_id"];
|
|
761
892
|
if (typeof sessionId === "string" && sessionId) {
|
|
762
|
-
clearSurfaced(sessionId);
|
|
763
|
-
clearCited(sessionId);
|
|
893
|
+
clearSurfaced(tenantId, sessionId);
|
|
894
|
+
clearCited(tenantId, sessionId);
|
|
764
895
|
}
|
|
765
896
|
}
|
|
766
897
|
return c.json({ ok: true, event });
|
|
@@ -772,6 +903,7 @@ function registerFactRoutes(app, deps) {
|
|
|
772
903
|
if (!deps.factRecall) {
|
|
773
904
|
return c.json({ error: "fact recall not wired in this deployment" }, 503);
|
|
774
905
|
}
|
|
906
|
+
const tenantId = c.get("tenantId");
|
|
775
907
|
const q = c.req.query("q") ?? "";
|
|
776
908
|
const subject = c.req.query("subject");
|
|
777
909
|
const predicate = c.req.query("predicate");
|
|
@@ -807,10 +939,10 @@ function registerFactRoutes(app, deps) {
|
|
|
807
939
|
...(kind !== undefined ? { kind: kind } : {}),
|
|
808
940
|
...(minConfidence !== undefined ? { minConfidence } : {}),
|
|
809
941
|
};
|
|
810
|
-
const result = await deps.factRecall.search(query);
|
|
942
|
+
const result = await deps.factRecall.search(tenantId, query);
|
|
811
943
|
const source = c.req.header("x-recall-source") ?? "http";
|
|
812
944
|
const runtime = c.req.header("x-recall-runtime") ?? null;
|
|
813
|
-
void logFactQuery({
|
|
945
|
+
void logFactQuery(tenantId, {
|
|
814
946
|
source,
|
|
815
947
|
runtime,
|
|
816
948
|
query: q || null,
|
|
@@ -837,31 +969,40 @@ function registerFactRoutes(app, deps) {
|
|
|
837
969
|
return c.json({ error: "subject is required" }, 400);
|
|
838
970
|
}
|
|
839
971
|
const predicate = c.req.query("predicate");
|
|
840
|
-
const
|
|
972
|
+
const tenantId = c.get("tenantId");
|
|
973
|
+
const chains = await deps.factStore.getHistory(tenantId, subject, predicate);
|
|
841
974
|
return c.json({ subject, predicate: predicate ?? null, chains });
|
|
842
975
|
});
|
|
843
976
|
app.get("/api/recall/facts/stats", async (c) => {
|
|
977
|
+
const tenantId = c.get("tenantId");
|
|
844
978
|
const daysStr = c.req.query("days") ?? "7";
|
|
845
979
|
const days = Number.parseInt(daysStr, 10);
|
|
846
980
|
if (!Number.isFinite(days) || days < 1 || days > 365) {
|
|
847
981
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
848
982
|
}
|
|
849
|
-
const stats = await factRecallStats(days, ...(deps.factQueryLogPath !== undefined ? [deps.factQueryLogPath] : []));
|
|
983
|
+
const stats = await factRecallStats(tenantId, days, ...(deps.factQueryLogPath !== undefined ? [deps.factQueryLogPath] : []));
|
|
850
984
|
return c.json(stats);
|
|
851
985
|
});
|
|
852
986
|
}
|
|
853
987
|
function registerLiveRoutes(app, deps) {
|
|
854
|
-
|
|
988
|
+
// Pre-existing bug fixed in passing (C2): PgSessionStore's recentWrites/
|
|
989
|
+
// recentMarkers are async (return a Promise), but SqliteSessionStore's are
|
|
990
|
+
// synchronous — these handlers must be async + await the call so pg
|
|
991
|
+
// callers get the resolved array, not an unresolved Promise serialized as
|
|
992
|
+
// JSON.
|
|
993
|
+
app.get("/api/live/recent-writes", async (c) => {
|
|
855
994
|
if (!deps.liveStore)
|
|
856
995
|
return c.json({ writes: [] });
|
|
996
|
+
const tenantId = c.get("tenantId");
|
|
857
997
|
const limit = parseLimit(c.req.query("limit"), 50, 200);
|
|
858
|
-
return c.json({ writes: deps.liveStore.recentWrites(limit) });
|
|
998
|
+
return c.json({ writes: await deps.liveStore.recentWrites(tenantId, limit) });
|
|
859
999
|
});
|
|
860
|
-
app.get("/api/live/recent-markers", (c) => {
|
|
1000
|
+
app.get("/api/live/recent-markers", async (c) => {
|
|
861
1001
|
if (!deps.liveStore)
|
|
862
1002
|
return c.json({ markers: [] });
|
|
1003
|
+
const tenantId = c.get("tenantId");
|
|
863
1004
|
const limit = parseLimit(c.req.query("limit"), 50, 200);
|
|
864
|
-
return c.json({ markers: deps.liveStore.recentMarkers(limit) });
|
|
1005
|
+
return c.json({ markers: await deps.liveStore.recentMarkers(tenantId, limit) });
|
|
865
1006
|
});
|
|
866
1007
|
}
|
|
867
1008
|
function registerDatasetRoute(app, deps) {
|
|
@@ -869,7 +1010,7 @@ function registerDatasetRoute(app, deps) {
|
|
|
869
1010
|
if (!deps.dbPath)
|
|
870
1011
|
return c.json({ error: "dataset endpoint requires dbPath" }, 503);
|
|
871
1012
|
const includePaths = c.req.query("include_paths") === "true";
|
|
872
|
-
return c.json(buildDataset(deps.dbPath, { includePaths }));
|
|
1013
|
+
return c.json(buildDataset(deps.dbPath, DEFAULT_TEAM_ID, { includePaths }));
|
|
873
1014
|
});
|
|
874
1015
|
}
|
|
875
1016
|
// ── Data management ─────────────────────────────────────────────
|
|
@@ -980,9 +1121,10 @@ function registerActionRoutes(app, deps) {
|
|
|
980
1121
|
if (!parsed)
|
|
981
1122
|
return c.json({ error: "invalid action payload" }, 400);
|
|
982
1123
|
const store = deps.liveStore;
|
|
1124
|
+
const tenantId = c.get("tenantId");
|
|
983
1125
|
const id = store instanceof PgSessionStore
|
|
984
|
-
? await writeActionPg(store.pool, parsed)
|
|
985
|
-
: writeAction(store.rawDb(), parsed);
|
|
1126
|
+
? await writeActionPg(store.pool, tenantId, parsed)
|
|
1127
|
+
: writeAction(store.rawDb(), tenantId, parsed);
|
|
986
1128
|
store.invalidateOverlayCache();
|
|
987
1129
|
return c.json({ id, timestamp: new Date().toISOString() });
|
|
988
1130
|
});
|
|
@@ -998,9 +1140,10 @@ function registerActionRoutes(app, deps) {
|
|
|
998
1140
|
if (inputs.length === 0)
|
|
999
1141
|
return c.json({ accepted: 0, ids: [] });
|
|
1000
1142
|
const store = deps.liveStore;
|
|
1143
|
+
const tenantId = c.get("tenantId");
|
|
1001
1144
|
const ids = store instanceof PgSessionStore
|
|
1002
|
-
? await writeActionsBatchPg(store.pool, inputs)
|
|
1003
|
-
: writeActionsBatch(store.rawDb(), inputs);
|
|
1145
|
+
? await writeActionsBatchPg(store.pool, tenantId, inputs)
|
|
1146
|
+
: writeActionsBatch(store.rawDb(), tenantId, inputs);
|
|
1004
1147
|
store.invalidateOverlayCache();
|
|
1005
1148
|
return c.json({ accepted: ids.length, ids });
|
|
1006
1149
|
});
|
|
@@ -1008,9 +1151,10 @@ function registerActionRoutes(app, deps) {
|
|
|
1008
1151
|
if (!deps.liveStore)
|
|
1009
1152
|
return c.json({ error: "actions require liveStore" }, 503);
|
|
1010
1153
|
const store = deps.liveStore;
|
|
1154
|
+
const tenantId = c.get("tenantId");
|
|
1011
1155
|
const result = store instanceof PgSessionStore
|
|
1012
|
-
? await undoActionPg(store.pool, c.req.param("id"))
|
|
1013
|
-
: undoAction(store.rawDb(), c.req.param("id"));
|
|
1156
|
+
? await undoActionPg(store.pool, tenantId, c.req.param("id"))
|
|
1157
|
+
: undoAction(store.rawDb(), tenantId, c.req.param("id"));
|
|
1014
1158
|
if (!result)
|
|
1015
1159
|
return c.json({ error: "action not found or already undone" }, 404);
|
|
1016
1160
|
store.invalidateOverlayCache();
|
|
@@ -1029,9 +1173,10 @@ function registerActionRoutes(app, deps) {
|
|
|
1029
1173
|
...(kind ? { kind } : {}),
|
|
1030
1174
|
};
|
|
1031
1175
|
const store = deps.liveStore;
|
|
1176
|
+
const tenantId = c.get("tenantId");
|
|
1032
1177
|
const rows = store instanceof PgSessionStore
|
|
1033
|
-
? await listActionsPg(store.pool, opts)
|
|
1034
|
-
: listActions(store.rawDb(), opts);
|
|
1178
|
+
? await listActionsPg(store.pool, tenantId, opts)
|
|
1179
|
+
: listActions(store.rawDb(), tenantId, opts);
|
|
1035
1180
|
return c.json({ actions: rows });
|
|
1036
1181
|
});
|
|
1037
1182
|
}
|
|
@@ -1039,13 +1184,18 @@ function registerClassifierRoutes(app, deps) {
|
|
|
1039
1184
|
app.get("/api/classifier/info", (c) => {
|
|
1040
1185
|
const provider = deps.classifier?.provider ?? "ollama";
|
|
1041
1186
|
const model = deps.classifier?.model ?? "qwen3.5:4b";
|
|
1187
|
+
const openAiSwapAvailable = deps.classifier?.openAiSwapAvailable ?? false;
|
|
1042
1188
|
return c.json({
|
|
1043
1189
|
provider,
|
|
1044
1190
|
model,
|
|
1045
|
-
available_providers:
|
|
1191
|
+
available_providers: openAiSwapAvailable
|
|
1192
|
+
? ["deepseek", "ollama", "openai"]
|
|
1193
|
+
: ["deepseek", "ollama"],
|
|
1194
|
+
openai_swap_available: openAiSwapAvailable,
|
|
1046
1195
|
env_present: {
|
|
1047
1196
|
deepseek: Boolean(process.env["DEEPSEEK_API_KEY"]),
|
|
1048
1197
|
ollama: true,
|
|
1198
|
+
openai: openAiSwapAvailable,
|
|
1049
1199
|
},
|
|
1050
1200
|
default_models: {
|
|
1051
1201
|
deepseek: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat"],
|
|
@@ -1060,8 +1210,15 @@ function registerClassifierRoutes(app, deps) {
|
|
|
1060
1210
|
const body = (await c.req.json().catch(() => null));
|
|
1061
1211
|
const provider = body?.provider;
|
|
1062
1212
|
const model = body?.model;
|
|
1063
|
-
|
|
1064
|
-
|
|
1213
|
+
// "openai-compatible" (the Providers-registry kind for local/self-hosted
|
|
1214
|
+
// endpoints like LM Studio) and "openai" (the registry's real-OpenAI kind)
|
|
1215
|
+
// both ride ClassifierBox's single "openai" ClassifierProvider — both are
|
|
1216
|
+
// just DeepSeekClient against a baseUrl. That baseUrl is fixed at daemon
|
|
1217
|
+
// startup (NLM_CLASSIFIER_BASE_URL), so this swap is model selection
|
|
1218
|
+
// only, never endpoint selection.
|
|
1219
|
+
const isOpenAiKind = provider === "openai" || provider === "openai-compatible";
|
|
1220
|
+
if (provider !== "deepseek" && provider !== "ollama" && !isOpenAiKind) {
|
|
1221
|
+
return c.json({ error: "runtime swap supports 'deepseek', 'ollama', 'openai', or 'openai-compatible'" }, 400);
|
|
1065
1222
|
}
|
|
1066
1223
|
if (!model || typeof model !== "string" || model.length === 0) {
|
|
1067
1224
|
return c.json({ error: "model is required" }, 400);
|
|
@@ -1069,7 +1226,14 @@ function registerClassifierRoutes(app, deps) {
|
|
|
1069
1226
|
if (provider === "deepseek" && !process.env["DEEPSEEK_API_KEY"]) {
|
|
1070
1227
|
return c.json({ error: "DEEPSEEK_API_KEY not set — cannot swap to deepseek" }, 400);
|
|
1071
1228
|
}
|
|
1072
|
-
deps.classifier.
|
|
1229
|
+
if (isOpenAiKind && !deps.classifier.openAiSwapAvailable) {
|
|
1230
|
+
return c.json({
|
|
1231
|
+
error: "provider 'openai' has no baseUrl configured — set NLM_CLASSIFIER=openai and " +
|
|
1232
|
+
"NLM_CLASSIFIER_BASE_URL, then restart the daemon before swapping models on it",
|
|
1233
|
+
}, 400);
|
|
1234
|
+
}
|
|
1235
|
+
const classifierProvider = isOpenAiKind ? "openai" : provider;
|
|
1236
|
+
deps.classifier.swap(classifierProvider, model);
|
|
1073
1237
|
return c.json({ provider: deps.classifier.provider, model: deps.classifier.model });
|
|
1074
1238
|
});
|
|
1075
1239
|
}
|
|
@@ -1080,7 +1244,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1080
1244
|
app.get("/api/sources", async (c) => {
|
|
1081
1245
|
if (!deps.sources)
|
|
1082
1246
|
return c.json({ sources: [] });
|
|
1083
|
-
|
|
1247
|
+
const tenantId = c.get("tenantId");
|
|
1248
|
+
return c.json({ sources: await deps.sources.list(tenantId) });
|
|
1084
1249
|
});
|
|
1085
1250
|
app.post("/api/sources", async (c) => {
|
|
1086
1251
|
if (!deps.sources)
|
|
@@ -1089,10 +1254,11 @@ function registerSourceRoutes(app, deps) {
|
|
|
1089
1254
|
const parsed = parseSourceInsert(body);
|
|
1090
1255
|
if (!parsed)
|
|
1091
1256
|
return c.json({ error: "invalid source payload" }, 400);
|
|
1092
|
-
|
|
1257
|
+
const tenantId = c.get("tenantId");
|
|
1258
|
+
if (await deps.sources.getByName(tenantId, parsed.name)) {
|
|
1093
1259
|
return c.json({ error: `source named '${parsed.name}' already exists` }, 409);
|
|
1094
1260
|
}
|
|
1095
|
-
return c.json(await deps.sources.insert(parsed), 201);
|
|
1261
|
+
return c.json(await deps.sources.insert(tenantId, parsed), 201);
|
|
1096
1262
|
});
|
|
1097
1263
|
app.patch("/api/sources/:id", async (c) => {
|
|
1098
1264
|
if (!deps.sources)
|
|
@@ -1104,7 +1270,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1104
1270
|
const patch = parseSourceUpdate(body);
|
|
1105
1271
|
if (!patch)
|
|
1106
1272
|
return c.json({ error: "invalid patch payload" }, 400);
|
|
1107
|
-
const
|
|
1273
|
+
const tenantId = c.get("tenantId");
|
|
1274
|
+
const updated = await deps.sources.update(tenantId, id, patch);
|
|
1108
1275
|
if (!updated)
|
|
1109
1276
|
return c.json({ error: `source ${id} not found` }, 404);
|
|
1110
1277
|
return c.json(updated);
|
|
@@ -1115,7 +1282,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1115
1282
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1116
1283
|
if (!Number.isFinite(id))
|
|
1117
1284
|
return c.json({ error: "invalid id" }, 400);
|
|
1118
|
-
const
|
|
1285
|
+
const tenantId = c.get("tenantId");
|
|
1286
|
+
const ok = await deps.sources.delete(tenantId, id);
|
|
1119
1287
|
if (!ok)
|
|
1120
1288
|
return c.json({ error: `source ${id} not found` }, 404);
|
|
1121
1289
|
return c.json({ deleted: id });
|
|
@@ -1126,7 +1294,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1126
1294
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1127
1295
|
if (!Number.isFinite(id))
|
|
1128
1296
|
return c.json({ error: "invalid id" }, 400);
|
|
1129
|
-
const
|
|
1297
|
+
const tenantId = c.get("tenantId");
|
|
1298
|
+
const token = await deps.sources.regenerateToken(tenantId, id);
|
|
1130
1299
|
if (!token)
|
|
1131
1300
|
return c.json({ error: "regenerate-token only applies to webhook sources" }, 400);
|
|
1132
1301
|
return c.json({ token });
|
|
@@ -1167,7 +1336,11 @@ function registerIngestRoute(app, deps) {
|
|
|
1167
1336
|
sourceId: source.id,
|
|
1168
1337
|
};
|
|
1169
1338
|
const ingest = deps.ingest;
|
|
1170
|
-
|
|
1339
|
+
// M4 (spec §3/§8): the ingested session is stamped with the resolved
|
|
1340
|
+
// source row's own tenant_id, not the caller's general-gate tenantId —
|
|
1341
|
+
// ingest auth is a distinct credential (the source's webhook token)
|
|
1342
|
+
// from the team_tokens Bearer auth the rest of /api/* uses.
|
|
1343
|
+
void ingestSession(input, ingest, source.tenantId).catch((e) => {
|
|
1171
1344
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1172
1345
|
console.error(`[ingest] background failure for ${id}: ${msg}`);
|
|
1173
1346
|
});
|
|
@@ -1181,7 +1354,8 @@ function registerProviderRoutes(app, deps) {
|
|
|
1181
1354
|
app.get("/api/providers", async (c) => {
|
|
1182
1355
|
if (!deps.providers)
|
|
1183
1356
|
return c.json({ providers: [] });
|
|
1184
|
-
|
|
1357
|
+
const tenantId = c.get("tenantId");
|
|
1358
|
+
return c.json({ providers: await deps.providers.list(tenantId) });
|
|
1185
1359
|
});
|
|
1186
1360
|
app.post("/api/providers", async (c) => {
|
|
1187
1361
|
if (!deps.providers)
|
|
@@ -1190,10 +1364,11 @@ function registerProviderRoutes(app, deps) {
|
|
|
1190
1364
|
const parsed = parseProviderInsert(body);
|
|
1191
1365
|
if (!parsed)
|
|
1192
1366
|
return c.json({ error: "invalid provider payload" }, 400);
|
|
1193
|
-
|
|
1367
|
+
const tenantId = c.get("tenantId");
|
|
1368
|
+
if (await deps.providers.getByName(tenantId, parsed.name)) {
|
|
1194
1369
|
return c.json({ error: `provider named '${parsed.name}' already exists` }, 409);
|
|
1195
1370
|
}
|
|
1196
|
-
return c.json(await deps.providers.insert(parsed), 201);
|
|
1371
|
+
return c.json(await deps.providers.insert(tenantId, parsed), 201);
|
|
1197
1372
|
});
|
|
1198
1373
|
app.patch("/api/providers/:id", async (c) => {
|
|
1199
1374
|
if (!deps.providers)
|
|
@@ -1205,7 +1380,8 @@ function registerProviderRoutes(app, deps) {
|
|
|
1205
1380
|
const patch = parseProviderUpdate(body);
|
|
1206
1381
|
if (!patch)
|
|
1207
1382
|
return c.json({ error: "invalid patch payload" }, 400);
|
|
1208
|
-
const
|
|
1383
|
+
const tenantId = c.get("tenantId");
|
|
1384
|
+
const updated = await deps.providers.update(tenantId, id, patch);
|
|
1209
1385
|
if (!updated)
|
|
1210
1386
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1211
1387
|
return c.json(updated);
|
|
@@ -1216,7 +1392,8 @@ function registerProviderRoutes(app, deps) {
|
|
|
1216
1392
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1217
1393
|
if (!Number.isFinite(id))
|
|
1218
1394
|
return c.json({ error: "invalid id" }, 400);
|
|
1219
|
-
const
|
|
1395
|
+
const tenantId = c.get("tenantId");
|
|
1396
|
+
const ok = await deps.providers.delete(tenantId, id);
|
|
1220
1397
|
if (!ok)
|
|
1221
1398
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1222
1399
|
return c.json({ deleted: id });
|
|
@@ -1227,10 +1404,11 @@ function registerProviderRoutes(app, deps) {
|
|
|
1227
1404
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1228
1405
|
if (!Number.isFinite(id))
|
|
1229
1406
|
return c.json({ error: "invalid id" }, 400);
|
|
1230
|
-
const
|
|
1407
|
+
const tenantId = c.get("tenantId");
|
|
1408
|
+
const provider = await deps.providers.get(tenantId, id);
|
|
1231
1409
|
if (!provider)
|
|
1232
1410
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1233
|
-
const key = await deps.providers.getSecret(id);
|
|
1411
|
+
const key = await deps.providers.getSecret(tenantId, id);
|
|
1234
1412
|
try {
|
|
1235
1413
|
const models = await listModels(provider, { apiKey: key });
|
|
1236
1414
|
return c.json({ models });
|
|
@@ -1246,10 +1424,11 @@ function registerProviderRoutes(app, deps) {
|
|
|
1246
1424
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1247
1425
|
if (!Number.isFinite(id))
|
|
1248
1426
|
return c.json({ error: "invalid id" }, 400);
|
|
1249
|
-
const
|
|
1427
|
+
const tenantId = c.get("tenantId");
|
|
1428
|
+
const provider = await deps.providers.get(tenantId, id);
|
|
1250
1429
|
if (!provider)
|
|
1251
1430
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1252
|
-
const key = await deps.providers.getSecret(id);
|
|
1431
|
+
const key = await deps.providers.getSecret(tenantId, id);
|
|
1253
1432
|
const startedAt = Date.now();
|
|
1254
1433
|
try {
|
|
1255
1434
|
const models = await listModels(provider, { apiKey: key });
|
|
@@ -1268,17 +1447,44 @@ function registerProviderRoutes(app, deps) {
|
|
|
1268
1447
|
function registerSessionRoute(app, deps) {
|
|
1269
1448
|
app.get("/api/session/:id", async (c) => {
|
|
1270
1449
|
const id = c.req.param("id");
|
|
1271
|
-
const
|
|
1450
|
+
const tenantId = c.get("tenantId");
|
|
1451
|
+
const session = await deps.store.getById(tenantId, id);
|
|
1272
1452
|
if (!session) {
|
|
1273
1453
|
return c.json({ error: `session ${id} not found` }, 404);
|
|
1274
1454
|
}
|
|
1275
1455
|
return c.json(session);
|
|
1276
1456
|
});
|
|
1457
|
+
// Learner strip data for the session drawer (spec 2026-10-06 M5): concept
|
|
1458
|
+
// rows for enrollments bound to this session's workstream, reusing the
|
|
1459
|
+
// same picker the recall hook uses. Always 200 with possibly-empty arrays
|
|
1460
|
+
// once the session resolves — the UI hides the section when empty.
|
|
1461
|
+
app.get("/api/learner/by-session/:id", async (c) => {
|
|
1462
|
+
const tenantId = c.get("tenantId");
|
|
1463
|
+
if (!deps.learnerRecall) {
|
|
1464
|
+
return c.json({ error: "learner recall not wired" }, 404);
|
|
1465
|
+
}
|
|
1466
|
+
const id = c.req.param("id");
|
|
1467
|
+
const session = await deps.store.getById(tenantId, id);
|
|
1468
|
+
if (!session) {
|
|
1469
|
+
return c.json({ error: `session ${id} not found` }, 404);
|
|
1470
|
+
}
|
|
1471
|
+
const concepts = await pickRelatedConcepts(tenantId, [{ id }], deps.learnerRecall);
|
|
1472
|
+
const wsMap = await deps.learnerRecall.sessions.getWorkstreamIds(tenantId, [id]);
|
|
1473
|
+
const wsId = wsMap.get(id);
|
|
1474
|
+
const enrollments = wsId
|
|
1475
|
+
? await deps.learnerRecall.learners.listEnrollmentsByWorkstreams(tenantId, [wsId])
|
|
1476
|
+
: [];
|
|
1477
|
+
return c.json({
|
|
1478
|
+
enrollments: enrollments.map((e) => ({ learnerId: e.learnerId, courseId: e.courseId, status: e.status })),
|
|
1479
|
+
concepts,
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
1277
1482
|
// Post-hoc supersedence write path. Mirrors the `mark_superseded` MCP tool
|
|
1278
1483
|
// so UI + CLI + MCP all share one backend gesture. Path param is the
|
|
1279
1484
|
// predecessor (the session being retired); body names the successor.
|
|
1280
1485
|
app.post("/api/session/:id/supersede", async (c) => {
|
|
1281
1486
|
const predecessorId = c.req.param("id");
|
|
1487
|
+
const tenantId = c.get("tenantId");
|
|
1282
1488
|
let body;
|
|
1283
1489
|
try {
|
|
1284
1490
|
body = await c.req.json();
|
|
@@ -1299,7 +1505,7 @@ function registerSessionRoute(app, deps) {
|
|
|
1299
1505
|
return c.json({ error: "reason must be a string if provided" }, 400);
|
|
1300
1506
|
}
|
|
1301
1507
|
try {
|
|
1302
|
-
await deps.store.markSuperseded(predecessorId, successorId);
|
|
1508
|
+
await deps.store.markSuperseded(tenantId, predecessorId, successorId);
|
|
1303
1509
|
}
|
|
1304
1510
|
catch (e) {
|
|
1305
1511
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1308,7 +1514,7 @@ function registerSessionRoute(app, deps) {
|
|
|
1308
1514
|
: 500;
|
|
1309
1515
|
return c.json({ error: msg }, status);
|
|
1310
1516
|
}
|
|
1311
|
-
void appendSupersedence({
|
|
1517
|
+
void appendSupersedence(tenantId, {
|
|
1312
1518
|
predecessorId,
|
|
1313
1519
|
successorId,
|
|
1314
1520
|
source: c.req.header("x-supersedence-source") ?? "http",
|
|
@@ -1343,7 +1549,8 @@ function parseActionInput(raw) {
|
|
|
1343
1549
|
};
|
|
1344
1550
|
}
|
|
1345
1551
|
const VALID_SOURCE_KINDS = [
|
|
1346
|
-
"claude-code", "codex", "hermes", "
|
|
1552
|
+
"claude-code", "codex", "hermes", "hermes-agent", "muse", "aider", "cursor",
|
|
1553
|
+
"windsurf", "opencode", "pi", "jsonl-generic", "webhook",
|
|
1347
1554
|
];
|
|
1348
1555
|
function parseSourceInsert(raw) {
|
|
1349
1556
|
if (!raw || typeof raw !== "object")
|
|
@@ -1479,12 +1686,13 @@ function registerSignalRoutes(app, deps) {
|
|
|
1479
1686
|
return c.json({ error: e instanceof Error ? e.message : "invalid signal" }, 400);
|
|
1480
1687
|
}
|
|
1481
1688
|
const rawBody = body;
|
|
1482
|
-
|
|
1689
|
+
const tenantId = c.get("tenantId");
|
|
1690
|
+
signal = await stampSignalScope(signal, tenantId, {
|
|
1483
1691
|
repoPath: typeof rawBody["repo_path"] === "string" ? rawBody["repo_path"] : null,
|
|
1484
1692
|
sessionScopeReader: deps.sessionScopeReader,
|
|
1485
1693
|
});
|
|
1486
1694
|
try {
|
|
1487
|
-
await deps.signalStore.insert(signal);
|
|
1695
|
+
await deps.signalStore.insert(tenantId, signal);
|
|
1488
1696
|
}
|
|
1489
1697
|
catch {
|
|
1490
1698
|
return c.json({ error: "signal insert failed" }, 500);
|
|
@@ -1500,11 +1708,11 @@ function registerSignalRoutes(app, deps) {
|
|
|
1500
1708
|
try {
|
|
1501
1709
|
const exemplar = extractExemplar(signal, { installScope: deps.installScope });
|
|
1502
1710
|
if (exemplar) {
|
|
1503
|
-
const { id, skipped } = await exemplarStore.insert(exemplar);
|
|
1711
|
+
const { id, skipped } = await exemplarStore.insert(tenantId, exemplar);
|
|
1504
1712
|
if (!skipped && deps.codeEmbedder) {
|
|
1505
1713
|
void deps.codeEmbedder
|
|
1506
1714
|
.embed(composeEmbedText(exemplar.taskContext, exemplar.code), "document")
|
|
1507
|
-
.then((r) => exemplarStore.upsertEmbedding(id, r.vector))
|
|
1715
|
+
.then((r) => exemplarStore.upsertEmbedding(tenantId, id, r.vector))
|
|
1508
1716
|
.catch(() => { });
|
|
1509
1717
|
}
|
|
1510
1718
|
}
|
|
@@ -1521,7 +1729,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1521
1729
|
if (!repo)
|
|
1522
1730
|
return c.json({ error: "repo is required" }, 400);
|
|
1523
1731
|
const model = c.req.query("model");
|
|
1524
|
-
const
|
|
1732
|
+
const tenantId = c.get("tenantId");
|
|
1733
|
+
const block = await buildFailureModeBlock(tenantId, deps.signalStore, { installScope: deps.installScope, repo, ...(model ? { model } : {}) });
|
|
1525
1734
|
return c.json({ repo, model: model ?? null, block });
|
|
1526
1735
|
});
|
|
1527
1736
|
app.get("/api/signals/stats", async (c) => {
|
|
@@ -1534,7 +1743,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1534
1743
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
1535
1744
|
}
|
|
1536
1745
|
const sinceTs = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
1537
|
-
const
|
|
1746
|
+
const tenantId = c.get("tenantId");
|
|
1747
|
+
const rows = await deps.signalStore.listForAggregation(tenantId, { installScope: deps.installScope, sinceTs });
|
|
1538
1748
|
const modes = aggregateFailureModes(rows);
|
|
1539
1749
|
return c.json({ days, total: rows.length, modes });
|
|
1540
1750
|
});
|
|
@@ -1564,11 +1774,12 @@ function registerSignalRoutes(app, deps) {
|
|
|
1564
1774
|
return c.json({ error: e instanceof Error ? e.message : "invalid exemplar" }, 400);
|
|
1565
1775
|
}
|
|
1566
1776
|
inp = { ...inp, scope: scopeStampEnabled() ? inp.scope : null };
|
|
1567
|
-
const
|
|
1777
|
+
const tenantId = c.get("tenantId");
|
|
1778
|
+
const { id, skipped } = await deps.exemplarStore.insert(tenantId, inp);
|
|
1568
1779
|
if (!skipped && deps.codeEmbedder) {
|
|
1569
1780
|
// Best-effort embedding: fire-and-forget, never blocks the response.
|
|
1570
1781
|
deps.codeEmbedder.embed(composeEmbedText(inp.taskContext, inp.code), "document")
|
|
1571
|
-
.then((r) => deps.exemplarStore.upsertEmbedding(id, r.vector))
|
|
1782
|
+
.then((r) => deps.exemplarStore.upsertEmbedding(tenantId, id, r.vector))
|
|
1572
1783
|
.catch(() => { });
|
|
1573
1784
|
}
|
|
1574
1785
|
return c.json({ id, skipped, status: "accepted" }, 202);
|
|
@@ -1596,7 +1807,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1596
1807
|
if (patch.retired === undefined && patch.outcome === undefined) {
|
|
1597
1808
|
return c.json({ error: "provide retire (boolean) and/or outcome" }, 400);
|
|
1598
1809
|
}
|
|
1599
|
-
const
|
|
1810
|
+
const tenantId = c.get("tenantId");
|
|
1811
|
+
const result = await deps.exemplarStore.setVerdict(tenantId, c.req.param("id"), patch, "human");
|
|
1600
1812
|
if (result.status === "not_found")
|
|
1601
1813
|
return c.json({ error: "exemplar not found" }, 404);
|
|
1602
1814
|
return c.json({ id: c.req.param("id"), status: result.status }, 200);
|
|
@@ -1617,7 +1829,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1617
1829
|
const lang = c.req.query("lang");
|
|
1618
1830
|
const model = c.req.query("model");
|
|
1619
1831
|
const includeNegatives = c.req.query("negatives") !== "0";
|
|
1620
|
-
const
|
|
1832
|
+
const tenantId = c.get("tenantId");
|
|
1833
|
+
const result = await recallCode(tenantId, {
|
|
1621
1834
|
query,
|
|
1622
1835
|
installScope: deps.installScope,
|
|
1623
1836
|
...(repo ? { repo } : {}),
|
|
@@ -1629,6 +1842,52 @@ function registerSignalRoutes(app, deps) {
|
|
|
1629
1842
|
return c.json(result);
|
|
1630
1843
|
});
|
|
1631
1844
|
}
|
|
1845
|
+
// ── Reprocess job supervision (single operator process, LOCAL disposition) ──
|
|
1846
|
+
// Thin HTTP wrapping over JobSupervisor (core/jobs/job-supervisor.ts).
|
|
1847
|
+
// start() is synchronous and throws BY DESIGN both when a run is already
|
|
1848
|
+
// active and when the injected spawnChild adapter itself fails to launch
|
|
1849
|
+
// the child — this route is the required catch point so neither case ever
|
|
1850
|
+
// crashes the daemon (binding note from Task 1's review).
|
|
1851
|
+
function registerJobRoutes(app, deps) {
|
|
1852
|
+
app.post("/api/jobs/reprocess", async (c) => {
|
|
1853
|
+
if (!deps.jobSupervisor)
|
|
1854
|
+
return c.json({ error: "job supervisor not wired in this deployment" }, 503);
|
|
1855
|
+
let body = null;
|
|
1856
|
+
try {
|
|
1857
|
+
const text = await c.req.text();
|
|
1858
|
+
body = text ? JSON.parse(text) : null;
|
|
1859
|
+
}
|
|
1860
|
+
catch {
|
|
1861
|
+
return c.json({ error: "body must be JSON" }, 400);
|
|
1862
|
+
}
|
|
1863
|
+
const rawArgs = body?.args;
|
|
1864
|
+
if (rawArgs !== undefined && (!Array.isArray(rawArgs) || !rawArgs.every((a) => typeof a === "string"))) {
|
|
1865
|
+
return c.json({ error: "args must be an array of strings" }, 400);
|
|
1866
|
+
}
|
|
1867
|
+
// Passed straight through as argv entries by the real spawnChild
|
|
1868
|
+
// adapter (never through a shell) — no interpolation risk here either
|
|
1869
|
+
// way, but validating shape (not content) keeps this a passthrough,
|
|
1870
|
+
// exactly as the reprocess CLI itself validates its own flags.
|
|
1871
|
+
const args = rawArgs ?? [];
|
|
1872
|
+
try {
|
|
1873
|
+
deps.jobSupervisor.start(args);
|
|
1874
|
+
}
|
|
1875
|
+
catch (e) {
|
|
1876
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1877
|
+
if (message === "reprocess job already active") {
|
|
1878
|
+
return c.json({ error: message, snapshot: deps.jobSupervisor.snapshot() }, 409);
|
|
1879
|
+
}
|
|
1880
|
+
return c.json({ error: message }, 500);
|
|
1881
|
+
}
|
|
1882
|
+
return c.json({ ok: true, snapshot: deps.jobSupervisor.snapshot() }, 202);
|
|
1883
|
+
});
|
|
1884
|
+
app.delete("/api/jobs/reprocess", (c) => {
|
|
1885
|
+
if (!deps.jobSupervisor)
|
|
1886
|
+
return c.json({ error: "job supervisor not wired in this deployment" }, 503);
|
|
1887
|
+
deps.jobSupervisor.stop();
|
|
1888
|
+
return c.json({ ok: true, snapshot: deps.jobSupervisor.snapshot() }, 200);
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1632
1891
|
function mountSpa(app, dist) {
|
|
1633
1892
|
const indexHtml = join(dist, "index.html");
|
|
1634
1893
|
if (!existsSync(indexHtml))
|