@vellumai/assistant 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +45 -29
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/docs/architecture/memory.md +5 -2
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +470 -25
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +6 -33
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +12 -7
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +457 -8
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +38 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +2 -20
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +36 -16
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +60 -5
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-request-resolvers.ts +3 -32
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +11 -10
- package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
- package/src/cli/commands/oauth/connect.ts +124 -40
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +13 -5
- package/src/config/loader.ts +199 -27
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +76 -12
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
- package/src/daemon/conversation-agent-loop.ts +183 -43
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +22 -10
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +211 -29
- package/src/daemon/conversation-tool-setup.ts +66 -19
- package/src/daemon/conversation.ts +18 -23
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +26 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +47 -22
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +50 -4
- package/src/daemon/host-file-proxy.ts +44 -8
- package/src/daemon/host-transfer-proxy.ts +97 -6
- package/src/daemon/lifecycle.ts +167 -101
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +66 -15
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +22 -1
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +119 -0
- package/src/filing/filing-service.ts +29 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +149 -38
- package/src/ipc/gateway-client.ts +37 -3
- package/src/ipc/skill-server.ts +99 -42
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +1 -16
- package/src/memory/context-search/sources/memory.ts +2 -3
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +72 -61
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +11 -67
- package/src/memory/graph/graph-search.ts +4 -24
- package/src/memory/graph/retriever.test.ts +12 -1
- package/src/memory/graph/retriever.ts +10 -15
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +53 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +85 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +7 -0
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +10 -72
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +346 -255
- package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
- package/src/memory/v2/__tests__/injection.test.ts +297 -190
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +113 -196
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +77 -14
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +69 -20
- package/src/memory/v2/injection.ts +75 -68
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +41 -1
- package/src/memory/v2/qdrant.ts +306 -46
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +77 -110
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +26 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +17 -10
- package/src/notifications/copy-composer.ts +47 -0
- package/src/notifications/decision-engine.ts +46 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +36 -4
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +914 -0
- package/src/proactive-artifact/job.ts +366 -0
- package/src/proactive-artifact/message-copy.ts +58 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +27 -15
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +165 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +37 -17
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +136 -17
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/include-graph.ts +35 -13
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +14 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +43 -3
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +14 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -70,16 +70,6 @@ const state = {
|
|
|
70
70
|
points: Array<{ score?: number; payload: Record<string, unknown> }>;
|
|
71
71
|
}>,
|
|
72
72
|
},
|
|
73
|
-
// Separate response queue for the dedicated skills collection so a test
|
|
74
|
-
// querying both concept pages and skills doesn't have to interleave.
|
|
75
|
-
skillQueryResponses: {
|
|
76
|
-
dense: [] as Array<{
|
|
77
|
-
points: Array<{ score?: number; payload: Record<string, unknown> }>;
|
|
78
|
-
}>,
|
|
79
|
-
sparse: [] as Array<{
|
|
80
|
-
points: Array<{ score?: number; payload: Record<string, unknown> }>;
|
|
81
|
-
}>,
|
|
82
|
-
},
|
|
83
73
|
queryCalls: [] as Array<{
|
|
84
74
|
collection: string;
|
|
85
75
|
using: string;
|
|
@@ -146,12 +136,12 @@ class MockQdrantClient {
|
|
|
146
136
|
limit: params.limit,
|
|
147
137
|
filter: params.filter,
|
|
148
138
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return
|
|
139
|
+
// Both `dense` and `summary_dense` consume from the dense queue (and
|
|
140
|
+
// similarly for sparse). The four-channel hybrid query fires them in
|
|
141
|
+
// order: body-dense, body-sparse, summary-dense, summary-sparse — so
|
|
142
|
+
// the queue order matches the call order.
|
|
143
|
+
const channel = params.using.endsWith("sparse") ? "sparse" : "dense";
|
|
144
|
+
return state.queryResponses[channel].shift() ?? { points: [] };
|
|
155
145
|
}
|
|
156
146
|
}
|
|
157
147
|
|
|
@@ -159,10 +149,7 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
159
149
|
QdrantClient: MockQdrantClient,
|
|
160
150
|
}));
|
|
161
151
|
|
|
162
|
-
const { simBatch,
|
|
163
|
-
await import("../sim.js");
|
|
164
|
-
const { _resetMemoryV2SkillQdrantForTests } =
|
|
165
|
-
await import("../skill-qdrant.js");
|
|
152
|
+
const { simBatch, clamp01, effectiveWeights } = await import("../sim.js");
|
|
166
153
|
const { _resetMemoryV2QdrantForTests } = await import("../qdrant.js");
|
|
167
154
|
|
|
168
155
|
// ---------------------------------------------------------------------------
|
|
@@ -175,15 +162,12 @@ function resetState(): void {
|
|
|
175
162
|
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
176
163
|
state.queryResponses.dense.length = 0;
|
|
177
164
|
state.queryResponses.sparse.length = 0;
|
|
178
|
-
state.skillQueryResponses.dense.length = 0;
|
|
179
|
-
state.skillQueryResponses.sparse.length = 0;
|
|
180
165
|
state.queryCalls.length = 0;
|
|
181
166
|
// Bun's `mock.module` persists across files in the same process, so the
|
|
182
|
-
// qdrant
|
|
183
|
-
// from a sibling test file. Reset
|
|
184
|
-
//
|
|
167
|
+
// qdrant module's singleton may already hold a MockQdrantClient instance
|
|
168
|
+
// from a sibling test file. Reset readiness so each test in this file
|
|
169
|
+
// gets a fresh `new QdrantClient()` resolved against our mock.
|
|
185
170
|
_resetMemoryV2QdrantForTests();
|
|
186
|
-
_resetMemoryV2SkillQdrantForTests();
|
|
187
171
|
}
|
|
188
172
|
|
|
189
173
|
function configWithWeights(
|
|
@@ -205,10 +189,18 @@ function configWithWeights(
|
|
|
205
189
|
/**
|
|
206
190
|
* Stage a single Qdrant response that maps each (slug, denseScore?, sparseScore?)
|
|
207
191
|
* tuple onto the dense or sparse channel, mirroring how `hybridQueryConceptPages`
|
|
208
|
-
* merges per-channel hits.
|
|
192
|
+
* merges per-channel hits. Optional `summaryDenseScore` / `summarySparseScore`
|
|
193
|
+
* stage the summary-side channels — pages without those entries fall through
|
|
194
|
+
* to body-only scoring at fusion time.
|
|
209
195
|
*/
|
|
210
196
|
function stageHybridResponse(
|
|
211
|
-
hits: Array<{
|
|
197
|
+
hits: Array<{
|
|
198
|
+
slug: string;
|
|
199
|
+
denseScore?: number;
|
|
200
|
+
sparseScore?: number;
|
|
201
|
+
summaryDenseScore?: number;
|
|
202
|
+
summarySparseScore?: number;
|
|
203
|
+
}>,
|
|
212
204
|
): void {
|
|
213
205
|
state.queryResponses.dense.push({
|
|
214
206
|
points: hits
|
|
@@ -220,6 +212,20 @@ function stageHybridResponse(
|
|
|
220
212
|
.filter((h) => h.sparseScore !== undefined)
|
|
221
213
|
.map((h) => ({ score: h.sparseScore, payload: { slug: h.slug } })),
|
|
222
214
|
});
|
|
215
|
+
// The four-channel hybrid query also fires `summary_dense` and
|
|
216
|
+
// `summary_sparse` queries against the same collection. Tests that don't
|
|
217
|
+
// care about summary scores leave those channels empty so the fallback
|
|
218
|
+
// (body-only) path runs.
|
|
219
|
+
state.queryResponses.dense.push({
|
|
220
|
+
points: hits
|
|
221
|
+
.filter((h) => h.summaryDenseScore !== undefined)
|
|
222
|
+
.map((h) => ({ score: h.summaryDenseScore, payload: { slug: h.slug } })),
|
|
223
|
+
});
|
|
224
|
+
state.queryResponses.sparse.push({
|
|
225
|
+
points: hits
|
|
226
|
+
.filter((h) => h.summarySparseScore !== undefined)
|
|
227
|
+
.map((h) => ({ score: h.summarySparseScore, payload: { slug: h.slug } })),
|
|
228
|
+
});
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
beforeEach(resetState);
|
|
@@ -488,15 +494,16 @@ describe("simBatch", () => {
|
|
|
488
494
|
expect(out.get("loud-page")).toBe(1);
|
|
489
495
|
});
|
|
490
496
|
|
|
491
|
-
test("forwards the candidate slugs as a Qdrant slug-IN filter", async () => {
|
|
497
|
+
test("forwards the candidate slugs as a Qdrant slug-IN filter on every channel", async () => {
|
|
492
498
|
const config = configWithWeights(0.7, 0.3);
|
|
493
499
|
stageHybridResponse([]);
|
|
494
500
|
|
|
495
501
|
await simBatch("query", ["alice", "bob", "carol"], config);
|
|
496
502
|
|
|
497
|
-
//
|
|
498
|
-
// filter and the same per-channel limit
|
|
499
|
-
|
|
503
|
+
// All four channels (body dense + sparse, summary dense + sparse) ran
|
|
504
|
+
// with the same slug-restriction filter and the same per-channel limit
|
|
505
|
+
// equal to the candidate count.
|
|
506
|
+
expect(state.queryCalls).toHaveLength(4);
|
|
500
507
|
for (const call of state.queryCalls) {
|
|
501
508
|
expect(call.limit).toBe(3);
|
|
502
509
|
expect(call.filter).toEqual({
|
|
@@ -516,194 +523,104 @@ describe("simBatch", () => {
|
|
|
516
523
|
expect(state.sparseCalls).toEqual(["hello world"]);
|
|
517
524
|
});
|
|
518
525
|
|
|
519
|
-
test("
|
|
520
|
-
|
|
526
|
+
test("takes max(body, summary) per slug — summary higher than body wins", async () => {
|
|
527
|
+
// Body channels return a modest score; summary channels return a much
|
|
528
|
+
// higher score. The max collapses to the summary score.
|
|
529
|
+
const config = configWithWeights(1.0, 0.0);
|
|
521
530
|
stageHybridResponse([
|
|
522
|
-
{
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
531
|
+
{
|
|
532
|
+
slug: "alice",
|
|
533
|
+
denseScore: 0.3,
|
|
534
|
+
summaryDenseScore: 0.7,
|
|
535
|
+
},
|
|
526
536
|
]);
|
|
527
537
|
|
|
528
|
-
const out = await simBatch("query", ["
|
|
529
|
-
|
|
530
|
-
for (const [, score] of out) {
|
|
531
|
-
expect(score).toBeGreaterThanOrEqual(0);
|
|
532
|
-
expect(score).toBeLessThanOrEqual(1);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// ---------------------------------------------------------------------------
|
|
538
|
-
// simSkillBatch
|
|
539
|
-
// ---------------------------------------------------------------------------
|
|
538
|
+
const out = await simBatch("query", ["alice"], config);
|
|
540
539
|
|
|
541
|
-
|
|
542
|
-
* Stage a single hybrid response on the dedicated skills queues. Mirrors
|
|
543
|
-
* `stageHybridResponse` but uses `payload.id` (skills' Qdrant payload key)
|
|
544
|
-
* instead of `payload.slug`.
|
|
545
|
-
*/
|
|
546
|
-
function stageSkillHybridResponse(
|
|
547
|
-
hits: Array<{ id: string; denseScore?: number; sparseScore?: number }>,
|
|
548
|
-
): void {
|
|
549
|
-
state.skillQueryResponses.dense.push({
|
|
550
|
-
points: hits
|
|
551
|
-
.filter((h) => h.denseScore !== undefined)
|
|
552
|
-
.map((h) => ({ score: h.denseScore, payload: { id: h.id } })),
|
|
540
|
+
expect(out.get("alice")).toBeCloseTo(0.7, 6);
|
|
553
541
|
});
|
|
554
|
-
state.skillQueryResponses.sparse.push({
|
|
555
|
-
points: hits
|
|
556
|
-
.filter((h) => h.sparseScore !== undefined)
|
|
557
|
-
.map((h) => ({ score: h.sparseScore, payload: { id: h.id } })),
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
describe("simSkillBatch", () => {
|
|
562
|
-
test("empty id list returns empty map without touching backends", async () => {
|
|
563
|
-
const config = configWithWeights(0.7, 0.3);
|
|
564
|
-
|
|
565
|
-
const out = await simSkillBatch("anything", [], config);
|
|
566
542
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
state.embedCalls.length = 0;
|
|
578
|
-
state.sparseCalls.length = 0;
|
|
579
|
-
state.queryCalls.length = 0;
|
|
580
|
-
const out = await simSkillBatch(text, ["example-skill-a"], config);
|
|
581
|
-
expect(out.size).toBe(0);
|
|
582
|
-
expect(state.embedCalls).toHaveLength(0);
|
|
583
|
-
expect(state.sparseCalls).toHaveLength(0);
|
|
584
|
-
expect(state.queryCalls).toHaveLength(0);
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
test("queries the dedicated skills collection and forwards an id-IN filter", async () => {
|
|
589
|
-
const config = configWithWeights(0.7, 0.3);
|
|
590
|
-
stageSkillHybridResponse([]);
|
|
543
|
+
test("takes max(body, summary) per slug — body higher than summary wins", async () => {
|
|
544
|
+
// Inverse case: body dominates, max stays at body.
|
|
545
|
+
const config = configWithWeights(1.0, 0.0);
|
|
546
|
+
stageHybridResponse([
|
|
547
|
+
{
|
|
548
|
+
slug: "alice",
|
|
549
|
+
denseScore: 0.9,
|
|
550
|
+
summaryDenseScore: 0.4,
|
|
551
|
+
},
|
|
552
|
+
]);
|
|
591
553
|
|
|
592
|
-
await
|
|
593
|
-
"query",
|
|
594
|
-
["example-skill-a", "example-skill-b"],
|
|
595
|
-
config,
|
|
596
|
-
);
|
|
554
|
+
const out = await simBatch("query", ["alice"], config);
|
|
597
555
|
|
|
598
|
-
expect(
|
|
599
|
-
for (const call of state.queryCalls) {
|
|
600
|
-
expect(call.collection).toBe("memory_v2_skills");
|
|
601
|
-
// The candidate ids are forwarded as a Qdrant filter so Qdrant scores
|
|
602
|
-
// exactly the candidate set, not its global top-K. Without this,
|
|
603
|
-
// candidate ids absent from the global top-K silently score 0.
|
|
604
|
-
expect(call.filter).toEqual({
|
|
605
|
-
must: [
|
|
606
|
-
{ key: "id", match: { any: ["example-skill-a", "example-skill-b"] } },
|
|
607
|
-
],
|
|
608
|
-
});
|
|
609
|
-
// Limit equals the candidate count.
|
|
610
|
-
expect(call.limit).toBe(2);
|
|
611
|
-
}
|
|
556
|
+
expect(out.get("alice")).toBeCloseTo(0.9, 6);
|
|
612
557
|
});
|
|
613
558
|
|
|
614
|
-
test("
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
559
|
+
test("falls back to body-only when the page has no summary embedding", async () => {
|
|
560
|
+
// Pages predating the summary field have no summary_dense/sparse vectors.
|
|
561
|
+
// Their summary channels return no hits — the max collapses to body.
|
|
562
|
+
const config = configWithWeights(1.0, 0.0);
|
|
563
|
+
stageHybridResponse([
|
|
564
|
+
{
|
|
565
|
+
slug: "legacy-page",
|
|
566
|
+
denseScore: 0.6,
|
|
567
|
+
// summaryDenseScore / summarySparseScore omitted
|
|
568
|
+
},
|
|
619
569
|
]);
|
|
620
570
|
|
|
621
|
-
const out = await
|
|
622
|
-
"query",
|
|
623
|
-
["example-skill-a", "example-skill-b"],
|
|
624
|
-
config,
|
|
625
|
-
);
|
|
571
|
+
const out = await simBatch("query", ["legacy-page"], config);
|
|
626
572
|
|
|
627
|
-
|
|
628
|
-
// example-skill-b: 0.4 * 0.25 + 0.6 * 0.5 = 0.4
|
|
629
|
-
expect(out.get("example-skill-a")).toBeCloseTo(0.8, 6);
|
|
630
|
-
expect(out.get("example-skill-b")).toBeCloseTo(0.4, 6);
|
|
573
|
+
expect(out.get("legacy-page")).toBeCloseTo(0.6, 6);
|
|
631
574
|
});
|
|
632
575
|
|
|
633
|
-
test("
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
576
|
+
test("normalizes body and summary sparse channels independently", async () => {
|
|
577
|
+
// Summary sparse scores live on a different scale than body sparse —
|
|
578
|
+
// a small absolute summary-sparse value (1.5) on the only page that
|
|
579
|
+
// has summary signal still normalizes to 1.0 within the summary
|
|
580
|
+
// channel, so the summary-only fused score should win out.
|
|
581
|
+
const config = configWithWeights(0.0, 1.0);
|
|
582
|
+
stageHybridResponse([
|
|
583
|
+
{
|
|
584
|
+
slug: "alice",
|
|
585
|
+
denseScore: 0.0,
|
|
586
|
+
sparseScore: 100, // body sparse max in this batch
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
slug: "bob",
|
|
590
|
+
denseScore: 0.0,
|
|
591
|
+
sparseScore: 0.5, // body sparse normalized = 0.005
|
|
592
|
+
summaryDenseScore: 0.0,
|
|
593
|
+
summarySparseScore: 1.5, // summary sparse max in this batch
|
|
594
|
+
},
|
|
638
595
|
]);
|
|
639
596
|
|
|
640
|
-
const out = await
|
|
641
|
-
"query",
|
|
642
|
-
["example-skill-a", "example-skill-b"],
|
|
643
|
-
config,
|
|
644
|
-
);
|
|
597
|
+
const out = await simBatch("query", ["alice", "bob"], config);
|
|
645
598
|
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
599
|
+
// Alice has only body. Body sparse normalized to 1.0; sparse_weight=1.0 → 1.0.
|
|
600
|
+
expect(out.get("alice")).toBeCloseTo(1.0, 6);
|
|
601
|
+
// Bob's summary side normalizes its 1.5 (only sparse-bearing summary
|
|
602
|
+
// hit) — a single sparse-bearing hit is below the adaptive-spread
|
|
603
|
+
// floor, so the channel collapses to base weights and the lone
|
|
604
|
+
// sparseNormalized=1.0 hit yields a fused summary score of 1.0.
|
|
605
|
+
// Body side has only bob's tiny sparse=0.5 against the body batch max
|
|
606
|
+
// of 100 → ~0.005. The max picks the summary side.
|
|
607
|
+
expect(out.get("bob")).toBeCloseTo(1.0, 6);
|
|
650
608
|
});
|
|
651
609
|
|
|
652
|
-
test("
|
|
653
|
-
// The bug we're guarding against: when the skills collection has more
|
|
654
|
-
// skills than `ids.length`, calling `hybridQuerySkills` without a filter
|
|
655
|
-
// returns Qdrant's global top-K. Candidate ids absent from that top-K
|
|
656
|
-
// would silently score 0. The fix is to forward the candidate ids as a
|
|
657
|
-
// server-side restriction so Qdrant scores exactly the candidate set.
|
|
610
|
+
test("returned scores are always in [0, 1] for arbitrary inputs", async () => {
|
|
658
611
|
const config = configWithWeights(0.7, 0.3);
|
|
659
|
-
|
|
660
|
-
{
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
{ id: "example-skill-c", denseScore: 0.9, sparseScore: 1 },
|
|
612
|
+
stageHybridResponse([
|
|
613
|
+
{ slug: "a", denseScore: 0.99, sparseScore: 100 },
|
|
614
|
+
{ slug: "b", denseScore: 0.5, sparseScore: 50 },
|
|
615
|
+
{ slug: "c", denseScore: 0.0, sparseScore: 1 },
|
|
616
|
+
{ slug: "d", denseScore: 0.123, sparseScore: 0 }, // explicit zero
|
|
665
617
|
]);
|
|
666
618
|
|
|
667
|
-
const out = await
|
|
668
|
-
"query",
|
|
669
|
-
["example-skill-a", "example-skill-b"],
|
|
670
|
-
config,
|
|
671
|
-
);
|
|
619
|
+
const out = await simBatch("query", ["a", "b", "c", "d"], config);
|
|
672
620
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
for (const call of state.queryCalls) {
|
|
677
|
-
expect(call.filter).toEqual({
|
|
678
|
-
must: [
|
|
679
|
-
{ key: "id", match: { any: ["example-skill-a", "example-skill-b"] } },
|
|
680
|
-
],
|
|
681
|
-
});
|
|
621
|
+
for (const [, score] of out) {
|
|
622
|
+
expect(score).toBeGreaterThanOrEqual(0);
|
|
623
|
+
expect(score).toBeLessThanOrEqual(1);
|
|
682
624
|
}
|
|
683
|
-
// Only candidate ids appear in the result map.
|
|
684
|
-
expect(out.has("example-skill-a")).toBe(true);
|
|
685
|
-
expect(out.has("example-skill-c")).toBe(false);
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
test("returned scores are clamped into [0, 1]", async () => {
|
|
689
|
-
const config = configWithWeights(0.8, 0.5); // intentionally sums to > 1
|
|
690
|
-
stageSkillHybridResponse([
|
|
691
|
-
{ id: "example-skill-a", denseScore: 1.0, sparseScore: 1 },
|
|
692
|
-
]);
|
|
693
|
-
|
|
694
|
-
const out = await simSkillBatch("query", ["example-skill-a"], config);
|
|
695
|
-
|
|
696
|
-
expect(out.get("example-skill-a")).toBe(1);
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
test("embeds the query text exactly once via dense + sparse backends", async () => {
|
|
700
|
-
const config = configWithWeights(0.7, 0.3);
|
|
701
|
-
stageSkillHybridResponse([]);
|
|
702
|
-
|
|
703
|
-
await simSkillBatch("hello skill", ["example-skill-a"], config);
|
|
704
|
-
|
|
705
|
-
expect(state.embedCalls).toHaveLength(1);
|
|
706
|
-
expect(state.embedCalls[0].inputs).toEqual(["hello skill"]);
|
|
707
|
-
expect(state.sparseCalls).toEqual(["hello skill"]);
|
|
708
625
|
});
|
|
709
626
|
});
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for `assistant/src/memory/v2/skill-store.ts`.
|
|
3
3
|
*
|
|
4
|
-
* Coverage matrix
|
|
4
|
+
* Coverage matrix:
|
|
5
5
|
* - `seedV2SkillEntries` enumerates the catalog and calls
|
|
6
|
-
* `
|
|
6
|
+
* `upsertConceptPageEmbedding` with `slug: "skills/<id>"` for each
|
|
7
|
+
* enabled skill in the unified `memory_v2_concept_pages` collection.
|
|
7
8
|
* - It skips skills whose declared feature flag is disabled.
|
|
8
|
-
* - It calls `
|
|
9
|
-
*
|
|
9
|
+
* - It calls `pruneSlugsWithPrefixExcept("skills/", ...)` with the active
|
|
10
|
+
* id list as suffixes, so stale skill slugs in the unified collection
|
|
11
|
+
* get pruned without touching concept-page slugs.
|
|
12
|
+
* - It populates the `entries` cache so `getSkillCapability` returns each
|
|
13
|
+
* entry — accepting both bare ids (`"example-skill"`) and unified-collection
|
|
14
|
+
* slugs (`"skills/example-skill"`).
|
|
10
15
|
* - It swallows errors from the embedding backend — the function resolves
|
|
11
16
|
* and the cache is unchanged from prior state.
|
|
12
17
|
*
|
|
@@ -29,6 +34,18 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
29
34
|
// Programmable test state — drives every mocked dependency below.
|
|
30
35
|
// ---------------------------------------------------------------------------
|
|
31
36
|
|
|
37
|
+
interface UpsertCall {
|
|
38
|
+
slug: string;
|
|
39
|
+
dense: number[];
|
|
40
|
+
sparse: { indices: number[]; values: number[] };
|
|
41
|
+
updatedAt: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PruneCall {
|
|
45
|
+
prefix: string;
|
|
46
|
+
activeSuffixes: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
interface TestState {
|
|
33
50
|
catalog: SkillSummary[];
|
|
34
51
|
resolved: ResolvedSkill[];
|
|
@@ -38,14 +55,8 @@ interface TestState {
|
|
|
38
55
|
embedThrows: Error | null;
|
|
39
56
|
embedReturn: number[][];
|
|
40
57
|
sparseReturn: { indices: number[]; values: number[] };
|
|
41
|
-
upsertCalls:
|
|
42
|
-
|
|
43
|
-
content: string;
|
|
44
|
-
dense: number[];
|
|
45
|
-
sparse: { indices: number[]; values: number[] };
|
|
46
|
-
updatedAt: number;
|
|
47
|
-
}>;
|
|
48
|
-
pruneCalls: Array<readonly string[]>;
|
|
58
|
+
upsertCalls: UpsertCall[];
|
|
59
|
+
pruneCalls: PruneCall[];
|
|
49
60
|
upsertThrows: Error | null;
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -99,13 +110,16 @@ mock.module("../../embedding-backend.js", () => ({
|
|
|
99
110
|
generateSparseEmbedding: () => state.sparseReturn,
|
|
100
111
|
}));
|
|
101
112
|
|
|
102
|
-
mock.module("../
|
|
103
|
-
|
|
113
|
+
mock.module("../qdrant.js", () => ({
|
|
114
|
+
upsertConceptPageEmbedding: async (params: UpsertCall) => {
|
|
104
115
|
if (state.upsertThrows) throw state.upsertThrows;
|
|
105
116
|
state.upsertCalls.push(params);
|
|
106
117
|
},
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
pruneSlugsWithPrefixExcept: async (
|
|
119
|
+
prefix: string,
|
|
120
|
+
activeSuffixes: readonly string[],
|
|
121
|
+
) => {
|
|
122
|
+
state.pruneCalls.push({ prefix, activeSuffixes });
|
|
109
123
|
},
|
|
110
124
|
}));
|
|
111
125
|
|
|
@@ -160,7 +174,7 @@ afterEach(resetState);
|
|
|
160
174
|
// ---------------------------------------------------------------------------
|
|
161
175
|
|
|
162
176
|
describe("seedV2SkillEntries", () => {
|
|
163
|
-
test("
|
|
177
|
+
test("upserts each enabled skill into the unified collection under skills/<id>", async () => {
|
|
164
178
|
const skillA = makeSummary({
|
|
165
179
|
id: "example-skill-a",
|
|
166
180
|
displayName: "Skill A",
|
|
@@ -182,15 +196,16 @@ describe("seedV2SkillEntries", () => {
|
|
|
182
196
|
await seedV2SkillEntries();
|
|
183
197
|
|
|
184
198
|
expect(state.upsertCalls).toHaveLength(2);
|
|
185
|
-
const
|
|
186
|
-
expect(
|
|
187
|
-
|
|
188
|
-
// Each upsert carries the per-skill dense + sparse +
|
|
189
|
-
|
|
199
|
+
const slugs = state.upsertCalls.map((c) => c.slug).sort();
|
|
200
|
+
expect(slugs).toEqual(["skills/example-skill-a", "skills/example-skill-b"]);
|
|
201
|
+
|
|
202
|
+
// Each upsert carries the per-skill dense + sparse + updatedAt payload,
|
|
203
|
+
// keyed under the unified `skills/<id>` slug.
|
|
204
|
+
const callA = state.upsertCalls.find(
|
|
205
|
+
(c) => c.slug === "skills/example-skill-a",
|
|
206
|
+
)!;
|
|
190
207
|
expect(callA.dense).toEqual([0.1, 0.2, 0.3]);
|
|
191
208
|
expect(callA.sparse).toEqual(state.sparseReturn);
|
|
192
|
-
expect(callA.content).toContain("Skill A");
|
|
193
|
-
expect(callA.content).toContain("(example-skill-a)");
|
|
194
209
|
expect(callA.updatedAt).toBeGreaterThan(0);
|
|
195
210
|
});
|
|
196
211
|
|
|
@@ -207,12 +222,11 @@ describe("seedV2SkillEntries", () => {
|
|
|
207
222
|
await seedV2SkillEntries();
|
|
208
223
|
|
|
209
224
|
expect(state.upsertCalls).toHaveLength(1);
|
|
210
|
-
expect(state.upsertCalls[0].
|
|
225
|
+
expect(state.upsertCalls[0].slug).toBe("skills/example-skill-a");
|
|
211
226
|
});
|
|
212
227
|
|
|
213
228
|
test("does not re-seed an installed-but-disabled skill from the remote catalog", async () => {
|
|
214
|
-
// Regression
|
|
215
|
-
// (Codex P1): if `seenIds` is built only from enabled skills, a locally
|
|
229
|
+
// Regression: if `seenIds` is built only from enabled skills, a locally
|
|
216
230
|
// installed-but-disabled skill falls through to the catalog loop and gets
|
|
217
231
|
// embedded as if it were a discoverable uninstalled skill — contradicting
|
|
218
232
|
// the user's explicit disablement.
|
|
@@ -223,8 +237,6 @@ describe("seedV2SkillEntries", () => {
|
|
|
223
237
|
{ summary: enabledSkill, state: "enabled" },
|
|
224
238
|
{ summary: disabledSkill, state: "disabled" },
|
|
225
239
|
];
|
|
226
|
-
// The remote catalog also contains the disabled skill (same id) — the
|
|
227
|
-
// seed function must NOT pull it back in via `getCatalog()`.
|
|
228
240
|
state.fullCatalog = [
|
|
229
241
|
{
|
|
230
242
|
id: "example-skill-b",
|
|
@@ -237,7 +249,7 @@ describe("seedV2SkillEntries", () => {
|
|
|
237
249
|
await seedV2SkillEntries();
|
|
238
250
|
|
|
239
251
|
expect(state.upsertCalls).toHaveLength(1);
|
|
240
|
-
expect(state.upsertCalls[0].
|
|
252
|
+
expect(state.upsertCalls[0].slug).toBe("skills/example-skill-a");
|
|
241
253
|
});
|
|
242
254
|
|
|
243
255
|
test("seeds genuinely uninstalled catalog skills alongside enabled installed skills", async () => {
|
|
@@ -263,8 +275,11 @@ describe("seedV2SkillEntries", () => {
|
|
|
263
275
|
|
|
264
276
|
await seedV2SkillEntries();
|
|
265
277
|
|
|
266
|
-
const
|
|
267
|
-
expect(
|
|
278
|
+
const slugs = state.upsertCalls.map((c) => c.slug).sort();
|
|
279
|
+
expect(slugs).toEqual([
|
|
280
|
+
"skills/example-skill-a",
|
|
281
|
+
"skills/uninstalled-skill",
|
|
282
|
+
]);
|
|
268
283
|
});
|
|
269
284
|
|
|
270
285
|
test("skips skills whose declared feature flag is disabled", async () => {
|
|
@@ -284,10 +299,10 @@ describe("seedV2SkillEntries", () => {
|
|
|
284
299
|
await seedV2SkillEntries();
|
|
285
300
|
|
|
286
301
|
expect(state.upsertCalls).toHaveLength(1);
|
|
287
|
-
expect(state.upsertCalls[0].
|
|
302
|
+
expect(state.upsertCalls[0].slug).toBe("skills/example-skill-b");
|
|
288
303
|
});
|
|
289
304
|
|
|
290
|
-
test("calls
|
|
305
|
+
test("calls pruneSlugsWithPrefixExcept with the active id list and the skills/ prefix", async () => {
|
|
291
306
|
const skillA = makeSummary({ id: "example-skill-a" });
|
|
292
307
|
const skillB = makeSummary({ id: "example-skill-b" });
|
|
293
308
|
state.catalog = [skillA, skillB];
|
|
@@ -309,13 +324,14 @@ describe("seedV2SkillEntries", () => {
|
|
|
309
324
|
await seedV2SkillEntries();
|
|
310
325
|
|
|
311
326
|
expect(state.pruneCalls).toHaveLength(1);
|
|
312
|
-
expect(
|
|
327
|
+
expect(state.pruneCalls[0].prefix).toBe("skills/");
|
|
328
|
+
expect([...state.pruneCalls[0].activeSuffixes].sort()).toEqual([
|
|
313
329
|
"example-skill-a",
|
|
314
330
|
"example-skill-b",
|
|
315
331
|
]);
|
|
316
332
|
});
|
|
317
333
|
|
|
318
|
-
test("passes only the active (post-flag-filter) ids to
|
|
334
|
+
test("passes only the active (post-flag-filter) ids to pruneSlugsWithPrefixExcept", async () => {
|
|
319
335
|
const flagged = makeSummary({
|
|
320
336
|
id: "example-skill-a",
|
|
321
337
|
featureFlag: "off-flag",
|
|
@@ -327,8 +343,6 @@ describe("seedV2SkillEntries", () => {
|
|
|
327
343
|
{ summary: unflagged, state: "enabled" },
|
|
328
344
|
];
|
|
329
345
|
state.flagsEnabled = { "off-flag": false };
|
|
330
|
-
// Remote catalog must be non-empty so catalogAvailable is true and
|
|
331
|
-
// pruning is not skipped.
|
|
332
346
|
state.fullCatalog = [
|
|
333
347
|
{ id: "example-skill-a", name: "example-skill-a", description: "A" },
|
|
334
348
|
{ id: "example-skill-b", name: "example-skill-b", description: "B" },
|
|
@@ -338,44 +352,35 @@ describe("seedV2SkillEntries", () => {
|
|
|
338
352
|
await seedV2SkillEntries();
|
|
339
353
|
|
|
340
354
|
expect(state.pruneCalls).toHaveLength(1);
|
|
341
|
-
expect(
|
|
355
|
+
expect(state.pruneCalls[0].prefix).toBe("skills/");
|
|
356
|
+
expect([...state.pruneCalls[0].activeSuffixes]).toEqual([
|
|
357
|
+
"example-skill-b",
|
|
358
|
+
]);
|
|
342
359
|
});
|
|
343
360
|
|
|
344
|
-
test("populates the entries cache so getSkillCapability
|
|
361
|
+
test("populates the entries cache so getSkillCapability resolves both bare id and unified slug", async () => {
|
|
345
362
|
const skillA = makeSummary({
|
|
346
363
|
id: "example-skill-a",
|
|
347
364
|
displayName: "Skill A",
|
|
348
365
|
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
});
|
|
353
|
-
state.catalog = [skillA, skillB];
|
|
354
|
-
state.resolved = [
|
|
355
|
-
{ summary: skillA, state: "enabled" },
|
|
356
|
-
{ summary: skillB, state: "enabled" },
|
|
357
|
-
];
|
|
358
|
-
state.embedReturn = [
|
|
359
|
-
[0.1, 0.2, 0.3],
|
|
360
|
-
[0.4, 0.5, 0.6],
|
|
361
|
-
];
|
|
366
|
+
state.catalog = [skillA];
|
|
367
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
368
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
362
369
|
|
|
363
370
|
expect(getSkillCapability("example-skill-a")).toBeNull();
|
|
364
371
|
|
|
365
372
|
await seedV2SkillEntries();
|
|
366
373
|
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
expect(
|
|
371
|
-
expect(
|
|
372
|
-
|
|
373
|
-
expect(
|
|
374
|
-
expect(entryB?.id).toBe("example-skill-b");
|
|
375
|
-
expect(entryB?.content).toContain("Skill B");
|
|
374
|
+
// Bare id and unified-slug forms both resolve to the same entry.
|
|
375
|
+
const byId = getSkillCapability("example-skill-a");
|
|
376
|
+
const bySlug = getSkillCapability("skills/example-skill-a");
|
|
377
|
+
expect(byId).not.toBeNull();
|
|
378
|
+
expect(byId?.id).toBe("example-skill-a");
|
|
379
|
+
expect(byId?.content).toContain("Skill A");
|
|
380
|
+
expect(bySlug).toEqual(byId);
|
|
376
381
|
|
|
377
|
-
// Unknown ids return null even when the cache is populated.
|
|
378
382
|
expect(getSkillCapability("unknown-skill")).toBeNull();
|
|
383
|
+
expect(getSkillCapability("skills/unknown-skill")).toBeNull();
|
|
379
384
|
});
|
|
380
385
|
|
|
381
386
|
test("swallows errors from embedWithBackend and leaves prior cache intact", async () => {
|
|
@@ -426,9 +431,10 @@ describe("seedV2SkillEntries", () => {
|
|
|
426
431
|
await seedV2SkillEntries();
|
|
427
432
|
|
|
428
433
|
expect(state.upsertCalls).toHaveLength(1);
|
|
429
|
-
expect(state.upsertCalls[0].
|
|
434
|
+
expect(state.upsertCalls[0].slug).toBe("skills/remote-only");
|
|
430
435
|
expect(state.pruneCalls).toHaveLength(1);
|
|
431
|
-
expect(
|
|
436
|
+
expect(state.pruneCalls[0].prefix).toBe("skills/");
|
|
437
|
+
expect([...state.pruneCalls[0].activeSuffixes]).toEqual(["remote-only"]);
|
|
432
438
|
});
|
|
433
439
|
|
|
434
440
|
test("skips pruning when catalog fetch returns empty (network failure guard)", async () => {
|