@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
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* file-based override and falls back to the bundled prompt when the
|
|
5
5
|
* override is missing/empty/unreadable.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import {
|
|
9
|
+
mkdirSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
symlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
8
15
|
import { homedir, tmpdir } from "node:os";
|
|
9
16
|
import { join } from "node:path";
|
|
10
17
|
import {
|
|
@@ -67,7 +74,14 @@ beforeEach(() => {
|
|
|
67
74
|
});
|
|
68
75
|
|
|
69
76
|
afterEach(() => {
|
|
70
|
-
for (const entry of [
|
|
77
|
+
for (const entry of [
|
|
78
|
+
"custom-prompt.md",
|
|
79
|
+
"empty.md",
|
|
80
|
+
"no-placeholder.md",
|
|
81
|
+
"huge.md",
|
|
82
|
+
"link.md",
|
|
83
|
+
"fifo",
|
|
84
|
+
]) {
|
|
71
85
|
rmSync(join(tmpWorkspace, entry), { force: true });
|
|
72
86
|
}
|
|
73
87
|
});
|
|
@@ -178,4 +192,49 @@ describe("resolveConsolidationPrompt — failure modes", () => {
|
|
|
178
192
|
const data = warnCalls[0].data as Record<string, unknown>;
|
|
179
193
|
expect(data.reason).toBe("empty_override");
|
|
180
194
|
});
|
|
195
|
+
|
|
196
|
+
test("falls back to bundled prompt when the override exceeds the size limit", () => {
|
|
197
|
+
const path = join(tmpWorkspace, "huge.md");
|
|
198
|
+
// 1 MiB + 1 byte — just over the cap so we don't waste test memory.
|
|
199
|
+
writeFileSync(path, Buffer.alloc(1 * 1024 * 1024 + 1, 0x61));
|
|
200
|
+
|
|
201
|
+
const result = resolveConsolidationPrompt(path, CUTOFF);
|
|
202
|
+
|
|
203
|
+
expect(result).toBe(bundledPrompt());
|
|
204
|
+
expect(warnCalls).toHaveLength(1);
|
|
205
|
+
const data = warnCalls[0].data as Record<string, unknown>;
|
|
206
|
+
expect(data.reason).toBe("oversized_override");
|
|
207
|
+
expect(data.size).toBe(1 * 1024 * 1024 + 1);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("falls back to bundled prompt when the override is a symlink", () => {
|
|
211
|
+
const target = join(tmpWorkspace, "custom-prompt.md");
|
|
212
|
+
writeFileSync(target, "real prompt body\n");
|
|
213
|
+
const link = join(tmpWorkspace, "link.md");
|
|
214
|
+
symlinkSync(target, link);
|
|
215
|
+
|
|
216
|
+
const result = resolveConsolidationPrompt(link, CUTOFF);
|
|
217
|
+
|
|
218
|
+
expect(result).toBe(bundledPrompt());
|
|
219
|
+
expect(warnCalls).toHaveLength(1);
|
|
220
|
+
const data = warnCalls[0].data as Record<string, unknown>;
|
|
221
|
+
expect(data.reason).toBe("not_regular_file");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("falls back to bundled prompt when the override is a FIFO", () => {
|
|
225
|
+
const fifoPath = join(tmpWorkspace, "fifo");
|
|
226
|
+
try {
|
|
227
|
+
execFileSync("mkfifo", [fifoPath]);
|
|
228
|
+
} catch {
|
|
229
|
+
// mkfifo unavailable on this platform — skip without failing.
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = resolveConsolidationPrompt(fifoPath, CUTOFF);
|
|
234
|
+
|
|
235
|
+
expect(result).toBe(bundledPrompt());
|
|
236
|
+
expect(warnCalls).toHaveLength(1);
|
|
237
|
+
const data = warnCalls[0].data as Record<string, unknown>;
|
|
238
|
+
expect(data.reason).toBe("not_regular_file");
|
|
239
|
+
});
|
|
181
240
|
});
|
|
@@ -27,10 +27,39 @@ mock.module("../../qdrant-client.js", () => ({
|
|
|
27
27
|
// records every call and lets each test program the next response.
|
|
28
28
|
type MockPoint = {
|
|
29
29
|
id: string;
|
|
30
|
-
vector: {
|
|
30
|
+
vector: {
|
|
31
|
+
dense: number[];
|
|
32
|
+
sparse: { indices: number[]; values: number[] };
|
|
33
|
+
summary_dense?: number[];
|
|
34
|
+
summary_sparse?: { indices: number[]; values: number[] };
|
|
35
|
+
};
|
|
31
36
|
payload: { slug: string; updated_at: number };
|
|
32
37
|
};
|
|
33
38
|
|
|
39
|
+
type MockCollectionInfo = {
|
|
40
|
+
config: {
|
|
41
|
+
params: {
|
|
42
|
+
vectors?: Record<string, { size: number }> | { size: number };
|
|
43
|
+
sparse_vectors?: Record<string, unknown>;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const FULL_SCHEMA_INFO: MockCollectionInfo = {
|
|
49
|
+
config: {
|
|
50
|
+
params: {
|
|
51
|
+
vectors: {
|
|
52
|
+
dense: { size: 384 },
|
|
53
|
+
summary_dense: { size: 384 },
|
|
54
|
+
},
|
|
55
|
+
sparse_vectors: {
|
|
56
|
+
sparse: {},
|
|
57
|
+
summary_sparse: {},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
34
63
|
const state = {
|
|
35
64
|
collectionExistsBeforeCreate: false,
|
|
36
65
|
collectionExistsCalls: 0,
|
|
@@ -39,6 +68,10 @@ const state = {
|
|
|
39
68
|
createIndexCalls: [] as Array<{ field_name: string; field_schema: string }>,
|
|
40
69
|
upsertCalls: [] as Array<{ wait: boolean; points: MockPoint[] }>,
|
|
41
70
|
deleteCalls: [] as Array<{ wait: boolean; points: string[] }>,
|
|
71
|
+
// Tracks `client.deleteCollection(name)` calls (distinct from `delete()`,
|
|
72
|
+
// which targets points). The schema-drift recreate path drops the
|
|
73
|
+
// collection entirely and we want to assert it ran exactly once.
|
|
74
|
+
deleteCollectionCalls: [] as string[],
|
|
42
75
|
queryCalls: [] as Array<{
|
|
43
76
|
using: string;
|
|
44
77
|
query: unknown;
|
|
@@ -55,6 +88,17 @@ const state = {
|
|
|
55
88
|
}>,
|
|
56
89
|
},
|
|
57
90
|
createCollectionThrows: null as Error | null,
|
|
91
|
+
// Schema returned by `client.getCollection`. Tests that exercise the
|
|
92
|
+
// drift path point this at a partial schema; the default mirrors a fully
|
|
93
|
+
// migrated collection so the no-drift path is the silent default.
|
|
94
|
+
getCollectionInfo: FULL_SCHEMA_INFO as MockCollectionInfo,
|
|
95
|
+
getCollectionThrows: null as Error | null,
|
|
96
|
+
getCollectionCalls: 0,
|
|
97
|
+
// Point count returned by `client.count`. Used by `countConceptPagePoints`
|
|
98
|
+
// which the lifecycle hook reads for the empty-after-create recovery path.
|
|
99
|
+
countResult: 0,
|
|
100
|
+
countThrows: null as Error | null,
|
|
101
|
+
countCalls: 0,
|
|
58
102
|
// Throw queue for upsert: first call shifts and throws if non-null;
|
|
59
103
|
// subsequent calls succeed once the queue is exhausted.
|
|
60
104
|
upsertThrowQueue: [] as Array<Error | null>,
|
|
@@ -66,13 +110,29 @@ class MockQdrantClient {
|
|
|
66
110
|
state.collectionExistsCalls++;
|
|
67
111
|
return { exists: state.collectionExistsBeforeCreate };
|
|
68
112
|
}
|
|
113
|
+
async getCollection(_name: string) {
|
|
114
|
+
state.getCollectionCalls++;
|
|
115
|
+
if (state.getCollectionThrows) throw state.getCollectionThrows;
|
|
116
|
+
return state.getCollectionInfo;
|
|
117
|
+
}
|
|
69
118
|
async createCollection(_name: string, params: unknown) {
|
|
70
119
|
state.createCollectionCalls++;
|
|
71
120
|
state.createCollectionParams = params;
|
|
72
121
|
if (state.createCollectionThrows) throw state.createCollectionThrows;
|
|
73
122
|
state.collectionExistsBeforeCreate = true;
|
|
123
|
+
state.getCollectionInfo = FULL_SCHEMA_INFO;
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
async deleteCollection(name: string) {
|
|
127
|
+
state.deleteCollectionCalls.push(name);
|
|
128
|
+
state.collectionExistsBeforeCreate = false;
|
|
74
129
|
return {};
|
|
75
130
|
}
|
|
131
|
+
async count(_name: string, _opts: { exact: boolean }) {
|
|
132
|
+
state.countCalls++;
|
|
133
|
+
if (state.countThrows) throw state.countThrows;
|
|
134
|
+
return { count: state.countResult };
|
|
135
|
+
}
|
|
76
136
|
async createPayloadIndex(
|
|
77
137
|
_name: string,
|
|
78
138
|
params: { field_name: string; field_schema: string },
|
|
@@ -102,7 +162,14 @@ class MockQdrantClient {
|
|
|
102
162
|
},
|
|
103
163
|
) {
|
|
104
164
|
state.queryCalls.push(params);
|
|
105
|
-
|
|
165
|
+
// Both `dense` and `summary_dense` consume from the dense queue (and
|
|
166
|
+
// similarly for sparse). The four-channel hybrid query fires them in
|
|
167
|
+
// order: body-dense, body-sparse, summary-dense, summary-sparse — so
|
|
168
|
+
// queue order matches call order.
|
|
169
|
+
const queue =
|
|
170
|
+
state.queryResponses[
|
|
171
|
+
params.using.endsWith("sparse") ? "sparse" : "dense"
|
|
172
|
+
];
|
|
106
173
|
return queue.shift() ?? { points: [] };
|
|
107
174
|
}
|
|
108
175
|
}
|
|
@@ -116,6 +183,7 @@ const {
|
|
|
116
183
|
upsertConceptPageEmbedding,
|
|
117
184
|
deleteConceptPageEmbedding,
|
|
118
185
|
hybridQueryConceptPages,
|
|
186
|
+
countConceptPagePoints,
|
|
119
187
|
MEMORY_V2_COLLECTION,
|
|
120
188
|
_resetMemoryV2QdrantForTests,
|
|
121
189
|
} = await import("../qdrant.js");
|
|
@@ -128,10 +196,17 @@ function resetState(): void {
|
|
|
128
196
|
state.createIndexCalls.length = 0;
|
|
129
197
|
state.upsertCalls.length = 0;
|
|
130
198
|
state.deleteCalls.length = 0;
|
|
199
|
+
state.deleteCollectionCalls.length = 0;
|
|
131
200
|
state.queryCalls.length = 0;
|
|
132
201
|
state.queryResponses.dense.length = 0;
|
|
133
202
|
state.queryResponses.sparse.length = 0;
|
|
134
203
|
state.createCollectionThrows = null;
|
|
204
|
+
state.getCollectionInfo = FULL_SCHEMA_INFO;
|
|
205
|
+
state.getCollectionThrows = null;
|
|
206
|
+
state.getCollectionCalls = 0;
|
|
207
|
+
state.countResult = 0;
|
|
208
|
+
state.countThrows = null;
|
|
209
|
+
state.countCalls = 0;
|
|
135
210
|
state.upsertThrowQueue.length = 0;
|
|
136
211
|
_resetMemoryV2QdrantForTests();
|
|
137
212
|
}
|
|
@@ -140,7 +215,7 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
140
215
|
beforeEach(resetState);
|
|
141
216
|
afterEach(resetState);
|
|
142
217
|
|
|
143
|
-
test("creates the collection with named dense + sparse vectors", async () => {
|
|
218
|
+
test("creates the collection with named dense + sparse vectors (body and summary)", async () => {
|
|
144
219
|
state.collectionExistsBeforeCreate = false;
|
|
145
220
|
|
|
146
221
|
await ensureConceptPageCollection();
|
|
@@ -149,8 +224,12 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
149
224
|
const params = state.createCollectionParams as {
|
|
150
225
|
vectors: {
|
|
151
226
|
dense: { size: number; distance: string; on_disk: boolean };
|
|
227
|
+
summary_dense: { size: number; distance: string; on_disk: boolean };
|
|
228
|
+
};
|
|
229
|
+
sparse_vectors: {
|
|
230
|
+
sparse: Record<string, unknown>;
|
|
231
|
+
summary_sparse: Record<string, unknown>;
|
|
152
232
|
};
|
|
153
|
-
sparse_vectors: { sparse: Record<string, unknown> };
|
|
154
233
|
hnsw_config: { on_disk: boolean; m: number; ef_construct: number };
|
|
155
234
|
on_disk_payload: boolean;
|
|
156
235
|
};
|
|
@@ -159,7 +238,14 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
159
238
|
distance: "Cosine",
|
|
160
239
|
on_disk: true,
|
|
161
240
|
});
|
|
241
|
+
// Summary side mirrors body so the activation pipeline can fuse symmetrically.
|
|
242
|
+
expect(params.vectors.summary_dense).toEqual({
|
|
243
|
+
size: 384,
|
|
244
|
+
distance: "Cosine",
|
|
245
|
+
on_disk: true,
|
|
246
|
+
});
|
|
162
247
|
expect(params.sparse_vectors.sparse).toEqual({});
|
|
248
|
+
expect(params.sparse_vectors.summary_sparse).toEqual({});
|
|
163
249
|
expect(params.hnsw_config).toEqual({
|
|
164
250
|
on_disk: true,
|
|
165
251
|
m: 16,
|
|
@@ -190,6 +276,22 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
190
276
|
expect(state.collectionExistsCalls).toBe(1);
|
|
191
277
|
});
|
|
192
278
|
|
|
279
|
+
test("deduplicates concurrent collection creation", async () => {
|
|
280
|
+
state.collectionExistsBeforeCreate = false;
|
|
281
|
+
|
|
282
|
+
await Promise.all([
|
|
283
|
+
ensureConceptPageCollection(),
|
|
284
|
+
ensureConceptPageCollection(),
|
|
285
|
+
ensureConceptPageCollection(),
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
expect(state.collectionExistsCalls).toBe(1);
|
|
289
|
+
expect(state.createCollectionCalls).toBe(1);
|
|
290
|
+
expect(state.createIndexCalls).toEqual([
|
|
291
|
+
{ field_name: "slug", field_schema: "keyword" },
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
|
|
193
295
|
test("treats 409-on-create as success (concurrent creation race)", async () => {
|
|
194
296
|
state.collectionExistsBeforeCreate = false;
|
|
195
297
|
const conflict = Object.assign(new Error("Conflict"), { status: 409 });
|
|
@@ -203,6 +305,115 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
203
305
|
// expected to have created it (it ran the same code).
|
|
204
306
|
expect(state.createIndexCalls).toEqual([]);
|
|
205
307
|
});
|
|
308
|
+
|
|
309
|
+
test("detects missing summary_dense / summary_sparse on an existing collection and recreates", async () => {
|
|
310
|
+
// Pre-#29823 schema: only body channels, no summary_*.
|
|
311
|
+
state.collectionExistsBeforeCreate = true;
|
|
312
|
+
state.getCollectionInfo = {
|
|
313
|
+
config: {
|
|
314
|
+
params: {
|
|
315
|
+
vectors: { dense: { size: 384 } },
|
|
316
|
+
sparse_vectors: { sparse: {} },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const result = await ensureConceptPageCollection();
|
|
322
|
+
|
|
323
|
+
// Drift path probed once, dropped the collection once, and recreated
|
|
324
|
+
// with the full four-vector schema (the create-success branch resets
|
|
325
|
+
// `getCollectionInfo` to FULL_SCHEMA_INFO so a follow-up probe agrees).
|
|
326
|
+
expect(state.getCollectionCalls).toBe(1);
|
|
327
|
+
expect(state.deleteCollectionCalls).toEqual([MEMORY_V2_COLLECTION]);
|
|
328
|
+
expect(state.createCollectionCalls).toBe(1);
|
|
329
|
+
expect(result).toEqual({ migrated: true });
|
|
330
|
+
|
|
331
|
+
// Recreated schema carries summary_dense + summary_sparse.
|
|
332
|
+
const params = state.createCollectionParams as {
|
|
333
|
+
vectors: Record<string, unknown>;
|
|
334
|
+
sparse_vectors: Record<string, unknown>;
|
|
335
|
+
};
|
|
336
|
+
expect(params.vectors.summary_dense).toBeDefined();
|
|
337
|
+
expect(params.sparse_vectors.summary_sparse).toBeDefined();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("leaves a fully migrated collection untouched", async () => {
|
|
341
|
+
// Default `getCollectionInfo` is FULL_SCHEMA_INFO — already migrated.
|
|
342
|
+
state.collectionExistsBeforeCreate = true;
|
|
343
|
+
|
|
344
|
+
const result = await ensureConceptPageCollection();
|
|
345
|
+
|
|
346
|
+
expect(state.getCollectionCalls).toBe(1);
|
|
347
|
+
expect(state.deleteCollectionCalls).toEqual([]);
|
|
348
|
+
expect(state.createCollectionCalls).toBe(0);
|
|
349
|
+
expect(result).toEqual({ migrated: false });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("getCollection failure is treated as compatible (no destructive recreate)", async () => {
|
|
353
|
+
state.collectionExistsBeforeCreate = true;
|
|
354
|
+
state.getCollectionThrows = new Error("transient REST error");
|
|
355
|
+
|
|
356
|
+
const result = await ensureConceptPageCollection();
|
|
357
|
+
|
|
358
|
+
expect(state.getCollectionCalls).toBe(1);
|
|
359
|
+
expect(state.deleteCollectionCalls).toEqual([]);
|
|
360
|
+
expect(state.createCollectionCalls).toBe(0);
|
|
361
|
+
expect(result).toEqual({ migrated: false });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("concurrent ensure during a schema rebuild only deletes/creates once", async () => {
|
|
365
|
+
state.collectionExistsBeforeCreate = true;
|
|
366
|
+
state.getCollectionInfo = {
|
|
367
|
+
config: {
|
|
368
|
+
params: {
|
|
369
|
+
vectors: { dense: { size: 384 } },
|
|
370
|
+
sparse_vectors: { sparse: {} },
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const results = await Promise.all([
|
|
376
|
+
ensureConceptPageCollection(),
|
|
377
|
+
ensureConceptPageCollection(),
|
|
378
|
+
ensureConceptPageCollection(),
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
expect(state.deleteCollectionCalls).toEqual([MEMORY_V2_COLLECTION]);
|
|
382
|
+
expect(state.createCollectionCalls).toBe(1);
|
|
383
|
+
// All three concurrent callers see the same migrated signal so any one
|
|
384
|
+
// of them is safe to enqueue the reembed (the lifecycle hook is the
|
|
385
|
+
// single producer in practice).
|
|
386
|
+
expect(results).toEqual([
|
|
387
|
+
{ migrated: true },
|
|
388
|
+
{ migrated: true },
|
|
389
|
+
{ migrated: true },
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("memory v2 qdrant — point count", () => {
|
|
395
|
+
beforeEach(resetState);
|
|
396
|
+
afterEach(resetState);
|
|
397
|
+
|
|
398
|
+
test("returns the approximate Qdrant count for the v2 collection", async () => {
|
|
399
|
+
state.collectionExistsBeforeCreate = true;
|
|
400
|
+
state.countResult = 1185;
|
|
401
|
+
|
|
402
|
+
const count = await countConceptPagePoints();
|
|
403
|
+
|
|
404
|
+
expect(count).toBe(1185);
|
|
405
|
+
expect(state.countCalls).toBe(1);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("returns 0 when the count call fails (treated as needs-reembed)", async () => {
|
|
409
|
+
state.collectionExistsBeforeCreate = true;
|
|
410
|
+
state.countThrows = new Error("Qdrant unreachable");
|
|
411
|
+
|
|
412
|
+
const count = await countConceptPagePoints();
|
|
413
|
+
|
|
414
|
+
expect(count).toBe(0);
|
|
415
|
+
expect(state.countCalls).toBe(1);
|
|
416
|
+
});
|
|
206
417
|
});
|
|
207
418
|
|
|
208
419
|
describe("memory v2 qdrant — upsert", () => {
|
|
@@ -233,12 +444,67 @@ describe("memory v2 qdrant — upsert", () => {
|
|
|
233
444
|
indices: [1, 2],
|
|
234
445
|
values: [0.5, 0.5],
|
|
235
446
|
});
|
|
447
|
+
// No summary vectors when caller didn't pass them — Qdrant accepts a
|
|
448
|
+
// partial named-vector subset, and pages without a frontmatter summary
|
|
449
|
+
// legitimately have nothing to embed on the summary side.
|
|
450
|
+
const vectorRecord = point.vector as unknown as Record<string, unknown>;
|
|
451
|
+
expect(vectorRecord.summary_dense).toBeUndefined();
|
|
452
|
+
expect(vectorRecord.summary_sparse).toBeUndefined();
|
|
236
453
|
// Point ID is a UUID-shaped string derived from the slug.
|
|
237
454
|
expect(point.id).toMatch(
|
|
238
455
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
|
|
239
456
|
);
|
|
240
457
|
});
|
|
241
458
|
|
|
459
|
+
test("upserts summary vectors alongside body vectors when both are provided", async () => {
|
|
460
|
+
state.collectionExistsBeforeCreate = true;
|
|
461
|
+
|
|
462
|
+
await upsertConceptPageEmbedding({
|
|
463
|
+
slug: "summarized-page",
|
|
464
|
+
dense: [0.1, 0.2, 0.3],
|
|
465
|
+
sparse: { indices: [1, 2], values: [0.5, 0.5] },
|
|
466
|
+
summary: {
|
|
467
|
+
dense: [0.4, 0.5, 0.6],
|
|
468
|
+
sparse: { indices: [3, 4], values: [0.7, 0.7] },
|
|
469
|
+
},
|
|
470
|
+
updatedAt: 1714000000000,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(state.upsertCalls).toHaveLength(1);
|
|
474
|
+
const [point] = state.upsertCalls[0].points;
|
|
475
|
+
const vectorRecord = point.vector as unknown as Record<string, unknown>;
|
|
476
|
+
expect(vectorRecord.dense).toEqual([0.1, 0.2, 0.3]);
|
|
477
|
+
expect(vectorRecord.sparse).toEqual({
|
|
478
|
+
indices: [1, 2],
|
|
479
|
+
values: [0.5, 0.5],
|
|
480
|
+
});
|
|
481
|
+
expect(vectorRecord.summary_dense).toEqual([0.4, 0.5, 0.6]);
|
|
482
|
+
expect(vectorRecord.summary_sparse).toEqual({
|
|
483
|
+
indices: [3, 4],
|
|
484
|
+
values: [0.7, 0.7],
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("omits summary vectors when the summary block is undefined", async () => {
|
|
489
|
+
// The grouped-shape signature enforces summary as a paired { dense, sparse }
|
|
490
|
+
// block; passing `undefined` (or omitting it) leaves the summary vectors off
|
|
491
|
+
// the point entirely so query-time fusion stays symmetric.
|
|
492
|
+
state.collectionExistsBeforeCreate = true;
|
|
493
|
+
|
|
494
|
+
await upsertConceptPageEmbedding({
|
|
495
|
+
slug: "no-summary",
|
|
496
|
+
dense: [0.1],
|
|
497
|
+
sparse: { indices: [1], values: [1] },
|
|
498
|
+
// summary intentionally omitted
|
|
499
|
+
updatedAt: 1,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const [point] = state.upsertCalls[0].points;
|
|
503
|
+
const vectorRecord = point.vector as unknown as Record<string, unknown>;
|
|
504
|
+
expect(vectorRecord.summary_dense).toBeUndefined();
|
|
505
|
+
expect(vectorRecord.summary_sparse).toBeUndefined();
|
|
506
|
+
});
|
|
507
|
+
|
|
242
508
|
test("two upserts for the same slug share the same point id (overwrites in place)", async () => {
|
|
243
509
|
state.collectionExistsBeforeCreate = true;
|
|
244
510
|
|
|
@@ -341,8 +607,9 @@ describe("memory v2 qdrant — hybrid query", () => {
|
|
|
341
607
|
beforeEach(resetState);
|
|
342
608
|
afterEach(resetState);
|
|
343
609
|
|
|
344
|
-
test("runs
|
|
610
|
+
test("runs all four channels (body dense/sparse + summary dense/sparse) and returns per-channel scores", async () => {
|
|
345
611
|
state.collectionExistsBeforeCreate = true;
|
|
612
|
+
// Body channel hits.
|
|
346
613
|
state.queryResponses.dense.push({
|
|
347
614
|
points: [
|
|
348
615
|
{ score: 0.91, payload: { slug: "alice-prefers-vs-code" } },
|
|
@@ -355,6 +622,14 @@ describe("memory v2 qdrant — hybrid query", () => {
|
|
|
355
622
|
{ score: 3, payload: { slug: "bob-uses-zsh" } },
|
|
356
623
|
],
|
|
357
624
|
});
|
|
625
|
+
// Summary channel hits — queue order is body-dense, body-sparse,
|
|
626
|
+
// summary-dense, summary-sparse, so push summaries after bodies.
|
|
627
|
+
state.queryResponses.dense.push({
|
|
628
|
+
points: [{ score: 0.81, payload: { slug: "alice-prefers-vs-code" } }],
|
|
629
|
+
});
|
|
630
|
+
state.queryResponses.sparse.push({
|
|
631
|
+
points: [{ score: 9, payload: { slug: "alice-prefers-vs-code" } }],
|
|
632
|
+
});
|
|
358
633
|
|
|
359
634
|
const results = await hybridQueryConceptPages(
|
|
360
635
|
[0.1, 0.2, 0.3],
|
|
@@ -362,14 +637,19 @@ describe("memory v2 qdrant — hybrid query", () => {
|
|
|
362
637
|
5,
|
|
363
638
|
);
|
|
364
639
|
|
|
365
|
-
//
|
|
366
|
-
expect(state.queryCalls).toHaveLength(
|
|
640
|
+
// All four queries fired with the same limit and distinct `using`.
|
|
641
|
+
expect(state.queryCalls).toHaveLength(4);
|
|
367
642
|
const usings = state.queryCalls.map((c) => c.using).sort();
|
|
368
|
-
expect(usings).toEqual([
|
|
643
|
+
expect(usings).toEqual([
|
|
644
|
+
"dense",
|
|
645
|
+
"sparse",
|
|
646
|
+
"summary_dense",
|
|
647
|
+
"summary_sparse",
|
|
648
|
+
]);
|
|
369
649
|
expect(state.queryCalls.every((c) => c.limit === 5)).toBe(true);
|
|
370
650
|
expect(state.queryCalls.every((c) => c.with_payload === true)).toBe(true);
|
|
371
651
|
|
|
372
|
-
//
|
|
652
|
+
// Alice has hits on all four channels; bob is body-only.
|
|
373
653
|
expect(results).toHaveLength(2);
|
|
374
654
|
const alice = results.find((r) => r.slug === "alice-prefers-vs-code");
|
|
375
655
|
const bob = results.find((r) => r.slug === "bob-uses-zsh");
|
|
@@ -377,6 +657,8 @@ describe("memory v2 qdrant — hybrid query", () => {
|
|
|
377
657
|
slug: "alice-prefers-vs-code",
|
|
378
658
|
denseScore: 0.91,
|
|
379
659
|
sparseScore: 12,
|
|
660
|
+
summaryDenseScore: 0.81,
|
|
661
|
+
summarySparseScore: 9,
|
|
380
662
|
});
|
|
381
663
|
expect(bob).toEqual({
|
|
382
664
|
slug: "bob-uses-zsh",
|
|
@@ -387,6 +669,8 @@ describe("memory v2 qdrant — hybrid query", () => {
|
|
|
387
669
|
|
|
388
670
|
test("dense-only hits leave sparseScore undefined (and vice versa)", async () => {
|
|
389
671
|
state.collectionExistsBeforeCreate = true;
|
|
672
|
+
// Body dense + sparse hits. Summary channels stay empty (no push) →
|
|
673
|
+
// they fall through to `{ points: [] }` and produce no summary scores.
|
|
390
674
|
state.queryResponses.dense.push({
|
|
391
675
|
points: [{ score: 0.7, payload: { slug: "dense-only" } }],
|
|
392
676
|
});
|
|
@@ -404,8 +688,41 @@ describe("memory v2 qdrant — hybrid query", () => {
|
|
|
404
688
|
const sparseOnly = results.find((r) => r.slug === "sparse-only");
|
|
405
689
|
expect(denseOnly).toEqual({ slug: "dense-only", denseScore: 0.7 });
|
|
406
690
|
expect(denseOnly?.sparseScore).toBeUndefined();
|
|
691
|
+
expect(denseOnly?.summaryDenseScore).toBeUndefined();
|
|
407
692
|
expect(sparseOnly).toEqual({ slug: "sparse-only", sparseScore: 2 });
|
|
408
693
|
expect(sparseOnly?.denseScore).toBeUndefined();
|
|
694
|
+
expect(sparseOnly?.summarySparseScore).toBeUndefined();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("returns summary-channel scores when only the summary side hits", async () => {
|
|
698
|
+
// Page has no body hits but matches via the summary embedding —
|
|
699
|
+
// exercises the path where `simBatch` falls back to summary-only.
|
|
700
|
+
state.collectionExistsBeforeCreate = true;
|
|
701
|
+
// Body channels empty.
|
|
702
|
+
state.queryResponses.dense.push({ points: [] });
|
|
703
|
+
state.queryResponses.sparse.push({ points: [] });
|
|
704
|
+
// Summary channels hit.
|
|
705
|
+
state.queryResponses.dense.push({
|
|
706
|
+
points: [{ score: 0.6, payload: { slug: "summary-only" } }],
|
|
707
|
+
});
|
|
708
|
+
state.queryResponses.sparse.push({
|
|
709
|
+
points: [{ score: 4, payload: { slug: "summary-only" } }],
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const results = await hybridQueryConceptPages(
|
|
713
|
+
[0.1],
|
|
714
|
+
{ indices: [1], values: [1] },
|
|
715
|
+
5,
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const summaryOnly = results.find((r) => r.slug === "summary-only");
|
|
719
|
+
expect(summaryOnly).toEqual({
|
|
720
|
+
slug: "summary-only",
|
|
721
|
+
summaryDenseScore: 0.6,
|
|
722
|
+
summarySparseScore: 4,
|
|
723
|
+
});
|
|
724
|
+
expect(summaryOnly?.denseScore).toBeUndefined();
|
|
725
|
+
expect(summaryOnly?.sparseScore).toBeUndefined();
|
|
409
726
|
});
|
|
410
727
|
|
|
411
728
|
test("does not use Qdrant-side RRF fusion (separate per-channel queries)", async () => {
|