nlm-memory 0.21.0 → 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 +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 +45 -13
- 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/mcp/server.js
CHANGED
|
@@ -30,16 +30,24 @@ import { normalizeSignal } from "../core/signals/ingest-signal.js";
|
|
|
30
30
|
import { stampSignalScope } from "../core/signals/stamp-scope.js";
|
|
31
31
|
import { deriveOutcome } from "../core/outcome/rollup.js";
|
|
32
32
|
import { buildSqliteOutcomeDeps } from "../core/storage/sqlite-outcome-store.js";
|
|
33
|
+
import { completeLearnerCourse, enrollCourse, getCourseSummary, recordLearningEvent, } from "../core/learner/service.js";
|
|
34
|
+
import { truncateSummary } from "../core/hook/pointer-block.js";
|
|
33
35
|
import { SIGNAL_OUTCOMES } from "../shared/types.js";
|
|
34
36
|
const CHARACTER_LIMIT = 25_000;
|
|
35
37
|
const DEFAULT_LIMIT = 10;
|
|
36
38
|
const SERVER_NAME = "nlm-memory-mcp-server";
|
|
37
39
|
const SERVER_VERSION = "0.5.9";
|
|
38
40
|
/** TOON encoding cuts token usage on large recall payloads. Opt in via
|
|
39
|
-
* NLM_FORMAT=toon
|
|
40
|
-
|
|
41
|
+
* NLM_FORMAT=toon. Defaults to JSON.
|
|
42
|
+
*
|
|
43
|
+
* Read at call time, not module scope: on the HTTP transport this module is
|
|
44
|
+
* imported by the daemon long before buildStack() calls autoloadEnv(), so an
|
|
45
|
+
* eager read would capture the environment before ~/.nlm/.env was loaded and
|
|
46
|
+
* silently ignore the setting. Only the stdio path, where the runtime passes
|
|
47
|
+
* env directly to the child, would have worked. */
|
|
48
|
+
const useToon = () => process.env.NLM_FORMAT === "toon";
|
|
41
49
|
function format(data) {
|
|
42
|
-
if (
|
|
50
|
+
if (useToon()) {
|
|
43
51
|
try {
|
|
44
52
|
return toonEncode(data);
|
|
45
53
|
}
|
|
@@ -91,7 +99,7 @@ export function mcpRuntimeFromClient(client) {
|
|
|
91
99
|
const name = client?.name?.trim().toLowerCase();
|
|
92
100
|
return name ? name : null;
|
|
93
101
|
}
|
|
94
|
-
export async function recallSessionsHandler(deps, input, runtime = null) {
|
|
102
|
+
export async function recallSessionsHandler(deps, tenantId, input, runtime = null) {
|
|
95
103
|
try {
|
|
96
104
|
const rewrite = input.rewrite ?? mcpRewriteDefault();
|
|
97
105
|
const query = {
|
|
@@ -107,12 +115,12 @@ export async function recallSessionsHandler(deps, input, runtime = null) {
|
|
|
107
115
|
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
108
116
|
...(input.workstream !== undefined ? { workstream: input.workstream } : {}),
|
|
109
117
|
};
|
|
110
|
-
const result = await deps.recall.search(query);
|
|
118
|
+
const result = await deps.recall.search(tenantId, query);
|
|
111
119
|
const conversationId = resolveConversationByQuery(input.query ?? "") ?? undefined;
|
|
112
120
|
// Telemetry — the MCP path is the real agent-usage path; without this it
|
|
113
121
|
// is invisible to query_log.jsonl and the Recall page. Fire-and-forget,
|
|
114
122
|
// mirrors the HTTP /api/recall handler.
|
|
115
|
-
void logQuery({
|
|
123
|
+
void logQuery(tenantId, {
|
|
116
124
|
source: "mcp",
|
|
117
125
|
runtime,
|
|
118
126
|
query: input.query ?? null,
|
|
@@ -130,20 +138,20 @@ export async function recallSessionsHandler(deps, input, runtime = null) {
|
|
|
130
138
|
return err(e);
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
|
-
export async function workSummaryHandler(deps, input) {
|
|
141
|
+
export async function workSummaryHandler(deps, tenantId, input) {
|
|
134
142
|
if (!deps.workDigest) {
|
|
135
143
|
return okText("work_summary is not available in this deployment.");
|
|
136
144
|
}
|
|
137
145
|
try {
|
|
138
146
|
const date = input.date ?? localToday();
|
|
139
|
-
const digest = await buildWorkDigest(deps.workDigest, date);
|
|
147
|
+
const digest = await buildWorkDigest(deps.workDigest, tenantId, date);
|
|
140
148
|
return okText(composeWorkDigest(digest));
|
|
141
149
|
}
|
|
142
150
|
catch (e) {
|
|
143
151
|
return err(e);
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
|
-
export async function recallWorkstreamHandler(deps, input, runtime = null) {
|
|
154
|
+
export async function recallWorkstreamHandler(deps, tenantId, input, runtime = null) {
|
|
147
155
|
if (!deps.workstreams)
|
|
148
156
|
return okText("recall_workstream is not available in this deployment.");
|
|
149
157
|
try {
|
|
@@ -152,13 +160,13 @@ export async function recallWorkstreamHandler(deps, input, runtime = null) {
|
|
|
152
160
|
return okText("Provide a workstream id or label.");
|
|
153
161
|
const conversationId = resolveConversationByQuery(idOrLabel) ?? undefined;
|
|
154
162
|
const ws = deps.workstreams.store;
|
|
155
|
-
const found = (await ws.getById(idOrLabel)) ?? (await ws.findByNormalizedLabel(normalizeLabel(idOrLabel)));
|
|
163
|
+
const found = (await ws.getById(tenantId, idOrLabel)) ?? (await ws.findByNormalizedLabel(tenantId, normalizeLabel(idOrLabel)));
|
|
156
164
|
if (!found)
|
|
157
165
|
return okText(`No workstream matches "${idOrLabel}".`);
|
|
158
|
-
const view = await rollupWorkstream({ workstreams: deps.workstreams.store, sessions: deps.workstreams.sessions, facts: deps.workstreams.facts, exemplars: deps.workstreams.exemplars }, found.id);
|
|
166
|
+
const view = await rollupWorkstream({ workstreams: deps.workstreams.store, sessions: deps.workstreams.sessions, facts: deps.workstreams.facts, exemplars: deps.workstreams.exemplars }, tenantId, found.id);
|
|
159
167
|
if (!view)
|
|
160
168
|
return okText(`No workstream matches "${idOrLabel}".`);
|
|
161
|
-
void logQuery({
|
|
169
|
+
void logQuery(tenantId, {
|
|
162
170
|
source: "mcp",
|
|
163
171
|
runtime,
|
|
164
172
|
query: idOrLabel,
|
|
@@ -176,17 +184,17 @@ export async function recallWorkstreamHandler(deps, input, runtime = null) {
|
|
|
176
184
|
return err(e);
|
|
177
185
|
}
|
|
178
186
|
}
|
|
179
|
-
/** idOrLabel -> live survivor workstream (merged_into resolved) | null. One source of truth for lifecycle handlers. */
|
|
180
|
-
async function resolveWorkstream(store, idOrLabel) {
|
|
181
|
-
const found = (await store.getById(idOrLabel)) ?? (await store.findByNormalizedLabel(normalizeLabel(idOrLabel)));
|
|
187
|
+
/** idOrLabel -> live survivor workstream (merged_into resolved) | null, within tenantId. One source of truth for lifecycle handlers. */
|
|
188
|
+
async function resolveWorkstream(store, tenantId, idOrLabel) {
|
|
189
|
+
const found = (await store.getById(tenantId, idOrLabel)) ?? (await store.findByNormalizedLabel(tenantId, normalizeLabel(idOrLabel)));
|
|
182
190
|
if (!found)
|
|
183
191
|
return null;
|
|
184
|
-
const all = await store.listAll();
|
|
192
|
+
const all = await store.listAll(tenantId);
|
|
185
193
|
const byId = new Map(all.map((w) => [w.id, { id: w.id, mergedInto: w.mergedInto }]));
|
|
186
194
|
const survivorId = resolveWorkstreamId(found.id, byId);
|
|
187
|
-
return survivorId === found.id ? found : ((await store.getById(survivorId)) ?? found);
|
|
195
|
+
return survivorId === found.id ? found : ((await store.getById(tenantId, survivorId)) ?? found);
|
|
188
196
|
}
|
|
189
|
-
export async function renameWorkstreamHandler(deps, input) {
|
|
197
|
+
export async function renameWorkstreamHandler(deps, tenantId, input) {
|
|
190
198
|
if (!deps.workstreams)
|
|
191
199
|
return okText("rename_workstream is not available in this deployment.");
|
|
192
200
|
try {
|
|
@@ -195,38 +203,38 @@ export async function renameWorkstreamHandler(deps, input) {
|
|
|
195
203
|
if (!idOrLabel || !label)
|
|
196
204
|
return okText("Provide the workstream (id or label) and the new label.");
|
|
197
205
|
const ws = deps.workstreams.store;
|
|
198
|
-
const target = await resolveWorkstream(ws, idOrLabel);
|
|
206
|
+
const target = await resolveWorkstream(ws, tenantId, idOrLabel);
|
|
199
207
|
if (!target)
|
|
200
208
|
return okText(`No workstream matches "${idOrLabel}".`);
|
|
201
|
-
const collision = await ws.findByNormalizedLabel(normalizeLabel(label));
|
|
209
|
+
const collision = await ws.findByNormalizedLabel(tenantId, normalizeLabel(label));
|
|
202
210
|
if (collision && collision.id !== target.id) {
|
|
203
211
|
return okText(`Label "${label}" is already used by workstream "${collision.label}" (${collision.id}).`);
|
|
204
212
|
}
|
|
205
|
-
await ws.setLabel(target.id, label);
|
|
213
|
+
await ws.setLabel(tenantId, target.id, label);
|
|
206
214
|
return okText(`Renamed workstream ${target.id}: "${target.label}" -> "${label}".`);
|
|
207
215
|
}
|
|
208
216
|
catch (e) {
|
|
209
217
|
return err(e);
|
|
210
218
|
}
|
|
211
219
|
}
|
|
212
|
-
export async function retireWorkstreamHandler(deps, input) {
|
|
220
|
+
export async function retireWorkstreamHandler(deps, tenantId, input) {
|
|
213
221
|
if (!deps.workstreams)
|
|
214
222
|
return okText("retire_workstream is not available in this deployment.");
|
|
215
223
|
try {
|
|
216
224
|
const idOrLabel = (input.idOrLabel ?? "").trim();
|
|
217
225
|
if (!idOrLabel)
|
|
218
226
|
return okText("Provide a workstream id or label.");
|
|
219
|
-
const target = await resolveWorkstream(deps.workstreams.store, idOrLabel);
|
|
227
|
+
const target = await resolveWorkstream(deps.workstreams.store, tenantId, idOrLabel);
|
|
220
228
|
if (!target)
|
|
221
229
|
return okText(`No workstream matches "${idOrLabel}".`);
|
|
222
|
-
await deps.workstreams.store.setStatus(target.id, "retired");
|
|
230
|
+
await deps.workstreams.store.setStatus(tenantId, target.id, "retired");
|
|
223
231
|
return okText(`Retired workstream "${target.label}" (${target.id}).`);
|
|
224
232
|
}
|
|
225
233
|
catch (e) {
|
|
226
234
|
return err(e);
|
|
227
235
|
}
|
|
228
236
|
}
|
|
229
|
-
export async function mergeWorkstreamsHandler(deps, input) {
|
|
237
|
+
export async function mergeWorkstreamsHandler(deps, tenantId, input) {
|
|
230
238
|
if (!deps.workstreams)
|
|
231
239
|
return okText("merge_workstreams is not available in this deployment.");
|
|
232
240
|
try {
|
|
@@ -235,22 +243,22 @@ export async function mergeWorkstreamsHandler(deps, input) {
|
|
|
235
243
|
if (!fromArg || !intoArg)
|
|
236
244
|
return okText("Provide both `from` and `into` (workstream id or label).");
|
|
237
245
|
const ws = deps.workstreams.store;
|
|
238
|
-
const from = await resolveWorkstream(ws, fromArg);
|
|
239
|
-
const into = await resolveWorkstream(ws, intoArg);
|
|
246
|
+
const from = await resolveWorkstream(ws, tenantId, fromArg);
|
|
247
|
+
const into = await resolveWorkstream(ws, tenantId, intoArg);
|
|
240
248
|
if (!from)
|
|
241
249
|
return okText(`No workstream matches "${fromArg}".`);
|
|
242
250
|
if (!into)
|
|
243
251
|
return okText(`No workstream matches "${intoArg}".`);
|
|
244
252
|
if (from.id === into.id)
|
|
245
253
|
return okText(`"${fromArg}" and "${intoArg}" resolve to the same workstream — nothing to merge.`);
|
|
246
|
-
await ws.merge(from.id, into.id);
|
|
254
|
+
await ws.merge(tenantId, from.id, into.id);
|
|
247
255
|
return okText(`Merged "${from.label}" (${from.id}) into "${into.label}" (${into.id}).`);
|
|
248
256
|
}
|
|
249
257
|
catch (e) {
|
|
250
258
|
return err(e);
|
|
251
259
|
}
|
|
252
260
|
}
|
|
253
|
-
export async function rebindSessionHandler(deps, input) {
|
|
261
|
+
export async function rebindSessionHandler(deps, tenantId, input) {
|
|
254
262
|
if (!deps.workstreams)
|
|
255
263
|
return okText("rebind_session is not available in this deployment.");
|
|
256
264
|
try {
|
|
@@ -258,10 +266,10 @@ export async function rebindSessionHandler(deps, input) {
|
|
|
258
266
|
const wsArg = (input.workstream ?? "").trim();
|
|
259
267
|
if (!sessionId || !wsArg)
|
|
260
268
|
return okText("Provide both a sessionId and a workstream (id or label).");
|
|
261
|
-
const ws = await resolveWorkstream(deps.workstreams.store, wsArg);
|
|
269
|
+
const ws = await resolveWorkstream(deps.workstreams.store, tenantId, wsArg);
|
|
262
270
|
if (!ws)
|
|
263
271
|
return okText(`No workstream matches "${wsArg}".`);
|
|
264
|
-
await deps.store.setWorkstreamBinding(sessionId, ws.id, "operator", null);
|
|
272
|
+
await deps.store.setWorkstreamBinding(tenantId, sessionId, ws.id, "operator", null);
|
|
265
273
|
return okText(`Rebound session ${sessionId} -> workstream "${ws.label}" (${ws.id}).`);
|
|
266
274
|
}
|
|
267
275
|
catch (e) {
|
|
@@ -275,9 +283,9 @@ function localToday() {
|
|
|
275
283
|
const da = String(d.getDate()).padStart(2, "0");
|
|
276
284
|
return `${d.getFullYear()}-${mo}-${da}`;
|
|
277
285
|
}
|
|
278
|
-
export async function getSessionHandler(deps, input) {
|
|
286
|
+
export async function getSessionHandler(deps, tenantId, input) {
|
|
279
287
|
try {
|
|
280
|
-
const session = await deps.store.getById(input.id);
|
|
288
|
+
const session = await deps.store.getById(tenantId, input.id);
|
|
281
289
|
if (!session) {
|
|
282
290
|
return err(new Error(`session ${input.id} not found`));
|
|
283
291
|
}
|
|
@@ -286,26 +294,34 @@ export async function getSessionHandler(deps, input) {
|
|
|
286
294
|
...(session.supersedes ?? []),
|
|
287
295
|
...(session.supersededBy ? [session.supersededBy] : []),
|
|
288
296
|
];
|
|
289
|
-
const linked = linkedIds.length > 0 ? await deps.store.getByIds(linkedIds) : [];
|
|
297
|
+
const linked = linkedIds.length > 0 ? await deps.store.getByIds(tenantId, linkedIds) : [];
|
|
290
298
|
const byId = new Map(linked.map((s) => [s.id, s]));
|
|
291
299
|
// Load supersedence log once so we can join reason + recordedBy onto supersededBy
|
|
292
300
|
const supersedenceLog = session.supersededBy
|
|
293
|
-
? await readSupersedenceLog()
|
|
301
|
+
? await readSupersedenceLog(tenantId)
|
|
294
302
|
: [];
|
|
295
303
|
const supersedenceMap = new Map(supersedenceLog.map((e) => [e.predecessorId, e]));
|
|
296
304
|
const supersedes = (session.supersedes ?? []).map((id) => {
|
|
297
305
|
const s = byId.get(id);
|
|
298
306
|
return s ? { id, label: s.label, summary: s.summary } : { id, label: "", summary: "" };
|
|
299
307
|
});
|
|
308
|
+
// Supersedence-enrichment fencing (program spec §4.6 hardening 2): even
|
|
309
|
+
// though readSupersedenceLog now reads the caller's own tenant file, a
|
|
310
|
+
// log entry's `reason`/`recordedBy` may only surface for a successor id
|
|
311
|
+
// that resolved through the tenant-filtered getByIds above (`byId`) —
|
|
312
|
+
// defense in depth against any pre-M6 legacy entry that predates
|
|
313
|
+
// per-tenant log files. An unresolved id contributes nothing — not even
|
|
314
|
+
// its reason — same as the by-id not-found shape (case 4).
|
|
300
315
|
const supersededBy = session.supersededBy
|
|
301
316
|
? (() => {
|
|
302
317
|
const s = byId.get(session.supersededBy);
|
|
318
|
+
if (!s)
|
|
319
|
+
return { id: session.supersededBy, label: "", summary: "" };
|
|
303
320
|
const logEntry = supersedenceMap.get(session.id);
|
|
304
|
-
const base = s
|
|
305
|
-
? { id: session.supersededBy, label: s.label, summary: s.summary }
|
|
306
|
-
: { id: session.supersededBy, label: "", summary: "" };
|
|
307
321
|
return {
|
|
308
|
-
|
|
322
|
+
id: session.supersededBy,
|
|
323
|
+
label: s.label,
|
|
324
|
+
summary: s.summary,
|
|
309
325
|
reason: logEntry?.reason,
|
|
310
326
|
recordedBy: logEntry?.source,
|
|
311
327
|
};
|
|
@@ -317,7 +333,7 @@ export async function getSessionHandler(deps, input) {
|
|
|
317
333
|
let outcome;
|
|
318
334
|
if (deps.outcomeDb) {
|
|
319
335
|
try {
|
|
320
|
-
outcome = await deriveOutcome(session.id, await buildSqliteOutcomeDeps(deps.outcomeDb));
|
|
336
|
+
outcome = await deriveOutcome(tenantId, session.id, await buildSqliteOutcomeDeps(deps.outcomeDb, tenantId));
|
|
321
337
|
}
|
|
322
338
|
catch {
|
|
323
339
|
outcome = undefined;
|
|
@@ -329,7 +345,7 @@ export async function getSessionHandler(deps, input) {
|
|
|
329
345
|
return err(e);
|
|
330
346
|
}
|
|
331
347
|
}
|
|
332
|
-
export async function recallFactsHandler(deps, input, runtime = null) {
|
|
348
|
+
export async function recallFactsHandler(deps, tenantId, input, runtime = null) {
|
|
333
349
|
if (!deps.factRecall) {
|
|
334
350
|
return err(new Error("fact recall not wired in this deployment"));
|
|
335
351
|
}
|
|
@@ -348,10 +364,10 @@ export async function recallFactsHandler(deps, input, runtime = null) {
|
|
|
348
364
|
? { minConfidence: input.minConfidence }
|
|
349
365
|
: {}),
|
|
350
366
|
};
|
|
351
|
-
const result = await deps.factRecall.search(query);
|
|
367
|
+
const result = await deps.factRecall.search(tenantId, query);
|
|
352
368
|
const conversationId = resolveConversationByQuery(input.query ?? "") ?? undefined;
|
|
353
369
|
// Telemetry — see recallSessionsHandler. Fire-and-forget.
|
|
354
|
-
void logFactQuery({
|
|
370
|
+
void logFactQuery(tenantId, {
|
|
355
371
|
source: "mcp",
|
|
356
372
|
runtime,
|
|
357
373
|
query: input.query ?? null,
|
|
@@ -370,12 +386,12 @@ export async function recallFactsHandler(deps, input, runtime = null) {
|
|
|
370
386
|
return err(e);
|
|
371
387
|
}
|
|
372
388
|
}
|
|
373
|
-
export async function getFactHistoryHandler(deps, input) {
|
|
389
|
+
export async function getFactHistoryHandler(deps, tenantId, input) {
|
|
374
390
|
if (!deps.factStore) {
|
|
375
391
|
return err(new Error("fact store not wired in this deployment"));
|
|
376
392
|
}
|
|
377
393
|
try {
|
|
378
|
-
const chains = await deps.factStore.getHistory(input.subject, input.predicate);
|
|
394
|
+
const chains = await deps.factStore.getHistory(tenantId, input.subject, input.predicate);
|
|
379
395
|
return ok({ subject: input.subject, predicate: input.predicate ?? null, chains });
|
|
380
396
|
}
|
|
381
397
|
catch (e) {
|
|
@@ -537,13 +553,13 @@ Args:
|
|
|
537
553
|
|
|
538
554
|
Idempotent. Re-marking the same pair is a no-op. Returns the linked pair on
|
|
539
555
|
success; errors if either id is unknown or the two ids are equal.`;
|
|
540
|
-
export async function markSupersededHandler(deps, input) {
|
|
556
|
+
export async function markSupersededHandler(deps, tenantId, input) {
|
|
541
557
|
if (!input.predecessor_id || !input.successor_id) {
|
|
542
558
|
return err(new Error("predecessor_id and successor_id are required"));
|
|
543
559
|
}
|
|
544
560
|
try {
|
|
545
|
-
await deps.store.markSuperseded(input.predecessor_id, input.successor_id);
|
|
546
|
-
void appendSupersedence({
|
|
561
|
+
await deps.store.markSuperseded(tenantId, input.predecessor_id, input.successor_id);
|
|
562
|
+
void appendSupersedence(tenantId, {
|
|
547
563
|
predecessorId: input.predecessor_id,
|
|
548
564
|
successorId: input.successor_id,
|
|
549
565
|
source: "mcp",
|
|
@@ -581,7 +597,7 @@ Args:
|
|
|
581
597
|
|
|
582
598
|
Idempotent: calling it twice on the same id is a no-op. Errors if fact_id
|
|
583
599
|
is unknown.`;
|
|
584
|
-
export async function supersedeFactHandler(deps, input) {
|
|
600
|
+
export async function supersedeFactHandler(deps, tenantId, input) {
|
|
585
601
|
if (!input.fact_id || input.fact_id.length < 4) {
|
|
586
602
|
return err(new Error("fact_id is required"));
|
|
587
603
|
}
|
|
@@ -589,8 +605,8 @@ export async function supersedeFactHandler(deps, input) {
|
|
|
589
605
|
return err(new Error("fact store not available"));
|
|
590
606
|
}
|
|
591
607
|
try {
|
|
592
|
-
await deps.factStore.retire(input.fact_id);
|
|
593
|
-
void appendFactSupersedence({
|
|
608
|
+
await deps.factStore.retire(tenantId, input.fact_id);
|
|
609
|
+
void appendFactSupersedence(tenantId, {
|
|
594
610
|
factId: input.fact_id,
|
|
595
611
|
source: "mcp",
|
|
596
612
|
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
@@ -605,20 +621,31 @@ export async function supersedeFactHandler(deps, input) {
|
|
|
605
621
|
return err(e);
|
|
606
622
|
}
|
|
607
623
|
}
|
|
608
|
-
export async function citeSessionHandler(input) {
|
|
624
|
+
export async function citeSessionHandler(tenantId, input) {
|
|
609
625
|
if (!input.id || input.id.length < MIN_CITE_ID_LEN) {
|
|
610
626
|
return err(new Error(`id must be at least ${MIN_CITE_ID_LEN} characters`));
|
|
611
627
|
}
|
|
612
628
|
try {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
629
|
+
// Agents rarely pass conversation_id, so resolve it server-side. The
|
|
630
|
+
// surfaced-memo only knows sessions the recall HOOK injected, which is
|
|
631
|
+
// empty whenever ambient recall is off or failing — that gap silently
|
|
632
|
+
// unattributed every pull-driven citation. Fall back to the same
|
|
633
|
+
// transcript scan the recall handlers use: the runtime has already
|
|
634
|
+
// written this cite_session tool_use (carrying `id`) to the transcript
|
|
635
|
+
// before this handler runs, so the id is a reliable join key.
|
|
636
|
+
const conversationId = input.conversation_id ??
|
|
637
|
+
resolveConversationForSession(tenantId, input.id) ??
|
|
638
|
+
resolveConversationByQuery(input.id) ??
|
|
639
|
+
"mcp_tool";
|
|
640
|
+
const logged = await appendCitation(tenantId, {
|
|
641
|
+
conversationId,
|
|
617
642
|
citedId: input.id,
|
|
618
643
|
kind: "tool_use",
|
|
619
644
|
...(input.reason !== undefined ? { responsePreview: input.reason } : {}),
|
|
620
645
|
});
|
|
621
|
-
|
|
646
|
+
// An unattributable citation is dropped by the write path; reporting it as
|
|
647
|
+
// logged would make a dead telemetry path look healthy.
|
|
648
|
+
return ok({ logged, id: input.id, ...(logged ? {} : { reason: "unattributable_conversation" }) });
|
|
622
649
|
}
|
|
623
650
|
catch (e) {
|
|
624
651
|
return err(e);
|
|
@@ -640,7 +667,7 @@ Args:
|
|
|
640
667
|
* ambiguity). Tier-A correlation in rollup.ts keys on session_id only, so
|
|
641
668
|
* this choice does not affect verdict derivation. */
|
|
642
669
|
const REPORT_OUTCOME_KIND = "review";
|
|
643
|
-
export async function reportOutcomeHandler(deps, input) {
|
|
670
|
+
export async function reportOutcomeHandler(deps, tenantId, input) {
|
|
644
671
|
if (!input.session_id && !input.correlation_key) {
|
|
645
672
|
return err(new Error("session_id or correlation_key is required"));
|
|
646
673
|
}
|
|
@@ -662,28 +689,28 @@ export async function reportOutcomeHandler(deps, input) {
|
|
|
662
689
|
...(input.correlation_key ? { correlation_key: input.correlation_key } : {}),
|
|
663
690
|
},
|
|
664
691
|
}, deps.installScope);
|
|
665
|
-
signal = await stampSignalScope(signal, { sessionScopeReader: deps.sessionScopeReader });
|
|
666
|
-
await deps.signalStore.insert(signal);
|
|
692
|
+
signal = await stampSignalScope(signal, tenantId, { sessionScopeReader: deps.sessionScopeReader });
|
|
693
|
+
await deps.signalStore.insert(tenantId, signal);
|
|
667
694
|
return ok({ recorded: true, id: signal.id, outcome: signal.outcome });
|
|
668
695
|
}
|
|
669
696
|
catch (e) {
|
|
670
697
|
return err(e);
|
|
671
698
|
}
|
|
672
699
|
}
|
|
673
|
-
export async function listMergeSuggestionsHandler(deps, input) {
|
|
700
|
+
export async function listMergeSuggestionsHandler(deps, tenantId, input) {
|
|
674
701
|
if (!deps.workstreams)
|
|
675
702
|
return okText("list_merge_suggestions is not available in this deployment.");
|
|
676
703
|
try {
|
|
677
704
|
const minScore = typeof input.minScore === "number" ? input.minScore : 0.5;
|
|
678
|
-
const all = (await deps.workstreams.store.listAll()).filter((w) => w.status === "active");
|
|
705
|
+
const all = (await deps.workstreams.store.listAll(tenantId)).filter((w) => w.status === "active");
|
|
679
706
|
if (all.length < 2)
|
|
680
707
|
return okText("Not enough active workstreams to suggest merges.");
|
|
681
708
|
const ids = all.map((w) => w.id);
|
|
682
|
-
const entMap = await deps.workstreams.store.entitiesFor(ids);
|
|
709
|
+
const entMap = await deps.workstreams.store.entitiesFor(tenantId, ids);
|
|
683
710
|
const items = await Promise.all(all.map(async (w) => ({
|
|
684
711
|
id: w.id, label: w.label,
|
|
685
712
|
entities: entMap.get(w.id) ?? [],
|
|
686
|
-
sessionIds: await deps.workstreams.sessions.listSessionIdsByWorkstreams([w.id]),
|
|
713
|
+
sessionIds: await deps.workstreams.sessions.listSessionIdsByWorkstreams(tenantId, [w.id]),
|
|
687
714
|
})));
|
|
688
715
|
const suggestions = suggestMerges(items, minScore);
|
|
689
716
|
if (suggestions.length === 0)
|
|
@@ -699,7 +726,144 @@ export async function listMergeSuggestionsHandler(deps, input) {
|
|
|
699
726
|
return err(e);
|
|
700
727
|
}
|
|
701
728
|
}
|
|
702
|
-
|
|
729
|
+
const LEARNER_ENROLL_DESCRIPTION = `\
|
|
730
|
+
Enroll a learner in a course, binding the enrollment to a workstream so later
|
|
731
|
+
sessions resolve their learning state at recall time. The course plan itself
|
|
732
|
+
stays with the caller — NLM stores only its reference, revision, and
|
|
733
|
+
fingerprint, and refuses writes when a re-enrolled plan differs.`;
|
|
734
|
+
const LEARNER_RECORD_DESCRIPTION = `\
|
|
735
|
+
Record a learning event: concepts covered plus per-concept attempts (task,
|
|
736
|
+
response, result/help/kind), an interpretation, and the next step. Identical
|
|
737
|
+
event ids are safe no-ops; conflicting ids fail rather than rewriting
|
|
738
|
+
history. A submitted exercise is not evidence until recorded here.`;
|
|
739
|
+
const LEARNER_SUMMARY_DESCRIPTION = `\
|
|
740
|
+
Summarize what a learner has demonstrably learned in a course: per-concept
|
|
741
|
+
status (exposed/practicing/demonstrated/retained), the latest next step, and
|
|
742
|
+
sessions bound to the enrollment's workstream since the last recorded event
|
|
743
|
+
(unrecorded candidates — record them with learner_record, don't assume).`;
|
|
744
|
+
const LEARNER_COMPLETE_DESCRIPTION = `\
|
|
745
|
+
Mark a learner's course completed. Preserves completion history and actual
|
|
746
|
+
evidence; untaught or untested topics stay explicitly marked.`;
|
|
747
|
+
const LEARNER_SUMMARY_DEFAULT_LIMIT = 50;
|
|
748
|
+
const LEARNER_UNRECORDED_CAP = 10;
|
|
749
|
+
export async function learnerEnrollHandler(deps, tenantId, input) {
|
|
750
|
+
if (!deps.learners)
|
|
751
|
+
return okText("learner tools are not available in this deployment.");
|
|
752
|
+
try {
|
|
753
|
+
const enrollment = await enrollCourse({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, {
|
|
754
|
+
learnerId: input.learner,
|
|
755
|
+
...(input.label !== undefined ? { learnerLabel: input.label } : {}),
|
|
756
|
+
courseId: input.course,
|
|
757
|
+
workstream: input.workstream,
|
|
758
|
+
planRef: input.plan_ref,
|
|
759
|
+
planRevision: input.plan_revision,
|
|
760
|
+
planFingerprint: input.plan_fingerprint,
|
|
761
|
+
});
|
|
762
|
+
return okText(`Enrolled ${input.learner} in ${input.course} (${enrollment.status}, plan rev ${enrollment.planRevision}).`);
|
|
763
|
+
}
|
|
764
|
+
catch (e) {
|
|
765
|
+
return err(e);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
export async function learnerRecordHandler(deps, tenantId, input) {
|
|
769
|
+
if (!deps.learners)
|
|
770
|
+
return okText("learner tools are not available in this deployment.");
|
|
771
|
+
try {
|
|
772
|
+
const result = await recordLearningEvent({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, input.learner, input.course, {
|
|
773
|
+
id: input.event.id,
|
|
774
|
+
date: input.event.date,
|
|
775
|
+
covered: input.event.covered,
|
|
776
|
+
attempts: input.event.attempts,
|
|
777
|
+
interpretation: input.event.interpretation,
|
|
778
|
+
nextStep: input.event.next_step,
|
|
779
|
+
});
|
|
780
|
+
return result === "inserted"
|
|
781
|
+
? okText(`Recorded event ${input.event.id} for ${input.learner}/${input.course}.`)
|
|
782
|
+
: okText(`Event ${input.event.id} already recorded (identical) — no change.`);
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
return err(e);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
export async function learnerSummaryHandler(deps, tenantId, input) {
|
|
789
|
+
if (!deps.learners)
|
|
790
|
+
return okText("learner tools are not available in this deployment.");
|
|
791
|
+
try {
|
|
792
|
+
let course = input.course?.trim() ? input.course.trim() : undefined;
|
|
793
|
+
if (!course) {
|
|
794
|
+
const enrollments = await deps.learners.store.listEnrollments(tenantId, input.learner);
|
|
795
|
+
const active = enrollments.filter((e) => e.status === "active");
|
|
796
|
+
if (active.length === 0)
|
|
797
|
+
return okText(`No active enrollments for learner ${input.learner}.`);
|
|
798
|
+
if (active.length > 1) {
|
|
799
|
+
return okText(`Multiple active courses for ${input.learner}; specify course: ${active.map((e) => e.courseId).join(", ")}.`);
|
|
800
|
+
}
|
|
801
|
+
course = active[0].courseId;
|
|
802
|
+
}
|
|
803
|
+
const summary = await getCourseSummary({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, input.learner, course);
|
|
804
|
+
if (!summary)
|
|
805
|
+
return okText(`No enrollment for learner ${input.learner} in course ${course}.`);
|
|
806
|
+
const lines = [];
|
|
807
|
+
lines.push(`LEARNER ${summary.learnerId} · ${summary.courseId} (${summary.status}, rev ${summary.planRevision}, ${summary.eventCount} events)`);
|
|
808
|
+
const limit = input.limit ?? LEARNER_SUMMARY_DEFAULT_LIMIT;
|
|
809
|
+
const concepts = summary.concepts.slice(0, Math.max(0, limit));
|
|
810
|
+
if (concepts.length === 0) {
|
|
811
|
+
lines.push("(no concepts recorded yet)");
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
lines.push("CONCEPTS:");
|
|
815
|
+
for (const c of concepts) {
|
|
816
|
+
const n = c.attemptCount === 1 ? "1 attempt" : `${c.attemptCount} attempts`;
|
|
817
|
+
lines.push(` - [${c.status}] ${c.concept} (${n})`);
|
|
818
|
+
}
|
|
819
|
+
if (summary.concepts.length > concepts.length) {
|
|
820
|
+
lines.push(` (+${summary.concepts.length - concepts.length} more — raise limit)`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
lines.push(`Next: ${summary.nextStep ? truncateSummary(summary.nextStep) : "(none recorded)"}`);
|
|
824
|
+
if (summary.workstreamId) {
|
|
825
|
+
const ids = await deps.learners.sessions.listSessionIdsByWorkstreams(tenantId, [summary.workstreamId]);
|
|
826
|
+
if (ids.length > 0) {
|
|
827
|
+
const sessions = await deps.learners.sessions.getByIds(tenantId, ids);
|
|
828
|
+
const cutoff = summary.lastEventDate ?? "";
|
|
829
|
+
const unrecorded = sessions
|
|
830
|
+
.filter((s) => s.startedAt.slice(0, 10) > cutoff)
|
|
831
|
+
.sort((a, b) => (a.startedAt < b.startedAt ? -1 : 1))
|
|
832
|
+
.slice(0, LEARNER_UNRECORDED_CAP);
|
|
833
|
+
if (unrecorded.length > 0) {
|
|
834
|
+
lines.push(`Unrecorded sessions (${unrecorded.length}):`);
|
|
835
|
+
for (const s of unrecorded) {
|
|
836
|
+
lines.push(` - ${s.id} · ${truncateSummary(s.label, 80)} (${s.startedAt.slice(0, 10)})`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return okText(lines.join("\n"));
|
|
842
|
+
}
|
|
843
|
+
catch (e) {
|
|
844
|
+
return err(e);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
export async function learnerCompleteHandler(deps, tenantId, input) {
|
|
848
|
+
if (!deps.learners)
|
|
849
|
+
return okText("learner tools are not available in this deployment.");
|
|
850
|
+
try {
|
|
851
|
+
await completeLearnerCourse({ learners: deps.learners.store, workstreams: deps.learners.workstreams }, tenantId, input.learner, input.course);
|
|
852
|
+
return okText(`Marked ${input.course} completed for ${input.learner}.`);
|
|
853
|
+
}
|
|
854
|
+
catch (e) {
|
|
855
|
+
return err(e);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* The composition root for the MCP surface (program spec §4.6 "M2 threading
|
|
860
|
+
* semantics until M3"): `tenantId` is required here, not defaulted, and
|
|
861
|
+
* every tool handler below receives it explicitly rather than reading a
|
|
862
|
+
* module-level constant. The stdio caller (src/cli/nlm.ts) is local-mode
|
|
863
|
+
* only and always passes DEFAULT_TEAM_ID; the HTTP /mcp mount
|
|
864
|
+
* (src/http/app.ts) passes the token-resolved team (M3).
|
|
865
|
+
*/
|
|
866
|
+
export function createMcpServer(deps, tenantId) {
|
|
703
867
|
const server = new McpServer({
|
|
704
868
|
name: SERVER_NAME,
|
|
705
869
|
version: SERVER_VERSION,
|
|
@@ -746,7 +910,7 @@ export function createMcpServer(deps) {
|
|
|
746
910
|
idempotentHint: true,
|
|
747
911
|
openWorldHint: true,
|
|
748
912
|
},
|
|
749
|
-
}, async (args) => recallSessionsHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
|
|
913
|
+
}, async (args) => recallSessionsHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
|
|
750
914
|
server.registerTool("get_session", {
|
|
751
915
|
title: "Get Full NLM Session",
|
|
752
916
|
description: GET_SESSION_DESCRIPTION,
|
|
@@ -759,7 +923,7 @@ export function createMcpServer(deps) {
|
|
|
759
923
|
idempotentHint: true,
|
|
760
924
|
openWorldHint: true,
|
|
761
925
|
},
|
|
762
|
-
}, async (args) => getSessionHandler(deps, args));
|
|
926
|
+
}, async (args) => getSessionHandler(deps, tenantId, args));
|
|
763
927
|
if (deps.factRecall && deps.factStore) {
|
|
764
928
|
server.registerTool("recall_facts", {
|
|
765
929
|
title: "Recall Facts from NLM",
|
|
@@ -809,7 +973,7 @@ export function createMcpServer(deps) {
|
|
|
809
973
|
idempotentHint: true,
|
|
810
974
|
openWorldHint: true,
|
|
811
975
|
},
|
|
812
|
-
}, async (args) => recallFactsHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
|
|
976
|
+
}, async (args) => recallFactsHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
|
|
813
977
|
server.registerTool("get_fact_history", {
|
|
814
978
|
title: "Get Fact Supersedence History",
|
|
815
979
|
description: GET_FACT_HISTORY_DESCRIPTION,
|
|
@@ -826,7 +990,7 @@ export function createMcpServer(deps) {
|
|
|
826
990
|
idempotentHint: true,
|
|
827
991
|
openWorldHint: true,
|
|
828
992
|
},
|
|
829
|
-
}, async (args) => getFactHistoryHandler(deps, args));
|
|
993
|
+
}, async (args) => getFactHistoryHandler(deps, tenantId, args));
|
|
830
994
|
server.registerTool("supersede_fact", {
|
|
831
995
|
title: "Supersede a Stale NLM Fact",
|
|
832
996
|
description: SUPERSEDE_FACT_DESCRIPTION,
|
|
@@ -846,7 +1010,7 @@ export function createMcpServer(deps) {
|
|
|
846
1010
|
idempotentHint: true,
|
|
847
1011
|
openWorldHint: false,
|
|
848
1012
|
},
|
|
849
|
-
}, async (args) => supersedeFactHandler(deps, args));
|
|
1013
|
+
}, async (args) => supersedeFactHandler(deps, tenantId, args));
|
|
850
1014
|
}
|
|
851
1015
|
server.registerTool("cite_session", {
|
|
852
1016
|
title: "Cite NLM Session",
|
|
@@ -868,7 +1032,7 @@ export function createMcpServer(deps) {
|
|
|
868
1032
|
idempotentHint: false,
|
|
869
1033
|
openWorldHint: false,
|
|
870
1034
|
},
|
|
871
|
-
}, async (args) => citeSessionHandler(args));
|
|
1035
|
+
}, async (args) => citeSessionHandler(tenantId, args));
|
|
872
1036
|
if (deps.signalStore && deps.installScope !== undefined) {
|
|
873
1037
|
server.registerTool("report_outcome", {
|
|
874
1038
|
title: "Report Session Outcome",
|
|
@@ -900,7 +1064,7 @@ export function createMcpServer(deps) {
|
|
|
900
1064
|
idempotentHint: true,
|
|
901
1065
|
openWorldHint: false,
|
|
902
1066
|
},
|
|
903
|
-
}, async (args) => reportOutcomeHandler(deps, args));
|
|
1067
|
+
}, async (args) => reportOutcomeHandler(deps, tenantId, args));
|
|
904
1068
|
}
|
|
905
1069
|
server.registerTool("mark_superseded", {
|
|
906
1070
|
title: "Mark NLM Session Superseded",
|
|
@@ -925,7 +1089,7 @@ export function createMcpServer(deps) {
|
|
|
925
1089
|
idempotentHint: true,
|
|
926
1090
|
openWorldHint: false,
|
|
927
1091
|
},
|
|
928
|
-
}, async (args) => markSupersededHandler(deps, args));
|
|
1092
|
+
}, async (args) => markSupersededHandler(deps, tenantId, args));
|
|
929
1093
|
server.registerTool("work_summary", {
|
|
930
1094
|
title: "Daily work summary",
|
|
931
1095
|
description: "The operator's agent-assisted work recap for a day: where attention went (active time by topic), focus quality (context switches, longest block), and progress (decisions, open loops). Optional `date` (YYYY-MM-DD); defaults to today.",
|
|
@@ -937,7 +1101,7 @@ export function createMcpServer(deps) {
|
|
|
937
1101
|
.describe("Day to summarize, YYYY-MM-DD. Defaults to today."),
|
|
938
1102
|
},
|
|
939
1103
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
940
|
-
}, async (args) => workSummaryHandler(deps, args));
|
|
1104
|
+
}, async (args) => workSummaryHandler(deps, tenantId, args));
|
|
941
1105
|
server.registerTool("recall_workstream", {
|
|
942
1106
|
title: "Recall a workstream's accumulated context",
|
|
943
1107
|
description: "Return the coherent project view for a workstream: its member sessions, current decisions and open loops, accumulated facts, and code exemplars. Accepts a workstream id or label; merge chains resolve to the live workstream.",
|
|
@@ -945,7 +1109,7 @@ export function createMcpServer(deps) {
|
|
|
945
1109
|
idOrLabel: z.string().describe("Workstream id (ws_...) or label (e.g. 'NLM')."),
|
|
946
1110
|
},
|
|
947
1111
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
948
|
-
}, async (args) => recallWorkstreamHandler(deps, args, mcpRuntimeFromClient(server.server.getClientVersion())));
|
|
1112
|
+
}, async (args) => recallWorkstreamHandler(deps, tenantId, args, mcpRuntimeFromClient(server.server.getClientVersion())));
|
|
949
1113
|
server.registerTool("rebind_session", {
|
|
950
1114
|
title: "Rebind a session to a different workstream",
|
|
951
1115
|
description: "Move a session's primary workstream binding (operator correction). Sets binding_source=operator. The session's facts and exemplars roll up under the new workstream automatically (rollup is by session binding).",
|
|
@@ -954,7 +1118,7 @@ export function createMcpServer(deps) {
|
|
|
954
1118
|
workstream: z.string().describe("Target workstream id (ws_...) or label."),
|
|
955
1119
|
},
|
|
956
1120
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
957
|
-
}, async (args) => rebindSessionHandler(deps, args));
|
|
1121
|
+
}, async (args) => rebindSessionHandler(deps, tenantId, args));
|
|
958
1122
|
server.registerTool("merge_workstreams", {
|
|
959
1123
|
title: "Merge one workstream into another",
|
|
960
1124
|
description: "Supersede a duplicate workstream into the one to keep: sets merged_into, marks it merged, and unions its entity index. Sessions, facts, and exemplars resolve to the survivor automatically (no session rewrite). Accepts ids or labels; merge chains resolve.",
|
|
@@ -963,7 +1127,7 @@ export function createMcpServer(deps) {
|
|
|
963
1127
|
into: z.string().describe("Workstream to keep (the survivor); id or label."),
|
|
964
1128
|
},
|
|
965
1129
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
966
|
-
}, async (args) => mergeWorkstreamsHandler(deps, args));
|
|
1130
|
+
}, async (args) => mergeWorkstreamsHandler(deps, tenantId, args));
|
|
967
1131
|
server.registerTool("rename_workstream", {
|
|
968
1132
|
title: "Rename a workstream",
|
|
969
1133
|
description: "Relabel a workstream. Refuses a label that collides with a different existing workstream's normalized label (prevents accidental duplicates). Accepts id or label.",
|
|
@@ -972,19 +1136,19 @@ export function createMcpServer(deps) {
|
|
|
972
1136
|
label: z.string().describe("New label."),
|
|
973
1137
|
},
|
|
974
1138
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
975
|
-
}, async (args) => renameWorkstreamHandler(deps, args));
|
|
1139
|
+
}, async (args) => renameWorkstreamHandler(deps, tenantId, args));
|
|
976
1140
|
server.registerTool("retire_workstream", {
|
|
977
1141
|
title: "Retire a workstream",
|
|
978
1142
|
description: "Mark a workstream retired (status=retired). Operator cleanup for dead one-off workstreams. Reversible by re-setting status. Accepts id or label.",
|
|
979
1143
|
inputSchema: { idOrLabel: z.string().describe("Workstream id or label.") },
|
|
980
1144
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
981
|
-
}, async (args) => retireWorkstreamHandler(deps, args));
|
|
1145
|
+
}, async (args) => retireWorkstreamHandler(deps, tenantId, args));
|
|
982
1146
|
server.registerTool("list_merge_suggestions", {
|
|
983
1147
|
title: "Suggest duplicate workstreams to merge",
|
|
984
1148
|
description: "Score active workstream pairs by shared entities, co-occurring sessions, and label similarity; list likely duplicates for one-click merge_workstreams. Computed on demand; read-only.",
|
|
985
1149
|
inputSchema: { minScore: z.number().optional().describe("Minimum similarity score 0..1 (default 0.5).") },
|
|
986
1150
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
987
|
-
}, async (args) => listMergeSuggestionsHandler(deps, args));
|
|
1151
|
+
}, async (args) => listMergeSuggestionsHandler(deps, tenantId, args));
|
|
988
1152
|
if (deps.exemplarStore &&
|
|
989
1153
|
deps.installScope &&
|
|
990
1154
|
process.env["NLM_CODE_EXEMPLARS_ENABLED"] === "1") {
|
|
@@ -1026,7 +1190,7 @@ export function createMcpServer(deps) {
|
|
|
1026
1190
|
openWorldHint: false,
|
|
1027
1191
|
},
|
|
1028
1192
|
}, async (args) => {
|
|
1029
|
-
const result = await recallCode({
|
|
1193
|
+
const result = await recallCode(tenantId, {
|
|
1030
1194
|
query: args.query,
|
|
1031
1195
|
installScope,
|
|
1032
1196
|
...(args.repo ? { repo: args.repo } : {}),
|
|
@@ -1039,7 +1203,7 @@ export function createMcpServer(deps) {
|
|
|
1039
1203
|
...result.positives.map((e) => e.id),
|
|
1040
1204
|
...result.negatives.map((e) => e.id),
|
|
1041
1205
|
];
|
|
1042
|
-
void logQuery({
|
|
1206
|
+
void logQuery(tenantId, {
|
|
1043
1207
|
source: "mcp",
|
|
1044
1208
|
runtime: mcpRuntimeFromClient(server.server.getClientVersion()),
|
|
1045
1209
|
query: args.query,
|
|
@@ -1072,10 +1236,69 @@ export function createMcpServer(deps) {
|
|
|
1072
1236
|
if (patch.retired === undefined && patch.outcome === undefined) {
|
|
1073
1237
|
return { content: [{ type: "text", text: JSON.stringify({ error: "provide retire and/or outcome" }) }] };
|
|
1074
1238
|
}
|
|
1075
|
-
const res = await exemplarStore.setVerdict(a.exemplar_id, patch, "human");
|
|
1239
|
+
const res = await exemplarStore.setVerdict(tenantId, a.exemplar_id, patch, "human");
|
|
1076
1240
|
return { content: [{ type: "text", text: JSON.stringify({ exemplar_id: a.exemplar_id, status: res.status }) }] };
|
|
1077
1241
|
});
|
|
1078
1242
|
}
|
|
1243
|
+
if (deps.learners) {
|
|
1244
|
+
server.registerTool("learner_enroll", {
|
|
1245
|
+
title: "Enroll Learner in Course",
|
|
1246
|
+
description: LEARNER_ENROLL_DESCRIPTION,
|
|
1247
|
+
inputSchema: {
|
|
1248
|
+
learner: z.string().min(1).describe("Learner id (lowercase slug). Auto-created on first enroll."),
|
|
1249
|
+
label: z.string().optional().describe("Display label for a new learner; defaults to the id."),
|
|
1250
|
+
course: z.string().min(1).describe("Course id (lowercase slug)."),
|
|
1251
|
+
workstream: z.string().min(1).describe("Workstream id or label to bind the enrollment to."),
|
|
1252
|
+
plan_ref: z.string().min(1).describe("Caller-side course plan reference (NLM stores the ref, not the plan)."),
|
|
1253
|
+
plan_revision: z.number().int().min(1).describe("Plan revision; a changed plan must bump, never silently upsert."),
|
|
1254
|
+
plan_fingerprint: z.string().min(1).describe("Plan content fingerprint; mismatch blocks writes until reconciled."),
|
|
1255
|
+
},
|
|
1256
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1257
|
+
}, async (args) => learnerEnrollHandler(deps, tenantId, args));
|
|
1258
|
+
server.registerTool("learner_record", {
|
|
1259
|
+
title: "Record Learning Event",
|
|
1260
|
+
description: LEARNER_RECORD_DESCRIPTION,
|
|
1261
|
+
inputSchema: {
|
|
1262
|
+
learner: z.string().min(1).describe("Learner id."),
|
|
1263
|
+
course: z.string().min(1).describe("Course id."),
|
|
1264
|
+
event: z.object({
|
|
1265
|
+
id: z.string().min(1).describe("Event id (lowercase slug, unique per enrollment)."),
|
|
1266
|
+
date: z.string().describe("ISO date YYYY-MM-DD, never in the future."),
|
|
1267
|
+
covered: z.array(z.string()).describe("Concept ids covered (subject.concept)."),
|
|
1268
|
+
attempts: z.array(z.object({
|
|
1269
|
+
concept: z.string().describe("Concept id attempted."),
|
|
1270
|
+
task: z.string().min(1).describe("The task given."),
|
|
1271
|
+
response: z.string().min(1).describe("Short excerpt of the learner's response."),
|
|
1272
|
+
result: z.enum(["correct", "partial", "incorrect"]),
|
|
1273
|
+
help: z.enum(["none", "hint", "worked-example"]),
|
|
1274
|
+
kind: z.enum(["application", "retrieval", "transfer"]),
|
|
1275
|
+
})).describe("Per-concept attempts (may be empty for an untested lesson)."),
|
|
1276
|
+
interpretation: z.string().describe("Tentative explanation of the evidence (may be empty)."),
|
|
1277
|
+
next_step: z.string().min(1).describe("What the next teacher should try or ask."),
|
|
1278
|
+
}).describe("The learning event to record."),
|
|
1279
|
+
},
|
|
1280
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1281
|
+
}, async (args) => learnerRecordHandler(deps, tenantId, args));
|
|
1282
|
+
server.registerTool("learner_summary", {
|
|
1283
|
+
title: "Summarize Learner Progress",
|
|
1284
|
+
description: LEARNER_SUMMARY_DESCRIPTION,
|
|
1285
|
+
inputSchema: {
|
|
1286
|
+
learner: z.string().min(1).describe("Learner id."),
|
|
1287
|
+
course: z.string().optional().describe("Course id. Omit only when exactly one active enrollment exists."),
|
|
1288
|
+
limit: z.number().int().min(1).max(200).optional().describe("Max concepts to list (default 50)."),
|
|
1289
|
+
},
|
|
1290
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1291
|
+
}, async (args) => learnerSummaryHandler(deps, tenantId, args));
|
|
1292
|
+
server.registerTool("learner_complete_course", {
|
|
1293
|
+
title: "Complete Learner Course",
|
|
1294
|
+
description: LEARNER_COMPLETE_DESCRIPTION,
|
|
1295
|
+
inputSchema: {
|
|
1296
|
+
learner: z.string().min(1).describe("Learner id."),
|
|
1297
|
+
course: z.string().min(1).describe("Course id."),
|
|
1298
|
+
},
|
|
1299
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1300
|
+
}, async (args) => learnerCompleteHandler(deps, tenantId, args));
|
|
1301
|
+
}
|
|
1079
1302
|
return server;
|
|
1080
1303
|
}
|
|
1081
1304
|
const RECALL_CODE_DESCRIPTION = `\
|