nlm-memory 0.20.1 → 0.21.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -12
- package/assets/replay-eval-dark.svg +26 -0
- package/assets/replay-eval-light.svg +26 -0
- package/dist/cli/digest.js +69 -4
- package/dist/cli/digest.js.map +1 -1
- package/dist/cli/nlm.d.ts +14 -0
- package/dist/cli/nlm.js +556 -71
- 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-backfill.d.ts +34 -0
- package/dist/cli/scope-backfill.js +237 -0
- package/dist/cli/scope-backfill.js.map +1 -0
- package/dist/cli/scope-coverage.d.ts +44 -0
- package/dist/cli/scope-coverage.js +208 -0
- package/dist/cli/scope-coverage.js.map +1 -0
- 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/claude-code.d.ts +14 -0
- package/dist/core/adapters/claude-code.js +30 -0
- package/dist/core/adapters/claude-code.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 +46 -13
- package/dist/core/dataset/build-dataset.js.map +1 -1
- package/dist/core/digest/compose.d.ts +11 -3
- package/dist/core/digest/compose.js +70 -17
- package/dist/core/digest/compose.js.map +1 -1
- package/dist/core/digest/hook-injection.d.ts +43 -0
- package/dist/core/digest/hook-injection.js +87 -0
- package/dist/core/digest/hook-injection.js.map +1 -0
- 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 +2 -1
- package/dist/core/exemplars/capture-from-session.js +4 -4
- 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/extract-exemplar.d.ts +2 -0
- package/dist/core/exemplars/extract-exemplar.js +7 -1
- package/dist/core/exemplars/extract-exemplar.js.map +1 -1
- package/dist/core/exemplars/ingest-exemplar.d.ts +1 -0
- package/dist/core/exemplars/ingest-exemplar.js +1 -0
- package/dist/core/exemplars/ingest-exemplar.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/claude-settings.d.ts +1 -1
- package/dist/core/hook/claude-settings.js +0 -1
- package/dist/core/hook/claude-settings.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/backfill-derivables.d.ts +92 -0
- package/dist/core/ingest/backfill-derivables.js +175 -0
- package/dist/core/ingest/backfill-derivables.js.map +1 -0
- package/dist/core/ingest/ingest-session.d.ts +1 -1
- package/dist/core/ingest/ingest-session.js +23 -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 +8 -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 +7 -2
- package/dist/core/ingest/reprocess.js.map +1 -1
- package/dist/core/ingest/transcript-derivables.d.ts +27 -0
- package/dist/core/ingest/transcript-derivables.js +138 -0
- package/dist/core/ingest/transcript-derivables.js.map +1 -0
- 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 +33 -0
- package/dist/core/outcome/coverage.js +46 -0
- package/dist/core/outcome/coverage.js.map +1 -0
- package/dist/core/outcome/rollup.d.ts +33 -0
- package/dist/core/outcome/rollup.js +113 -0
- package/dist/core/outcome/rollup.js.map +1 -0
- 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 +7 -4
- package/dist/core/recall/hook-recall-log.js +14 -8
- 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 +13 -12
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scan-once.js +15 -12
- 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 +45 -10
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/scope/alias-map.d.ts +10 -0
- package/dist/core/scope/alias-map.js +41 -0
- package/dist/core/scope/alias-map.js.map +1 -0
- package/dist/core/scope/derive-scope.d.ts +8 -0
- package/dist/core/scope/derive-scope.js +54 -0
- package/dist/core/scope/derive-scope.js.map +1 -0
- package/dist/core/scope/scope-clause.d.ts +16 -0
- package/dist/core/scope/scope-clause.js +21 -0
- package/dist/core/scope/scope-clause.js.map +1 -0
- package/dist/core/scope/stamp-flag.d.ts +1 -0
- package/dist/core/scope/stamp-flag.js +4 -0
- package/dist/core/scope/stamp-flag.js.map +1 -0
- package/dist/core/signals/code-signal.d.ts +1 -0
- package/dist/core/signals/code-signal.js +1 -0
- package/dist/core/signals/code-signal.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/ingest-signal.d.ts +1 -1
- package/dist/core/signals/ingest-signal.js +3 -1
- package/dist/core/signals/ingest-signal.js.map +1 -1
- package/dist/core/signals/stamp-scope.d.ts +18 -0
- package/dist/core/signals/stamp-scope.js +28 -0
- package/dist/core/signals/stamp-scope.js.map +1 -0
- 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 +62 -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 +25 -21
- package/dist/core/storage/pg-session-store.js +186 -114
- 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 +26 -13
- 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 +17 -11
- package/dist/core/storage/pg-workstream-store.js +47 -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 +86 -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 +175 -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 +104 -0
- package/dist/core/storage/sqlite-outcome-store.js +211 -0
- package/dist/core/storage/sqlite-outcome-store.js.map +1 -0
- package/dist/core/storage/sqlite-session-store.d.ts +50 -21
- package/dist/core/storage/sqlite-session-store.js +233 -149
- 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 +29 -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 +17 -11
- package/dist/core/storage/sqlite-workstream-store.js +53 -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 +21 -14
- package/dist/core/storage/supersedence-log.js.map +1 -1
- package/dist/core/telemetry/probe-filter.d.ts +35 -0
- package/dist/core/telemetry/probe-filter.js +56 -0
- package/dist/core/telemetry/probe-filter.js.map +1 -0
- 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/update-check/check.d.ts +6 -0
- package/dist/core/update-check/check.js +35 -0
- package/dist/core/update-check/check.js.map +1 -1
- 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 +3 -2
- package/dist/core/workstream/bind.js +29 -10
- package/dist/core/workstream/bind.js.map +1 -1
- package/dist/core/workstream/model.d.ts +1 -0
- package/dist/core/workstream/model.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 +8 -5
- package/dist/hook/pre-compact-hook.js.map +1 -1
- package/dist/hook/prompt-recall-hook.d.ts +7 -3
- package/dist/hook/prompt-recall-hook.js +35 -20
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/recall-over-http.d.ts +28 -3
- package/dist/hook/recall-over-http.js +58 -12
- 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 +57 -16
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +11 -8
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/http/app.d.ts +47 -3
- package/dist/http/app.js +379 -132
- 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 +94 -15
- package/dist/mcp/server.js +402 -79
- 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 +85 -0
- package/dist/ports/outcome.js +11 -0
- package/dist/ports/outcome.js.map +1 -0
- package/dist/ports/session-store.d.ts +20 -20
- 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 +20 -12
- package/dist/shared/net.d.ts +3 -0
- package/dist/shared/net.js +4 -0
- package/dist/shared/net.js.map +1 -0
- package/dist/shared/types.d.ts +55 -0
- package/dist/shared/types.js +4 -1
- 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/029_project_scope.sql +21 -0
- package/migrations/032_session_derivables.sql +11 -0
- 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/030_project_scope.sql +23 -0
- package/migrations/pg/031_widen_session_edges_kind.sql +11 -0
- package/migrations/pg/032_session_derivables.sql +17 -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 +258 -59
- package/package.json +10 -5
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/prompt-recall-hook.mjs +256 -58
- package/plugin/scripts/session-start-hook.mjs +283 -55
- package/plugin/scripts/stop-hook.mjs +199 -38
- package/dist/hook/subagent-start-hook.d.ts +0 -28
- package/dist/hook/subagent-start-hook.js +0 -81
- package/dist/hook/subagent-start-hook.js.map +0 -1
- package/dist/ui/assets/index-CMpQkbQy.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" };
|
|
@@ -30,7 +29,7 @@ import { logQuery, recallStats } from "../core/recall/query-log.js";
|
|
|
30
29
|
import { recentQueryLog } from "../core/recall/recent-log.js";
|
|
31
30
|
import { appendCitation, citationStats } from "../core/recall/citation-log.js";
|
|
32
31
|
import { appendSupersedence } from "../core/storage/supersedence-log.js";
|
|
33
|
-
import { getUpdateStatus } from "../core/update-check/check.js";
|
|
32
|
+
import { getCachedUpdateStatus, getUpdateStatus } from "../core/update-check/check.js";
|
|
34
33
|
import { buildClearCookie, buildSessionCookie, deriveSessionValue, parseCookies, sanitizeNextPath, SESSION_COOKIE_NAME, verifySessionCookie, } from "./ui-auth.js";
|
|
35
34
|
import { createNonceStore } from "./auth-nonce.js";
|
|
36
35
|
import { clearSurfaced, loadSurfaced, recordSurfaced, resolveConversationForSession } from "../core/hook/memo.js";
|
|
@@ -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";
|
|
@@ -48,6 +48,8 @@ import { ingestSession, deriveSessionId } from "../core/ingest/ingest-session.js
|
|
|
48
48
|
import { listActions, listActionsPg, undoAction, undoActionPg, writeAction, writeActionPg, writeActionsBatch, writeActionsBatchPg, } from "../core/actions/actions-log.js";
|
|
49
49
|
import { PgSessionStore } from "../core/storage/pg-session-store.js";
|
|
50
50
|
import { normalizeSignal } from "../core/signals/ingest-signal.js";
|
|
51
|
+
import { scopeStampEnabled } from "../core/scope/stamp-flag.js";
|
|
52
|
+
import { stampSignalScope } from "../core/signals/stamp-scope.js";
|
|
51
53
|
import { normalizeExemplar } from "../core/exemplars/ingest-exemplar.js";
|
|
52
54
|
import { extractExemplar } from "../core/exemplars/extract-exemplar.js";
|
|
53
55
|
import { recallCode } from "../core/exemplars/recall-code.js";
|
|
@@ -59,6 +61,11 @@ import { warmupSnapshot } from "../core/health/warmup-state.js";
|
|
|
59
61
|
import { laneHealthSnapshot } from "../core/health/embedding-lane-state.js";
|
|
60
62
|
import { inflightSnapshot } from "../core/health/embed-inflight.js";
|
|
61
63
|
import { corpusSnapshot } from "../core/health/corpus-state.js";
|
|
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";
|
|
62
69
|
const HERMES_RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
|
|
63
70
|
const MIME_TYPES = {
|
|
64
71
|
".html": "text/html; charset=utf-8",
|
|
@@ -84,6 +91,16 @@ const DATA_STAT_TABLES = [
|
|
|
84
91
|
"sources",
|
|
85
92
|
"providers",
|
|
86
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
|
+
*/
|
|
87
104
|
function sqliteDataStats(db, dbPath) {
|
|
88
105
|
const countOf = (table) => {
|
|
89
106
|
try {
|
|
@@ -215,9 +232,10 @@ const VALID_KINDS = ["decision", "open"];
|
|
|
215
232
|
const VALID_FACT_KINDS = ["decision", "open", "attribute"];
|
|
216
233
|
export function createApp(deps) {
|
|
217
234
|
const app = new Hono();
|
|
218
|
-
const boundPort = process.env["NLM_PORT"] ? Number.parseInt(process.env["NLM_PORT"], 10) :
|
|
219
|
-
installLocalOnlyMiddleware(app, boundPort);
|
|
220
|
-
|
|
235
|
+
const boundPort = process.env["NLM_PORT"] ? Number.parseInt(process.env["NLM_PORT"], 10) : Number.parseInt(DEFAULT_NLM_PORT, 10);
|
|
236
|
+
installLocalOnlyMiddleware(app, boundPort, deps);
|
|
237
|
+
installHostedModeGate(app);
|
|
238
|
+
registerHealthRoute(app, deps);
|
|
221
239
|
registerMcpRoute(app, deps);
|
|
222
240
|
registerRecallRoutes(app, deps);
|
|
223
241
|
registerHookRoutes(app);
|
|
@@ -233,6 +251,7 @@ export function createApp(deps) {
|
|
|
233
251
|
registerProviderRoutes(app, deps);
|
|
234
252
|
registerSessionRoute(app, deps);
|
|
235
253
|
registerSignalRoutes(app, deps);
|
|
254
|
+
registerJobRoutes(app, deps);
|
|
236
255
|
const nonceStore = createNonceStore();
|
|
237
256
|
registerNonceRoute(app, nonceStore);
|
|
238
257
|
if (deps.uiDist) {
|
|
@@ -267,25 +286,68 @@ function registerNonceRoute(app, nonceStore) {
|
|
|
267
286
|
function uiAuthMode() {
|
|
268
287
|
return process.env["NLM_UI_AUTH"] === "cookie" ? "cookie" : "none";
|
|
269
288
|
}
|
|
270
|
-
|
|
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) {
|
|
271
314
|
const skipLocalGate = !!process.env["VITEST"] || process.env["NODE_ENV"] === "test";
|
|
272
315
|
const extras = parseAllowedOrigins(process.env["NLM_ALLOWED_ORIGINS"] ?? "");
|
|
273
316
|
app.use("/api/*", async (c, next) => {
|
|
274
|
-
if (skipLocalGate)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
}
|
|
279
322
|
}
|
|
280
323
|
if (c.req.path === "/api/health") {
|
|
281
324
|
return next();
|
|
282
325
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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();
|
|
286
341
|
}
|
|
287
342
|
if (uiAuthMode() === "none") {
|
|
288
|
-
// 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);
|
|
289
351
|
return next();
|
|
290
352
|
}
|
|
291
353
|
const token = process.env["NLM_MCP_TOKEN"];
|
|
@@ -296,26 +358,55 @@ function installLocalOnlyMiddleware(app, boundPort) {
|
|
|
296
358
|
}
|
|
297
359
|
// UI session cookie (HMAC of the token, set by /ui/auth bootstrap).
|
|
298
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.
|
|
299
363
|
const cookies = parseCookies(c.req.header("cookie"));
|
|
300
364
|
if (verifySessionCookie(cookies[SESSION_COOKIE_NAME], token)) {
|
|
301
365
|
// Rolling expiry: every authenticated hit re-issues Set-Cookie so an
|
|
302
366
|
// actively-used session never sees an expiry. Only 30 days of true
|
|
303
367
|
// inactivity force a re-bootstrap.
|
|
304
368
|
c.header("Set-Cookie", buildSessionCookie(deriveSessionValue(token)));
|
|
369
|
+
c.set("tenantId", DEFAULT_TEAM_ID);
|
|
305
370
|
return next();
|
|
306
371
|
}
|
|
307
372
|
// Bearer: programmatic clients (Hermes WebUI, agents, the MCP path).
|
|
308
|
-
//
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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);
|
|
314
379
|
return next();
|
|
315
380
|
}
|
|
316
381
|
return c.json({ error: "unauthorized" }, 401);
|
|
317
382
|
});
|
|
318
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
|
+
}
|
|
319
410
|
// ── UI session-cookie gate ───────────────────────────────────────────
|
|
320
411
|
//
|
|
321
412
|
// Closes the port-forward bypass: any client reaching localhost:3940
|
|
@@ -396,13 +487,30 @@ function renderAuthPage() {
|
|
|
396
487
|
<p class="hint">Sessions roll forward on every visit, so this page only appears after ~30 days of inactivity. To turn auth off entirely, run <code>nlm config ui-auth off</code>.</p>
|
|
397
488
|
</main></body></html>`;
|
|
398
489
|
}
|
|
399
|
-
function registerHealthRoute(app) {
|
|
400
|
-
app.get("/api/health", (c) => {
|
|
490
|
+
function registerHealthRoute(app, deps) {
|
|
491
|
+
app.get("/api/health", async (c) => {
|
|
401
492
|
const snap = corpusSnapshot();
|
|
402
493
|
const corpus = snap
|
|
403
494
|
? { state: snap.state, dbBytes: snap.dbBytes, sessions: snap.sessions, entities: snap.entities, lastComputedAt: snap.lastComputedAt }
|
|
404
495
|
: null;
|
|
405
|
-
|
|
496
|
+
// Cache-only read (see check.ts) — never fetches the registry, so this
|
|
497
|
+
// stays fast and never fails the health check when npm is unreachable.
|
|
498
|
+
const cachedUpdate = await getCachedUpdateStatus({
|
|
499
|
+
currentVersion: pkg.version,
|
|
500
|
+
...deps.updateCheckDeps,
|
|
501
|
+
});
|
|
502
|
+
const update = {
|
|
503
|
+
current: cachedUpdate.current,
|
|
504
|
+
latest: cachedUpdate.disabled ? null : cachedUpdate.latest,
|
|
505
|
+
behind: cachedUpdate.disabled ? false : cachedUpdate.behind,
|
|
506
|
+
};
|
|
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 });
|
|
406
514
|
});
|
|
407
515
|
// Passive update poll for the UI. Same daily-cached check the CLI
|
|
408
516
|
// startup banner uses — see src/core/update-check/check.ts for the
|
|
@@ -419,30 +527,33 @@ function registerHealthRoute(app) {
|
|
|
419
527
|
function registerMcpRoute(app, deps) {
|
|
420
528
|
if (!deps.mcpDeps)
|
|
421
529
|
return;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
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 — " +
|
|
425
532
|
"refusing to mount an unauthenticated /mcp endpoint");
|
|
426
533
|
}
|
|
427
534
|
const capturedMcpDeps = deps.mcpDeps;
|
|
535
|
+
const teamTokens = deps.teamTokens;
|
|
428
536
|
app.all("/mcp", async (c) => {
|
|
429
537
|
const auth = c.req.header("authorization") ?? "";
|
|
430
538
|
const match = /^Bearer\s+(\S+)$/i.exec(auth);
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
if (!match || given.length !== want.length || !timingSafeEqual(given, want)) {
|
|
539
|
+
const resolved = await resolveTeamByToken(teamTokens, match?.[1]);
|
|
540
|
+
if (!resolved) {
|
|
434
541
|
return c.json({ error: "unauthorized" }, 401);
|
|
435
542
|
}
|
|
436
543
|
// No sessionIdGenerator = stateless mode: no session ID in responses,
|
|
437
544
|
// no session validation. Correct for per-request agent calls.
|
|
438
545
|
const transport = new WebStandardStreamableHTTPServerTransport({});
|
|
439
|
-
const server = createMcpServer(capturedMcpDeps);
|
|
546
|
+
const server = createMcpServer(capturedMcpDeps, resolved.teamId);
|
|
440
547
|
await server.connect(transport);
|
|
441
548
|
return transport.handleRequest(c.req.raw);
|
|
442
549
|
});
|
|
443
550
|
}
|
|
444
551
|
function registerRecallRoutes(app, deps) {
|
|
445
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");
|
|
446
557
|
const q = c.req.query("q") ?? "";
|
|
447
558
|
const entity = c.req.query("entity");
|
|
448
559
|
const kind = c.req.query("kind");
|
|
@@ -486,6 +597,18 @@ function registerRecallRoutes(app, deps) {
|
|
|
486
597
|
else if (source === "hook") {
|
|
487
598
|
withRelatedFacts = true;
|
|
488
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
|
+
}
|
|
489
612
|
// Spec §E: `?withExemplars=true` attaches relatedExemplars (passive code exemplar recall).
|
|
490
613
|
const withExemplars = c.req.query("withExemplars") === "true";
|
|
491
614
|
// Task #303: superseded sessions are opt-in over HTTP and DEFAULT OFF.
|
|
@@ -501,12 +624,13 @@ function registerRecallRoutes(app, deps) {
|
|
|
501
624
|
...(kind !== undefined ? { kind: kind } : {}),
|
|
502
625
|
...(rewrite !== undefined ? { rewrite } : {}),
|
|
503
626
|
...(withRelatedFacts !== undefined ? { withRelatedFacts } : {}),
|
|
627
|
+
...(withRelatedConcepts !== undefined ? { withRelatedConcepts } : {}),
|
|
504
628
|
...(withExemplars ? { withRelatedExemplars: true } : {}),
|
|
505
629
|
...(includeSuperseded !== undefined ? { includeSuperseded } : {}),
|
|
506
630
|
};
|
|
507
|
-
const result = await deps.recall.search(query);
|
|
631
|
+
const result = await deps.recall.search(tenantId, query);
|
|
508
632
|
// Fire-and-forget telemetry — never blocks the response.
|
|
509
|
-
void logQuery({
|
|
633
|
+
void logQuery(tenantId, {
|
|
510
634
|
source,
|
|
511
635
|
runtime,
|
|
512
636
|
query: q || null,
|
|
@@ -521,22 +645,29 @@ function registerRecallRoutes(app, deps) {
|
|
|
521
645
|
return c.json(result);
|
|
522
646
|
});
|
|
523
647
|
app.get("/api/recall/stats", async (c) => {
|
|
648
|
+
const tenantId = c.get("tenantId");
|
|
524
649
|
const daysStr = c.req.query("days") ?? "7";
|
|
525
650
|
const days = Number.parseInt(daysStr, 10);
|
|
526
651
|
if (!Number.isFinite(days) || days < 1 || days > 365) {
|
|
527
652
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
528
653
|
}
|
|
529
|
-
const stats = await recallStats(days, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
|
|
654
|
+
const stats = await recallStats(tenantId, days, ...(deps.queryLogPath !== undefined ? [deps.queryLogPath] : []));
|
|
530
655
|
return c.json(stats);
|
|
531
656
|
});
|
|
532
657
|
app.get("/api/recall/recent", (c) => {
|
|
533
|
-
const
|
|
534
|
-
|
|
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] : []));
|
|
535
665
|
return c.json({ entries });
|
|
536
666
|
});
|
|
537
667
|
// Citation events from the Stop hook. One POST per surfaced ID the
|
|
538
668
|
// assistant cited in its response. Feeds the recall precision metric.
|
|
539
669
|
app.post("/api/recall/cite-event", async (c) => {
|
|
670
|
+
const tenantId = c.get("tenantId");
|
|
540
671
|
let body;
|
|
541
672
|
try {
|
|
542
673
|
body = (await c.req.json());
|
|
@@ -554,7 +685,7 @@ function registerRecallRoutes(app, deps) {
|
|
|
554
685
|
}
|
|
555
686
|
const responsePreview = body["response_preview"];
|
|
556
687
|
const kind = body["kind"];
|
|
557
|
-
await appendCitation({
|
|
688
|
+
await appendCitation(tenantId, {
|
|
558
689
|
conversationId,
|
|
559
690
|
citedId,
|
|
560
691
|
...(kind === "tool_use" || kind === "prose" ? { kind } : {}),
|
|
@@ -565,12 +696,13 @@ function registerRecallRoutes(app, deps) {
|
|
|
565
696
|
return c.json({ ok: true });
|
|
566
697
|
});
|
|
567
698
|
app.get("/api/recall/cite-stats", async (c) => {
|
|
699
|
+
const tenantId = c.get("tenantId");
|
|
568
700
|
const daysStr = c.req.query("days") ?? "7";
|
|
569
701
|
const days = Number.parseInt(daysStr, 10);
|
|
570
702
|
if (!Number.isFinite(days) || days < 1 || days > 365) {
|
|
571
703
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
572
704
|
}
|
|
573
|
-
const stats = await citationStats(days, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
|
|
705
|
+
const stats = await citationStats(tenantId, days, ...(deps.citationLogPath !== undefined ? [deps.citationLogPath] : []));
|
|
574
706
|
return c.json(stats);
|
|
575
707
|
});
|
|
576
708
|
// Explicit citation from the cite_session MCP tool. One POST per session
|
|
@@ -578,6 +710,7 @@ function registerRecallRoutes(app, deps) {
|
|
|
578
710
|
// so the training extractor can distinguish deterministic tool citations
|
|
579
711
|
// from stop-hook detected prose citations.
|
|
580
712
|
app.post("/api/citation/explicit", async (c) => {
|
|
713
|
+
const tenantId = c.get("tenantId");
|
|
581
714
|
let body;
|
|
582
715
|
try {
|
|
583
716
|
body = (await c.req.json());
|
|
@@ -589,10 +722,10 @@ function registerRecallRoutes(app, deps) {
|
|
|
589
722
|
if (typeof id !== "string" || !id) {
|
|
590
723
|
return c.json({ error: "id required" }, 400);
|
|
591
724
|
}
|
|
592
|
-
await appendCitation({
|
|
725
|
+
await appendCitation(tenantId, {
|
|
593
726
|
conversationId: typeof body["conversation_id"] === "string"
|
|
594
727
|
? body["conversation_id"]
|
|
595
|
-
: (resolveConversationForSession(id) ?? "mcp_tool"),
|
|
728
|
+
: (resolveConversationForSession(tenantId, id) ?? "mcp_tool"),
|
|
596
729
|
citedId: id,
|
|
597
730
|
kind: "tool_use",
|
|
598
731
|
...(typeof body["reason"] === "string" ? { responsePreview: body["reason"] } : {}),
|
|
@@ -607,6 +740,7 @@ function registerHookRoutes(app) {
|
|
|
607
740
|
// suppressed by stale "already surfaced" gates.
|
|
608
741
|
// Payload: { conversation_id, transcript_path?, surfaced_set?, ts? }
|
|
609
742
|
app.post("/api/hook/pre-compact", async (c) => {
|
|
743
|
+
const tenantId = c.get("tenantId");
|
|
610
744
|
let body;
|
|
611
745
|
try {
|
|
612
746
|
body = (await c.req.json());
|
|
@@ -618,10 +752,18 @@ function registerHookRoutes(app) {
|
|
|
618
752
|
if (typeof conversationId !== "string" || !conversationId) {
|
|
619
753
|
return c.json({ error: "conversation_id required" }, 400);
|
|
620
754
|
}
|
|
621
|
-
const flushed = loadSurfaced(conversationId).size;
|
|
622
|
-
clearSurfaced(conversationId);
|
|
755
|
+
const flushed = loadSurfaced(tenantId, conversationId).size;
|
|
756
|
+
clearSurfaced(tenantId, conversationId);
|
|
623
757
|
const compactedAt = new Date().toISOString();
|
|
624
|
-
|
|
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");
|
|
625
767
|
try {
|
|
626
768
|
mkdirSync(dirname(logPath), { recursive: true });
|
|
627
769
|
appendFileSync(logPath, `${JSON.stringify({ ts: compactedAt, kind: "pre-compact", conversationId, flushed })}\n`, "utf8");
|
|
@@ -631,38 +773,6 @@ function registerHookRoutes(app) {
|
|
|
631
773
|
}
|
|
632
774
|
return c.json({ ok: true, flushed, compacted_at: compactedAt });
|
|
633
775
|
});
|
|
634
|
-
// SubagentStart hook: logging-only stub. Records the parent→subagent link
|
|
635
|
-
// so future corpus-linking logic can correlate subagent sessions back to
|
|
636
|
-
// their dispatching conversation.
|
|
637
|
-
// Payload: { parent_conversation_id, subagent_session_id, subagent_description?, ts? }
|
|
638
|
-
app.post("/api/hook/subagent-start", async (c) => {
|
|
639
|
-
let body;
|
|
640
|
-
try {
|
|
641
|
-
body = (await c.req.json());
|
|
642
|
-
}
|
|
643
|
-
catch {
|
|
644
|
-
return c.json({ error: "body must be JSON" }, 400);
|
|
645
|
-
}
|
|
646
|
-
const parentConversationId = body["parent_conversation_id"];
|
|
647
|
-
const subagentSessionId = body["subagent_session_id"];
|
|
648
|
-
if (typeof parentConversationId !== "string" || !parentConversationId) {
|
|
649
|
-
return c.json({ error: "parent_conversation_id required" }, 400);
|
|
650
|
-
}
|
|
651
|
-
if (typeof subagentSessionId !== "string" || !subagentSessionId) {
|
|
652
|
-
return c.json({ error: "subagent_session_id required" }, 400);
|
|
653
|
-
}
|
|
654
|
-
const subagentDescription = typeof body["subagent_description"] === "string" ? body["subagent_description"] : "";
|
|
655
|
-
const ts = typeof body["ts"] === "string" ? body["ts"] : new Date().toISOString();
|
|
656
|
-
const logPath = process.env["NLM_SUBAGENT_LOG"] ?? join(homedir(), ".nlm", "subagent-log.jsonl");
|
|
657
|
-
try {
|
|
658
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
659
|
-
appendFileSync(logPath, `${JSON.stringify({ ts, parent_conversation_id: parentConversationId, subagent_session_id: subagentSessionId, subagent_description: subagentDescription })}\n`, "utf8");
|
|
660
|
-
}
|
|
661
|
-
catch {
|
|
662
|
-
// Log failure must not fail the endpoint.
|
|
663
|
-
}
|
|
664
|
-
return c.json({ ok: true, recorded: true });
|
|
665
|
-
});
|
|
666
776
|
}
|
|
667
777
|
// ── NousResearch Hermes Agent lifecycle hooks ─────────────────────────────
|
|
668
778
|
//
|
|
@@ -697,7 +807,8 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
697
807
|
return c.json({ context: null });
|
|
698
808
|
}
|
|
699
809
|
try {
|
|
700
|
-
const
|
|
810
|
+
const tenantId = c.get("tenantId");
|
|
811
|
+
const result = await deps.recall.search(tenantId, {
|
|
701
812
|
query: userMessage,
|
|
702
813
|
mode: "keyword",
|
|
703
814
|
limit: 5,
|
|
@@ -710,7 +821,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
710
821
|
startedAt: r.startedAt,
|
|
711
822
|
matchScore: r.matchScore,
|
|
712
823
|
}));
|
|
713
|
-
const surfaced = loadSurfaced(sessionId);
|
|
824
|
+
const surfaced = loadSurfaced(tenantId, sessionId);
|
|
714
825
|
const selected = selectHits({ hits, surfaced, scoreThreshold: 0, relativeFloor: HERMES_RELATIVE_FLOOR, perFireCap: 3, perConversationCap: 10 });
|
|
715
826
|
if (selected.length === 0 &&
|
|
716
827
|
(result.relatedFacts ?? []).length === 0 &&
|
|
@@ -718,7 +829,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
718
829
|
return c.json({ context: null });
|
|
719
830
|
}
|
|
720
831
|
if (selected.length > 0) {
|
|
721
|
-
recordSurfaced(sessionId, selected.map((h) => h.id));
|
|
832
|
+
recordSurfaced(tenantId, sessionId, selected.map((h) => h.id));
|
|
722
833
|
}
|
|
723
834
|
return c.json({
|
|
724
835
|
context: formatPointerBlock(selected, result.relatedFacts ?? [], (result.relatedExemplars ?? []).map((e) => ({
|
|
@@ -733,6 +844,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
733
844
|
// post-turn: scan assistant_response for session IDs that were surfaced in
|
|
734
845
|
// this conversation and log prose citation events.
|
|
735
846
|
app.post("/api/hook/hermes-agent/post-turn", async (c) => {
|
|
847
|
+
const tenantId = c.get("tenantId");
|
|
736
848
|
let body;
|
|
737
849
|
try {
|
|
738
850
|
body = (await c.req.json());
|
|
@@ -748,7 +860,7 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
748
860
|
if (typeof assistantResponse !== "string" || !assistantResponse) {
|
|
749
861
|
return c.json({ ok: true, cited: 0 });
|
|
750
862
|
}
|
|
751
|
-
const surfacedIds = [...loadSurfaced(sessionId)];
|
|
863
|
+
const surfacedIds = [...loadSurfaced(tenantId, sessionId)];
|
|
752
864
|
const cited = [];
|
|
753
865
|
for (const id of surfacedIds) {
|
|
754
866
|
if (assistantResponse.includes(id))
|
|
@@ -756,13 +868,14 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
756
868
|
}
|
|
757
869
|
const preview = assistantResponse.slice(0, 200);
|
|
758
870
|
for (const citedId of cited) {
|
|
759
|
-
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] : []));
|
|
760
872
|
}
|
|
761
873
|
return c.json({ ok: true, cited: cited.length });
|
|
762
874
|
});
|
|
763
875
|
// session-lifecycle: memo housekeeping for on_session_{start,end,finalize,reset}.
|
|
764
876
|
// start is a no-op (memo is created lazily). end/finalize/reset clear the memo.
|
|
765
877
|
app.post("/api/hook/hermes-agent/session-lifecycle", async (c) => {
|
|
878
|
+
const tenantId = c.get("tenantId");
|
|
766
879
|
let body;
|
|
767
880
|
try {
|
|
768
881
|
body = (await c.req.json());
|
|
@@ -777,8 +890,8 @@ function registerHermesAgentHookRoutes(app, deps) {
|
|
|
777
890
|
if (event !== "start") {
|
|
778
891
|
const sessionId = body["session_id"];
|
|
779
892
|
if (typeof sessionId === "string" && sessionId) {
|
|
780
|
-
clearSurfaced(sessionId);
|
|
781
|
-
clearCited(sessionId);
|
|
893
|
+
clearSurfaced(tenantId, sessionId);
|
|
894
|
+
clearCited(tenantId, sessionId);
|
|
782
895
|
}
|
|
783
896
|
}
|
|
784
897
|
return c.json({ ok: true, event });
|
|
@@ -790,6 +903,7 @@ function registerFactRoutes(app, deps) {
|
|
|
790
903
|
if (!deps.factRecall) {
|
|
791
904
|
return c.json({ error: "fact recall not wired in this deployment" }, 503);
|
|
792
905
|
}
|
|
906
|
+
const tenantId = c.get("tenantId");
|
|
793
907
|
const q = c.req.query("q") ?? "";
|
|
794
908
|
const subject = c.req.query("subject");
|
|
795
909
|
const predicate = c.req.query("predicate");
|
|
@@ -825,10 +939,10 @@ function registerFactRoutes(app, deps) {
|
|
|
825
939
|
...(kind !== undefined ? { kind: kind } : {}),
|
|
826
940
|
...(minConfidence !== undefined ? { minConfidence } : {}),
|
|
827
941
|
};
|
|
828
|
-
const result = await deps.factRecall.search(query);
|
|
942
|
+
const result = await deps.factRecall.search(tenantId, query);
|
|
829
943
|
const source = c.req.header("x-recall-source") ?? "http";
|
|
830
944
|
const runtime = c.req.header("x-recall-runtime") ?? null;
|
|
831
|
-
void logFactQuery({
|
|
945
|
+
void logFactQuery(tenantId, {
|
|
832
946
|
source,
|
|
833
947
|
runtime,
|
|
834
948
|
query: q || null,
|
|
@@ -855,31 +969,40 @@ function registerFactRoutes(app, deps) {
|
|
|
855
969
|
return c.json({ error: "subject is required" }, 400);
|
|
856
970
|
}
|
|
857
971
|
const predicate = c.req.query("predicate");
|
|
858
|
-
const
|
|
972
|
+
const tenantId = c.get("tenantId");
|
|
973
|
+
const chains = await deps.factStore.getHistory(tenantId, subject, predicate);
|
|
859
974
|
return c.json({ subject, predicate: predicate ?? null, chains });
|
|
860
975
|
});
|
|
861
976
|
app.get("/api/recall/facts/stats", async (c) => {
|
|
977
|
+
const tenantId = c.get("tenantId");
|
|
862
978
|
const daysStr = c.req.query("days") ?? "7";
|
|
863
979
|
const days = Number.parseInt(daysStr, 10);
|
|
864
980
|
if (!Number.isFinite(days) || days < 1 || days > 365) {
|
|
865
981
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
866
982
|
}
|
|
867
|
-
const stats = await factRecallStats(days, ...(deps.factQueryLogPath !== undefined ? [deps.factQueryLogPath] : []));
|
|
983
|
+
const stats = await factRecallStats(tenantId, days, ...(deps.factQueryLogPath !== undefined ? [deps.factQueryLogPath] : []));
|
|
868
984
|
return c.json(stats);
|
|
869
985
|
});
|
|
870
986
|
}
|
|
871
987
|
function registerLiveRoutes(app, deps) {
|
|
872
|
-
|
|
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) => {
|
|
873
994
|
if (!deps.liveStore)
|
|
874
995
|
return c.json({ writes: [] });
|
|
996
|
+
const tenantId = c.get("tenantId");
|
|
875
997
|
const limit = parseLimit(c.req.query("limit"), 50, 200);
|
|
876
|
-
return c.json({ writes: deps.liveStore.recentWrites(limit) });
|
|
998
|
+
return c.json({ writes: await deps.liveStore.recentWrites(tenantId, limit) });
|
|
877
999
|
});
|
|
878
|
-
app.get("/api/live/recent-markers", (c) => {
|
|
1000
|
+
app.get("/api/live/recent-markers", async (c) => {
|
|
879
1001
|
if (!deps.liveStore)
|
|
880
1002
|
return c.json({ markers: [] });
|
|
1003
|
+
const tenantId = c.get("tenantId");
|
|
881
1004
|
const limit = parseLimit(c.req.query("limit"), 50, 200);
|
|
882
|
-
return c.json({ markers: deps.liveStore.recentMarkers(limit) });
|
|
1005
|
+
return c.json({ markers: await deps.liveStore.recentMarkers(tenantId, limit) });
|
|
883
1006
|
});
|
|
884
1007
|
}
|
|
885
1008
|
function registerDatasetRoute(app, deps) {
|
|
@@ -887,7 +1010,7 @@ function registerDatasetRoute(app, deps) {
|
|
|
887
1010
|
if (!deps.dbPath)
|
|
888
1011
|
return c.json({ error: "dataset endpoint requires dbPath" }, 503);
|
|
889
1012
|
const includePaths = c.req.query("include_paths") === "true";
|
|
890
|
-
return c.json(buildDataset(deps.dbPath, { includePaths }));
|
|
1013
|
+
return c.json(buildDataset(deps.dbPath, DEFAULT_TEAM_ID, { includePaths }));
|
|
891
1014
|
});
|
|
892
1015
|
}
|
|
893
1016
|
// ── Data management ─────────────────────────────────────────────
|
|
@@ -998,9 +1121,10 @@ function registerActionRoutes(app, deps) {
|
|
|
998
1121
|
if (!parsed)
|
|
999
1122
|
return c.json({ error: "invalid action payload" }, 400);
|
|
1000
1123
|
const store = deps.liveStore;
|
|
1124
|
+
const tenantId = c.get("tenantId");
|
|
1001
1125
|
const id = store instanceof PgSessionStore
|
|
1002
|
-
? await writeActionPg(store.pool, parsed)
|
|
1003
|
-
: writeAction(store.rawDb(), parsed);
|
|
1126
|
+
? await writeActionPg(store.pool, tenantId, parsed)
|
|
1127
|
+
: writeAction(store.rawDb(), tenantId, parsed);
|
|
1004
1128
|
store.invalidateOverlayCache();
|
|
1005
1129
|
return c.json({ id, timestamp: new Date().toISOString() });
|
|
1006
1130
|
});
|
|
@@ -1016,9 +1140,10 @@ function registerActionRoutes(app, deps) {
|
|
|
1016
1140
|
if (inputs.length === 0)
|
|
1017
1141
|
return c.json({ accepted: 0, ids: [] });
|
|
1018
1142
|
const store = deps.liveStore;
|
|
1143
|
+
const tenantId = c.get("tenantId");
|
|
1019
1144
|
const ids = store instanceof PgSessionStore
|
|
1020
|
-
? await writeActionsBatchPg(store.pool, inputs)
|
|
1021
|
-
: writeActionsBatch(store.rawDb(), inputs);
|
|
1145
|
+
? await writeActionsBatchPg(store.pool, tenantId, inputs)
|
|
1146
|
+
: writeActionsBatch(store.rawDb(), tenantId, inputs);
|
|
1022
1147
|
store.invalidateOverlayCache();
|
|
1023
1148
|
return c.json({ accepted: ids.length, ids });
|
|
1024
1149
|
});
|
|
@@ -1026,9 +1151,10 @@ function registerActionRoutes(app, deps) {
|
|
|
1026
1151
|
if (!deps.liveStore)
|
|
1027
1152
|
return c.json({ error: "actions require liveStore" }, 503);
|
|
1028
1153
|
const store = deps.liveStore;
|
|
1154
|
+
const tenantId = c.get("tenantId");
|
|
1029
1155
|
const result = store instanceof PgSessionStore
|
|
1030
|
-
? await undoActionPg(store.pool, c.req.param("id"))
|
|
1031
|
-
: 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"));
|
|
1032
1158
|
if (!result)
|
|
1033
1159
|
return c.json({ error: "action not found or already undone" }, 404);
|
|
1034
1160
|
store.invalidateOverlayCache();
|
|
@@ -1047,9 +1173,10 @@ function registerActionRoutes(app, deps) {
|
|
|
1047
1173
|
...(kind ? { kind } : {}),
|
|
1048
1174
|
};
|
|
1049
1175
|
const store = deps.liveStore;
|
|
1176
|
+
const tenantId = c.get("tenantId");
|
|
1050
1177
|
const rows = store instanceof PgSessionStore
|
|
1051
|
-
? await listActionsPg(store.pool, opts)
|
|
1052
|
-
: listActions(store.rawDb(), opts);
|
|
1178
|
+
? await listActionsPg(store.pool, tenantId, opts)
|
|
1179
|
+
: listActions(store.rawDb(), tenantId, opts);
|
|
1053
1180
|
return c.json({ actions: rows });
|
|
1054
1181
|
});
|
|
1055
1182
|
}
|
|
@@ -1057,13 +1184,18 @@ function registerClassifierRoutes(app, deps) {
|
|
|
1057
1184
|
app.get("/api/classifier/info", (c) => {
|
|
1058
1185
|
const provider = deps.classifier?.provider ?? "ollama";
|
|
1059
1186
|
const model = deps.classifier?.model ?? "qwen3.5:4b";
|
|
1187
|
+
const openAiSwapAvailable = deps.classifier?.openAiSwapAvailable ?? false;
|
|
1060
1188
|
return c.json({
|
|
1061
1189
|
provider,
|
|
1062
1190
|
model,
|
|
1063
|
-
available_providers:
|
|
1191
|
+
available_providers: openAiSwapAvailable
|
|
1192
|
+
? ["deepseek", "ollama", "openai"]
|
|
1193
|
+
: ["deepseek", "ollama"],
|
|
1194
|
+
openai_swap_available: openAiSwapAvailable,
|
|
1064
1195
|
env_present: {
|
|
1065
1196
|
deepseek: Boolean(process.env["DEEPSEEK_API_KEY"]),
|
|
1066
1197
|
ollama: true,
|
|
1198
|
+
openai: openAiSwapAvailable,
|
|
1067
1199
|
},
|
|
1068
1200
|
default_models: {
|
|
1069
1201
|
deepseek: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat"],
|
|
@@ -1078,8 +1210,15 @@ function registerClassifierRoutes(app, deps) {
|
|
|
1078
1210
|
const body = (await c.req.json().catch(() => null));
|
|
1079
1211
|
const provider = body?.provider;
|
|
1080
1212
|
const model = body?.model;
|
|
1081
|
-
|
|
1082
|
-
|
|
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);
|
|
1083
1222
|
}
|
|
1084
1223
|
if (!model || typeof model !== "string" || model.length === 0) {
|
|
1085
1224
|
return c.json({ error: "model is required" }, 400);
|
|
@@ -1087,7 +1226,14 @@ function registerClassifierRoutes(app, deps) {
|
|
|
1087
1226
|
if (provider === "deepseek" && !process.env["DEEPSEEK_API_KEY"]) {
|
|
1088
1227
|
return c.json({ error: "DEEPSEEK_API_KEY not set — cannot swap to deepseek" }, 400);
|
|
1089
1228
|
}
|
|
1090
|
-
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);
|
|
1091
1237
|
return c.json({ provider: deps.classifier.provider, model: deps.classifier.model });
|
|
1092
1238
|
});
|
|
1093
1239
|
}
|
|
@@ -1098,7 +1244,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1098
1244
|
app.get("/api/sources", async (c) => {
|
|
1099
1245
|
if (!deps.sources)
|
|
1100
1246
|
return c.json({ sources: [] });
|
|
1101
|
-
|
|
1247
|
+
const tenantId = c.get("tenantId");
|
|
1248
|
+
return c.json({ sources: await deps.sources.list(tenantId) });
|
|
1102
1249
|
});
|
|
1103
1250
|
app.post("/api/sources", async (c) => {
|
|
1104
1251
|
if (!deps.sources)
|
|
@@ -1107,10 +1254,11 @@ function registerSourceRoutes(app, deps) {
|
|
|
1107
1254
|
const parsed = parseSourceInsert(body);
|
|
1108
1255
|
if (!parsed)
|
|
1109
1256
|
return c.json({ error: "invalid source payload" }, 400);
|
|
1110
|
-
|
|
1257
|
+
const tenantId = c.get("tenantId");
|
|
1258
|
+
if (await deps.sources.getByName(tenantId, parsed.name)) {
|
|
1111
1259
|
return c.json({ error: `source named '${parsed.name}' already exists` }, 409);
|
|
1112
1260
|
}
|
|
1113
|
-
return c.json(await deps.sources.insert(parsed), 201);
|
|
1261
|
+
return c.json(await deps.sources.insert(tenantId, parsed), 201);
|
|
1114
1262
|
});
|
|
1115
1263
|
app.patch("/api/sources/:id", async (c) => {
|
|
1116
1264
|
if (!deps.sources)
|
|
@@ -1122,7 +1270,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1122
1270
|
const patch = parseSourceUpdate(body);
|
|
1123
1271
|
if (!patch)
|
|
1124
1272
|
return c.json({ error: "invalid patch payload" }, 400);
|
|
1125
|
-
const
|
|
1273
|
+
const tenantId = c.get("tenantId");
|
|
1274
|
+
const updated = await deps.sources.update(tenantId, id, patch);
|
|
1126
1275
|
if (!updated)
|
|
1127
1276
|
return c.json({ error: `source ${id} not found` }, 404);
|
|
1128
1277
|
return c.json(updated);
|
|
@@ -1133,7 +1282,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1133
1282
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1134
1283
|
if (!Number.isFinite(id))
|
|
1135
1284
|
return c.json({ error: "invalid id" }, 400);
|
|
1136
|
-
const
|
|
1285
|
+
const tenantId = c.get("tenantId");
|
|
1286
|
+
const ok = await deps.sources.delete(tenantId, id);
|
|
1137
1287
|
if (!ok)
|
|
1138
1288
|
return c.json({ error: `source ${id} not found` }, 404);
|
|
1139
1289
|
return c.json({ deleted: id });
|
|
@@ -1144,7 +1294,8 @@ function registerSourceRoutes(app, deps) {
|
|
|
1144
1294
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1145
1295
|
if (!Number.isFinite(id))
|
|
1146
1296
|
return c.json({ error: "invalid id" }, 400);
|
|
1147
|
-
const
|
|
1297
|
+
const tenantId = c.get("tenantId");
|
|
1298
|
+
const token = await deps.sources.regenerateToken(tenantId, id);
|
|
1148
1299
|
if (!token)
|
|
1149
1300
|
return c.json({ error: "regenerate-token only applies to webhook sources" }, 400);
|
|
1150
1301
|
return c.json({ token });
|
|
@@ -1185,7 +1336,11 @@ function registerIngestRoute(app, deps) {
|
|
|
1185
1336
|
sourceId: source.id,
|
|
1186
1337
|
};
|
|
1187
1338
|
const ingest = deps.ingest;
|
|
1188
|
-
|
|
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) => {
|
|
1189
1344
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1190
1345
|
console.error(`[ingest] background failure for ${id}: ${msg}`);
|
|
1191
1346
|
});
|
|
@@ -1199,7 +1354,8 @@ function registerProviderRoutes(app, deps) {
|
|
|
1199
1354
|
app.get("/api/providers", async (c) => {
|
|
1200
1355
|
if (!deps.providers)
|
|
1201
1356
|
return c.json({ providers: [] });
|
|
1202
|
-
|
|
1357
|
+
const tenantId = c.get("tenantId");
|
|
1358
|
+
return c.json({ providers: await deps.providers.list(tenantId) });
|
|
1203
1359
|
});
|
|
1204
1360
|
app.post("/api/providers", async (c) => {
|
|
1205
1361
|
if (!deps.providers)
|
|
@@ -1208,10 +1364,11 @@ function registerProviderRoutes(app, deps) {
|
|
|
1208
1364
|
const parsed = parseProviderInsert(body);
|
|
1209
1365
|
if (!parsed)
|
|
1210
1366
|
return c.json({ error: "invalid provider payload" }, 400);
|
|
1211
|
-
|
|
1367
|
+
const tenantId = c.get("tenantId");
|
|
1368
|
+
if (await deps.providers.getByName(tenantId, parsed.name)) {
|
|
1212
1369
|
return c.json({ error: `provider named '${parsed.name}' already exists` }, 409);
|
|
1213
1370
|
}
|
|
1214
|
-
return c.json(await deps.providers.insert(parsed), 201);
|
|
1371
|
+
return c.json(await deps.providers.insert(tenantId, parsed), 201);
|
|
1215
1372
|
});
|
|
1216
1373
|
app.patch("/api/providers/:id", async (c) => {
|
|
1217
1374
|
if (!deps.providers)
|
|
@@ -1223,7 +1380,8 @@ function registerProviderRoutes(app, deps) {
|
|
|
1223
1380
|
const patch = parseProviderUpdate(body);
|
|
1224
1381
|
if (!patch)
|
|
1225
1382
|
return c.json({ error: "invalid patch payload" }, 400);
|
|
1226
|
-
const
|
|
1383
|
+
const tenantId = c.get("tenantId");
|
|
1384
|
+
const updated = await deps.providers.update(tenantId, id, patch);
|
|
1227
1385
|
if (!updated)
|
|
1228
1386
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1229
1387
|
return c.json(updated);
|
|
@@ -1234,7 +1392,8 @@ function registerProviderRoutes(app, deps) {
|
|
|
1234
1392
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1235
1393
|
if (!Number.isFinite(id))
|
|
1236
1394
|
return c.json({ error: "invalid id" }, 400);
|
|
1237
|
-
const
|
|
1395
|
+
const tenantId = c.get("tenantId");
|
|
1396
|
+
const ok = await deps.providers.delete(tenantId, id);
|
|
1238
1397
|
if (!ok)
|
|
1239
1398
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1240
1399
|
return c.json({ deleted: id });
|
|
@@ -1245,10 +1404,11 @@ function registerProviderRoutes(app, deps) {
|
|
|
1245
1404
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1246
1405
|
if (!Number.isFinite(id))
|
|
1247
1406
|
return c.json({ error: "invalid id" }, 400);
|
|
1248
|
-
const
|
|
1407
|
+
const tenantId = c.get("tenantId");
|
|
1408
|
+
const provider = await deps.providers.get(tenantId, id);
|
|
1249
1409
|
if (!provider)
|
|
1250
1410
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1251
|
-
const key = await deps.providers.getSecret(id);
|
|
1411
|
+
const key = await deps.providers.getSecret(tenantId, id);
|
|
1252
1412
|
try {
|
|
1253
1413
|
const models = await listModels(provider, { apiKey: key });
|
|
1254
1414
|
return c.json({ models });
|
|
@@ -1264,10 +1424,11 @@ function registerProviderRoutes(app, deps) {
|
|
|
1264
1424
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1265
1425
|
if (!Number.isFinite(id))
|
|
1266
1426
|
return c.json({ error: "invalid id" }, 400);
|
|
1267
|
-
const
|
|
1427
|
+
const tenantId = c.get("tenantId");
|
|
1428
|
+
const provider = await deps.providers.get(tenantId, id);
|
|
1268
1429
|
if (!provider)
|
|
1269
1430
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1270
|
-
const key = await deps.providers.getSecret(id);
|
|
1431
|
+
const key = await deps.providers.getSecret(tenantId, id);
|
|
1271
1432
|
const startedAt = Date.now();
|
|
1272
1433
|
try {
|
|
1273
1434
|
const models = await listModels(provider, { apiKey: key });
|
|
@@ -1286,17 +1447,44 @@ function registerProviderRoutes(app, deps) {
|
|
|
1286
1447
|
function registerSessionRoute(app, deps) {
|
|
1287
1448
|
app.get("/api/session/:id", async (c) => {
|
|
1288
1449
|
const id = c.req.param("id");
|
|
1289
|
-
const
|
|
1450
|
+
const tenantId = c.get("tenantId");
|
|
1451
|
+
const session = await deps.store.getById(tenantId, id);
|
|
1290
1452
|
if (!session) {
|
|
1291
1453
|
return c.json({ error: `session ${id} not found` }, 404);
|
|
1292
1454
|
}
|
|
1293
1455
|
return c.json(session);
|
|
1294
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
|
+
});
|
|
1295
1482
|
// Post-hoc supersedence write path. Mirrors the `mark_superseded` MCP tool
|
|
1296
1483
|
// so UI + CLI + MCP all share one backend gesture. Path param is the
|
|
1297
1484
|
// predecessor (the session being retired); body names the successor.
|
|
1298
1485
|
app.post("/api/session/:id/supersede", async (c) => {
|
|
1299
1486
|
const predecessorId = c.req.param("id");
|
|
1487
|
+
const tenantId = c.get("tenantId");
|
|
1300
1488
|
let body;
|
|
1301
1489
|
try {
|
|
1302
1490
|
body = await c.req.json();
|
|
@@ -1317,7 +1505,7 @@ function registerSessionRoute(app, deps) {
|
|
|
1317
1505
|
return c.json({ error: "reason must be a string if provided" }, 400);
|
|
1318
1506
|
}
|
|
1319
1507
|
try {
|
|
1320
|
-
await deps.store.markSuperseded(predecessorId, successorId);
|
|
1508
|
+
await deps.store.markSuperseded(tenantId, predecessorId, successorId);
|
|
1321
1509
|
}
|
|
1322
1510
|
catch (e) {
|
|
1323
1511
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1326,7 +1514,7 @@ function registerSessionRoute(app, deps) {
|
|
|
1326
1514
|
: 500;
|
|
1327
1515
|
return c.json({ error: msg }, status);
|
|
1328
1516
|
}
|
|
1329
|
-
void appendSupersedence({
|
|
1517
|
+
void appendSupersedence(tenantId, {
|
|
1330
1518
|
predecessorId,
|
|
1331
1519
|
successorId,
|
|
1332
1520
|
source: c.req.header("x-supersedence-source") ?? "http",
|
|
@@ -1361,7 +1549,8 @@ function parseActionInput(raw) {
|
|
|
1361
1549
|
};
|
|
1362
1550
|
}
|
|
1363
1551
|
const VALID_SOURCE_KINDS = [
|
|
1364
|
-
"claude-code", "codex", "hermes", "
|
|
1552
|
+
"claude-code", "codex", "hermes", "hermes-agent", "muse", "aider", "cursor",
|
|
1553
|
+
"windsurf", "opencode", "pi", "jsonl-generic", "webhook",
|
|
1365
1554
|
];
|
|
1366
1555
|
function parseSourceInsert(raw) {
|
|
1367
1556
|
if (!raw || typeof raw !== "object")
|
|
@@ -1496,8 +1685,14 @@ function registerSignalRoutes(app, deps) {
|
|
|
1496
1685
|
catch (e) {
|
|
1497
1686
|
return c.json({ error: e instanceof Error ? e.message : "invalid signal" }, 400);
|
|
1498
1687
|
}
|
|
1688
|
+
const rawBody = body;
|
|
1689
|
+
const tenantId = c.get("tenantId");
|
|
1690
|
+
signal = await stampSignalScope(signal, tenantId, {
|
|
1691
|
+
repoPath: typeof rawBody["repo_path"] === "string" ? rawBody["repo_path"] : null,
|
|
1692
|
+
sessionScopeReader: deps.sessionScopeReader,
|
|
1693
|
+
});
|
|
1499
1694
|
try {
|
|
1500
|
-
await deps.signalStore.insert(signal);
|
|
1695
|
+
await deps.signalStore.insert(tenantId, signal);
|
|
1501
1696
|
}
|
|
1502
1697
|
catch {
|
|
1503
1698
|
return c.json({ error: "signal insert failed" }, 500);
|
|
@@ -1513,11 +1708,11 @@ function registerSignalRoutes(app, deps) {
|
|
|
1513
1708
|
try {
|
|
1514
1709
|
const exemplar = extractExemplar(signal, { installScope: deps.installScope });
|
|
1515
1710
|
if (exemplar) {
|
|
1516
|
-
const { id, skipped } = await exemplarStore.insert(exemplar);
|
|
1711
|
+
const { id, skipped } = await exemplarStore.insert(tenantId, exemplar);
|
|
1517
1712
|
if (!skipped && deps.codeEmbedder) {
|
|
1518
1713
|
void deps.codeEmbedder
|
|
1519
1714
|
.embed(composeEmbedText(exemplar.taskContext, exemplar.code), "document")
|
|
1520
|
-
.then((r) => exemplarStore.upsertEmbedding(id, r.vector))
|
|
1715
|
+
.then((r) => exemplarStore.upsertEmbedding(tenantId, id, r.vector))
|
|
1521
1716
|
.catch(() => { });
|
|
1522
1717
|
}
|
|
1523
1718
|
}
|
|
@@ -1534,7 +1729,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1534
1729
|
if (!repo)
|
|
1535
1730
|
return c.json({ error: "repo is required" }, 400);
|
|
1536
1731
|
const model = c.req.query("model");
|
|
1537
|
-
const
|
|
1732
|
+
const tenantId = c.get("tenantId");
|
|
1733
|
+
const block = await buildFailureModeBlock(tenantId, deps.signalStore, { installScope: deps.installScope, repo, ...(model ? { model } : {}) });
|
|
1538
1734
|
return c.json({ repo, model: model ?? null, block });
|
|
1539
1735
|
});
|
|
1540
1736
|
app.get("/api/signals/stats", async (c) => {
|
|
@@ -1547,7 +1743,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1547
1743
|
return c.json({ error: "days must be 1..365" }, 400);
|
|
1548
1744
|
}
|
|
1549
1745
|
const sinceTs = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
1550
|
-
const
|
|
1746
|
+
const tenantId = c.get("tenantId");
|
|
1747
|
+
const rows = await deps.signalStore.listForAggregation(tenantId, { installScope: deps.installScope, sinceTs });
|
|
1551
1748
|
const modes = aggregateFailureModes(rows);
|
|
1552
1749
|
return c.json({ days, total: rows.length, modes });
|
|
1553
1750
|
});
|
|
@@ -1576,11 +1773,13 @@ function registerSignalRoutes(app, deps) {
|
|
|
1576
1773
|
catch (e) {
|
|
1577
1774
|
return c.json({ error: e instanceof Error ? e.message : "invalid exemplar" }, 400);
|
|
1578
1775
|
}
|
|
1579
|
-
|
|
1776
|
+
inp = { ...inp, scope: scopeStampEnabled() ? inp.scope : null };
|
|
1777
|
+
const tenantId = c.get("tenantId");
|
|
1778
|
+
const { id, skipped } = await deps.exemplarStore.insert(tenantId, inp);
|
|
1580
1779
|
if (!skipped && deps.codeEmbedder) {
|
|
1581
1780
|
// Best-effort embedding: fire-and-forget, never blocks the response.
|
|
1582
1781
|
deps.codeEmbedder.embed(composeEmbedText(inp.taskContext, inp.code), "document")
|
|
1583
|
-
.then((r) => deps.exemplarStore.upsertEmbedding(id, r.vector))
|
|
1782
|
+
.then((r) => deps.exemplarStore.upsertEmbedding(tenantId, id, r.vector))
|
|
1584
1783
|
.catch(() => { });
|
|
1585
1784
|
}
|
|
1586
1785
|
return c.json({ id, skipped, status: "accepted" }, 202);
|
|
@@ -1608,7 +1807,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1608
1807
|
if (patch.retired === undefined && patch.outcome === undefined) {
|
|
1609
1808
|
return c.json({ error: "provide retire (boolean) and/or outcome" }, 400);
|
|
1610
1809
|
}
|
|
1611
|
-
const
|
|
1810
|
+
const tenantId = c.get("tenantId");
|
|
1811
|
+
const result = await deps.exemplarStore.setVerdict(tenantId, c.req.param("id"), patch, "human");
|
|
1612
1812
|
if (result.status === "not_found")
|
|
1613
1813
|
return c.json({ error: "exemplar not found" }, 404);
|
|
1614
1814
|
return c.json({ id: c.req.param("id"), status: result.status }, 200);
|
|
@@ -1629,7 +1829,8 @@ function registerSignalRoutes(app, deps) {
|
|
|
1629
1829
|
const lang = c.req.query("lang");
|
|
1630
1830
|
const model = c.req.query("model");
|
|
1631
1831
|
const includeNegatives = c.req.query("negatives") !== "0";
|
|
1632
|
-
const
|
|
1832
|
+
const tenantId = c.get("tenantId");
|
|
1833
|
+
const result = await recallCode(tenantId, {
|
|
1633
1834
|
query,
|
|
1634
1835
|
installScope: deps.installScope,
|
|
1635
1836
|
...(repo ? { repo } : {}),
|
|
@@ -1641,6 +1842,52 @@ function registerSignalRoutes(app, deps) {
|
|
|
1641
1842
|
return c.json(result);
|
|
1642
1843
|
});
|
|
1643
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
|
+
}
|
|
1644
1891
|
function mountSpa(app, dist) {
|
|
1645
1892
|
const indexHtml = join(dist, "index.html");
|
|
1646
1893
|
if (!existsSync(indexHtml))
|