@vellumai/assistant 0.7.2 → 0.8.0
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/ARCHITECTURE.md +45 -29
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/docs/architecture/memory.md +5 -2
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +470 -25
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +6 -33
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +12 -7
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +457 -8
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +38 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +2 -20
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +36 -16
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +60 -5
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-request-resolvers.ts +3 -32
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +11 -10
- package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
- package/src/cli/commands/oauth/connect.ts +124 -40
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +13 -5
- package/src/config/loader.ts +199 -27
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +76 -12
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
- package/src/daemon/conversation-agent-loop.ts +183 -43
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +22 -10
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +211 -29
- package/src/daemon/conversation-tool-setup.ts +66 -19
- package/src/daemon/conversation.ts +18 -23
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +26 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +47 -22
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +50 -4
- package/src/daemon/host-file-proxy.ts +44 -8
- package/src/daemon/host-transfer-proxy.ts +97 -6
- package/src/daemon/lifecycle.ts +167 -101
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +66 -15
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +22 -1
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +119 -0
- package/src/filing/filing-service.ts +29 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +149 -38
- package/src/ipc/gateway-client.ts +37 -3
- package/src/ipc/skill-server.ts +99 -42
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +1 -16
- package/src/memory/context-search/sources/memory.ts +2 -3
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +72 -61
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +11 -67
- package/src/memory/graph/graph-search.ts +4 -24
- package/src/memory/graph/retriever.test.ts +12 -1
- package/src/memory/graph/retriever.ts +10 -15
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +53 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +85 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +7 -0
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +10 -72
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +346 -255
- package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
- package/src/memory/v2/__tests__/injection.test.ts +297 -190
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +113 -196
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +77 -14
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +69 -20
- package/src/memory/v2/injection.ts +75 -68
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +41 -1
- package/src/memory/v2/qdrant.ts +306 -46
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +77 -110
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +26 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +17 -10
- package/src/notifications/copy-composer.ts +47 -0
- package/src/notifications/decision-engine.ts +46 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +36 -4
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +914 -0
- package/src/proactive-artifact/job.ts +366 -0
- package/src/proactive-artifact/message-copy.ts +58 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +27 -15
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +165 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +37 -17
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +136 -17
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/include-graph.ts +35 -13
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +14 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +43 -3
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +14 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
package/src/memory/v2/qdrant.ts
CHANGED
|
@@ -48,20 +48,43 @@ export interface ConceptPagePayload {
|
|
|
48
48
|
export interface ConceptPageQueryResult {
|
|
49
49
|
slug: string;
|
|
50
50
|
/**
|
|
51
|
-
* Dense cosine similarity, when the slug appeared in
|
|
52
|
-
* `undefined` if the slug only appeared in the
|
|
51
|
+
* Dense cosine similarity against the page body, when the slug appeared in
|
|
52
|
+
* the body dense top-`limit`. `undefined` if the slug only appeared in the
|
|
53
|
+
* sparse channel — or in a summary-side channel.
|
|
53
54
|
*/
|
|
54
55
|
denseScore?: number;
|
|
55
56
|
/**
|
|
56
|
-
* Sparse score, when the slug appeared in the
|
|
57
|
-
* `undefined` if the slug only appeared in the dense
|
|
58
|
-
* different scale than `denseScore` — callers must
|
|
57
|
+
* Sparse score against the page body, when the slug appeared in the body
|
|
58
|
+
* sparse top-`limit`. `undefined` if the slug only appeared in the dense
|
|
59
|
+
* channel. Lives on a different scale than `denseScore` — callers must
|
|
60
|
+
* normalize before fusing.
|
|
59
61
|
*/
|
|
60
62
|
sparseScore?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Dense cosine similarity against the page's frontmatter `summary`, when
|
|
65
|
+
* the page has a summary embedded and the slug appeared in the summary
|
|
66
|
+
* dense top-`limit`. `undefined` for pages without a summary embedding —
|
|
67
|
+
* those fall back to body-only scoring.
|
|
68
|
+
*/
|
|
69
|
+
summaryDenseScore?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Sparse score against the page's frontmatter `summary`, paired with
|
|
72
|
+
* `summaryDenseScore`. `undefined` for pages without a summary embedding.
|
|
73
|
+
*/
|
|
74
|
+
summarySparseScore?: number;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
let _client: QdrantRestClient | null = null;
|
|
64
78
|
let _collectionReady = false;
|
|
79
|
+
let _collectionReadyPromise: Promise<{ migrated: boolean }> | null = null;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Named vectors the v2 concept-page collection must expose. Existing
|
|
83
|
+
* collections that lack any of these get destructively recreated by
|
|
84
|
+
* `ensureConceptPageCollectionOnce` — see the `migrated` return flag.
|
|
85
|
+
*/
|
|
86
|
+
const REQUIRED_DENSE_VECTORS = ["dense", "summary_dense"] as const;
|
|
87
|
+
const REQUIRED_SPARSE_VECTORS = ["sparse", "summary_sparse"] as const;
|
|
65
88
|
|
|
66
89
|
/** Lazily create a Qdrant REST client bound to the resolved URL. */
|
|
67
90
|
function getClient(): QdrantRestClient {
|
|
@@ -75,27 +98,67 @@ function getClient(): QdrantRestClient {
|
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
/**
|
|
78
|
-
* Create the v2 concept-page collection if it does not already exist
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
101
|
+
* Create the v2 concept-page collection if it does not already exist, or
|
|
102
|
+
* destructively recreate it when the existing schema is missing any of the
|
|
103
|
+
* required named vectors (see `REQUIRED_DENSE_VECTORS` /
|
|
104
|
+
* `REQUIRED_SPARSE_VECTORS`). The latter case is signalled to callers via
|
|
105
|
+
* `{ migrated: true }` so they can enqueue a backfill — pre-#29823
|
|
106
|
+
* collections lack `summary_dense` / `summary_sparse` and every query
|
|
107
|
+
* referencing those named vectors fails with HTTP 400 until the collection
|
|
108
|
+
* is rebuilt. Mirrors `VellumQdrantClient.ensureCollection` for v1.
|
|
85
109
|
*/
|
|
86
|
-
export async function ensureConceptPageCollection(): Promise<
|
|
87
|
-
|
|
110
|
+
export async function ensureConceptPageCollection(): Promise<{
|
|
111
|
+
migrated: boolean;
|
|
112
|
+
}> {
|
|
113
|
+
if (_collectionReady) return { migrated: false };
|
|
114
|
+
if (_collectionReadyPromise) return _collectionReadyPromise;
|
|
115
|
+
|
|
116
|
+
_collectionReadyPromise = ensureConceptPageCollectionOnce().finally(() => {
|
|
117
|
+
_collectionReadyPromise = null;
|
|
118
|
+
});
|
|
119
|
+
return _collectionReadyPromise;
|
|
120
|
+
}
|
|
88
121
|
|
|
122
|
+
async function ensureConceptPageCollectionOnce(): Promise<{
|
|
123
|
+
migrated: boolean;
|
|
124
|
+
}> {
|
|
89
125
|
const client = getClient();
|
|
90
126
|
const config = getConfig();
|
|
91
127
|
const vectorSize = config.memory.qdrant.vectorSize;
|
|
92
128
|
const onDisk = config.memory.qdrant.onDisk;
|
|
93
129
|
|
|
130
|
+
let migrated = false;
|
|
131
|
+
|
|
94
132
|
try {
|
|
95
133
|
const exists = await client.collectionExists(MEMORY_V2_COLLECTION);
|
|
96
134
|
if (exists.exists) {
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
// Assume compatible on probe failure rather than risk a destructive
|
|
136
|
+
// recreate — mirrors v1's posture in `VellumQdrantClient.ensureCollection`.
|
|
137
|
+
let info: Awaited<ReturnType<typeof client.getCollection>>;
|
|
138
|
+
try {
|
|
139
|
+
info = await client.getCollection(MEMORY_V2_COLLECTION);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
log.warn(
|
|
142
|
+
{ err, collection: MEMORY_V2_COLLECTION },
|
|
143
|
+
"Failed to probe v2 collection schema; assuming compatible",
|
|
144
|
+
);
|
|
145
|
+
_collectionReady = true;
|
|
146
|
+
return { migrated: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const missing = missingNamedVectors(info);
|
|
150
|
+
if (missing.length === 0) {
|
|
151
|
+
_collectionReady = true;
|
|
152
|
+
return { migrated: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log.warn(
|
|
156
|
+
{ collection: MEMORY_V2_COLLECTION, missingNamedVectors: missing },
|
|
157
|
+
"Memory v2 concept-page collection schema drift detected — deleting and recreating; embeddings will be regenerated by background reembed",
|
|
158
|
+
);
|
|
159
|
+
await client.deleteCollection(MEMORY_V2_COLLECTION);
|
|
160
|
+
migrated = true;
|
|
161
|
+
// Fall through to creation below.
|
|
99
162
|
}
|
|
100
163
|
} catch (err) {
|
|
101
164
|
// Treat "not found"-shaped errors as "needs creation" and fall through.
|
|
@@ -115,15 +178,28 @@ export async function ensureConceptPageCollection(): Promise<void> {
|
|
|
115
178
|
distance: "Cosine",
|
|
116
179
|
on_disk: onDisk,
|
|
117
180
|
},
|
|
181
|
+
// Optional second dense vector covering the page's frontmatter
|
|
182
|
+
// `summary`. Pages without a summary store nothing under this name —
|
|
183
|
+
// Qdrant supports per-point named-vector subsets — so the named-vector
|
|
184
|
+
// index stays cheap until summaries are populated.
|
|
185
|
+
summary_dense: {
|
|
186
|
+
size: vectorSize,
|
|
187
|
+
distance: "Cosine",
|
|
188
|
+
on_disk: onDisk,
|
|
189
|
+
},
|
|
118
190
|
},
|
|
119
191
|
sparse_vectors: {
|
|
120
192
|
sparse: {}, // Qdrant auto-infers sparse vector params
|
|
193
|
+
summary_sparse: {}, // BM25 sparse vector for the summary
|
|
121
194
|
},
|
|
122
195
|
hnsw_config: {
|
|
123
196
|
on_disk: onDisk,
|
|
124
197
|
m: 16,
|
|
125
198
|
ef_construct: 100,
|
|
126
199
|
},
|
|
200
|
+
optimizers_config: {
|
|
201
|
+
default_segment_number: 2,
|
|
202
|
+
},
|
|
127
203
|
on_disk_payload: onDisk,
|
|
128
204
|
});
|
|
129
205
|
} catch (err) {
|
|
@@ -134,7 +210,7 @@ export async function ensureConceptPageCollection(): Promise<void> {
|
|
|
134
210
|
(err as { status: number }).status === 409
|
|
135
211
|
) {
|
|
136
212
|
_collectionReady = true;
|
|
137
|
-
return;
|
|
213
|
+
return { migrated };
|
|
138
214
|
}
|
|
139
215
|
throw err;
|
|
140
216
|
}
|
|
@@ -147,32 +223,86 @@ export async function ensureConceptPageCollection(): Promise<void> {
|
|
|
147
223
|
});
|
|
148
224
|
|
|
149
225
|
_collectionReady = true;
|
|
226
|
+
return { migrated };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Return the names of required named vectors absent from the collection's
|
|
231
|
+
* current schema. An empty array means the collection is fully migrated.
|
|
232
|
+
*
|
|
233
|
+
* If the response shape is unparseable (e.g. Qdrant returns an unexpected
|
|
234
|
+
* structure) we treat it as "everything is missing" so the caller's drift
|
|
235
|
+
* branch fires — combined with the `getCollection` try/catch in the caller,
|
|
236
|
+
* a thrown probe falls back to "assume compatible" while a parsed-but-empty
|
|
237
|
+
* response triggers the safer recreate.
|
|
238
|
+
*/
|
|
239
|
+
function missingNamedVectors(
|
|
240
|
+
info: Awaited<ReturnType<QdrantRestClient["getCollection"]>>,
|
|
241
|
+
): string[] {
|
|
242
|
+
const params = info.config?.params;
|
|
243
|
+
const dense = params?.vectors;
|
|
244
|
+
const sparse = (params as { sparse_vectors?: unknown } | undefined)
|
|
245
|
+
?.sparse_vectors;
|
|
246
|
+
const denseNames =
|
|
247
|
+
dense && typeof dense === "object" && !("size" in dense)
|
|
248
|
+
? new Set(Object.keys(dense))
|
|
249
|
+
: new Set<string>();
|
|
250
|
+
const sparseNames =
|
|
251
|
+
sparse && typeof sparse === "object"
|
|
252
|
+
? new Set(Object.keys(sparse as Record<string, unknown>))
|
|
253
|
+
: new Set<string>();
|
|
254
|
+
|
|
255
|
+
const missing: string[] = [];
|
|
256
|
+
for (const name of REQUIRED_DENSE_VECTORS) {
|
|
257
|
+
if (!denseNames.has(name)) missing.push(name);
|
|
258
|
+
}
|
|
259
|
+
for (const name of REQUIRED_SPARSE_VECTORS) {
|
|
260
|
+
if (!sparseNames.has(name)) missing.push(name);
|
|
261
|
+
}
|
|
262
|
+
return missing;
|
|
150
263
|
}
|
|
151
264
|
|
|
152
265
|
/**
|
|
153
266
|
* Upsert a concept page's dense + sparse embedding. The point ID is derived
|
|
154
267
|
* deterministically from the slug so subsequent calls for the same slug
|
|
155
268
|
* replace the prior point in place rather than accumulating duplicates.
|
|
269
|
+
*
|
|
270
|
+
* `summary` is optional — supplied when the page's frontmatter carries a
|
|
271
|
+
* `summary`, omitted otherwise. Pages without a summary store only the body
|
|
272
|
+
* vectors and fall back to body-only scoring at query time. The grouped
|
|
273
|
+
* shape enforces at the type level that summary dense and sparse are
|
|
274
|
+
* always written together.
|
|
156
275
|
*/
|
|
157
276
|
export async function upsertConceptPageEmbedding(params: {
|
|
158
277
|
slug: string;
|
|
159
278
|
dense: number[];
|
|
160
279
|
sparse: SparseEmbedding;
|
|
280
|
+
summary?: { dense: number[]; sparse: SparseEmbedding };
|
|
161
281
|
updatedAt: number;
|
|
162
282
|
}): Promise<void> {
|
|
163
283
|
await ensureConceptPageCollection();
|
|
164
284
|
|
|
165
|
-
const { slug, dense, sparse, updatedAt } = params;
|
|
285
|
+
const { slug, dense, sparse, summary, updatedAt } = params;
|
|
166
286
|
const client = getClient();
|
|
167
287
|
const pointId = pointIdForSlug(slug);
|
|
168
288
|
|
|
289
|
+
// Qdrant lets us upsert any subset of named vectors per point. The summary
|
|
290
|
+
// entries appear only when the caller passed a `summary` block — pairing
|
|
291
|
+
// them at the type level keeps query-time fusion symmetric with the body
|
|
292
|
+
// channels.
|
|
293
|
+
const vector: Record<string, number[] | SparseEmbedding> = { dense, sparse };
|
|
294
|
+
if (summary) {
|
|
295
|
+
vector.summary_dense = summary.dense;
|
|
296
|
+
vector.summary_sparse = summary.sparse;
|
|
297
|
+
}
|
|
298
|
+
|
|
169
299
|
const upsertOnce = () =>
|
|
170
300
|
client.upsert(MEMORY_V2_COLLECTION, {
|
|
171
301
|
wait: true,
|
|
172
302
|
points: [
|
|
173
303
|
{
|
|
174
304
|
id: pointId,
|
|
175
|
-
vector
|
|
305
|
+
vector,
|
|
176
306
|
payload: { slug, updated_at: updatedAt },
|
|
177
307
|
},
|
|
178
308
|
],
|
|
@@ -215,15 +345,134 @@ export async function deleteConceptPageEmbedding(slug: string): Promise<void> {
|
|
|
215
345
|
}
|
|
216
346
|
}
|
|
217
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Remove every point whose slug starts with the given prefix and whose
|
|
350
|
+
* remaining suffix is not in `activeSuffixes`. Used by the skill-seed flow to
|
|
351
|
+
* drop stale `skills/<id>` slugs after a skill is uninstalled or disabled,
|
|
352
|
+
* since skills now share the concept-page collection rather than living in a
|
|
353
|
+
* dedicated one.
|
|
354
|
+
*
|
|
355
|
+
* Idempotent: when the live `<prefix>*` slugs already match `activeSuffixes`,
|
|
356
|
+
* the function performs a single scroll and no deletes.
|
|
357
|
+
*/
|
|
358
|
+
export async function pruneSlugsWithPrefixExcept(
|
|
359
|
+
prefix: string,
|
|
360
|
+
activeSuffixes: readonly string[],
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
await ensureConceptPageCollection();
|
|
363
|
+
|
|
364
|
+
const client = getClient();
|
|
365
|
+
const activeSet = new Set(activeSuffixes);
|
|
366
|
+
|
|
367
|
+
const doPrune = async (): Promise<void> => {
|
|
368
|
+
const stalePointIds: Array<string | number> = [];
|
|
369
|
+
let offset: string | number | undefined = undefined;
|
|
370
|
+
const maxIterations = 10_000;
|
|
371
|
+
const batchSize = 256;
|
|
372
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
373
|
+
const result = await client.scroll(MEMORY_V2_COLLECTION, {
|
|
374
|
+
limit: batchSize,
|
|
375
|
+
with_payload: true,
|
|
376
|
+
with_vector: false,
|
|
377
|
+
...(offset !== undefined ? { offset } : {}),
|
|
378
|
+
});
|
|
379
|
+
for (const point of result.points) {
|
|
380
|
+
const slug = (point.payload as { slug?: unknown } | null)?.slug;
|
|
381
|
+
if (typeof slug !== "string") continue;
|
|
382
|
+
if (!slug.startsWith(prefix)) continue;
|
|
383
|
+
const suffix = slug.slice(prefix.length);
|
|
384
|
+
if (!activeSet.has(suffix)) {
|
|
385
|
+
stalePointIds.push(point.id);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const next = result.next_page_offset;
|
|
389
|
+
if (next == null) break;
|
|
390
|
+
offset = typeof next === "string" ? next : (next as number);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (stalePointIds.length === 0) return;
|
|
394
|
+
|
|
395
|
+
await client.delete(MEMORY_V2_COLLECTION, {
|
|
396
|
+
wait: true,
|
|
397
|
+
points: stalePointIds,
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
await doPrune();
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (isCollectionMissing(err)) {
|
|
405
|
+
_collectionReady = false;
|
|
406
|
+
await ensureConceptPageCollection();
|
|
407
|
+
await doPrune();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Approximate count of points in the v2 concept-page collection. Used by the
|
|
416
|
+
* daemon-startup rebuild hook to detect "collection exists but empty" — the
|
|
417
|
+
* crash-mid-rebuild recovery case where a prior boot dropped + recreated the
|
|
418
|
+
* collection but died before reembed completed. Returns `0` if the collection
|
|
419
|
+
* does not exist or the count call fails (treated as "needs reembed" by the
|
|
420
|
+
* caller).
|
|
421
|
+
*/
|
|
422
|
+
export async function countConceptPagePoints(): Promise<number> {
|
|
423
|
+
await ensureConceptPageCollection();
|
|
424
|
+
try {
|
|
425
|
+
const result = await getClient().count(MEMORY_V2_COLLECTION, {
|
|
426
|
+
exact: false,
|
|
427
|
+
});
|
|
428
|
+
return result.count;
|
|
429
|
+
} catch (err) {
|
|
430
|
+
log.warn(
|
|
431
|
+
{ err, collection: MEMORY_V2_COLLECTION },
|
|
432
|
+
"Failed to count v2 concept-page collection — treating as empty",
|
|
433
|
+
);
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Best-effort delete of the legacy `memory_v2_skills` Qdrant collection. Skill
|
|
440
|
+
* embeddings now live alongside concept pages in `memory_v2_concept_pages`
|
|
441
|
+
* under the `skills/<id>` slug prefix, so the dedicated collection is dead
|
|
442
|
+
* weight on installs upgraded from the split-collection era. Fire-and-forget:
|
|
443
|
+
* on a fresh install (collection never existed) or a transient Qdrant
|
|
444
|
+
* unavailable, we log and move on.
|
|
445
|
+
*/
|
|
446
|
+
export async function dropLegacySkillsCollection(): Promise<void> {
|
|
447
|
+
try {
|
|
448
|
+
const client = getClient();
|
|
449
|
+
const exists = await client.collectionExists("memory_v2_skills");
|
|
450
|
+
if (!exists.exists) return;
|
|
451
|
+
await client.deleteCollection("memory_v2_skills");
|
|
452
|
+
log.info("Deleted legacy memory_v2_skills Qdrant collection");
|
|
453
|
+
} catch (err) {
|
|
454
|
+
log.warn(
|
|
455
|
+
{ err },
|
|
456
|
+
"Failed to drop legacy memory_v2_skills collection — non-fatal",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
218
461
|
/**
|
|
219
462
|
* Run separate dense and sparse queries against the concept-page collection
|
|
220
463
|
* and return per-channel scores per slug. Callers fuse these — typically via
|
|
221
464
|
* a normalized weighted-sum — because RRF would discard the score magnitudes
|
|
222
465
|
* the activation formula needs.
|
|
223
466
|
*
|
|
467
|
+
* Four channels are queried concurrently: body dense, body sparse, summary
|
|
468
|
+
* dense, summary sparse. The summary channels only return hits for pages whose
|
|
469
|
+
* frontmatter carries a `summary` (and therefore stored `summary_dense` /
|
|
470
|
+
* `summary_sparse` named vectors at upsert time). Pages without a summary
|
|
471
|
+
* surface body-only scores; callers fall back to body-only fusion for those.
|
|
472
|
+
*
|
|
224
473
|
* Each channel returns up to `limit` hits. A slug is included in the result
|
|
225
|
-
* if it appears in
|
|
226
|
-
*
|
|
474
|
+
* if it appears in any channel; missing channel scores stay `undefined` so
|
|
475
|
+
* callers can distinguish "no match in this channel" from "match with score 0".
|
|
227
476
|
*
|
|
228
477
|
* `restrictToSlugs`, when provided, filters the search server-side to only
|
|
229
478
|
* those slugs (Qdrant `slug IN [...]` filter). Used by `simBatch` when the
|
|
@@ -257,42 +506,51 @@ export async function hybridQueryConceptPages(
|
|
|
257
506
|
// Qdrant 1.13.x sparse-index crash that we've reproduced in the wild.
|
|
258
507
|
const skipSparse = options?.skipSparse ?? false;
|
|
259
508
|
|
|
260
|
-
const
|
|
509
|
+
const queryDense = (using: string) =>
|
|
261
510
|
client.query(MEMORY_V2_COLLECTION, {
|
|
262
511
|
query: dense,
|
|
263
|
-
using
|
|
512
|
+
using,
|
|
264
513
|
limit,
|
|
265
514
|
with_payload: true,
|
|
266
515
|
filter,
|
|
267
516
|
});
|
|
268
|
-
const
|
|
517
|
+
const querySparse = (using: string) =>
|
|
269
518
|
client.query(MEMORY_V2_COLLECTION, {
|
|
270
519
|
query: sparse,
|
|
271
|
-
using
|
|
520
|
+
using,
|
|
272
521
|
limit,
|
|
273
522
|
with_payload: true,
|
|
274
523
|
filter,
|
|
275
524
|
});
|
|
276
525
|
|
|
277
|
-
// Run
|
|
278
|
-
// When sparse is gated off
|
|
279
|
-
// below stays uniform; the empty `points: []` matches
|
|
280
|
-
// no-hit Qdrant response.
|
|
526
|
+
// Run all four channels concurrently — they hit independent named vectors.
|
|
527
|
+
// When sparse is gated off the sparse channels still resolve a Promise so
|
|
528
|
+
// the destructuring below stays uniform; the empty `points: []` matches
|
|
529
|
+
// the shape of a no-hit Qdrant response.
|
|
281
530
|
const emptyResult = {
|
|
282
531
|
points: [] as Array<{ payload?: unknown; score?: number }>,
|
|
283
532
|
};
|
|
284
533
|
const runQueries = async () =>
|
|
285
|
-
Promise.all([
|
|
534
|
+
Promise.all([
|
|
535
|
+
queryDense("dense"),
|
|
536
|
+
skipSparse ? emptyResult : querySparse("sparse"),
|
|
537
|
+
queryDense("summary_dense"),
|
|
538
|
+
skipSparse ? emptyResult : querySparse("summary_sparse"),
|
|
539
|
+
]);
|
|
286
540
|
|
|
287
541
|
let denseResults;
|
|
288
542
|
let sparseResults;
|
|
543
|
+
let summaryDenseResults;
|
|
544
|
+
let summarySparseResults;
|
|
289
545
|
try {
|
|
290
|
-
[denseResults, sparseResults] =
|
|
546
|
+
[denseResults, sparseResults, summaryDenseResults, summarySparseResults] =
|
|
547
|
+
await runQueries();
|
|
291
548
|
} catch (err) {
|
|
292
549
|
if (isCollectionMissing(err)) {
|
|
293
550
|
_collectionReady = false;
|
|
294
551
|
await ensureConceptPageCollection();
|
|
295
|
-
[denseResults, sparseResults] =
|
|
552
|
+
[denseResults, sparseResults, summaryDenseResults, summarySparseResults] =
|
|
553
|
+
await runQueries();
|
|
296
554
|
} else {
|
|
297
555
|
throw err;
|
|
298
556
|
}
|
|
@@ -301,21 +559,22 @@ export async function hybridQueryConceptPages(
|
|
|
301
559
|
// Merge by slug. Missing-side scores stay undefined so the fuser can tell
|
|
302
560
|
// "no match in this channel" apart from "match with score 0".
|
|
303
561
|
const merged = new Map<string, ConceptPageQueryResult>();
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
existing.sparseScore = point.score ?? 0;
|
|
315
|
-
} else {
|
|
316
|
-
merged.set(slug, { slug, sparseScore: point.score ?? 0 });
|
|
562
|
+
const recordHit = (
|
|
563
|
+
points: Array<{ payload?: unknown; score?: number }> | undefined,
|
|
564
|
+
set: (entry: ConceptPageQueryResult, score: number) => void,
|
|
565
|
+
): void => {
|
|
566
|
+
for (const point of points ?? []) {
|
|
567
|
+
const slug = (point.payload as { slug?: unknown } | null)?.slug;
|
|
568
|
+
if (typeof slug !== "string") continue;
|
|
569
|
+
const existing = merged.get(slug) ?? { slug };
|
|
570
|
+
set(existing, point.score ?? 0);
|
|
571
|
+
merged.set(slug, existing);
|
|
317
572
|
}
|
|
318
|
-
}
|
|
573
|
+
};
|
|
574
|
+
recordHit(denseResults.points, (e, s) => (e.denseScore = s));
|
|
575
|
+
recordHit(sparseResults.points, (e, s) => (e.sparseScore = s));
|
|
576
|
+
recordHit(summaryDenseResults.points, (e, s) => (e.summaryDenseScore = s));
|
|
577
|
+
recordHit(summarySparseResults.points, (e, s) => (e.summarySparseScore = s));
|
|
319
578
|
|
|
320
579
|
return Array.from(merged.values());
|
|
321
580
|
}
|
|
@@ -437,4 +696,5 @@ function pointIdForSlug(slug: string): string {
|
|
|
437
696
|
export function _resetMemoryV2QdrantForTests(): void {
|
|
438
697
|
_client = null;
|
|
439
698
|
_collectionReady = false;
|
|
699
|
+
_collectionReadyPromise = null;
|
|
440
700
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Memory v2 cross-encoder rerank — `(query, page-preview)` pairs scored by a local model. */
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import type { AssistantConfig } from "../../config/types.js";
|
|
6
|
+
import { getLogger } from "../../util/logger.js";
|
|
7
|
+
import { getWorkspaceDir } from "../../util/platform.js";
|
|
8
|
+
import { getOrCreateRerankBackend } from "../rerank-local.js";
|
|
9
|
+
import { readPage } from "./page-store.js";
|
|
10
|
+
|
|
11
|
+
const log = getLogger("memory-v2-reranker");
|
|
12
|
+
|
|
13
|
+
// ~512-token model context for bge-reranker-base; cap input to bound payload.
|
|
14
|
+
const PASSAGE_CHAR_CAP = 240;
|
|
15
|
+
|
|
16
|
+
interface CacheEntry {
|
|
17
|
+
scores: Map<string, number>;
|
|
18
|
+
ts: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CACHE_TTL_MS = 2 * 60 * 1000;
|
|
22
|
+
const CACHE_MAX_ENTRIES = 64;
|
|
23
|
+
const cache = new Map<string, CacheEntry>();
|
|
24
|
+
|
|
25
|
+
function cacheKey(query: string, slugs: readonly string[]): string {
|
|
26
|
+
const sorted = [...slugs].sort().join("\0");
|
|
27
|
+
return createHash("sha256").update(`${query}\0${sorted}`).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function evictExpired(now: number): void {
|
|
31
|
+
for (const [k, v] of cache) {
|
|
32
|
+
if (now - v.ts > CACHE_TTL_MS) cache.delete(k);
|
|
33
|
+
}
|
|
34
|
+
if (cache.size > CACHE_MAX_ENTRIES) {
|
|
35
|
+
const toDrop = cache.size - CACHE_MAX_ENTRIES;
|
|
36
|
+
let i = 0;
|
|
37
|
+
for (const k of cache.keys()) {
|
|
38
|
+
if (i++ >= toDrop) break;
|
|
39
|
+
cache.delete(k);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildPassage(slug: string, body: string): string {
|
|
45
|
+
const trimmed = body.replace(/^\s+/, "");
|
|
46
|
+
const blank = trimmed.search(/\n\s*\n/);
|
|
47
|
+
const para = blank === -1 ? trimmed : trimmed.slice(0, blank);
|
|
48
|
+
const stripped = para.replace(/^#+\s.*\n/, "").trim();
|
|
49
|
+
const compact = stripped.replace(/\s+/g, " ").slice(0, PASSAGE_CHAR_CAP);
|
|
50
|
+
return `${slug}\n${compact}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run the cross-encoder over each candidate's first-paragraph preview for
|
|
55
|
+
* one or more queries against the same candidate set. Returns one
|
|
56
|
+
* `Map<slug, score>` per query, in the same order as the `queries` array.
|
|
57
|
+
*
|
|
58
|
+
* Multi-query batching: the user-channel and assistant-channel queries share
|
|
59
|
+
* a candidate set per turn, so scoring them in a single tokenizer +
|
|
60
|
+
* forward-pass call avoids the ONNX-invocation overhead of two serialised
|
|
61
|
+
* worker round-trips. Cache hits short-circuit per-query independently —
|
|
62
|
+
* a whitespace-only query yields an empty Map without hitting the backend.
|
|
63
|
+
*
|
|
64
|
+
* Failures (worker down, page read errors) yield empty Maps so callers can
|
|
65
|
+
* fall back to pure fused scores. Per-batch normalisation and boost math
|
|
66
|
+
* live in `computeOwnActivation`.
|
|
67
|
+
*/
|
|
68
|
+
export async function rerankCandidates(
|
|
69
|
+
queries: readonly string[],
|
|
70
|
+
candidates: readonly string[],
|
|
71
|
+
config: AssistantConfig,
|
|
72
|
+
): Promise<Array<Map<string, number>>> {
|
|
73
|
+
if (queries.length === 0) return [];
|
|
74
|
+
if (candidates.length === 0) return queries.map(() => new Map());
|
|
75
|
+
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
evictExpired(now);
|
|
78
|
+
|
|
79
|
+
const results: Array<Map<string, number> | null> = queries.map(() => null);
|
|
80
|
+
const uncachedIndices: number[] = [];
|
|
81
|
+
for (let i = 0; i < queries.length; i++) {
|
|
82
|
+
const q = queries[i];
|
|
83
|
+
if (q.trim().length === 0) {
|
|
84
|
+
results[i] = new Map();
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const key = cacheKey(q, candidates);
|
|
88
|
+
const cached = cache.get(key);
|
|
89
|
+
if (cached) {
|
|
90
|
+
// Refresh insertion order so frequently-hit entries survive eviction.
|
|
91
|
+
cache.delete(key);
|
|
92
|
+
cache.set(key, { ...cached, ts: now });
|
|
93
|
+
results[i] = new Map(cached.scores);
|
|
94
|
+
} else {
|
|
95
|
+
uncachedIndices.push(i);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const finalize = (): Array<Map<string, number>> =>
|
|
100
|
+
results.map((r) => r ?? new Map());
|
|
101
|
+
|
|
102
|
+
if (uncachedIndices.length === 0) return finalize();
|
|
103
|
+
|
|
104
|
+
const workspaceDir = getWorkspaceDir();
|
|
105
|
+
const pages = await Promise.all(
|
|
106
|
+
candidates.map((slug) =>
|
|
107
|
+
readPage(workspaceDir, slug).catch((err) => {
|
|
108
|
+
log.debug({ err, slug }, "Reranker skipping page that failed to load");
|
|
109
|
+
return null;
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
const passages: string[] = [];
|
|
114
|
+
const slugsForPassages: string[] = [];
|
|
115
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
116
|
+
const page = pages[i];
|
|
117
|
+
if (!page) continue;
|
|
118
|
+
passages.push(buildPassage(candidates[i], page.body));
|
|
119
|
+
slugsForPassages.push(candidates[i]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (passages.length === 0) {
|
|
123
|
+
for (const i of uncachedIndices) results[i] = new Map();
|
|
124
|
+
return finalize();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// One tokenizer + ONNX forward pass over every uncached query × passage
|
|
128
|
+
// pair. Pairs are laid out query-major: queries[uncached[0]] × passages,
|
|
129
|
+
// then queries[uncached[1]] × passages, etc.
|
|
130
|
+
const batchQueries: string[] = [];
|
|
131
|
+
const batchPassages: string[] = [];
|
|
132
|
+
for (const qi of uncachedIndices) {
|
|
133
|
+
const q = queries[qi];
|
|
134
|
+
for (const p of passages) {
|
|
135
|
+
batchQueries.push(q);
|
|
136
|
+
batchPassages.push(p);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { model, dtype } = config.memory.v2.rerank;
|
|
141
|
+
let scores: number[];
|
|
142
|
+
try {
|
|
143
|
+
const backend = getOrCreateRerankBackend(model, dtype);
|
|
144
|
+
scores = await backend.score(batchQueries, batchPassages);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
log.warn(
|
|
147
|
+
{ err, model, n: batchPassages.length },
|
|
148
|
+
"Rerank backend failed; falling back to pure fused scores",
|
|
149
|
+
);
|
|
150
|
+
for (const i of uncachedIndices) results[i] = new Map();
|
|
151
|
+
return finalize();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (let j = 0; j < uncachedIndices.length; j++) {
|
|
155
|
+
const qi = uncachedIndices[j];
|
|
156
|
+
const offset = j * passages.length;
|
|
157
|
+
const result = new Map<string, number>();
|
|
158
|
+
for (let i = 0; i < slugsForPassages.length; i++) {
|
|
159
|
+
const s = scores[offset + i];
|
|
160
|
+
if (typeof s !== "number" || Number.isNaN(s)) continue;
|
|
161
|
+
// sigmoid output should already be in [0, 1]; clamp defensively.
|
|
162
|
+
result.set(slugsForPassages[i], Math.max(0, Math.min(1, s)));
|
|
163
|
+
}
|
|
164
|
+
results[qi] = result;
|
|
165
|
+
cache.set(cacheKey(queries[qi], candidates), {
|
|
166
|
+
scores: new Map(result),
|
|
167
|
+
ts: now,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return finalize();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @internal Test-only: clear the LRU cache. */
|
|
175
|
+
export function _resetRerankCacheForTests(): void {
|
|
176
|
+
cache.clear();
|
|
177
|
+
}
|