@vellumai/assistant 0.8.4 → 0.8.5
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 +2 -2
- package/docs/browser-use-architecture-phase2.md +1 -1
- package/knip.json +2 -1
- package/openapi.yaml +809 -11
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +34 -37
- package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
- package/src/__tests__/audit-log-rotation.test.ts +70 -16
- package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
- package/src/__tests__/btw-routes.test.ts +2 -3
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +3 -3
- package/src/__tests__/checker.test.ts +6 -15
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
- package/src/__tests__/computer-use-tools.test.ts +2 -4
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
- package/src/__tests__/conversation-agent-loop.test.ts +163 -122
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
- package/src/__tests__/conversation-clear-safety.test.ts +25 -25
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
- package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
- package/src/__tests__/conversation-error.test.ts +31 -0
- package/src/__tests__/conversation-fork-crud.test.ts +178 -15
- package/src/__tests__/conversation-lifecycle.test.ts +52 -11
- package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
- package/src/__tests__/conversation-skill-tools.test.ts +2 -5
- package/src/__tests__/conversation-store.test.ts +1 -1
- package/src/__tests__/conversation-sync-tags.test.ts +99 -32
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
- package/src/__tests__/credential-execution-tools.test.ts +6 -6
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/dynamic-page-surface.test.ts +2 -2
- package/src/__tests__/email-html-renderer.test.ts +12 -0
- package/src/__tests__/gateway-flag-listener.test.ts +237 -0
- package/src/__tests__/gemini-provider.test.ts +78 -0
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +7 -5
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
- package/src/__tests__/heartbeat-service.test.ts +4 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
- package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
- package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
- package/src/__tests__/llm-resolver.test.ts +77 -9
- package/src/__tests__/llm-usage-store.test.ts +66 -0
- package/src/__tests__/logger.test.ts +89 -0
- package/src/__tests__/mcp-abort-signal.test.ts +2 -2
- package/src/__tests__/media-generate-image.test.ts +31 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
- package/src/__tests__/model-intents.test.ts +2 -4
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/onboarding-template-contract.test.ts +1 -1
- package/src/__tests__/openai-provider.test.ts +46 -0
- package/src/__tests__/openai-responses-provider.test.ts +114 -12
- package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
- package/src/__tests__/platform.test.ts +2 -2
- package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
- package/src/__tests__/plugin-bootstrap.test.ts +2 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
- package/src/__tests__/plugin-types.test.ts +3 -2
- package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
- package/src/__tests__/pricing.test.ts +12 -0
- package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
- package/src/__tests__/registry.test.ts +2 -8
- package/src/__tests__/require-fresh-approval.test.ts +2 -2
- package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
- package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
- package/src/__tests__/skill-tool-factory.test.ts +1 -1
- package/src/__tests__/subagent-notify-parent.test.ts +1 -1
- package/src/__tests__/suggestion-routes.test.ts +1 -0
- package/src/__tests__/sync-message-contract.test.ts +59 -0
- package/src/__tests__/system-prompt.test.ts +145 -131
- package/src/__tests__/terminal-tools.test.ts +1 -1
- package/src/__tests__/tool-approval-handler.test.ts +1 -5
- package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
- package/src/__tests__/tool-executor.test.ts +9 -62
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
- package/src/__tests__/usage-routes.test.ts +3 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +6 -5
- package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
- package/src/acp/prepare-agent-env.ts +78 -0
- package/src/acp/session-manager.ts +1 -1
- package/src/agent/loop.ts +8 -0
- package/src/api/README.md +5 -0
- package/src/api/index.ts +4 -0
- package/src/api/package.json +10 -0
- package/src/background-wake/background-wake-routes.test.ts +233 -0
- package/src/background-wake/runtime-registry.ts +24 -0
- package/src/cli/commands/__tests__/browser.test.ts +23 -5
- package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
- package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
- package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
- package/src/cli/commands/browser.ts +247 -0
- package/src/cli/commands/domain.ts +91 -41
- package/src/cli/commands/inference.ts +93 -40
- package/src/cli/commands/memory-v2-compare-render.ts +115 -0
- package/src/cli/commands/memory-v2.ts +176 -1
- package/src/cli/commands/memory-v3-render.ts +344 -0
- package/src/cli/commands/memory-v3.ts +316 -0
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +21 -9
- package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
- package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/call-site-defaults.ts +7 -6
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
- package/src/config/schemas/call-site-catalog.ts +21 -7
- package/src/config/schemas/llm.ts +12 -1
- package/src/config/schemas/memory-v2.ts +246 -0
- package/src/config/schemas/memory.ts +2 -1
- package/src/context/compactor.ts +52 -0
- package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
- package/src/conversations/message-consolidation.ts +404 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
- package/src/daemon/conversation-agent-loop.ts +126 -76
- package/src/daemon/conversation-error.ts +31 -1
- package/src/daemon/conversation-lifecycle.ts +27 -22
- package/src/daemon/conversation-runtime-assembly.ts +10 -9
- package/src/daemon/conversation-tool-setup.ts +63 -3
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +14 -29
- package/src/daemon/disk-pressure-guard.ts +14 -2
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +11 -3
- package/src/daemon/host-browser-proxy.ts +5 -5
- package/src/daemon/host-cu-proxy.ts +4 -4
- package/src/daemon/host-file-proxy.ts +4 -4
- package/src/daemon/host-proxy-base.ts +4 -4
- package/src/daemon/host-transfer-proxy.ts +10 -10
- package/src/daemon/lifecycle.ts +23 -20
- package/src/daemon/meet-manifest-loader.ts +1 -7
- package/src/daemon/message-types/conversations.ts +6 -9
- package/src/daemon/message-types/home.ts +1 -13
- package/src/daemon/message-types/messages.ts +6 -14
- package/src/daemon/message-types/sync.ts +14 -0
- package/src/daemon/shutdown-handlers.ts +24 -5
- package/src/daemon/switch-inference-profile-tool.ts +52 -0
- package/src/daemon/tool-setup-types.ts +13 -0
- package/src/events/relationship-state-updated.ts +25 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/home-greeting.ts +0 -9
- package/src/home/suggested-prompts.ts +0 -9
- package/src/ipc/gateway-flag-listener.ts +123 -0
- package/src/ipc/skill-routes/registries.ts +8 -12
- package/src/memory/__tests__/db-async-query.test.ts +165 -0
- package/src/memory/__tests__/db-maintenance.test.ts +115 -0
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
- package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
- package/src/memory/auto-analysis-enqueue.ts +5 -1
- package/src/memory/conversation-crud.ts +71 -70
- package/src/memory/conversation-starters-cadence.ts +3 -1
- package/src/memory/conversation-title-service.ts +19 -3
- package/src/memory/db-async-query.ts +214 -0
- package/src/memory/db-init.ts +10 -0
- package/src/memory/db-maintenance.ts +30 -21
- package/src/memory/graph/bootstrap.ts +8 -1
- package/src/memory/graph/capability-seed.ts +7 -3
- package/src/memory/graph/conversation-graph-memory.ts +100 -17
- package/src/memory/graph/extraction.ts +1 -5
- package/src/memory/graph/graph-search.ts +7 -1
- package/src/memory/indexer.ts +28 -18
- package/src/memory/job-handlers/cleanup.ts +76 -18
- package/src/memory/job-handlers/conversation-starters.ts +1 -4
- package/src/memory/jobs/embed-pkb-file.ts +6 -1
- package/src/memory/jobs-store.ts +14 -0
- package/src/memory/jobs-worker.ts +55 -22
- package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
- package/src/memory/llm-request-log-source-local.ts +7 -0
- package/src/memory/llm-request-log-source.ts +9 -2
- package/src/memory/llm-request-log-store.ts +43 -1
- package/src/memory/llm-usage-store.ts +24 -0
- package/src/memory/memory-retrospective-enqueue.ts +8 -1
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/memory-v2-activation-log-store.ts +15 -6
- package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
- package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
- package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
- package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
- package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +33 -0
- package/src/memory/schema/conversations.ts +1 -1
- package/src/memory/schema/infrastructure.ts +21 -0
- package/src/memory/tool-usage-store.ts +36 -8
- package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
- package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
- package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
- package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
- package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
- package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
- package/src/memory/v2/__tests__/injection.test.ts +127 -98
- package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
- package/src/memory/v2/__tests__/router.test.ts +171 -3
- package/src/memory/v2/harness/compare.ts +57 -0
- package/src/memory/v2/harness/metrics.ts +124 -0
- package/src/memory/v2/harness/oracle.ts +145 -0
- package/src/memory/v2/harness/replay-input.ts +224 -0
- package/src/memory/v2/harness/retriever.ts +74 -0
- package/src/memory/v2/harness/router-retriever.ts +43 -0
- package/src/memory/v2/harness/runner.ts +106 -0
- package/src/memory/v2/harness/trace.ts +58 -0
- package/src/memory/v2/injection.ts +21 -15
- package/src/memory/v2/prompts/router.ts +26 -1
- package/src/memory/v2/qdrant.ts +14 -2
- package/src/memory/v2/router.ts +171 -18
- package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
- package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
- package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
- package/src/memory/v3/__tests__/edges.test.ts +563 -0
- package/src/memory/v3/__tests__/filter.test.ts +512 -0
- package/src/memory/v3/__tests__/gate.test.ts +574 -0
- package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
- package/src/memory/v3/__tests__/loop.test.ts +530 -0
- package/src/memory/v3/__tests__/retriever.test.ts +226 -0
- package/src/memory/v3/__tests__/scouts.test.ts +440 -0
- package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
- package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
- package/src/memory/v3/__tests__/traversal.test.ts +469 -0
- package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
- package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
- package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
- package/src/memory/v3/__tests__/validate.test.ts +245 -0
- package/src/memory/v3/auto-edges.ts +223 -0
- package/src/memory/v3/coactivation-store.ts +124 -0
- package/src/memory/v3/consolidation-job.ts +323 -0
- package/src/memory/v3/edge-learning-job.ts +160 -0
- package/src/memory/v3/edges.ts +249 -0
- package/src/memory/v3/filter.ts +281 -0
- package/src/memory/v3/gate.ts +334 -0
- package/src/memory/v3/index-composition.ts +113 -0
- package/src/memory/v3/llm-capture.ts +46 -0
- package/src/memory/v3/loop.ts +382 -0
- package/src/memory/v3/maintenance.ts +144 -0
- package/src/memory/v3/prompt-context.ts +33 -0
- package/src/memory/v3/prompts/consolidation.ts +458 -0
- package/src/memory/v3/prompts/system-prompts.ts +196 -0
- package/src/memory/v3/retriever.ts +33 -0
- package/src/memory/v3/scouts.ts +420 -0
- package/src/memory/v3/shadow-middleware.ts +305 -0
- package/src/memory/v3/traversal.ts +206 -0
- package/src/memory/v3/tree-index.ts +237 -0
- package/src/memory/v3/tree-store.ts +394 -0
- package/src/memory/v3/tree-walk.ts +351 -0
- package/src/memory/v3/types.ts +65 -0
- package/src/memory/v3/validate.ts +300 -0
- package/src/notifications/adapters/macos.ts +18 -1
- package/src/notifications/adapters/platform.ts +1 -1
- package/src/notifications/decision-engine.ts +1 -4
- package/src/notifications/emit-signal.ts +29 -49
- package/src/permissions/prompter.ts +3 -3
- package/src/permissions/question-prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +2 -2
- package/src/plugin-api/index.ts +4 -0
- package/src/plugin-api/types.ts +7 -33
- package/src/plugins/defaults/index.ts +6 -0
- package/src/plugins/defaults/injectors.ts +18 -11
- package/src/plugins/external-plugin-loader.ts +5 -68
- package/src/plugins/types.ts +11 -16
- package/src/proactive-artifact/aux-message-injector.ts +17 -4
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
- package/src/prompts/persona-resolver.ts +36 -21
- package/src/prompts/sections.ts +39 -7
- package/src/prompts/system-prompt.ts +50 -185
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +230 -8
- package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
- package/src/providers/__tests__/retry-callsite.test.ts +85 -5
- package/src/providers/anthropic/client.ts +32 -66
- package/src/providers/call-site-routing.ts +14 -2
- package/src/providers/connection-model-compat.ts +38 -0
- package/src/providers/connection-resolution.ts +16 -2
- package/src/providers/gemini/client.ts +49 -6
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/minimax/client.ts +106 -0
- package/src/providers/model-catalog.ts +43 -0
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openai/chat-completions-provider.ts +6 -3
- package/src/providers/openai/codex-models.ts +18 -0
- package/src/providers/openai/responses-provider.ts +78 -21
- package/src/providers/provider-send-message.ts +7 -1
- package/src/providers/retry.ts +34 -3
- package/src/providers/thinking-config.ts +26 -1
- package/src/providers/usage-tracking.ts +2 -0
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/agent-wake.ts +1 -0
- package/src/runtime/assistant-event-hub.ts +76 -6
- package/src/runtime/auth/route-policy.ts +36 -0
- package/src/runtime/btw-sidechain.ts +0 -6
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +10 -3
- package/src/runtime/pending-interactions.ts +0 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
- package/src/runtime/routes/acp-routes.test.ts +255 -6
- package/src/runtime/routes/acp-routes.ts +8 -1
- package/src/runtime/routes/avatar-routes.ts +10 -10
- package/src/runtime/routes/background-wake-routes.ts +188 -0
- package/src/runtime/routes/browser-tabs-routes.ts +200 -0
- package/src/runtime/routes/btw-routes.ts +0 -6
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +12 -4
- package/src/runtime/routes/conversation-management-routes.ts +77 -20
- package/src/runtime/routes/conversation-query-routes.ts +142 -36
- package/src/runtime/routes/conversation-routes.ts +252 -410
- package/src/runtime/routes/conversation-starter-routes.ts +6 -3
- package/src/runtime/routes/disk-pressure-routes.ts +1 -1
- package/src/runtime/routes/domain-routes.ts +60 -10
- package/src/runtime/routes/email-routes.ts +5 -2
- package/src/runtime/routes/events-routes.ts +54 -10
- package/src/runtime/routes/group-routes.ts +24 -8
- package/src/runtime/routes/host-browser-routes.ts +10 -2
- package/src/runtime/routes/host-cu-routes.ts +2 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
- package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
- package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
- package/src/runtime/routes/memory-item-routes.ts +8 -3
- package/src/runtime/routes/memory-v2-routes.ts +215 -5
- package/src/runtime/routes/memory-v3-routes.ts +316 -0
- package/src/runtime/routes/migration-routes.ts +21 -24
- package/src/runtime/routes/plugins-routes.ts +337 -0
- package/src/runtime/routes/rename-conversation-routes.ts +6 -2
- package/src/runtime/routes/secret-routes.ts +25 -5
- package/src/runtime/routes/settings-routes.ts +12 -11
- package/src/runtime/routes/slack-channel-routes.ts +5 -4
- package/src/runtime/routes/workspace-routes.ts +25 -10
- package/src/runtime/sync/resource-sync-events.ts +106 -38
- package/src/runtime/sync/sync-publisher.test.ts +49 -0
- package/src/runtime/sync/sync-publisher.ts +2 -1
- package/src/runtime/verification-outbound-actions.ts +73 -1
- package/src/telemetry/types.ts +12 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/acp/spawn.test.ts +119 -0
- package/src/tools/acp/spawn.ts +15 -2
- package/src/tools/apps/definitions.ts +2 -8
- package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
- package/src/tools/ask-question/ask-question-tool.ts +38 -45
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
- package/src/tools/browser/browser-execution.ts +16 -3
- package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
- package/src/tools/browser/cdp-client/factory.ts +100 -17
- package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
- package/src/tools/browser/cdp-client/types.ts +65 -0
- package/src/tools/browser/pinned-tabs.ts +96 -40
- package/src/tools/computer-use/definitions.ts +22 -78
- package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
- package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
- package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
- package/src/tools/credentials/vault.ts +3 -9
- package/src/tools/document/document-tool.ts +59 -0
- package/src/tools/execution-target.ts +21 -23
- package/src/tools/executor.ts +6 -1
- package/src/tools/filesystem/edit.ts +3 -9
- package/src/tools/filesystem/list.ts +3 -9
- package/src/tools/filesystem/read.ts +3 -9
- package/src/tools/filesystem/write.ts +3 -9
- package/src/tools/host-filesystem/edit.ts +3 -9
- package/src/tools/host-filesystem/read.ts +3 -9
- package/src/tools/host-filesystem/transfer.ts +3 -9
- package/src/tools/host-filesystem/write.ts +3 -9
- package/src/tools/host-terminal/host-shell.ts +3 -9
- package/src/tools/mcp/mcp-tool-factory.ts +1 -8
- package/src/tools/memory/register.test.ts +1 -1
- package/src/tools/memory/register.ts +4 -9
- package/src/tools/network/web-fetch.ts +3 -9
- package/src/tools/network/web-search.ts +25 -32
- package/src/tools/registry.ts +7 -23
- package/src/tools/schema-transforms.ts +1 -1
- package/src/tools/skills/execute.ts +3 -9
- package/src/tools/skills/load.ts +3 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -8
- package/src/tools/subagent/notify-parent.ts +3 -9
- package/src/tools/system/request-permission.ts +3 -9
- package/src/tools/terminal/shell.ts +3 -9
- package/src/tools/tool-defaults.ts +94 -0
- package/src/tools/types.ts +27 -98
- package/src/tools/ui-surface/definitions.ts +6 -22
- package/src/usage/pricing.ts +23 -0
- package/src/usage/types.ts +12 -0
- package/src/util/logger.ts +16 -7
- package/src/util/platform.ts +7 -2
- package/src/util/sqlite3-runtime.ts +65 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
- package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
- package/src/__tests__/message-complete-display-id.test.ts +0 -175
- package/src/daemon/query-complexity-router.ts +0 -75
- package/src/prompts/cache-boundary.ts +0 -8
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-assembly tests for the v3 retriever wiring in
|
|
3
|
+
* `handleCompareRetrievers` (`assistant/src/runtime/routes/memory-v2-routes.ts`).
|
|
4
|
+
*
|
|
5
|
+
* The compare route always includes the router retriever as comparand #1 and
|
|
6
|
+
* adds the v3 retriever as comparand #2 only when `config.memory.v3.enabled`.
|
|
7
|
+
* These tests exercise that gating end-to-end through the real handler and the
|
|
8
|
+
* real `runComparisonOverHistory`, with a fixture DB seeded with one logged
|
|
9
|
+
* router turn (mirroring `assistant/src/memory/v2/__tests__/harness-compare.test.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Neither the real router nor the real v3 loop runs here — both would hit a
|
|
12
|
+
* provider. `../loop.js` (the v3 loop) and `../../v2/harness/router-retriever.js`
|
|
13
|
+
* are `mock.module`-stubbed to return deterministic selections, so the tests
|
|
14
|
+
* assert *which retrievers were assembled* (by the names in the report), not
|
|
15
|
+
* their retrieval quality. `loadConfig` is stubbed so each test controls
|
|
16
|
+
* `memory.v3.enabled`; workspace/page-index helpers are stubbed to keep the
|
|
17
|
+
* handler off the filesystem.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
21
|
+
|
|
22
|
+
import type { AssistantConfig } from "../../../config/types.js";
|
|
23
|
+
import { getDb } from "../../db-connection.js";
|
|
24
|
+
import { initializeDb } from "../../db-init.js";
|
|
25
|
+
import type { MemoryV2ConceptRowRecord } from "../../memory-v2-activation-log-store.js";
|
|
26
|
+
import {
|
|
27
|
+
conversations,
|
|
28
|
+
memoryV2ActivationLogs,
|
|
29
|
+
messages,
|
|
30
|
+
} from "../../schema.js";
|
|
31
|
+
import type {
|
|
32
|
+
RetrievalInput,
|
|
33
|
+
RetrievalOutput,
|
|
34
|
+
} from "../../v2/harness/retriever.js";
|
|
35
|
+
|
|
36
|
+
initializeDb();
|
|
37
|
+
|
|
38
|
+
// Silence the route's logger.
|
|
39
|
+
mock.module("../../../util/logger.js", () => ({
|
|
40
|
+
getLogger: () =>
|
|
41
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// loadNowText / page-index read workspace files; a nonexistent dir yields "".
|
|
45
|
+
const WORKSPACE = "/tmp/v3-retriever-nonexistent-workspace";
|
|
46
|
+
|
|
47
|
+
// Controllable config: each test sets `v3Enabled` before invoking the handler.
|
|
48
|
+
let v3Enabled = false;
|
|
49
|
+
|
|
50
|
+
mock.module("../../../config/loader.js", () => ({
|
|
51
|
+
loadConfig: (): AssistantConfig =>
|
|
52
|
+
({
|
|
53
|
+
memory: {
|
|
54
|
+
v2: { enabled: true, router: { historical_pairs: 1 } },
|
|
55
|
+
v3: { enabled: v3Enabled },
|
|
56
|
+
},
|
|
57
|
+
}) as unknown as AssistantConfig,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
mock.module("../../../util/platform.js", () => ({
|
|
61
|
+
getWorkspaceDir: (): string => WORKSPACE,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// page-index is intentionally NOT mocked: it has a wide export surface
|
|
65
|
+
// (`invalidatePageIndex` etc.) that transitive importers in the route's
|
|
66
|
+
// dependency graph rely on, and `getPageIndex` over the nonexistent workspace
|
|
67
|
+
// returns a benign index. The retriever names are what we assert, not the
|
|
68
|
+
// page set, so the real (empty-ish) index is harmless here.
|
|
69
|
+
|
|
70
|
+
// Stub the router retriever — the real one calls a provider.
|
|
71
|
+
mock.module("../../v2/harness/router-retriever.js", () => ({
|
|
72
|
+
createRouterRetriever: () => ({
|
|
73
|
+
name: "router",
|
|
74
|
+
retrieve: async (): Promise<RetrievalOutput> => ({
|
|
75
|
+
selectedSlugs: ["p1"],
|
|
76
|
+
sourceBySlug: new Map([["p1", "router"]]),
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Stub the v3 loop — the real one runs scout/filter/tree/edge/gate lanes that
|
|
82
|
+
// hit providers, embeddings, and the filesystem.
|
|
83
|
+
mock.module("../loop.js", () => ({
|
|
84
|
+
runRetrievalLoop: async (
|
|
85
|
+
_input: RetrievalInput,
|
|
86
|
+
): Promise<RetrievalOutput> => ({
|
|
87
|
+
selectedSlugs: ["p2"],
|
|
88
|
+
sourceBySlug: new Map([["p2", "dense"]]),
|
|
89
|
+
}),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
// Import the handler only after the mocks are installed.
|
|
93
|
+
const { handleCompareRetrievers } =
|
|
94
|
+
await import("../../../runtime/routes/memory-v2-routes.js");
|
|
95
|
+
|
|
96
|
+
const ZERO_CONFIG = {
|
|
97
|
+
d: 0,
|
|
98
|
+
c_user: 0,
|
|
99
|
+
c_assistant: 0,
|
|
100
|
+
c_now: 0,
|
|
101
|
+
k: 0,
|
|
102
|
+
hops: 0,
|
|
103
|
+
top_k: 0,
|
|
104
|
+
epsilon: 0,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let seq = 0;
|
|
108
|
+
|
|
109
|
+
function ensureConversation(id: string): void {
|
|
110
|
+
getDb()
|
|
111
|
+
.insert(conversations)
|
|
112
|
+
.values({ id, createdAt: 0, updatedAt: 0 })
|
|
113
|
+
.onConflictDoNothing()
|
|
114
|
+
.run();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function insertMessage(
|
|
118
|
+
id: string,
|
|
119
|
+
conversationId: string,
|
|
120
|
+
role: string,
|
|
121
|
+
text: string,
|
|
122
|
+
createdAt: number,
|
|
123
|
+
): void {
|
|
124
|
+
ensureConversation(conversationId);
|
|
125
|
+
getDb()
|
|
126
|
+
.insert(messages)
|
|
127
|
+
.values({
|
|
128
|
+
id,
|
|
129
|
+
conversationId,
|
|
130
|
+
role,
|
|
131
|
+
content: JSON.stringify([{ type: "text", text }]),
|
|
132
|
+
createdAt,
|
|
133
|
+
})
|
|
134
|
+
.run();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function makeConcept(
|
|
138
|
+
slug: string,
|
|
139
|
+
status: MemoryV2ConceptRowRecord["status"],
|
|
140
|
+
): MemoryV2ConceptRowRecord {
|
|
141
|
+
return {
|
|
142
|
+
slug,
|
|
143
|
+
finalActivation: 0,
|
|
144
|
+
ownActivation: 0,
|
|
145
|
+
priorActivation: 0,
|
|
146
|
+
simUser: 0,
|
|
147
|
+
simAssistant: 0,
|
|
148
|
+
simNow: 0,
|
|
149
|
+
simUserRerankBoost: 0,
|
|
150
|
+
simAssistantRerankBoost: 0,
|
|
151
|
+
inRerankPool: false,
|
|
152
|
+
spreadContribution: 0,
|
|
153
|
+
source: "router",
|
|
154
|
+
status,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function insertRouterLog(
|
|
159
|
+
conversationId: string,
|
|
160
|
+
messageId: string,
|
|
161
|
+
turn: number,
|
|
162
|
+
concepts: MemoryV2ConceptRowRecord[],
|
|
163
|
+
createdAt: number,
|
|
164
|
+
): void {
|
|
165
|
+
ensureConversation(conversationId);
|
|
166
|
+
getDb()
|
|
167
|
+
.insert(memoryV2ActivationLogs)
|
|
168
|
+
.values({
|
|
169
|
+
id: `log-${seq++}`,
|
|
170
|
+
conversationId,
|
|
171
|
+
messageId,
|
|
172
|
+
turn,
|
|
173
|
+
mode: "router",
|
|
174
|
+
conceptsJson: JSON.stringify(concepts),
|
|
175
|
+
skillsJson: "[]",
|
|
176
|
+
configJson: JSON.stringify(ZERO_CONFIG),
|
|
177
|
+
createdAt,
|
|
178
|
+
})
|
|
179
|
+
.run();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Seed one router turn: user msg, assistant anchor, and the logged picks. */
|
|
183
|
+
function seedTurn(groundTruth: string[]): void {
|
|
184
|
+
insertMessage("u1", "c1", "user", "hello", 10);
|
|
185
|
+
insertMessage("a1", "c1", "assistant", "hi", 20); // anchor for turn 1
|
|
186
|
+
insertRouterLog(
|
|
187
|
+
"c1",
|
|
188
|
+
"a1",
|
|
189
|
+
1,
|
|
190
|
+
groundTruth.map((slug) => makeConcept(slug, "injected")),
|
|
191
|
+
20,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function reset(): void {
|
|
196
|
+
const db = getDb();
|
|
197
|
+
db.delete(memoryV2ActivationLogs).run();
|
|
198
|
+
db.delete(messages).run();
|
|
199
|
+
v3Enabled = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
describe("handleCompareRetrievers v3 wiring", () => {
|
|
203
|
+
beforeEach(reset);
|
|
204
|
+
|
|
205
|
+
test("includes only router when memory.v3.enabled is false", async () => {
|
|
206
|
+
seedTurn(["p1", "p2"]);
|
|
207
|
+
|
|
208
|
+
const report = await handleCompareRetrievers({ body: {} });
|
|
209
|
+
|
|
210
|
+
const names = report.retrievers.map((r) => r.name);
|
|
211
|
+
expect(names).toEqual(["router"]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("includes router and v3 when memory.v3.enabled is true", async () => {
|
|
215
|
+
v3Enabled = true;
|
|
216
|
+
seedTurn(["p1", "p2"]);
|
|
217
|
+
|
|
218
|
+
const report = await handleCompareRetrievers({ body: {} });
|
|
219
|
+
|
|
220
|
+
const names = report.retrievers.map((r) => r.name);
|
|
221
|
+
expect(names).toEqual(["router", "v3"]);
|
|
222
|
+
// Router is always comparand #1; v3 joins as comparand #2.
|
|
223
|
+
expect(names[0]).toBe("router");
|
|
224
|
+
expect(names[1]).toBe("v3");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `assistant/src/memory/v3/scouts.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The scout lanes read the v2 substrate (page index, injection-event EMA,
|
|
5
|
+
* Qdrant hybrid query, BM25, dense embed + calibration). Every one of those is
|
|
6
|
+
* stubbed via `mock.module` so the suite needs no real Qdrant, embedding
|
|
7
|
+
* backend, or LLM — and the SQLite-backed EMA is replaced by a hand-fed score
|
|
8
|
+
* map, so the injected `db` is an opaque sentinel the lane never dereferences.
|
|
9
|
+
*
|
|
10
|
+
* Coverage:
|
|
11
|
+
* - hot lane: ranks the EMA score map desc; hits are candidates but NOT
|
|
12
|
+
* sticky (the query-aware gate may drop them).
|
|
13
|
+
* - sparse lane: reads sparseScore, ranks desc, flags near-exact hits
|
|
14
|
+
* sticky + tree-bypass.
|
|
15
|
+
* - dense lane: per-subtree quota caps off-domain hits; MMR diversifies.
|
|
16
|
+
* - lane toggles: each disabled lane is fully suppressed (no ScoutResult).
|
|
17
|
+
* - empty query / empty corpus short-circuits.
|
|
18
|
+
* - honors AbortSignal.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
22
|
+
|
|
23
|
+
import type { PageIndex } from "../../v2/page-index.js";
|
|
24
|
+
import type { ConceptPageQueryResult } from "../../v2/qdrant.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Substrate stubs — installed before importing the module under test.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
// Per-call programmable substrate state. Each test rewires these before
|
|
31
|
+
// calling runScouts; the mock factories below close over the live refs.
|
|
32
|
+
let injectionScores = new Map<string, number>();
|
|
33
|
+
let pageSlugs: string[] = [];
|
|
34
|
+
let hybridHits: ConceptPageQueryResult[] = [];
|
|
35
|
+
let embedCalls = 0;
|
|
36
|
+
// Records the text the sparse lane keyed its BM25 query off, so tests can
|
|
37
|
+
// assert it is the user turn only (not the NOW context).
|
|
38
|
+
let lastBm25QueryText: string | null = null;
|
|
39
|
+
|
|
40
|
+
mock.module("../../v2/injection-events.js", () => ({
|
|
41
|
+
computeInjectionScores: () => injectionScores,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
mock.module("../../v2/page-index.js", () => ({
|
|
45
|
+
getPageIndex: async (): Promise<PageIndex> => ({
|
|
46
|
+
entries: pageSlugs.map((slug, i) => ({
|
|
47
|
+
id: i + 1,
|
|
48
|
+
slug,
|
|
49
|
+
summary: "",
|
|
50
|
+
edges: [],
|
|
51
|
+
modifiedAt: 0,
|
|
52
|
+
})),
|
|
53
|
+
bySlug: new Map(),
|
|
54
|
+
byId: new Map(),
|
|
55
|
+
rendered: "",
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
mock.module("../../v2/qdrant.js", () => ({
|
|
60
|
+
hybridQueryConceptPages: async (): Promise<ConceptPageQueryResult[]> =>
|
|
61
|
+
hybridHits,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
mock.module("../../v2/sparse-bm25.js", () => ({
|
|
65
|
+
// Non-empty indices so the sparse/dense lanes don't short-circuit on an
|
|
66
|
+
// "empty query embedding". The values are irrelevant — the stubbed Qdrant
|
|
67
|
+
// query ignores them and returns `hybridHits` directly.
|
|
68
|
+
generateBm25QueryEmbedding: (text: string) => {
|
|
69
|
+
lastBm25QueryText = text;
|
|
70
|
+
return text.trim().length > 0
|
|
71
|
+
? { indices: [1], values: [1] }
|
|
72
|
+
: { indices: [], values: [] };
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
mock.module("../../embedding-backend.js", () => ({
|
|
77
|
+
embedWithBackend: async () => {
|
|
78
|
+
embedCalls += 1;
|
|
79
|
+
return { provider: "local", model: "stub", vectors: [[0.1, 0.2, 0.3]] };
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
mock.module("../../anisotropy.js", () => ({
|
|
84
|
+
applyCorrectionIfCalibrated: async (vec: number[]) => vec,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const { runScouts } = await import("../scouts.js");
|
|
88
|
+
import type { RetrievalInput } from "../../v2/harness/retriever.js";
|
|
89
|
+
import type { ScoutDeps } from "../scouts.js";
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Fixtures
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
const DB_SENTINEL = { __opaque: true } as unknown as ScoutDeps["db"];
|
|
96
|
+
const DEPS: ScoutDeps = { db: DB_SENTINEL };
|
|
97
|
+
|
|
98
|
+
type Lanes = { hot: boolean; sparse: boolean; dense: boolean };
|
|
99
|
+
|
|
100
|
+
function makeInput(opts?: {
|
|
101
|
+
userMessage?: string;
|
|
102
|
+
nowText?: string;
|
|
103
|
+
lanes?: Partial<Lanes>;
|
|
104
|
+
denseQuota?: { activeDomain: number; offDomain: number };
|
|
105
|
+
hotLimit?: number;
|
|
106
|
+
signal?: AbortSignal;
|
|
107
|
+
}): RetrievalInput {
|
|
108
|
+
const lanes = {
|
|
109
|
+
hot: true,
|
|
110
|
+
sparse: true,
|
|
111
|
+
dense: true,
|
|
112
|
+
tree: true,
|
|
113
|
+
edges: true,
|
|
114
|
+
...opts?.lanes,
|
|
115
|
+
};
|
|
116
|
+
const config = {
|
|
117
|
+
memory: {
|
|
118
|
+
v3: {
|
|
119
|
+
lanes,
|
|
120
|
+
denseQuota: opts?.denseQuota ?? { activeDomain: 30, offDomain: 8 },
|
|
121
|
+
hotLimit: opts?.hotLimit ?? 50,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
} as unknown as RetrievalInput["config"];
|
|
125
|
+
return {
|
|
126
|
+
workspaceDir: "/tmp/ws",
|
|
127
|
+
recentTurnPairs: [
|
|
128
|
+
{ assistantMessage: "", userMessage: opts?.userMessage ?? "tell me" },
|
|
129
|
+
],
|
|
130
|
+
nowText: opts?.nowText ?? "now context",
|
|
131
|
+
priorEverInjected: [],
|
|
132
|
+
config,
|
|
133
|
+
signal: opts?.signal,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hit(
|
|
138
|
+
slug: string,
|
|
139
|
+
scores: Partial<ConceptPageQueryResult>,
|
|
140
|
+
): ConceptPageQueryResult {
|
|
141
|
+
return { slug, ...scores };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
injectionScores = new Map();
|
|
146
|
+
pageSlugs = [];
|
|
147
|
+
hybridHits = [];
|
|
148
|
+
embedCalls = 0;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Hot lane
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe("runScouts — hot lane", () => {
|
|
156
|
+
test("ranks EMA scores desc and seeds candidates without marking them sticky", async () => {
|
|
157
|
+
pageSlugs = ["people/alice", "work/proj", "essentials"];
|
|
158
|
+
injectionScores = new Map([
|
|
159
|
+
["work/proj", 0.2],
|
|
160
|
+
["people/alice", 0.9],
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const { scouts, sticky } = await runScouts(
|
|
164
|
+
makeInput({ lanes: { sparse: false, dense: false } }),
|
|
165
|
+
DEPS,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const hot = scouts.find((s) => s.lane === "hot");
|
|
169
|
+
expect(hot?.slugs).toEqual(["people/alice", "work/proj"]);
|
|
170
|
+
expect(hot?.scoreBySlug).toEqual({ "people/alice": 0.9, "work/proj": 0.2 });
|
|
171
|
+
// Hot hits are candidates only — the query-aware gate may still drop them,
|
|
172
|
+
// so they must NOT be force-kept via sticky.
|
|
173
|
+
expect(sticky.size).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("caps the hot lane to the top hotLimit by EMA", async () => {
|
|
177
|
+
pageSlugs = ["a", "b", "c", "d"];
|
|
178
|
+
injectionScores = new Map([
|
|
179
|
+
["a", 0.9],
|
|
180
|
+
["b", 0.7],
|
|
181
|
+
["c", 0.5],
|
|
182
|
+
["d", 0.3],
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const { scouts, sticky } = await runScouts(
|
|
186
|
+
makeInput({ lanes: { sparse: false, dense: false }, hotLimit: 2 }),
|
|
187
|
+
DEPS,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Only the top-2 by EMA survive the cap; the long tail is dropped so it
|
|
191
|
+
// can't flood the candidate set on a mature corpus.
|
|
192
|
+
const hot = scouts.find((s) => s.lane === "hot");
|
|
193
|
+
expect(hot?.slugs).toEqual(["a", "b"]);
|
|
194
|
+
expect(hot?.scoreBySlug).toEqual({ a: 0.9, b: 0.7 });
|
|
195
|
+
// Even the capped hot hits are not sticky.
|
|
196
|
+
expect(sticky.size).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("empty corpus yields no hot ScoutResult", async () => {
|
|
200
|
+
pageSlugs = [];
|
|
201
|
+
const { scouts } = await runScouts(
|
|
202
|
+
makeInput({ lanes: { sparse: false, dense: false } }),
|
|
203
|
+
DEPS,
|
|
204
|
+
);
|
|
205
|
+
expect(scouts.find((s) => s.lane === "hot")).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("no EMA events yields no hot ScoutResult", async () => {
|
|
209
|
+
pageSlugs = ["a", "b"];
|
|
210
|
+
injectionScores = new Map();
|
|
211
|
+
const { scouts, sticky } = await runScouts(
|
|
212
|
+
makeInput({ lanes: { sparse: false, dense: false } }),
|
|
213
|
+
DEPS,
|
|
214
|
+
);
|
|
215
|
+
expect(scouts.find((s) => s.lane === "hot")).toBeUndefined();
|
|
216
|
+
expect(sticky.size).toBe(0);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Sparse lane
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
describe("runScouts — sparse lane", () => {
|
|
225
|
+
test("reads sparseScore, ranks desc, flags near-exact sticky + bypass", async () => {
|
|
226
|
+
hybridHits = [
|
|
227
|
+
hit("docs/readme", { sparseScore: 4.0 }),
|
|
228
|
+
hit("docs/api", { sparseScore: 3.9 }), // within 90% of top -> near-exact
|
|
229
|
+
hit("misc/note", { sparseScore: 1.0 }), // below threshold
|
|
230
|
+
hit("dense/only", { denseScore: 0.8 }), // no sparseScore -> dropped
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const { scouts, sticky, bypass } = await runScouts(
|
|
234
|
+
makeInput({ lanes: { hot: false, dense: false } }),
|
|
235
|
+
DEPS,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const sparse = scouts.find((s) => s.lane === "sparse");
|
|
239
|
+
expect(sparse?.slugs).toEqual(["docs/readme", "docs/api", "misc/note"]);
|
|
240
|
+
// Near-exact: readme (top) and api (>= 90% of top). Not misc/note.
|
|
241
|
+
expect([...sticky].sort()).toEqual(["docs/api", "docs/readme"]);
|
|
242
|
+
expect([...bypass].sort()).toEqual(["docs/api", "docs/readme"]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("keys the BM25 query off the user turn only, not the NOW context", async () => {
|
|
246
|
+
hybridHits = [hit("docs/readme", { sparseScore: 4.0 })];
|
|
247
|
+
|
|
248
|
+
await runScouts(
|
|
249
|
+
makeInput({
|
|
250
|
+
userMessage: "favorite foods",
|
|
251
|
+
nowText: "ongoing project alpha and journal beta",
|
|
252
|
+
lanes: { hot: false, dense: false },
|
|
253
|
+
}),
|
|
254
|
+
DEPS,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// The sparse lane must search the user's words alone — folding NOW in would
|
|
258
|
+
// make NOW-referenced pages near-exact (sticky) on every turn.
|
|
259
|
+
expect(lastBm25QueryText).toBe("favorite foods");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("no sparse hits yields no sparse ScoutResult", async () => {
|
|
263
|
+
hybridHits = [hit("dense/only", { denseScore: 0.5 })];
|
|
264
|
+
const { scouts, sticky, bypass } = await runScouts(
|
|
265
|
+
makeInput({ lanes: { hot: false, dense: false } }),
|
|
266
|
+
DEPS,
|
|
267
|
+
);
|
|
268
|
+
expect(scouts.find((s) => s.lane === "sparse")).toBeUndefined();
|
|
269
|
+
expect(sticky.size).toBe(0);
|
|
270
|
+
expect(bypass.size).toBe(0);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Dense lane
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
describe("runScouts — dense lane", () => {
|
|
279
|
+
test("embeds the query and emits dense hits ranked by denseScore", async () => {
|
|
280
|
+
hybridHits = [
|
|
281
|
+
hit("work/a", { denseScore: 0.9 }),
|
|
282
|
+
hit("work/b", { denseScore: 0.7 }),
|
|
283
|
+
];
|
|
284
|
+
const { scouts } = await runScouts(
|
|
285
|
+
makeInput({ lanes: { hot: false, sparse: false } }),
|
|
286
|
+
DEPS,
|
|
287
|
+
);
|
|
288
|
+
expect(embedCalls).toBe(1);
|
|
289
|
+
const dense = scouts.find((s) => s.lane === "dense");
|
|
290
|
+
expect(dense?.slugs[0]).toBe("work/a");
|
|
291
|
+
expect(dense?.scoreBySlug).toEqual({ "work/a": 0.9, "work/b": 0.7 });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("per-subtree quota caps off-domain hits", async () => {
|
|
295
|
+
// Active domain = top hit's domain = "work". Off-domain quota = 1.
|
|
296
|
+
hybridHits = [
|
|
297
|
+
hit("work/a", { denseScore: 0.99 }),
|
|
298
|
+
hit("work/b", { denseScore: 0.98 }),
|
|
299
|
+
hit("work/c", { denseScore: 0.97 }),
|
|
300
|
+
hit("people/x", { denseScore: 0.5 }), // off-domain, claims the 1 slot
|
|
301
|
+
hit("notes/y", { denseScore: 0.4 }), // off-domain, over quota -> dropped
|
|
302
|
+
hit("misc/z", { denseScore: 0.3 }), // off-domain, over quota -> dropped
|
|
303
|
+
];
|
|
304
|
+
const { scouts } = await runScouts(
|
|
305
|
+
makeInput({
|
|
306
|
+
lanes: { hot: false, sparse: false },
|
|
307
|
+
denseQuota: { activeDomain: 30, offDomain: 1 },
|
|
308
|
+
}),
|
|
309
|
+
DEPS,
|
|
310
|
+
);
|
|
311
|
+
const dense = scouts.find((s) => s.lane === "dense");
|
|
312
|
+
const slugs = dense?.slugs ?? [];
|
|
313
|
+
// All three work/* survive (active quota 30); exactly one off-domain hit.
|
|
314
|
+
expect(slugs.filter((s) => s.startsWith("work/")).length).toBe(3);
|
|
315
|
+
const offDomain = slugs.filter((s) => !s.startsWith("work/"));
|
|
316
|
+
expect(offDomain).toEqual(["people/x"]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("active-domain quota caps same-subtree hits", async () => {
|
|
320
|
+
hybridHits = [
|
|
321
|
+
hit("work/a", { denseScore: 0.99 }),
|
|
322
|
+
hit("work/b", { denseScore: 0.98 }),
|
|
323
|
+
hit("work/c", { denseScore: 0.97 }), // over active quota 2 -> dropped
|
|
324
|
+
hit("people/x", { denseScore: 0.5 }),
|
|
325
|
+
];
|
|
326
|
+
const { scouts } = await runScouts(
|
|
327
|
+
makeInput({
|
|
328
|
+
lanes: { hot: false, sparse: false },
|
|
329
|
+
denseQuota: { activeDomain: 2, offDomain: 8 },
|
|
330
|
+
}),
|
|
331
|
+
DEPS,
|
|
332
|
+
);
|
|
333
|
+
const slugs = scouts.find((s) => s.lane === "dense")?.slugs ?? [];
|
|
334
|
+
expect(slugs.filter((s) => s.startsWith("work/")).length).toBe(2);
|
|
335
|
+
expect(slugs).toContain("people/x");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("MMR interleaves subtrees rather than emitting a same-subtree run", async () => {
|
|
339
|
+
// Five work/* then one people/* of comparable relevance. Pure score order
|
|
340
|
+
// would bury people/x last; MMR should pull it forward once work/ is
|
|
341
|
+
// over-represented.
|
|
342
|
+
hybridHits = [
|
|
343
|
+
hit("work/a", { denseScore: 0.95 }),
|
|
344
|
+
hit("work/b", { denseScore: 0.94 }),
|
|
345
|
+
hit("work/c", { denseScore: 0.93 }),
|
|
346
|
+
hit("work/d", { denseScore: 0.92 }),
|
|
347
|
+
hit("people/x", { denseScore: 0.9 }),
|
|
348
|
+
];
|
|
349
|
+
const { scouts } = await runScouts(
|
|
350
|
+
makeInput({
|
|
351
|
+
lanes: { hot: false, sparse: false },
|
|
352
|
+
denseQuota: { activeDomain: 30, offDomain: 8 },
|
|
353
|
+
}),
|
|
354
|
+
DEPS,
|
|
355
|
+
);
|
|
356
|
+
const slugs = scouts.find((s) => s.lane === "dense")?.slugs ?? [];
|
|
357
|
+
// people/x is not stranded at the very end despite the lowest raw score.
|
|
358
|
+
expect(slugs.indexOf("people/x")).toBeLessThan(slugs.length - 1);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("no dense hits yields no dense ScoutResult", async () => {
|
|
362
|
+
hybridHits = [hit("sparse/only", { sparseScore: 2.0 })];
|
|
363
|
+
const { scouts } = await runScouts(
|
|
364
|
+
makeInput({ lanes: { hot: false, sparse: false } }),
|
|
365
|
+
DEPS,
|
|
366
|
+
);
|
|
367
|
+
expect(scouts.find((s) => s.lane === "dense")).toBeUndefined();
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// Lane toggles
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
describe("runScouts — lane toggles", () => {
|
|
376
|
+
test("disabling a lane suppresses its ScoutResult", async () => {
|
|
377
|
+
pageSlugs = ["a"];
|
|
378
|
+
injectionScores = new Map([["a", 1]]);
|
|
379
|
+
hybridHits = [hit("docs/a", { sparseScore: 2.0, denseScore: 0.5 })];
|
|
380
|
+
|
|
381
|
+
const all = await runScouts(makeInput(), DEPS);
|
|
382
|
+
expect(all.scouts.map((s) => s.lane).sort()).toEqual([
|
|
383
|
+
"dense",
|
|
384
|
+
"hot",
|
|
385
|
+
"sparse",
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
const hotOnly = await runScouts(
|
|
389
|
+
makeInput({ lanes: { sparse: false, dense: false } }),
|
|
390
|
+
DEPS,
|
|
391
|
+
);
|
|
392
|
+
expect(hotOnly.scouts.map((s) => s.lane)).toEqual(["hot"]);
|
|
393
|
+
// Dense embed must not run when the dense lane is off.
|
|
394
|
+
embedCalls = 0;
|
|
395
|
+
await runScouts(makeInput({ lanes: { dense: false } }), DEPS);
|
|
396
|
+
expect(embedCalls).toBe(0);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("all lanes off yields empty result", async () => {
|
|
400
|
+
pageSlugs = ["a"];
|
|
401
|
+
injectionScores = new Map([["a", 1]]);
|
|
402
|
+
hybridHits = [hit("docs/a", { sparseScore: 2.0, denseScore: 0.5 })];
|
|
403
|
+
const { scouts, sticky, bypass } = await runScouts(
|
|
404
|
+
makeInput({ lanes: { hot: false, sparse: false, dense: false } }),
|
|
405
|
+
DEPS,
|
|
406
|
+
);
|
|
407
|
+
expect(scouts).toEqual([]);
|
|
408
|
+
expect(sticky.size).toBe(0);
|
|
409
|
+
expect(bypass.size).toBe(0);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// Misc
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
describe("runScouts — misc", () => {
|
|
418
|
+
test("empty query text skips sparse and dense lanes", async () => {
|
|
419
|
+
pageSlugs = ["a"];
|
|
420
|
+
injectionScores = new Map([["a", 1]]);
|
|
421
|
+
hybridHits = [hit("docs/a", { sparseScore: 2.0, denseScore: 0.5 })];
|
|
422
|
+
const { scouts } = await runScouts(
|
|
423
|
+
makeInput({ userMessage: " ", nowText: " " }),
|
|
424
|
+
DEPS,
|
|
425
|
+
);
|
|
426
|
+
// Hot lane is query-independent and still fires; sparse/dense are gated off.
|
|
427
|
+
expect(scouts.map((s) => s.lane)).toEqual(["hot"]);
|
|
428
|
+
expect(embedCalls).toBe(0);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("honors an already-aborted signal", async () => {
|
|
432
|
+
const controller = new AbortController();
|
|
433
|
+
controller.abort();
|
|
434
|
+
pageSlugs = ["a"];
|
|
435
|
+
injectionScores = new Map([["a", 1]]);
|
|
436
|
+
await expect(
|
|
437
|
+
runScouts(makeInput({ signal: controller.signal }), DEPS),
|
|
438
|
+
).rejects.toThrow();
|
|
439
|
+
});
|
|
440
|
+
});
|