@vellumai/assistant 0.7.1 → 0.7.2
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 +32 -49
- package/Dockerfile +1 -0
- package/README.md +1 -2
- package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
- package/bun.lock +26 -26
- package/docs/architecture/security.md +20 -0
- package/docs/plugins.md +7 -9
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
- package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
- package/node_modules/@vellumai/twilio-client/package.json +18 -0
- package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
- package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
- package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
- package/openapi.yaml +565 -12
- package/package.json +6 -3
- package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
- package/src/__tests__/app-bundler.test.ts +170 -1
- package/src/__tests__/app-control-flow.test.ts +374 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
- package/src/__tests__/app-executors.test.ts +30 -43
- package/src/__tests__/approval-routes-http.test.ts +23 -6
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
- package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
- package/src/__tests__/assistant-event-hub.test.ts +109 -2
- package/src/__tests__/assistant-event.test.ts +10 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
- package/src/__tests__/background-shell-host-bash.test.ts +14 -15
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
- package/src/__tests__/btw-routes.test.ts +13 -4
- package/src/__tests__/call-controller.test.ts +49 -1
- package/src/__tests__/call-domain.test.ts +0 -2
- package/src/__tests__/call-routes-http.test.ts +0 -2
- package/src/__tests__/channel-readiness-service.test.ts +59 -1
- package/src/__tests__/checker.test.ts +3 -4
- package/src/__tests__/config-loader-backfill.test.ts +90 -155
- package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-set-platform-guard.test.ts +48 -4
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-lifecycle.test.ts +36 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-slash-commands.test.ts +0 -4
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
- package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
- package/src/__tests__/credentials-cli.test.ts +5 -12
- package/src/__tests__/cu-unified-flow.test.ts +185 -23
- package/src/__tests__/daemon-credential-client.test.ts +101 -19
- package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +718 -1
- package/src/__tests__/helpers/call-route-handler.ts +7 -1
- package/src/__tests__/host-app-control-proxy.test.ts +602 -0
- package/src/__tests__/host-app-control-routes.test.ts +263 -0
- package/src/__tests__/host-bash-proxy.test.ts +246 -47
- package/src/__tests__/host-bash-routes.test.ts +294 -0
- package/src/__tests__/host-browser-proxy.test.ts +24 -22
- package/src/__tests__/host-browser-routes.test.ts +39 -13
- package/src/__tests__/host-cu-proxy.test.ts +41 -52
- package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
- package/src/__tests__/host-file-edit-tool.test.ts +47 -1
- package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
- package/src/__tests__/host-file-proxy.test.ts +37 -43
- package/src/__tests__/host-file-read-tool.test.ts +17 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
- package/src/__tests__/host-file-write-tool.test.ts +42 -1
- package/src/__tests__/host-proxy-base.test.ts +312 -0
- package/src/__tests__/host-shell-tool.test.ts +22 -4
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
- package/src/__tests__/host-transfer-proxy.test.ts +121 -22
- package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +29 -0
- package/src/__tests__/identity-routes.test.ts +103 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
- package/src/__tests__/inline-command-runner.test.ts +0 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
- package/src/__tests__/integration-status.test.ts +85 -5
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/mcp-auth-routes.test.ts +197 -0
- package/src/__tests__/mcp-cli.test.ts +338 -2
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
- package/src/__tests__/migration-import-commit-http.test.ts +108 -2
- package/src/__tests__/mock-gateway-ipc.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +0 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
- package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +97 -0
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/retry-backoff.test.ts +87 -0
- package/src/__tests__/runtime-events-sse.test.ts +10 -6
- package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
- package/src/__tests__/schedule-retry.test.ts +715 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +43 -41
- package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
- package/src/__tests__/skill-load-inline-command.test.ts +0 -51
- package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +9 -14
- package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-config.test.ts +0 -1
- package/src/__tests__/test-preload.ts +8 -0
- package/src/__tests__/tool-approval-handler.test.ts +3 -4
- package/src/__tests__/tool-audit-listener.test.ts +48 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +3 -5
- package/src/__tests__/twilio-validation.test.ts +93 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
- package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
- package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
- package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
- package/src/backup/__tests__/paths.test.ts +0 -22
- package/src/backup/__tests__/restore.test.ts +51 -151
- package/src/backup/paths.ts +2 -18
- package/src/backup/restore.ts +107 -231
- package/src/bundler/app-bundler.ts +51 -3
- package/src/calls/relay-server.ts +4 -44
- package/src/calls/twilio-config.ts +2 -17
- package/src/calls/twilio-rest.ts +33 -105
- package/src/calls/twilio-routes.ts +11 -12
- package/src/channels/types.ts +8 -7
- package/src/cli/commands/__tests__/backup.test.ts +6 -277
- package/src/cli/commands/__tests__/gateway.test.ts +288 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
- package/src/cli/commands/backup.ts +6 -331
- package/src/cli/commands/clients.ts +36 -37
- package/src/cli/commands/contacts.ts +73 -0
- package/src/cli/commands/conversations.ts +2 -5
- package/src/cli/commands/credentials.ts +15 -7
- package/src/cli/commands/domain.ts +66 -15
- package/src/cli/commands/gateway.ts +183 -0
- package/src/cli/commands/keys.ts +9 -6
- package/src/cli/commands/mcp.ts +116 -156
- package/src/cli/commands/memory-v2.ts +296 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
- package/src/cli/commands/platform/disconnect.ts +5 -4
- package/src/cli/commands/platform/index.ts +0 -18
- package/src/cli/lib/daemon-credential-client.ts +110 -28
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +67 -10
- package/src/config/bundled-skills/acp/SKILL.md +6 -0
- package/src/config/bundled-skills/acp/TOOLS.json +1 -22
- package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
- package/src/config/bundled-skills/app-control/SKILL.md +75 -0
- package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
- package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
- package/src/config/bundled-skills/document/TOOLS.json +0 -8
- package/src/config/bundled-skills/followups/TOOLS.json +0 -12
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
- package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
- package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
- package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
- package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
- package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
- package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
- package/src/config/bundled-skills/settings/SKILL.md +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +0 -12
- package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
- package/src/config/bundled-skills/subagent/SKILL.md +6 -2
- package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
- package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
- package/src/config/bundled-tool-registry.ts +21 -0
- package/src/config/env-registry.ts +0 -2
- package/src/config/env.ts +19 -12
- package/src/config/feature-flag-registry.json +21 -133
- package/src/config/loader.ts +73 -99
- package/src/config/sanitize-for-transfer.ts +2 -0
- package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
- package/src/config/schemas/calls.ts +0 -9
- package/src/config/schemas/heartbeat.ts +63 -0
- package/src/config/schemas/ingress.ts +10 -6
- package/src/config/schemas/llm.ts +5 -10
- package/src/config/schemas/memory-lifecycle.ts +77 -24
- package/src/config/schemas/memory-v2.ts +48 -4
- package/src/config/schemas/platform.ts +6 -0
- package/src/config/schemas/services.ts +1 -15
- package/src/config/schemas/skills.ts +0 -6
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/contacts/contact-store.ts +0 -30
- package/src/contacts/contacts-write.ts +0 -27
- package/src/context/window-manager.ts +1 -2
- package/src/credential-execution/feature-gates.ts +10 -10
- package/src/credential-execution/process-manager.ts +12 -41
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
- package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
- package/src/daemon/config-watcher.ts +4 -3
- package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
- package/src/daemon/conversation-agent-loop.ts +32 -28
- package/src/daemon/conversation-lifecycle.ts +8 -1
- package/src/daemon/conversation-process.ts +16 -11
- package/src/daemon/conversation-runtime-assembly.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +125 -4
- package/src/daemon/conversation-tool-setup.ts +16 -55
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/doordash-steps.ts +1 -1
- package/src/daemon/handlers/shared.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +293 -0
- package/src/daemon/host-bash-proxy.ts +84 -74
- package/src/daemon/host-browser-proxy.ts +67 -82
- package/src/daemon/host-cu-proxy.ts +81 -86
- package/src/daemon/host-file-proxy.ts +93 -69
- package/src/daemon/host-proxy-base.ts +294 -0
- package/src/daemon/host-proxy-preactivation.ts +82 -0
- package/src/daemon/host-transfer-proxy.ts +247 -129
- package/src/daemon/lifecycle.ts +115 -117
- package/src/daemon/message-protocol.ts +3 -8
- package/src/daemon/message-types/contacts.ts +23 -1
- package/src/daemon/message-types/conversations.ts +11 -8
- package/src/daemon/message-types/host-app-control.ts +150 -0
- package/src/daemon/message-types/host-bash.ts +4 -0
- package/src/daemon/message-types/host-cu.ts +2 -0
- package/src/daemon/message-types/host-file.ts +4 -0
- package/src/daemon/message-types/host-transfer.ts +3 -0
- package/src/daemon/message-types/schedules.ts +8 -3
- package/src/daemon/message-types/skills.ts +2 -2
- package/src/daemon/process-message.ts +18 -1
- package/src/daemon/shutdown-handlers.ts +0 -3
- package/src/daemon/tool-setup-types.ts +51 -0
- package/src/daemon/tool-side-effects.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -1
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
- package/src/heartbeat/heartbeat-run-store.ts +236 -0
- package/src/heartbeat/heartbeat-service.ts +280 -49
- package/src/home/__tests__/post-connect-feed.test.ts +99 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
- package/src/home/__tests__/suggested-prompts.test.ts +89 -0
- package/src/home/post-connect-feed.ts +68 -0
- package/src/home/relationship-state-writer.ts +17 -92
- package/src/home/suggested-prompts.ts +46 -10
- package/src/inbound/public-ingress-urls.ts +32 -34
- package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
- package/src/ipc/assistant-server.ts +14 -1
- package/src/ipc/cli-client.ts +32 -1
- package/src/live-voice/live-voice-metrics.ts +10 -10
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
- package/src/mcp/mcp-auth-orchestrator.ts +213 -0
- package/src/mcp/mcp-auth-state.ts +133 -0
- package/src/mcp/mcp-oauth-provider.ts +19 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
- package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
- package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
- package/src/memory/anisotropy.test.ts +247 -0
- package/src/memory/anisotropy.ts +443 -0
- package/src/memory/auto-analysis-constants.ts +17 -0
- package/src/memory/auto-analysis-guard.ts +5 -15
- package/src/memory/canonical-guardian-store.ts +7 -7
- package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
- package/src/memory/context-search/agent-protocol.ts +6 -6
- package/src/memory/context-search/agent-runner.ts +32 -7
- package/src/memory/context-search/sources/memory-v2.ts +17 -5
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-key-store.ts +2 -15
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-backend.ts +9 -21
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
- package/src/memory/graph/conversation-graph-memory.ts +1 -24
- package/src/memory/graph/graph-search.ts +8 -0
- package/src/memory/graph/retriever.ts +28 -0
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
- package/src/memory/jobs/embed-concept-page.ts +28 -2
- package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
- package/src/memory/jobs-store.ts +66 -22
- package/src/memory/jobs-worker.ts +112 -63
- package/src/memory/memory-v2-activation-log-store.ts +1 -1
- package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
- package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/pkb/pkb-search.ts +7 -0
- package/src/memory/qdrant-client.ts +50 -20
- package/src/memory/schema/infrastructure.ts +15 -0
- package/src/memory/search/semantic.ts +7 -0
- package/src/memory/sparse-tokenize.ts +49 -0
- package/src/memory/v2/__tests__/activation.test.ts +77 -95
- package/src/memory/v2/__tests__/injection.test.ts +43 -21
- package/src/memory/v2/__tests__/sim.test.ts +166 -6
- package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
- package/src/memory/v2/__tests__/static-context.test.ts +0 -1
- package/src/memory/v2/activation.ts +69 -88
- package/src/memory/v2/consolidation-job.ts +3 -5
- package/src/memory/v2/constants.ts +7 -0
- package/src/memory/v2/injection.ts +86 -53
- package/src/memory/v2/prompts/consolidation.ts +312 -91
- package/src/memory/v2/qdrant.ts +99 -1
- package/src/memory/v2/sim.ts +126 -16
- package/src/memory/v2/skill-qdrant.ts +12 -3
- package/src/memory/v2/skill-store.ts +16 -1
- package/src/memory/v2/sparse-bm25.ts +245 -0
- package/src/memory/v2/static-context.ts +6 -5
- package/src/messaging/providers/gmail/types.ts +0 -49
- package/src/messaging/providers/slack/adapter.ts +1 -31
- package/src/messaging/providers/slack/types.ts +0 -32
- package/src/notifications/README.md +10 -10
- package/src/notifications/broadcaster.ts +1 -1
- package/src/notifications/guardian-question-mode.ts +5 -5
- package/src/oauth/connect-orchestrator.ts +4 -0
- package/src/oauth/credential-token-resolver.ts +1 -3
- package/src/oauth/manual-token-connection.ts +0 -4
- package/src/outbound-proxy/index.ts +1 -37
- package/src/outbound-proxy/logging.ts +1 -1
- package/src/outbound-proxy/policy.ts +6 -5
- package/src/outbound-proxy/router.ts +2 -1
- package/src/permissions/approval-policy.test.ts +6 -275
- package/src/permissions/approval-policy.ts +0 -51
- package/src/permissions/checker.test.ts +0 -1
- package/src/permissions/checker.ts +3 -17
- package/src/permissions/gateway-threshold-reader.ts +2 -0
- package/src/permissions/prompter.ts +34 -1
- package/src/permissions/secret-prompter.ts +6 -2
- package/src/prompts/bootstrap-cleanup.ts +27 -0
- package/src/prompts/system-prompt.ts +3 -18
- package/src/prompts/templates/SOUL.md +13 -1
- package/src/providers/speech-to-text/provider-catalog.ts +7 -8
- package/src/runtime/assistant-event-hub.ts +118 -96
- package/src/runtime/assistant-event.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
- package/src/runtime/auth/middleware.ts +0 -96
- package/src/runtime/auth/route-policy.ts +19 -0
- package/src/runtime/btw-sidechain.ts +2 -3
- package/src/runtime/channel-invite-transport.ts +2 -48
- package/src/runtime/channel-invite-transports/email.ts +1 -1
- package/src/runtime/channel-invite-transports/slack.ts +1 -1
- package/src/runtime/channel-invite-transports/telegram.ts +1 -1
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
- package/src/runtime/channel-invite-types.ts +54 -0
- package/src/runtime/channel-readiness-service.ts +32 -13
- package/src/runtime/http-server.ts +3 -329
- package/src/runtime/http-types.ts +0 -5
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
- package/src/runtime/migrations/migration-transport.ts +7 -7
- package/src/runtime/migrations/vbundle-builder.ts +327 -60
- package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
- package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
- package/src/runtime/migrations/vbundle-importer.ts +245 -68
- package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
- package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
- package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
- package/src/runtime/migrations/vbundle-validator.ts +114 -0
- package/src/runtime/pending-interactions.ts +35 -9
- package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
- package/src/runtime/routes/approval-interception-types.ts +13 -0
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
- package/src/runtime/routes/backup-routes.ts +15 -38
- package/src/runtime/routes/btw-routes.ts +14 -37
- package/src/runtime/routes/client-routes.ts +1 -0
- package/src/runtime/routes/contact-prompt-routes.ts +183 -0
- package/src/runtime/routes/conversation-query-routes.ts +36 -1
- package/src/runtime/routes/conversation-routes.ts +30 -13
- package/src/runtime/routes/document-pdf-renderer.ts +165 -0
- package/src/runtime/routes/documents-routes.ts +30 -0
- package/src/runtime/routes/errors.ts +19 -4
- package/src/runtime/routes/events-routes.ts +12 -6
- package/src/runtime/routes/gateway-log-routes.ts +79 -0
- package/src/runtime/routes/guardian-approval-interception.ts +2 -8
- package/src/runtime/routes/heartbeat-routes.ts +103 -38
- package/src/runtime/routes/host-app-control-routes.ts +134 -0
- package/src/runtime/routes/host-bash-routes.ts +36 -6
- package/src/runtime/routes/host-browser-routes.ts +108 -13
- package/src/runtime/routes/host-cu-routes.ts +44 -14
- package/src/runtime/routes/host-file-routes.ts +33 -10
- package/src/runtime/routes/host-transfer-routes.ts +64 -24
- package/src/runtime/routes/http-adapter.ts +1 -0
- package/src/runtime/routes/identity-intro-cache.ts +30 -0
- package/src/runtime/routes/identity-routes.ts +15 -43
- package/src/runtime/routes/inbound-message-handler.ts +1 -9
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/mcp-auth-routes.ts +132 -0
- package/src/runtime/routes/memory-item-routes.ts +10 -12
- package/src/runtime/routes/memory-v2-routes.ts +441 -1
- package/src/runtime/routes/migration-routes.ts +96 -0
- package/src/runtime/routes/schedule-routes.ts +7 -0
- package/src/runtime/verification-templates.ts +4 -7
- package/src/schedule/integration-status.ts +66 -2
- package/src/schedule/recurrence-engine.ts +4 -1
- package/src/schedule/retry-backoff.ts +18 -0
- package/src/schedule/retry-policy.ts +82 -0
- package/src/schedule/schedule-recovery.ts +64 -0
- package/src/schedule/schedule-store.ts +106 -2
- package/src/schedule/scheduler-types.ts +25 -0
- package/src/schedule/scheduler.ts +63 -38
- package/src/security/oauth-callback-registry.ts +8 -0
- package/src/sequence/analytics.ts +5 -5
- package/src/sequence/engine.ts +1 -1
- package/src/skills/catalog-files.ts +2 -8
- package/src/skills/include-graph.ts +5 -5
- package/src/skills/remote-skill-policy.ts +5 -5
- package/src/skills/skill-file-provider.ts +1 -1
- package/src/skills/skill-file-types.ts +13 -0
- package/src/skills/skillssh-audit-types.ts +28 -0
- package/src/skills/skillssh-registry.ts +8 -21
- package/src/telemetry/types.ts +2 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
- package/src/tools/apps/executors.ts +56 -69
- package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
- package/src/tools/browser/cdp-client/factory.ts +23 -24
- package/src/tools/browser/cdp-client/index.ts +1 -14
- package/src/tools/computer-use/definitions.ts +42 -20
- package/src/tools/executor.ts +2 -0
- package/src/tools/host-filesystem/edit.ts +26 -0
- package/src/tools/host-filesystem/read.ts +26 -0
- package/src/tools/host-filesystem/transfer.ts +31 -1
- package/src/tools/host-filesystem/write.ts +26 -0
- package/src/tools/host-terminal/host-shell.ts +58 -0
- package/src/tools/schedule/create.ts +6 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/schedule/update.ts +10 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
- package/src/tools/shared/filesystem/path-policy.ts +25 -1
- package/src/tools/skills/load.ts +0 -32
- package/src/tools/tool-approval-handler.ts +1 -5
- package/src/tools/types.ts +4 -0
- package/src/usage/pricing.ts +1 -1
- package/src/workspace/hatched-date.ts +86 -0
- package/src/workspace/migrations/003-seed-device-id.ts +1 -1
- package/src/workspace/migrations/006-services-config.ts +8 -5
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
- package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
- package/src/workspace/migrations/utils.ts +21 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
- package/src/__tests__/twilio-rest.test.ts +0 -34
- package/src/backup/__tests__/backup-key.test.ts +0 -152
- package/src/backup/__tests__/backup-worker.test.ts +0 -782
- package/src/backup/__tests__/offsite-writer.test.ts +0 -641
- package/src/backup/__tests__/stream-crypt.test.ts +0 -228
- package/src/backup/backup-key.ts +0 -137
- package/src/backup/backup-worker.ts +0 -472
- package/src/backup/offsite-writer.ts +0 -222
- package/src/backup/stream-crypt.ts +0 -263
- package/src/daemon/message-types/pairing.ts +0 -58
- package/src/outbound-proxy/config.ts +0 -20
- package/src/outbound-proxy/health.ts +0 -18
- package/src/outbound-proxy/types.ts +0 -150
- package/src/runtime/capability-tokens.ts +0 -190
- package/src/signals/mcp-reload.ts +0 -18
package/src/memory/v2/qdrant.ts
CHANGED
|
@@ -236,6 +236,7 @@ export async function hybridQueryConceptPages(
|
|
|
236
236
|
sparse: SparseEmbedding,
|
|
237
237
|
limit: number,
|
|
238
238
|
restrictToSlugs?: readonly string[],
|
|
239
|
+
options?: { skipSparse?: boolean },
|
|
239
240
|
): Promise<ConceptPageQueryResult[]> {
|
|
240
241
|
if (restrictToSlugs && restrictToSlugs.length === 0) {
|
|
241
242
|
// An empty restriction means "no candidates"; skip the round-trip.
|
|
@@ -249,6 +250,13 @@ export async function hybridQueryConceptPages(
|
|
|
249
250
|
? { must: [{ key: "slug", match: { any: [...restrictToSlugs] } }] }
|
|
250
251
|
: undefined;
|
|
251
252
|
|
|
253
|
+
// When the caller weighted sparse to zero, skip the round-trip entirely.
|
|
254
|
+
// The downstream fuser (`fuseHit` in `sim.ts`) already treats a missing
|
|
255
|
+
// sparse score as a 0 contribution, so omitting the query is a pure
|
|
256
|
+
// optimization — and it's also the kill switch operators use to dodge a
|
|
257
|
+
// Qdrant 1.13.x sparse-index crash that we've reproduced in the wild.
|
|
258
|
+
const skipSparse = options?.skipSparse ?? false;
|
|
259
|
+
|
|
252
260
|
const denseQuery = () =>
|
|
253
261
|
client.query(MEMORY_V2_COLLECTION, {
|
|
254
262
|
query: dense,
|
|
@@ -267,7 +275,14 @@ export async function hybridQueryConceptPages(
|
|
|
267
275
|
});
|
|
268
276
|
|
|
269
277
|
// Run both queries concurrently — they hit independent named vectors.
|
|
270
|
-
|
|
278
|
+
// When sparse is gated off we still resolve a Promise so the destructuring
|
|
279
|
+
// below stays uniform; the empty `points: []` matches the shape of a
|
|
280
|
+
// no-hit Qdrant response.
|
|
281
|
+
const emptyResult = {
|
|
282
|
+
points: [] as Array<{ payload?: unknown; score?: number }>,
|
|
283
|
+
};
|
|
284
|
+
const runQueries = async () =>
|
|
285
|
+
Promise.all([denseQuery(), skipSparse ? emptyResult : sparseQuery()]);
|
|
271
286
|
|
|
272
287
|
let denseResults;
|
|
273
288
|
let sparseResults;
|
|
@@ -305,6 +320,89 @@ export async function hybridQueryConceptPages(
|
|
|
305
320
|
return Array.from(merged.values());
|
|
306
321
|
}
|
|
307
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Page through the v2 concept-page collection and return up to `maxSamples`
|
|
325
|
+
* stored dense vectors. Used by the anisotropy-fit pipeline to compute a
|
|
326
|
+
* corpus mean + top-k principal components without re-embedding every page.
|
|
327
|
+
*
|
|
328
|
+
* Sparse vectors are skipped — anisotropy is a dense-embedding phenomenon, and
|
|
329
|
+
* pulling the sparse side would just inflate the response. Payload is also
|
|
330
|
+
* skipped because the fit doesn't need slug identity.
|
|
331
|
+
*
|
|
332
|
+
* Returns an empty array when the collection is empty or missing. Caller
|
|
333
|
+
* decides what to do (typically: surface a "no vectors to fit" error).
|
|
334
|
+
*/
|
|
335
|
+
export async function sampleConceptPageDenseVectors(
|
|
336
|
+
maxSamples: number,
|
|
337
|
+
): Promise<number[][]> {
|
|
338
|
+
if (maxSamples <= 0) return [];
|
|
339
|
+
await ensureConceptPageCollection();
|
|
340
|
+
|
|
341
|
+
const client = getClient();
|
|
342
|
+
const out: number[][] = [];
|
|
343
|
+
let offset: string | number | undefined = undefined;
|
|
344
|
+
// Same pagination guard pattern as the rest of the file — bounds the loop
|
|
345
|
+
// even if Qdrant somehow keeps handing back a non-null offset.
|
|
346
|
+
const maxIterations = 10_000;
|
|
347
|
+
const batchSize = Math.min(256, maxSamples);
|
|
348
|
+
|
|
349
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
350
|
+
if (out.length >= maxSamples) break;
|
|
351
|
+
const remaining = maxSamples - out.length;
|
|
352
|
+
let result;
|
|
353
|
+
try {
|
|
354
|
+
result = await client.scroll(MEMORY_V2_COLLECTION, {
|
|
355
|
+
limit: Math.min(batchSize, remaining),
|
|
356
|
+
with_payload: false,
|
|
357
|
+
// Fetch only the dense named vector — sparse is irrelevant for
|
|
358
|
+
// anisotropy correction.
|
|
359
|
+
with_vector: ["dense"],
|
|
360
|
+
...(offset !== undefined ? { offset } : {}),
|
|
361
|
+
});
|
|
362
|
+
} catch (err) {
|
|
363
|
+
if (isCollectionMissing(err)) {
|
|
364
|
+
_collectionReady = false;
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const point of result.points) {
|
|
371
|
+
const v = extractDenseVector(point.vector);
|
|
372
|
+
if (v) out.push(v);
|
|
373
|
+
if (out.length >= maxSamples) break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const next = result.next_page_offset;
|
|
377
|
+
if (next == null) break;
|
|
378
|
+
offset = typeof next === "string" ? next : (next as number);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Pull the `dense` named-vector payload out of a Qdrant point. Defensively
|
|
386
|
+
* handles both the named-vector shape (`{ dense: [...] }`) and the legacy
|
|
387
|
+
* unnamed-vector shape (`number[]`) so older collection layouts don't trip
|
|
388
|
+
* the sampler. Returns `null` for shapes we don't recognise.
|
|
389
|
+
*/
|
|
390
|
+
function extractDenseVector(vector: unknown): number[] | null {
|
|
391
|
+
if (Array.isArray(vector)) {
|
|
392
|
+
if (vector.every((n) => typeof n === "number")) {
|
|
393
|
+
return vector as number[];
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
if (vector && typeof vector === "object") {
|
|
398
|
+
const dense = (vector as { dense?: unknown }).dense;
|
|
399
|
+
if (Array.isArray(dense) && dense.every((n) => typeof n === "number")) {
|
|
400
|
+
return dense as number[];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
308
406
|
/**
|
|
309
407
|
* Detect "collection not found" errors so callers can reset readiness and
|
|
310
408
|
* retry after an external deletion (e.g. workspace reset).
|
package/src/memory/v2/sim.ts
CHANGED
|
@@ -26,13 +26,12 @@
|
|
|
26
26
|
// only as a per-turn ordering signal, not compared across turns.
|
|
27
27
|
|
|
28
28
|
import type { AssistantConfig } from "../../config/types.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
generateSparseEmbedding,
|
|
32
|
-
} from "../embedding-backend.js";
|
|
29
|
+
import { applyCorrectionIfCalibrated } from "../anisotropy.js";
|
|
30
|
+
import { embedWithBackend } from "../embedding-backend.js";
|
|
33
31
|
import { clampUnitInterval } from "../validation.js";
|
|
34
32
|
import { hybridQueryConceptPages } from "./qdrant.js";
|
|
35
33
|
import { hybridQuerySkills } from "./skill-qdrant.js";
|
|
34
|
+
import { generateBm25QueryEmbedding } from "./sparse-bm25.js";
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
37
|
* Clamp a value into the closed unit interval [0, 1]. Re-exported under the
|
|
@@ -40,6 +39,79 @@ import { hybridQuerySkills } from "./skill-qdrant.js";
|
|
|
40
39
|
*/
|
|
41
40
|
export const clamp01 = clampUnitInterval;
|
|
42
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Built-in defaults for adaptive sparse weighting. Live here (not in the
|
|
44
|
+
* config schema) so operators don't see two new knobs in their config until
|
|
45
|
+
* they actually want to tune them.
|
|
46
|
+
*
|
|
47
|
+
* Below `MIN_SPREAD`, the sparse channel is treated as no-signal (its scores
|
|
48
|
+
* are uniform across the candidate set, so it can't rank anything) and the
|
|
49
|
+
* sparse weight collapses to 0. At or above `FULL_SPREAD`, sparse weight
|
|
50
|
+
* stays at its configured value. Linear interpolation between.
|
|
51
|
+
*/
|
|
52
|
+
const ADAPTIVE_SPARSE_MIN_SPREAD = 0.2;
|
|
53
|
+
const ADAPTIVE_SPARSE_FULL_SPREAD = 0.5;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Per-query effective dense + sparse weights, derived from the configured
|
|
57
|
+
* base weights and the spread of normalized sparse scores across the hit
|
|
58
|
+
* set. When the sparse channel can't discriminate (low spread or fewer
|
|
59
|
+
* than two sparse-bearing candidates), its weight collapses and dense
|
|
60
|
+
* weight is boosted to compensate so `dense + sparse` still equals
|
|
61
|
+
* `baseDense + baseSparse` and `fused` stays interpretable as a [0, 1]
|
|
62
|
+
* similarity.
|
|
63
|
+
*
|
|
64
|
+
* Pure function — exported so the diagnostic surface in
|
|
65
|
+
* `memory-v2-routes.explain-similarity` can show the effective weights and
|
|
66
|
+
* the measured spread alongside per-channel score statistics.
|
|
67
|
+
*/
|
|
68
|
+
export function effectiveWeights(
|
|
69
|
+
hits: ReadonlyArray<{ sparseScore?: number }>,
|
|
70
|
+
maxSparse: number,
|
|
71
|
+
baseDense: number,
|
|
72
|
+
baseSparse: number,
|
|
73
|
+
config: AssistantConfig,
|
|
74
|
+
): { dense: number; sparse: number; spread: number } {
|
|
75
|
+
// Short-circuit when the channel is already disabled or unscored. Returning
|
|
76
|
+
// base weights here keeps `fused` numerically identical to today's output
|
|
77
|
+
// for the no-sparse-signal cases the existing tests assume.
|
|
78
|
+
if (baseSparse === 0 || maxSparse === 0) {
|
|
79
|
+
return { dense: baseDense, sparse: baseSparse, spread: 0 };
|
|
80
|
+
}
|
|
81
|
+
let min = Infinity;
|
|
82
|
+
let max = -Infinity;
|
|
83
|
+
let count = 0;
|
|
84
|
+
for (const h of hits) {
|
|
85
|
+
if (h.sparseScore === undefined) continue;
|
|
86
|
+
const norm = h.sparseScore / maxSparse;
|
|
87
|
+
if (norm < min) min = norm;
|
|
88
|
+
if (norm > max) max = norm;
|
|
89
|
+
count++;
|
|
90
|
+
}
|
|
91
|
+
// With < 2 sparse-bearing hits the spread is undefined — fall back to base
|
|
92
|
+
// weights so single-hit retrievals still surface their sparse contribution
|
|
93
|
+
// (and the existing fusion-math tests stay green).
|
|
94
|
+
if (count < 2) {
|
|
95
|
+
return { dense: baseDense, sparse: baseSparse, spread: 0 };
|
|
96
|
+
}
|
|
97
|
+
const spread = max - min;
|
|
98
|
+
|
|
99
|
+
const minSpread =
|
|
100
|
+
config.memory.v2.min_sparse_spread ?? ADAPTIVE_SPARSE_MIN_SPREAD;
|
|
101
|
+
const fullSpread =
|
|
102
|
+
config.memory.v2.full_sparse_spread ?? ADAPTIVE_SPARSE_FULL_SPREAD;
|
|
103
|
+
// Degenerate config (full <= min): no interpolation range. Don't try to
|
|
104
|
+
// adapt; trust the operator's base weights and report the measured spread
|
|
105
|
+
// for diagnostics.
|
|
106
|
+
if (fullSpread <= minSpread) {
|
|
107
|
+
return { dense: baseDense, sparse: baseSparse, spread };
|
|
108
|
+
}
|
|
109
|
+
const factor = clamp01((spread - minSpread) / (fullSpread - minSpread));
|
|
110
|
+
const sparse = baseSparse * factor;
|
|
111
|
+
const dense = baseDense + (baseSparse - sparse);
|
|
112
|
+
return { dense, sparse, spread };
|
|
113
|
+
}
|
|
114
|
+
|
|
43
115
|
/**
|
|
44
116
|
* Compute hybrid (dense + sparse) similarity scores between a query text and
|
|
45
117
|
* a fixed set of candidate concept-page slugs.
|
|
@@ -63,8 +135,13 @@ export const clamp01 = clampUnitInterval;
|
|
|
63
135
|
* Edge cases:
|
|
64
136
|
* - Empty `candidateSlugs` → returns an empty map without touching Qdrant
|
|
65
137
|
* or the embedding backend.
|
|
66
|
-
* - Empty
|
|
67
|
-
*
|
|
138
|
+
* - Empty / whitespace-only `text` → returns an empty map without touching
|
|
139
|
+
* Qdrant or the embedding backend. The Gemini embedding API rejects empty
|
|
140
|
+
* content with HTTP 400, and short-circuiting here prevents the failure
|
|
141
|
+
* from cascading through `Promise.all` in `computeOwnActivation` (e.g.
|
|
142
|
+
* turn 1 has no prior assistant message, so its `simBatch` channel is
|
|
143
|
+
* called with `""`). Treating the channel's contribution as 0 is the
|
|
144
|
+
* same outcome a no-hit query would produce.
|
|
68
145
|
*/
|
|
69
146
|
export async function simBatch(
|
|
70
147
|
text: string,
|
|
@@ -74,12 +151,20 @@ export async function simBatch(
|
|
|
74
151
|
if (candidateSlugs.length === 0) {
|
|
75
152
|
return new Map();
|
|
76
153
|
}
|
|
154
|
+
if (text.trim().length === 0) {
|
|
155
|
+
return new Map();
|
|
156
|
+
}
|
|
77
157
|
|
|
78
|
-
// Sparse uses the
|
|
79
|
-
//
|
|
158
|
+
// Sparse uses BM25: the query side encodes binary occurrences per token,
|
|
159
|
+
// and the stored doc vectors carry the IDF · TF-saturated weights — Qdrant
|
|
160
|
+
// dot product then yields the BM25 score directly.
|
|
80
161
|
const denseResult = await embedWithBackend(config, [text]);
|
|
81
|
-
const denseVector =
|
|
82
|
-
|
|
162
|
+
const denseVector = await applyCorrectionIfCalibrated(
|
|
163
|
+
denseResult.vectors[0],
|
|
164
|
+
denseResult.provider,
|
|
165
|
+
denseResult.model,
|
|
166
|
+
);
|
|
167
|
+
const sparseVector = generateBm25QueryEmbedding(text);
|
|
83
168
|
|
|
84
169
|
const hits = await hybridQueryConceptPages(
|
|
85
170
|
denseVector,
|
|
@@ -93,8 +178,15 @@ export async function simBatch(
|
|
|
93
178
|
}
|
|
94
179
|
|
|
95
180
|
const maxSparse = computeMaxSparse(hits);
|
|
96
|
-
const { dense_weight:
|
|
181
|
+
const { dense_weight: baseDense, sparse_weight: baseSparse } =
|
|
97
182
|
config.memory.v2;
|
|
183
|
+
const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
|
|
184
|
+
hits,
|
|
185
|
+
maxSparse,
|
|
186
|
+
baseDense,
|
|
187
|
+
baseSparse,
|
|
188
|
+
config,
|
|
189
|
+
);
|
|
98
190
|
|
|
99
191
|
const scores = new Map<string, number>();
|
|
100
192
|
for (const hit of hits) {
|
|
@@ -122,8 +214,12 @@ export async function simBatch(
|
|
|
122
214
|
* Edge cases:
|
|
123
215
|
* - Empty `ids` → returns an empty map without touching Qdrant or the
|
|
124
216
|
* embedding backend.
|
|
125
|
-
* - Empty
|
|
126
|
-
*
|
|
217
|
+
* - Empty / whitespace-only `text` → returns an empty map without touching
|
|
218
|
+
* Qdrant or the embedding backend. Same rationale as {@link simBatch}:
|
|
219
|
+
* Gemini rejects empty content with HTTP 400, so the activation pipeline
|
|
220
|
+
* would otherwise fail on turn 1 (where the assistant-text channel is
|
|
221
|
+
* `""`). Treating the channel's contribution as 0 matches a no-hit
|
|
222
|
+
* query.
|
|
127
223
|
*/
|
|
128
224
|
export async function simSkillBatch(
|
|
129
225
|
text: string,
|
|
@@ -133,10 +229,17 @@ export async function simSkillBatch(
|
|
|
133
229
|
if (ids.length === 0) {
|
|
134
230
|
return new Map();
|
|
135
231
|
}
|
|
232
|
+
if (text.trim().length === 0) {
|
|
233
|
+
return new Map();
|
|
234
|
+
}
|
|
136
235
|
|
|
137
236
|
const denseResult = await embedWithBackend(config, [text]);
|
|
138
|
-
const denseVector =
|
|
139
|
-
|
|
237
|
+
const denseVector = await applyCorrectionIfCalibrated(
|
|
238
|
+
denseResult.vectors[0],
|
|
239
|
+
denseResult.provider,
|
|
240
|
+
denseResult.model,
|
|
241
|
+
);
|
|
242
|
+
const sparseVector = generateBm25QueryEmbedding(text);
|
|
140
243
|
|
|
141
244
|
const hits = await hybridQuerySkills(
|
|
142
245
|
denseVector,
|
|
@@ -160,8 +263,15 @@ export async function simSkillBatch(
|
|
|
160
263
|
}
|
|
161
264
|
|
|
162
265
|
const maxSparse = computeMaxSparse(filtered);
|
|
163
|
-
const { dense_weight:
|
|
266
|
+
const { dense_weight: baseDense, sparse_weight: baseSparse } =
|
|
164
267
|
config.memory.v2;
|
|
268
|
+
const { dense: denseWeight, sparse: sparseWeight } = effectiveWeights(
|
|
269
|
+
filtered,
|
|
270
|
+
maxSparse,
|
|
271
|
+
baseDense,
|
|
272
|
+
baseSparse,
|
|
273
|
+
config,
|
|
274
|
+
);
|
|
165
275
|
|
|
166
276
|
const scores = new Map<string, number>();
|
|
167
277
|
for (const hit of filtered) {
|
|
@@ -278,14 +278,14 @@ export async function pruneSkillsExcept(
|
|
|
278
278
|
* candidate set is already known so the activation scorer gets scores for
|
|
279
279
|
* exactly those ids rather than Qdrant's global top-`limit`. An empty list
|
|
280
280
|
* short-circuits to no results — the caller is asking for "nothing", not
|
|
281
|
-
* "everything". Undefined queries the full collection
|
|
282
|
-
* `selectSkillCandidates` to discover candidates from the global top-K).
|
|
281
|
+
* "everything". Undefined queries the full collection.
|
|
283
282
|
*/
|
|
284
283
|
export async function hybridQuerySkills(
|
|
285
284
|
dense: number[],
|
|
286
285
|
sparse: SparseEmbedding,
|
|
287
286
|
limit: number,
|
|
288
287
|
restrictToIds?: readonly string[],
|
|
288
|
+
options?: { skipSparse?: boolean },
|
|
289
289
|
): Promise<SkillQueryResult[]> {
|
|
290
290
|
if (restrictToIds && restrictToIds.length === 0) {
|
|
291
291
|
// An empty restriction means "no candidates"; skip the round-trip.
|
|
@@ -299,6 +299,11 @@ export async function hybridQuerySkills(
|
|
|
299
299
|
? { must: [{ key: "id", match: { any: [...restrictToIds] } }] }
|
|
300
300
|
: undefined;
|
|
301
301
|
|
|
302
|
+
// Same opt-in short-circuit as `hybridQueryConceptPages`: skip the sparse
|
|
303
|
+
// round-trip entirely so we sidestep the Qdrant 1.13.x sparse-index OOM
|
|
304
|
+
// crash when operators flip sparse off via `sparse_weight: 0`.
|
|
305
|
+
const skipSparse = options?.skipSparse ?? false;
|
|
306
|
+
|
|
302
307
|
const denseQuery = () =>
|
|
303
308
|
client.query(MEMORY_V2_SKILLS_COLLECTION, {
|
|
304
309
|
query: dense,
|
|
@@ -317,7 +322,11 @@ export async function hybridQuerySkills(
|
|
|
317
322
|
});
|
|
318
323
|
|
|
319
324
|
// Run both queries concurrently — they hit independent named vectors.
|
|
320
|
-
const
|
|
325
|
+
const emptyResult = {
|
|
326
|
+
points: [] as Array<{ payload?: unknown; score?: number }>,
|
|
327
|
+
};
|
|
328
|
+
const runQueries = async () =>
|
|
329
|
+
Promise.all([denseQuery(), skipSparse ? emptyResult : sparseQuery()]);
|
|
321
330
|
|
|
322
331
|
let denseResults;
|
|
323
332
|
let sparseResults;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
fromSkillSummary,
|
|
26
26
|
} from "../../skills/skill-memory.js";
|
|
27
27
|
import { getLogger } from "../../util/logger.js";
|
|
28
|
+
import { applyCorrectionIfCalibrated } from "../anisotropy.js";
|
|
28
29
|
import {
|
|
29
30
|
embedWithBackend,
|
|
30
31
|
generateSparseEmbedding,
|
|
@@ -122,10 +123,15 @@ export async function seedV2SkillEntries(): Promise<void> {
|
|
|
122
123
|
|
|
123
124
|
// Embed all content strings in one batched call. Sparse vectors are
|
|
124
125
|
// computed in-process (no network).
|
|
125
|
-
const
|
|
126
|
+
const embedded = await embedWithBackend(
|
|
126
127
|
config,
|
|
127
128
|
seeds.map((s) => s.content),
|
|
128
129
|
);
|
|
130
|
+
const denseVectors = await Promise.all(
|
|
131
|
+
embedded.vectors.map((v) =>
|
|
132
|
+
applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
129
135
|
|
|
130
136
|
const now = Date.now();
|
|
131
137
|
const nextEntries = new Map<string, SkillEntry>();
|
|
@@ -170,6 +176,15 @@ export function getSkillCapability(id: string): SkillEntry | null {
|
|
|
170
176
|
return entries?.get(id) ?? null;
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Every skill id in the cache — both installed-and-enabled skills and
|
|
181
|
+
* uninstalled-catalog skills. Empty before the first `seedV2SkillEntries`
|
|
182
|
+
* run completes.
|
|
183
|
+
*/
|
|
184
|
+
export function getAllSkillIds(): string[] {
|
|
185
|
+
return entries ? [...entries.keys()] : [];
|
|
186
|
+
}
|
|
187
|
+
|
|
173
188
|
/** @internal Test-only: clear the module-level cache. */
|
|
174
189
|
export function _resetSkillStoreForTests(): void {
|
|
175
190
|
entries = null;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Memory v2 — BM25 sparse channel
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Replaces the legacy TF-only sparse embedding (`generateSparseEmbedding` in
|
|
6
|
+
// `../embedding-backend.ts`) with a real Okapi BM25 implementation. Common
|
|
7
|
+
// words like "i", "am", "the" no longer dominate sparse matching the way they
|
|
8
|
+
// did when every token was weighted equally.
|
|
9
|
+
//
|
|
10
|
+
// BM25 score for document `d` and query `q`:
|
|
11
|
+
//
|
|
12
|
+
// score(d, q) = Σ_t∈q IDF(t) · TF_sat(d, t)
|
|
13
|
+
// TF_sat(d, t) = tf(d, t) · (k1 + 1)
|
|
14
|
+
// / (tf(d, t) + k1 · (1 - b + b · |d| / avg_dl))
|
|
15
|
+
// IDF(t) = log( (N - df(t) + 0.5) / (df(t) + 0.5) + 1 )
|
|
16
|
+
//
|
|
17
|
+
// `+1` inside the IDF log keeps the result non-negative even when df(t) > N/2,
|
|
18
|
+
// matching the variant Lucene uses for `BM25Similarity`.
|
|
19
|
+
//
|
|
20
|
+
// **Asymmetric encoding**: documents carry the full BM25 weight per token
|
|
21
|
+
// (IDF · TF_sat baked into the stored vector), and queries carry binary
|
|
22
|
+
// occurrence per token. Qdrant's sparse dot product then reduces to the BM25
|
|
23
|
+
// score directly. Putting BM25 on the doc side means the weights need
|
|
24
|
+
// recomputing whenever the corpus DF or avg_dl changes — operators trigger
|
|
25
|
+
// that with `assistant memory v2 reembed` after major content shifts.
|
|
26
|
+
|
|
27
|
+
import { readFile } from "node:fs/promises";
|
|
28
|
+
|
|
29
|
+
import type { SparseEmbedding } from "../embedding-types.js";
|
|
30
|
+
import {
|
|
31
|
+
SPARSE_VOCAB_SIZE,
|
|
32
|
+
tokenHash,
|
|
33
|
+
tokenizeStemmed,
|
|
34
|
+
} from "../sparse-tokenize.js";
|
|
35
|
+
import { listPages } from "./page-store.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Aggregate corpus statistics used to weight a BM25 document vector. Held in
|
|
39
|
+
* memory after a startup walk over `memory/concepts/`.
|
|
40
|
+
*/
|
|
41
|
+
export interface CorpusStats {
|
|
42
|
+
/** Total document count over which DF was accumulated. */
|
|
43
|
+
totalDocs: number;
|
|
44
|
+
/** hashedTokenIndex (in `[0, SPARSE_VOCAB_SIZE)`) → distinct-doc count. */
|
|
45
|
+
df: Map<number, number>;
|
|
46
|
+
/** Average document length in tokens, post-tokenize. */
|
|
47
|
+
avgDl: number;
|
|
48
|
+
/** Wall-clock millis at build time — used by diagnostics, not the formula. */
|
|
49
|
+
builtAt: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** BM25 hyperparameters. Standard Lucene/Elasticsearch defaults. */
|
|
53
|
+
export interface Bm25Params {
|
|
54
|
+
/** TF saturation curve. ~1.2 is standard. */
|
|
55
|
+
k1: number;
|
|
56
|
+
/** Length normalization. 0 = none, 1 = full. ~0.75 is standard. */
|
|
57
|
+
b: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let _conceptPageStats: CorpusStats | null = null;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Latest in-memory corpus stats for `memory/concepts/`, or `null` if a build
|
|
64
|
+
* has not yet completed. Callers must handle `null` and fall back to legacy
|
|
65
|
+
* TF-only behavior so the daemon remains usable during the brief startup
|
|
66
|
+
* window before {@link rebuildConceptPageCorpusStats} finishes.
|
|
67
|
+
*/
|
|
68
|
+
export function getConceptPageCorpusStats(): CorpusStats | null {
|
|
69
|
+
return _conceptPageStats;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Walk every concept page on disk, accumulate document frequency per hashed
|
|
74
|
+
* token bucket, and average document length. Atomically swaps the result into
|
|
75
|
+
* the module-level cache when the walk succeeds. On error the previous stats
|
|
76
|
+
* stay live.
|
|
77
|
+
*
|
|
78
|
+
* Reads bodies via `readPage`-equivalent direct file reads to avoid paying for
|
|
79
|
+
* frontmatter parsing on every page (we only need the body for sparse).
|
|
80
|
+
*/
|
|
81
|
+
export async function rebuildConceptPageCorpusStats(
|
|
82
|
+
workspaceDir: string,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const slugs = await listPages(workspaceDir);
|
|
85
|
+
if (slugs.length === 0) {
|
|
86
|
+
_conceptPageStats = {
|
|
87
|
+
totalDocs: 0,
|
|
88
|
+
df: new Map(),
|
|
89
|
+
avgDl: 0,
|
|
90
|
+
builtAt: Date.now(),
|
|
91
|
+
};
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const df = new Map<number, number>();
|
|
96
|
+
let totalTokens = 0;
|
|
97
|
+
let docsCounted = 0;
|
|
98
|
+
|
|
99
|
+
for (const slug of slugs) {
|
|
100
|
+
const body = await readPageBodyForStats(workspaceDir, slug);
|
|
101
|
+
if (body === null) continue;
|
|
102
|
+
const tokens = tokenizeStemmed(body);
|
|
103
|
+
if (tokens.length === 0) continue;
|
|
104
|
+
totalTokens += tokens.length;
|
|
105
|
+
docsCounted += 1;
|
|
106
|
+
const seen = new Set<number>();
|
|
107
|
+
for (const token of tokens) {
|
|
108
|
+
const idx = tokenHash(token, SPARSE_VOCAB_SIZE);
|
|
109
|
+
if (seen.has(idx)) continue;
|
|
110
|
+
seen.add(idx);
|
|
111
|
+
df.set(idx, (df.get(idx) ?? 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_conceptPageStats = {
|
|
116
|
+
totalDocs: docsCounted,
|
|
117
|
+
df,
|
|
118
|
+
avgDl: docsCounted > 0 ? totalTokens / docsCounted : 0,
|
|
119
|
+
builtAt: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read just the body of a page for stats accumulation. Skips the YAML
|
|
125
|
+
* frontmatter without invoking the schema-validating `readPage` parser, since
|
|
126
|
+
* any parse failure surfaced there would abort the whole rebuild — and we
|
|
127
|
+
* only need the prose half for tokenization.
|
|
128
|
+
*/
|
|
129
|
+
async function readPageBodyForStats(
|
|
130
|
+
workspaceDir: string,
|
|
131
|
+
slug: string,
|
|
132
|
+
): Promise<string | null> {
|
|
133
|
+
const path = `${workspaceDir}/memory/concepts/${slug}.md`;
|
|
134
|
+
let raw: string;
|
|
135
|
+
try {
|
|
136
|
+
raw = await readFile(path, "utf-8");
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// Strip a leading `---\n...\n---\n` block if present; otherwise return raw.
|
|
141
|
+
if (raw.startsWith("---")) {
|
|
142
|
+
const closing = raw.indexOf("\n---", 3);
|
|
143
|
+
if (closing !== -1) {
|
|
144
|
+
const after = raw.indexOf("\n", closing + 4);
|
|
145
|
+
if (after !== -1) return raw.slice(after + 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return raw;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Compute the BM25 IDF weight for a hashed token bucket. Returns `0` when the
|
|
153
|
+
* token's df equals the corpus size (a token in every document carries no
|
|
154
|
+
* discriminating power).
|
|
155
|
+
*/
|
|
156
|
+
function computeIdf(stats: CorpusStats, hashIdx: number): number {
|
|
157
|
+
const df = stats.df.get(hashIdx) ?? 0;
|
|
158
|
+
const numerator = stats.totalDocs - df + 0.5;
|
|
159
|
+
const denominator = df + 0.5;
|
|
160
|
+
return Math.log(numerator / denominator + 1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Document-side BM25-weighted sparse vector. Each emitted value is
|
|
165
|
+
* `IDF(t) · TF_sat(d, t)` so the dot product against a binary query vector
|
|
166
|
+
* (see {@link generateBm25QueryEmbedding}) yields the BM25 score.
|
|
167
|
+
*
|
|
168
|
+
* Returns an empty embedding for empty input or when the corpus is empty
|
|
169
|
+
* (every IDF would be zero anyway).
|
|
170
|
+
*/
|
|
171
|
+
export function generateBm25DocEmbedding(
|
|
172
|
+
text: string,
|
|
173
|
+
stats: CorpusStats,
|
|
174
|
+
params: Bm25Params,
|
|
175
|
+
): SparseEmbedding {
|
|
176
|
+
const tokens = tokenizeStemmed(text);
|
|
177
|
+
if (tokens.length === 0 || stats.totalDocs === 0) {
|
|
178
|
+
return { indices: [], values: [] };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Per-document term frequencies, keyed by hashed bucket.
|
|
182
|
+
const tf = new Map<number, number>();
|
|
183
|
+
for (const token of tokens) {
|
|
184
|
+
const idx = tokenHash(token, SPARSE_VOCAB_SIZE);
|
|
185
|
+
tf.set(idx, (tf.get(idx) ?? 0) + 1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const docLen = tokens.length;
|
|
189
|
+
// avg_dl can be 0 only when totalDocs is 0, which we already short-circuited.
|
|
190
|
+
const lengthFactor = 1 - params.b + (params.b * docLen) / stats.avgDl;
|
|
191
|
+
const indices: number[] = [];
|
|
192
|
+
const values: number[] = [];
|
|
193
|
+
|
|
194
|
+
for (const [idx, freq] of tf) {
|
|
195
|
+
const idf = computeIdf(stats, idx);
|
|
196
|
+
if (idf === 0) continue; // Skip tokens that contribute nothing to scores.
|
|
197
|
+
const saturated =
|
|
198
|
+
(freq * (params.k1 + 1)) / (freq + params.k1 * lengthFactor);
|
|
199
|
+
const weight = idf * saturated;
|
|
200
|
+
if (weight === 0) continue;
|
|
201
|
+
indices.push(idx);
|
|
202
|
+
values.push(weight);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { indices, values };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Query-side sparse vector — binary occurrence per distinct query token. The
|
|
210
|
+
* dot product `Σ_t v_q(t) · v_d(t)` against a BM25-weighted document vector
|
|
211
|
+
* is exactly the BM25 score, since `v_q(t) = 1` for tokens in the query and
|
|
212
|
+
* `0` otherwise.
|
|
213
|
+
*
|
|
214
|
+
* Stateless — does not need corpus stats, so callers can use this on every
|
|
215
|
+
* turn without coordinating with {@link rebuildConceptPageCorpusStats}.
|
|
216
|
+
*/
|
|
217
|
+
export function generateBm25QueryEmbedding(text: string): SparseEmbedding {
|
|
218
|
+
const tokens = tokenizeStemmed(text);
|
|
219
|
+
if (tokens.length === 0) {
|
|
220
|
+
return { indices: [], values: [] };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const seen = new Set<number>();
|
|
224
|
+
const indices: number[] = [];
|
|
225
|
+
const values: number[] = [];
|
|
226
|
+
for (const token of tokens) {
|
|
227
|
+
const idx = tokenHash(token, SPARSE_VOCAB_SIZE);
|
|
228
|
+
if (seen.has(idx)) continue;
|
|
229
|
+
seen.add(idx);
|
|
230
|
+
indices.push(idx);
|
|
231
|
+
values.push(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { indices, values };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @internal Test-only: reset module-level singletons. */
|
|
238
|
+
export function _resetCorpusStatsForTests(): void {
|
|
239
|
+
_conceptPageStats = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @internal Test-only: install a fixture stats table without disk I/O. */
|
|
243
|
+
export function _setCorpusStatsForTests(stats: CorpusStats | null): void {
|
|
244
|
+
_conceptPageStats = stats;
|
|
245
|
+
}
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// and returns a concatenated, header-wrapped block ready to splice into the
|
|
7
7
|
// current user message via the injector chain.
|
|
8
8
|
//
|
|
9
|
-
// Pairs with the v2 per-turn activation block (`
|
|
10
|
-
// `conversation-graph-memory.ts
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// Both land on the user message so
|
|
9
|
+
// Pairs with the v2 per-turn activation block (`maybeRouteV2Injection` in
|
|
10
|
+
// `conversation-graph-memory.ts`, which threads through `injectTextBlock`)
|
|
11
|
+
// — that block carries activated concept pages selected by the activation
|
|
12
|
+
// pipeline; this static block carries the always-relevant aggregate views
|
|
13
|
+
// written by consolidation and the user. Both land on the user message so
|
|
14
|
+
// the system prompt stays cache-stable.
|
|
14
15
|
//
|
|
15
16
|
// Refresh cadence is owned by the caller: the agent loop only passes the
|
|
16
17
|
// content through when `mode === "full"` (first turn / post-compaction),
|