@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,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3 retriever — the multi-lane bounded-descent retrieval loop
|
|
3
|
+
* ({@link runRetrievalLoop}) adapted to the harness {@link Retriever}
|
|
4
|
+
* interface.
|
|
5
|
+
*
|
|
6
|
+
* This is the offline, zero-production-risk shadow path: the comparison harness
|
|
7
|
+
* replays historical oracle turns and scores v3's selection against the v2
|
|
8
|
+
* router's logged picks (recall@k). Nothing here runs on a live injection turn
|
|
9
|
+
* — the loop reads the DB handle for its hot lane but never mutates production
|
|
10
|
+
* state, matching the {@link Retriever} contract.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
14
|
+
import type {
|
|
15
|
+
RetrievalInput,
|
|
16
|
+
RetrievalOutput,
|
|
17
|
+
Retriever,
|
|
18
|
+
} from "../v2/harness/retriever.js";
|
|
19
|
+
import { runRetrievalLoop } from "./loop.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wrap the v3 retrieval loop as a named harness {@link Retriever}.
|
|
23
|
+
*
|
|
24
|
+
* @param db handle threaded to {@link runRetrievalLoop} for the scout hot lane.
|
|
25
|
+
*/
|
|
26
|
+
export function createV3Retriever(db: DrizzleDb): Retriever {
|
|
27
|
+
return {
|
|
28
|
+
name: "v3",
|
|
29
|
+
retrieve(input: RetrievalInput): Promise<RetrievalOutput> {
|
|
30
|
+
return runRetrievalLoop(input, { db });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Memory v3 — Always-on scout lanes (hot / sparse / dense)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// The v3 retrieval loop opens each pass by fanning out a small set of cheap,
|
|
6
|
+
// always-on "scout" lanes over the v2 read-substrate. Scouts surface candidate
|
|
7
|
+
// concept-page slugs from three complementary signals before any LLM judging
|
|
8
|
+
// (the dense judge lives in a later PR) or tree descent runs:
|
|
9
|
+
//
|
|
10
|
+
// - hot: corpus-global access-frequency EMA via `computeInjectionScores`.
|
|
11
|
+
// Retriever-agnostic — v2 keeps writing `memory_v2_injection_events`,
|
|
12
|
+
// so a page the user has been touching is "hot" regardless of which
|
|
13
|
+
// retriever surfaced it. Hits are seeded as ordinary **candidates**
|
|
14
|
+
// (not sticky) so the query-aware downstream gate can still drop a
|
|
15
|
+
// recency page that doesn't bear on the turn.
|
|
16
|
+
// - sparse: BM25 keyword match. Near-exact (high-score) hits are both
|
|
17
|
+
// **sticky** and **tree-bypass** — a literal keyword hit is a strong
|
|
18
|
+
// enough signal that we shouldn't make the slug earn its place by
|
|
19
|
+
// walking the tree.
|
|
20
|
+
// - dense: embedding-similarity match, then an asymmetric per-subtree quota
|
|
21
|
+
// (generous active-domain slice, thin off-domain slice) plus MMR for
|
|
22
|
+
// diversity so a single dominant subtree can't crowd out the slate.
|
|
23
|
+
//
|
|
24
|
+
// Each lane is individually toggleable via `config.memory.v3.lanes`. This module
|
|
25
|
+
// performs **no** LLM calls and writes nothing — it is a pure read over the v2
|
|
26
|
+
// substrate. A later PR composes `runScouts` into the full descent loop.
|
|
27
|
+
|
|
28
|
+
import type { AssistantConfig } from "../../config/types.js";
|
|
29
|
+
import { applyCorrectionIfCalibrated } from "../anisotropy.js";
|
|
30
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
31
|
+
import { embedWithBackend } from "../embedding-backend.js";
|
|
32
|
+
import type { RetrievalInput } from "../v2/harness/retriever.js";
|
|
33
|
+
import type { ScoutResult } from "../v2/harness/trace.js";
|
|
34
|
+
import { computeInjectionScores } from "../v2/injection-events.js";
|
|
35
|
+
import { getPageIndex } from "../v2/page-index.js";
|
|
36
|
+
import { hybridQueryConceptPages } from "../v2/qdrant.js";
|
|
37
|
+
import { generateBm25QueryEmbedding } from "../v2/sparse-bm25.js";
|
|
38
|
+
|
|
39
|
+
/** Result of running the always-on scout fanout for one pass. */
|
|
40
|
+
export interface RunScoutsResult {
|
|
41
|
+
/** Per-lane contributions, one entry per *enabled* lane that produced hits. */
|
|
42
|
+
scouts: ScoutResult[];
|
|
43
|
+
/**
|
|
44
|
+
* Slugs the downstream gate should keep in the running regardless of later
|
|
45
|
+
* scoring — near-exact sparse hits. Hot-lane hits are deliberately excluded:
|
|
46
|
+
* they contribute candidates but must earn their place through the gate.
|
|
47
|
+
*/
|
|
48
|
+
sticky: Set<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Slugs strong enough (near-exact sparse) to skip the tree-descent gate
|
|
51
|
+
* entirely. A subset of `sticky`.
|
|
52
|
+
*/
|
|
53
|
+
bypass: Set<string>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Substrate dependencies injected for testability. */
|
|
57
|
+
export interface ScoutDeps {
|
|
58
|
+
db: DrizzleDb;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Tunables
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Per-lane hit cap before quota/diversity post-processing. The lanes are
|
|
67
|
+
* always-on and run every pass, so a generous-but-bounded cap keeps the dense
|
|
68
|
+
* Qdrant round-trip and the per-lane bookkeeping cheap while still giving the
|
|
69
|
+
* quota/MMR step enough raw candidates to choose from.
|
|
70
|
+
*/
|
|
71
|
+
const LANE_QUERY_LIMIT = 100;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sparse score at or above which a hit is treated as **near-exact** — sticky
|
|
75
|
+
* and tree-bypass. BM25 scores are unbounded above and corpus-relative, so the
|
|
76
|
+
* threshold is taken relative to the top sparse hit in the same pass rather
|
|
77
|
+
* than as a fixed magnitude: a hit within this fraction of the best sparse
|
|
78
|
+
* score for the query is "near-exact". A lone strong hit (it is its own max)
|
|
79
|
+
* always qualifies.
|
|
80
|
+
*/
|
|
81
|
+
const SPARSE_NEAR_EXACT_FRACTION = 0.9;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* MMR trade-off: `λ · relevance − (1 − λ) · redundancy`. Closer to 1 favors
|
|
85
|
+
* raw dense relevance; lower values push harder for subtree diversity. 0.7
|
|
86
|
+
* keeps relevance in the driver's seat while still breaking up runs of
|
|
87
|
+
* same-subtree hits.
|
|
88
|
+
*/
|
|
89
|
+
const DENSE_MMR_LAMBDA = 0.7;
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Public entry point
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Run the always-on scout lanes for one retrieval pass.
|
|
97
|
+
*
|
|
98
|
+
* The dense lane embeds `queryText` (the last user turn joined with
|
|
99
|
+
* `input.nowText`, the same shape the v2 router/activation path embeds). The
|
|
100
|
+
* sparse lane keys off `userText` (the user turn alone) so NOW context can't
|
|
101
|
+
* make whatever it mentions a near-exact sticky hit on every turn. Disabled
|
|
102
|
+
* lanes (per `config.memory.v3.lanes`) are skipped entirely: no substrate call,
|
|
103
|
+
* no `ScoutResult` entry.
|
|
104
|
+
*
|
|
105
|
+
* Honors `input.signal` — aborts between lanes and around the dense embed.
|
|
106
|
+
*/
|
|
107
|
+
export async function runScouts(
|
|
108
|
+
input: RetrievalInput,
|
|
109
|
+
deps: ScoutDeps,
|
|
110
|
+
): Promise<RunScoutsResult> {
|
|
111
|
+
const { config, signal } = input;
|
|
112
|
+
const lanes = config.memory.v3.lanes;
|
|
113
|
+
const queryText = deriveQueryText(input);
|
|
114
|
+
const userText = deriveUserText(input);
|
|
115
|
+
|
|
116
|
+
const scouts: ScoutResult[] = [];
|
|
117
|
+
const sticky = new Set<string>();
|
|
118
|
+
const bypass = new Set<string>();
|
|
119
|
+
|
|
120
|
+
// Hot lane — corpus-global EMA over the full slug universe. Cheap (single
|
|
121
|
+
// SQL pass) so it runs first. Hot hits are seeded as ordinary candidates but
|
|
122
|
+
// NOT sticky: the EMA ranks recency/frequency, not query relevance, so
|
|
123
|
+
// force-keeping the top-N recency pages would dominate every turn with
|
|
124
|
+
// operationally-frequent pages instead of the pages that bear on the query.
|
|
125
|
+
// Letting them pass through the query-aware gate lets irrelevant ones drop.
|
|
126
|
+
if (lanes.hot) {
|
|
127
|
+
signal?.throwIfAborted();
|
|
128
|
+
const hot = await runHotLane(input, deps);
|
|
129
|
+
if (hot) scouts.push(hot);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Sparse lane — BM25 keyword match on the user's words ONLY (not the NOW
|
|
133
|
+
// context). NOW is ambient standing text; folding it into a keyword query
|
|
134
|
+
// makes whatever pages NOW happens to mention score near-exact on every
|
|
135
|
+
// turn, and near-exact hits become sticky + tree-bypass — so NOW-referenced
|
|
136
|
+
// pages would be force-injected into every selection regardless of the
|
|
137
|
+
// query. Keying sparse off the user turn keeps lexical match, sticky, and
|
|
138
|
+
// bypass tied to what the user actually asked. (Dense still embeds NOW below;
|
|
139
|
+
// semantic context legitimately helps there.)
|
|
140
|
+
if (lanes.sparse && userText.length > 0) {
|
|
141
|
+
signal?.throwIfAborted();
|
|
142
|
+
const sparse = await runSparseLane(userText, signal);
|
|
143
|
+
if (sparse) {
|
|
144
|
+
scouts.push(sparse.result);
|
|
145
|
+
for (const slug of sparse.nearExact) {
|
|
146
|
+
sticky.add(slug);
|
|
147
|
+
bypass.add(slug);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Dense lane — embedding similarity, then per-subtree quota + MMR.
|
|
153
|
+
if (lanes.dense && queryText.length > 0) {
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
const dense = await runDenseLane(queryText, config, signal);
|
|
156
|
+
if (dense) scouts.push(dense);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { scouts, sticky, bypass };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Query-text derivation
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The just-arrived user turn's text — the last `recentTurnPairs` entry's
|
|
168
|
+
* `userMessage`. This is the keyword target for the sparse lane and the basis
|
|
169
|
+
* for near-exact sticky/bypass, which must reflect what the user actually
|
|
170
|
+
* asked rather than the ambient NOW context.
|
|
171
|
+
*/
|
|
172
|
+
function deriveUserText(input: RetrievalInput): string {
|
|
173
|
+
const lastPair = input.recentTurnPairs[input.recentTurnPairs.length - 1];
|
|
174
|
+
return (lastPair?.userMessage ?? "").trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build the dense-lane query text from the just-arrived user turn plus the NOW
|
|
179
|
+
* context. Mirrors the v2 activation path (`selectCandidates`): join the
|
|
180
|
+
* non-empty channels with a newline. NOW is included here because semantic
|
|
181
|
+
* embedding benefits from standing context; the sparse lane deliberately omits
|
|
182
|
+
* it (see {@link deriveUserText}).
|
|
183
|
+
*/
|
|
184
|
+
function deriveQueryText(input: RetrievalInput): string {
|
|
185
|
+
return [deriveUserText(input), input.nowText]
|
|
186
|
+
.filter((s) => s.trim().length > 0)
|
|
187
|
+
.join("\n")
|
|
188
|
+
.trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Hot lane
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
async function runHotLane(
|
|
196
|
+
input: RetrievalInput,
|
|
197
|
+
deps: ScoutDeps,
|
|
198
|
+
): Promise<ScoutResult | null> {
|
|
199
|
+
const index = await getPageIndex(input.workspaceDir);
|
|
200
|
+
const allSlugs = index.entries.map((e) => e.slug);
|
|
201
|
+
if (allSlugs.length === 0) return null;
|
|
202
|
+
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const scores = computeInjectionScores(deps.db, allSlugs, now);
|
|
205
|
+
if (scores.size === 0) return null;
|
|
206
|
+
|
|
207
|
+
// Slugs with no events in the read window are omitted by
|
|
208
|
+
// `computeInjectionScores`, so every entry here has score > 0. Cap to the
|
|
209
|
+
// top `hotLimit` by EMA: on a mature corpus — where nearly every page has
|
|
210
|
+
// been injected at some point — an uncapped lane would flood the candidate
|
|
211
|
+
// set with the entire corpus, so keep only the strongest recency signals.
|
|
212
|
+
const ranked = [...scores.entries()]
|
|
213
|
+
.sort((a, b) => sortByScoreDesc(a, b))
|
|
214
|
+
.slice(0, input.config.memory.v3.hotLimit);
|
|
215
|
+
const slugs = ranked.map(([slug]) => slug);
|
|
216
|
+
const scoreBySlug = Object.fromEntries(ranked);
|
|
217
|
+
return { lane: "hot", slugs, scoreBySlug };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Sparse lane
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
async function runSparseLane(
|
|
225
|
+
queryText: string,
|
|
226
|
+
signal: AbortSignal | undefined,
|
|
227
|
+
): Promise<{ result: ScoutResult; nearExact: string[] } | null> {
|
|
228
|
+
const sparse = generateBm25QueryEmbedding(queryText);
|
|
229
|
+
if (sparse.indices.length === 0) return null;
|
|
230
|
+
|
|
231
|
+
// Dense channel intentionally empty — this lane is BM25-only. `skipSparse:
|
|
232
|
+
// false` keeps the sparse round-trip on; we read `sparseScore` and ignore
|
|
233
|
+
// any dense scores the query happens to surface.
|
|
234
|
+
const hits = await hybridQueryConceptPages(
|
|
235
|
+
[],
|
|
236
|
+
sparse,
|
|
237
|
+
LANE_QUERY_LIMIT,
|
|
238
|
+
undefined,
|
|
239
|
+
{
|
|
240
|
+
skipSparse: false,
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
signal?.throwIfAborted();
|
|
244
|
+
|
|
245
|
+
const scored = hits
|
|
246
|
+
.map((hit) => ({ slug: hit.slug, score: hit.sparseScore }))
|
|
247
|
+
.filter((h): h is { slug: string; score: number } => h.score !== undefined)
|
|
248
|
+
.sort((a, b) => b.score - a.score);
|
|
249
|
+
if (scored.length === 0) return null;
|
|
250
|
+
|
|
251
|
+
const slugs = scored.map((h) => h.slug);
|
|
252
|
+
const scoreBySlug = Object.fromEntries(scored.map((h) => [h.slug, h.score]));
|
|
253
|
+
|
|
254
|
+
// Near-exact: within SPARSE_NEAR_EXACT_FRACTION of the top sparse score.
|
|
255
|
+
const topScore = scored[0].score;
|
|
256
|
+
const threshold = topScore * SPARSE_NEAR_EXACT_FRACTION;
|
|
257
|
+
const nearExact = scored
|
|
258
|
+
.filter((h) => topScore > 0 && h.score >= threshold)
|
|
259
|
+
.map((h) => h.slug);
|
|
260
|
+
|
|
261
|
+
return { result: { lane: "sparse", slugs, scoreBySlug }, nearExact };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Dense lane
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
async function runDenseLane(
|
|
269
|
+
queryText: string,
|
|
270
|
+
config: AssistantConfig,
|
|
271
|
+
signal: AbortSignal | undefined,
|
|
272
|
+
): Promise<ScoutResult | null> {
|
|
273
|
+
// Embed + apply anisotropy correction, mirroring v2 activation's read path.
|
|
274
|
+
const embedded = await embedWithBackend(config, [queryText], { signal });
|
|
275
|
+
const dense = await applyCorrectionIfCalibrated(
|
|
276
|
+
embedded.vectors[0],
|
|
277
|
+
embedded.provider,
|
|
278
|
+
embedded.model,
|
|
279
|
+
);
|
|
280
|
+
signal?.throwIfAborted();
|
|
281
|
+
|
|
282
|
+
const sparse = generateBm25QueryEmbedding(queryText);
|
|
283
|
+
const hits = await hybridQueryConceptPages(dense, sparse, LANE_QUERY_LIMIT);
|
|
284
|
+
signal?.throwIfAborted();
|
|
285
|
+
|
|
286
|
+
const scored = hits
|
|
287
|
+
.map((hit) => ({ slug: hit.slug, score: hit.denseScore }))
|
|
288
|
+
.filter((h): h is { slug: string; score: number } => h.score !== undefined)
|
|
289
|
+
.sort((a, b) => b.score - a.score);
|
|
290
|
+
if (scored.length === 0) return null;
|
|
291
|
+
|
|
292
|
+
const selected = applyQuotaAndMmr(scored, config.memory.v3);
|
|
293
|
+
if (selected.length === 0) return null;
|
|
294
|
+
|
|
295
|
+
const slugs = selected.map((h) => h.slug);
|
|
296
|
+
const scoreBySlug = Object.fromEntries(
|
|
297
|
+
selected.map((h) => [h.slug, h.score]),
|
|
298
|
+
);
|
|
299
|
+
return { lane: "dense", slugs, scoreBySlug };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface ScoredSlug {
|
|
303
|
+
slug: string;
|
|
304
|
+
score: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Apply the asymmetric per-subtree quota then MMR re-ranking to the dense hits.
|
|
309
|
+
*
|
|
310
|
+
* Quota: the conversation's **active domain** is the top-path segment of the
|
|
311
|
+
* single highest-scoring dense hit. That domain gets a generous slice
|
|
312
|
+
* (`denseQuota.activeDomain`); every other (off-)domain shares a thin slice
|
|
313
|
+
* (`denseQuota.offDomain`) so exploratory hits aren't fully starved but can't
|
|
314
|
+
* dominate either. Quotas are per-domain caps applied in score-descending
|
|
315
|
+
* order.
|
|
316
|
+
*
|
|
317
|
+
* MMR: re-rank the quota-passing pool by `λ · relevance − (1 − λ) · redundancy`
|
|
318
|
+
* where redundancy is how represented the candidate's subtree already is in the
|
|
319
|
+
* selected slate. Without per-page embeddings we use subtree co-membership as
|
|
320
|
+
* the diversity signal — same subtree ⇒ maximally redundant. This breaks up
|
|
321
|
+
* runs of same-subtree hits without an extra Qdrant round-trip.
|
|
322
|
+
*/
|
|
323
|
+
function applyQuotaAndMmr(
|
|
324
|
+
scored: readonly ScoredSlug[],
|
|
325
|
+
v3: AssistantConfig["memory"]["v3"],
|
|
326
|
+
): ScoredSlug[] {
|
|
327
|
+
if (scored.length === 0) return [];
|
|
328
|
+
|
|
329
|
+
const activeDomain = domainOf(scored[0].slug);
|
|
330
|
+
const { activeDomain: activeQuota, offDomain: offQuota } = v3.denseQuota;
|
|
331
|
+
|
|
332
|
+
// Per-subtree quota: active domain gets activeQuota slots; all off-domain
|
|
333
|
+
// hits compete for a shared offQuota pool. Walk in score-desc order so the
|
|
334
|
+
// strongest hits claim each quota first.
|
|
335
|
+
const perDomainCount = new Map<string, number>();
|
|
336
|
+
let offDomainCount = 0;
|
|
337
|
+
const quotaPassing: ScoredSlug[] = [];
|
|
338
|
+
for (const hit of scored) {
|
|
339
|
+
const domain = domainOf(hit.slug);
|
|
340
|
+
if (domain === activeDomain) {
|
|
341
|
+
const used = perDomainCount.get(domain) ?? 0;
|
|
342
|
+
if (used >= activeQuota) continue;
|
|
343
|
+
perDomainCount.set(domain, used + 1);
|
|
344
|
+
} else {
|
|
345
|
+
if (offDomainCount >= offQuota) continue;
|
|
346
|
+
offDomainCount += 1;
|
|
347
|
+
}
|
|
348
|
+
quotaPassing.push(hit);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return mmrReorder(quotaPassing, DENSE_MMR_LAMBDA);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Greedy MMR over a score-ranked pool using subtree co-membership as the
|
|
356
|
+
* redundancy signal. Each pick maximizes
|
|
357
|
+
* `λ · normalizedScore − (1 − λ) · subtreeShareInSelected`, so once a subtree
|
|
358
|
+
* is well-represented its remaining members are deprioritized in favor of
|
|
359
|
+
* fresh subtrees of comparable relevance. Pure / deterministic.
|
|
360
|
+
*/
|
|
361
|
+
function mmrReorder(pool: readonly ScoredSlug[], lambda: number): ScoredSlug[] {
|
|
362
|
+
if (pool.length <= 1) return [...pool];
|
|
363
|
+
|
|
364
|
+
// Normalize relevance to [0, 1] by the pool max so it shares a scale with the
|
|
365
|
+
// redundancy term (also [0, 1]). All-zero scores collapse to pure diversity.
|
|
366
|
+
const maxScore = pool[0].score;
|
|
367
|
+
const relevance = (hit: ScoredSlug): number =>
|
|
368
|
+
maxScore > 0 ? hit.score / maxScore : 0;
|
|
369
|
+
|
|
370
|
+
const remaining = [...pool];
|
|
371
|
+
const selected: ScoredSlug[] = [];
|
|
372
|
+
const selectedDomainCount = new Map<string, number>();
|
|
373
|
+
|
|
374
|
+
while (remaining.length > 0) {
|
|
375
|
+
let bestIdx = 0;
|
|
376
|
+
let bestMmr = -Infinity;
|
|
377
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
378
|
+
const hit = remaining[i];
|
|
379
|
+
const domain = domainOf(hit.slug);
|
|
380
|
+
const share =
|
|
381
|
+
selected.length === 0
|
|
382
|
+
? 0
|
|
383
|
+
: (selectedDomainCount.get(domain) ?? 0) / selected.length;
|
|
384
|
+
const mmr = lambda * relevance(hit) - (1 - lambda) * share;
|
|
385
|
+
if (mmr > bestMmr) {
|
|
386
|
+
bestMmr = mmr;
|
|
387
|
+
bestIdx = i;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const [pick] = remaining.splice(bestIdx, 1);
|
|
391
|
+
selected.push(pick);
|
|
392
|
+
const domain = domainOf(pick.slug);
|
|
393
|
+
selectedDomainCount.set(domain, (selectedDomainCount.get(domain) ?? 0) + 1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return selected;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Helpers
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The "domain" (subtree) of a page slug — its top path segment. Slugs are
|
|
405
|
+
* path-relative with `/` separators (e.g. `people/alice` → `people`); a flat
|
|
406
|
+
* slug (`essentials`) is its own domain.
|
|
407
|
+
*/
|
|
408
|
+
function domainOf(slug: string): string {
|
|
409
|
+
const slash = slug.indexOf("/");
|
|
410
|
+
return slash === -1 ? slug : slug.slice(0, slash);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Score-desc with a stable slug-ASCII tiebreak. */
|
|
414
|
+
function sortByScoreDesc(
|
|
415
|
+
a: readonly [string, number],
|
|
416
|
+
b: readonly [string, number],
|
|
417
|
+
): number {
|
|
418
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
419
|
+
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
420
|
+
}
|