@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
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
* - A new topic appearing on a later turn injects only the new slug.
|
|
9
9
|
* - `evictCompactedTurns` re-enables a previously-injected slug —
|
|
10
10
|
* after eviction the same slug appears again in `toInject`.
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* - Unified-pool skills: a `skills/<id>` slug ranked into the top-K is
|
|
12
|
+
* rendered under `### Skills You Can Use`, mixed concept-page+skill
|
|
13
|
+
* blocks render concept sections first then the skills suffix, both
|
|
14
|
+
* empty → null block, skills participate in `everInjected` so they
|
|
15
|
+
* deduplicate across turns just like concepts.
|
|
14
16
|
*
|
|
15
17
|
* Hermetic by design: the embedding backend, qdrant client, and `getConfig`
|
|
16
18
|
* are mocked at the module level so the suite never reaches a real backend.
|
|
17
|
-
* The skill
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* collection. The activation-store uses an in-memory SQLite database so
|
|
22
|
-
* writes are real but contained.
|
|
19
|
+
* The skill-store cache (`getSkillCapability`, `isSkillSlug`) is mocked so
|
|
20
|
+
* each test can stage skill content without touching the real catalog.
|
|
21
|
+
* The activation-store uses an in-memory SQLite database so writes are
|
|
22
|
+
* real but contained.
|
|
23
23
|
*
|
|
24
24
|
* Tests use a temp workspace (mkdtemp) and never touch `~/.vellum/`. Sample
|
|
25
25
|
* page content uses generic placeholders (Alice, Bob, etc.) per the cross-
|
|
@@ -114,8 +114,11 @@ class MockQdrantClient {
|
|
|
114
114
|
_name: string,
|
|
115
115
|
params: { using: string; limit: number; filter?: unknown },
|
|
116
116
|
) {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
// The four-channel hybrid query fires body-dense, body-sparse,
|
|
118
|
+
// summary-dense, summary-sparse in order; both dense channels share
|
|
119
|
+
// the dense queue and both sparse channels share the sparse queue.
|
|
120
|
+
const channel = params.using.endsWith("sparse") ? "sparse" : "dense";
|
|
121
|
+
return state.queryResponses[channel].shift() ?? { points: [] };
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
@@ -124,44 +127,32 @@ mock.module("@qdrant/js-client-rest", () => ({
|
|
|
124
127
|
}));
|
|
125
128
|
|
|
126
129
|
// ---------------------------------------------------------------------------
|
|
127
|
-
// Skill
|
|
130
|
+
// Skill-store mock
|
|
128
131
|
// ---------------------------------------------------------------------------
|
|
129
132
|
//
|
|
130
|
-
//
|
|
131
|
-
// `
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// directly.
|
|
133
|
+
// Skills now flow through the unified pipeline under the `skills/<id>` slug
|
|
134
|
+
// prefix — they are scored by `simBatch` against the same Qdrant collection
|
|
135
|
+
// as concept pages, ranked by `selectInjections`, and rendered alongside
|
|
136
|
+
// concept sections. The render path branches on `isSkillSlug(slug)` to fetch
|
|
137
|
+
// content from the in-process cache via `getSkillCapability` instead of
|
|
138
|
+
// reading a page from disk. Tests stage that cache and rely on the regular
|
|
139
|
+
// `stageTurn` plumbing to land skill slugs in the candidate set.
|
|
138
140
|
|
|
139
141
|
const skillState = {
|
|
140
|
-
/**
|
|
141
|
-
topSkillIds: [] as string[],
|
|
142
|
-
/** id → SkillEntry used by `getSkillCapability` and `getAllSkillIds`. */
|
|
142
|
+
/** id → SkillEntry consulted by `getSkillCapability`. */
|
|
143
143
|
entries: new Map<string, SkillEntry>(),
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
const realActivation = await import("../activation.js");
|
|
147
|
-
mock.module("../activation.js", () => ({
|
|
148
|
-
...realActivation,
|
|
149
|
-
// The injection wiring only consumes `topNow` — the candidate set and
|
|
150
|
-
// activation map are inputs to `selectSkillInjections`, not anything the
|
|
151
|
-
// injection logic introspects. Stub them to empty so the test stays focused
|
|
152
|
-
// on the wiring, not the pipeline internals (covered in activation.test.ts).
|
|
153
|
-
computeSkillActivation: async () => ({
|
|
154
|
-
activation: new Map<string, number>(),
|
|
155
|
-
breakdown: new Map(),
|
|
156
|
-
}),
|
|
157
|
-
selectSkillInjections: ({ topK }: { topK: number }) => ({
|
|
158
|
-
topNow: skillState.topSkillIds.slice(0, topK),
|
|
159
|
-
}),
|
|
160
|
-
}));
|
|
161
|
-
|
|
162
146
|
mock.module("../skill-store.js", () => ({
|
|
163
|
-
|
|
164
|
-
|
|
147
|
+
getSkillCapability: (idOrSlug: string) => {
|
|
148
|
+
const id = idOrSlug.startsWith("skills/")
|
|
149
|
+
? idOrSlug.slice("skills/".length)
|
|
150
|
+
: idOrSlug;
|
|
151
|
+
return skillState.entries.get(id) ?? null;
|
|
152
|
+
},
|
|
153
|
+
isSkillSlug: (slug: string) => slug.startsWith("skills/"),
|
|
154
|
+
SKILL_SLUG_PREFIX: "skills/",
|
|
155
|
+
skillSlugFor: (id: string) => `skills/${id}`,
|
|
165
156
|
}));
|
|
166
157
|
|
|
167
158
|
// ---------------------------------------------------------------------------
|
|
@@ -241,6 +232,18 @@ ref_files:
|
|
|
241
232
|
---
|
|
242
233
|
Demo body content.`,
|
|
243
234
|
);
|
|
235
|
+
// A page WITH a `summary` in its frontmatter — exercises the summary-only
|
|
236
|
+
// injection path. Body is intentionally longer than the summary so tests
|
|
237
|
+
// can assert that the body is *not* injected when the summary is present.
|
|
238
|
+
writeFileSync(
|
|
239
|
+
join(tmpWorkspace, "memory", "concepts", "summarized-page.md"),
|
|
240
|
+
`---
|
|
241
|
+
edges: []
|
|
242
|
+
ref_files: []
|
|
243
|
+
summary: A short prose description of the summarized page that retrieval injects in place of the full body.
|
|
244
|
+
---
|
|
245
|
+
Long-form body content that should NOT appear in the injection block when the page has a summary in frontmatter — the agent reads the file on demand instead.`,
|
|
246
|
+
);
|
|
244
247
|
});
|
|
245
248
|
|
|
246
249
|
afterAll(() => {
|
|
@@ -293,7 +296,6 @@ function makeConfig(
|
|
|
293
296
|
k: number;
|
|
294
297
|
hops: number;
|
|
295
298
|
top_k: number;
|
|
296
|
-
top_k_skills: number;
|
|
297
299
|
epsilon: number;
|
|
298
300
|
dense_weight: number;
|
|
299
301
|
sparse_weight: number;
|
|
@@ -308,8 +310,7 @@ function makeConfig(
|
|
|
308
310
|
c_now: 0.2,
|
|
309
311
|
k: 0.5,
|
|
310
312
|
hops: 2,
|
|
311
|
-
top_k:
|
|
312
|
-
top_k_skills: 5,
|
|
313
|
+
top_k: 25,
|
|
313
314
|
epsilon: 0.01,
|
|
314
315
|
dense_weight: 1.0,
|
|
315
316
|
sparse_weight: 0.0,
|
|
@@ -322,14 +323,26 @@ function makeConfig(
|
|
|
322
323
|
/**
|
|
323
324
|
* Stage one set of dense/sparse hits, used uniformly by every `simBatch`
|
|
324
325
|
* channel call (user/assistant/now) AND by the un-restricted ANN candidate
|
|
325
|
-
* query. The candidate query runs first, then three simBatch calls
|
|
326
|
-
*
|
|
326
|
+
* query. The candidate query runs first, then three simBatch calls — that's
|
|
327
|
+
* `channels` (= 4) logical hybrid queries. Each logical hybrid query now
|
|
328
|
+
* fires a four-channel fan-out (body dense, body sparse, summary dense,
|
|
329
|
+
* summary sparse), so we push 2 dense + 2 sparse responses per logical
|
|
330
|
+
* call to match the post-summary-vector wire pattern.
|
|
327
331
|
*
|
|
328
332
|
* Each entry is mapped to a hit per channel; pass `denseScore`/`sparseScore`
|
|
329
|
-
* undefined to omit a slug from that channel.
|
|
333
|
+
* undefined to omit a slug from that channel. `summaryDenseScore` /
|
|
334
|
+
* `summarySparseScore` route to the summary-side channels — tests that
|
|
335
|
+
* don't care about summary scoring leave them undefined and the summary
|
|
336
|
+
* channel falls back to body-only behavior.
|
|
330
337
|
*/
|
|
331
338
|
function stageTurn(
|
|
332
|
-
hits: Array<{
|
|
339
|
+
hits: Array<{
|
|
340
|
+
slug: string;
|
|
341
|
+
denseScore?: number;
|
|
342
|
+
sparseScore?: number;
|
|
343
|
+
summaryDenseScore?: number;
|
|
344
|
+
summarySparseScore?: number;
|
|
345
|
+
}>,
|
|
333
346
|
channels = 4,
|
|
334
347
|
): void {
|
|
335
348
|
// Clear any leftovers from a prior turn before staging this one so unused
|
|
@@ -350,6 +363,22 @@ function stageTurn(
|
|
|
350
363
|
.filter((h) => h.sparseScore !== undefined)
|
|
351
364
|
.map((h) => ({ score: h.sparseScore, payload: { slug: h.slug } })),
|
|
352
365
|
});
|
|
366
|
+
state.queryResponses.dense.push({
|
|
367
|
+
points: hits
|
|
368
|
+
.filter((h) => h.summaryDenseScore !== undefined)
|
|
369
|
+
.map((h) => ({
|
|
370
|
+
score: h.summaryDenseScore,
|
|
371
|
+
payload: { slug: h.slug },
|
|
372
|
+
})),
|
|
373
|
+
});
|
|
374
|
+
state.queryResponses.sparse.push({
|
|
375
|
+
points: hits
|
|
376
|
+
.filter((h) => h.summarySparseScore !== undefined)
|
|
377
|
+
.map((h) => ({
|
|
378
|
+
score: h.summarySparseScore,
|
|
379
|
+
payload: { slug: h.slug },
|
|
380
|
+
})),
|
|
381
|
+
});
|
|
353
382
|
}
|
|
354
383
|
}
|
|
355
384
|
|
|
@@ -358,7 +387,6 @@ function resetState(): void {
|
|
|
358
387
|
state.sparseReturn = { indices: [1, 2, 3], values: [0.5, 0.5, 0.5] };
|
|
359
388
|
state.queryResponses.dense.length = 0;
|
|
360
389
|
state.queryResponses.sparse.length = 0;
|
|
361
|
-
skillState.topSkillIds.length = 0;
|
|
362
390
|
skillState.entries.clear();
|
|
363
391
|
telemetryState.recordCalls.length = 0;
|
|
364
392
|
telemetryState.recordShouldThrow = false;
|
|
@@ -368,10 +396,8 @@ function resetState(): void {
|
|
|
368
396
|
_resetMemoryV2QdrantForTests();
|
|
369
397
|
}
|
|
370
398
|
|
|
371
|
-
/** Stage
|
|
372
|
-
function stageSkills(
|
|
373
|
-
skillState.topSkillIds.length = 0;
|
|
374
|
-
skillState.topSkillIds.push(...ids);
|
|
399
|
+
/** Stage skill-store cache entries for the upcoming render. */
|
|
400
|
+
function stageSkills(entries: SkillEntry[]): void {
|
|
375
401
|
for (const entry of entries) {
|
|
376
402
|
skillState.entries.set(entry.id, entry);
|
|
377
403
|
}
|
|
@@ -412,7 +438,7 @@ describe("injectMemoryV2Block", () => {
|
|
|
412
438
|
expect(result.block).not.toContain("<memory>");
|
|
413
439
|
expect(result.block).not.toContain("</memory>");
|
|
414
440
|
expect(result.block).not.toContain("## What I Remember Right Now");
|
|
415
|
-
expect(result.block).toContain("
|
|
441
|
+
expect(result.block).toContain("# memory/concepts/alice-vscode.md");
|
|
416
442
|
expect(result.block).toContain("VS Code");
|
|
417
443
|
|
|
418
444
|
// State persisted: alice's activation is above epsilon and recorded;
|
|
@@ -501,10 +527,10 @@ describe("injectMemoryV2Block", () => {
|
|
|
501
527
|
});
|
|
502
528
|
|
|
503
529
|
expect(result.toInject).toEqual(["carol-jazz"]);
|
|
504
|
-
expect(result.block).toContain("
|
|
530
|
+
expect(result.block).toContain("# memory/concepts/carol-jazz.md");
|
|
505
531
|
// The block only shows the new slug — alice's attachment lives on the
|
|
506
532
|
// previous turn's user message.
|
|
507
|
-
expect(result.block).not.toContain("
|
|
533
|
+
expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
|
|
508
534
|
|
|
509
535
|
const persisted = await hydrate(db, "conv-1");
|
|
510
536
|
expect(persisted!.everInjected).toEqual([
|
|
@@ -549,7 +575,7 @@ describe("injectMemoryV2Block", () => {
|
|
|
549
575
|
});
|
|
550
576
|
|
|
551
577
|
expect(result.toInject).toEqual(["alice-vscode"]);
|
|
552
|
-
expect(result.block).toContain("
|
|
578
|
+
expect(result.block).toContain("# memory/concepts/alice-vscode.md");
|
|
553
579
|
|
|
554
580
|
const persisted = await hydrate(db, "conv-1");
|
|
555
581
|
expect(persisted!.everInjected).toEqual([
|
|
@@ -557,6 +583,74 @@ describe("injectMemoryV2Block", () => {
|
|
|
557
583
|
]);
|
|
558
584
|
});
|
|
559
585
|
|
|
586
|
+
test("page with summary renders as path + summary, no body, with the CRITICAL header", async () => {
|
|
587
|
+
// Pages whose frontmatter carries a `summary` should inject only the
|
|
588
|
+
// summary text behind the path header — the agent reads the full file
|
|
589
|
+
// on demand. The leading `**CRITICAL:**` line tells the agent how to
|
|
590
|
+
// read the block.
|
|
591
|
+
stageTurn([{ slug: "summarized-page", denseScore: 0.9 }]);
|
|
592
|
+
|
|
593
|
+
const result = await injectMemoryV2Block({
|
|
594
|
+
database: db,
|
|
595
|
+
conversationId: "conv-1",
|
|
596
|
+
currentTurn: 1,
|
|
597
|
+
userMessage: "tell me about the summarized page",
|
|
598
|
+
assistantMessage: "",
|
|
599
|
+
nowText: "Now",
|
|
600
|
+
messageId: "msg-1",
|
|
601
|
+
config: makeConfig(),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
expect(result.block).not.toBeNull();
|
|
605
|
+
expect(result.block).toContain(
|
|
606
|
+
"**CRITICAL:** These are page summaries. Read the page file if it looks relevant.",
|
|
607
|
+
);
|
|
608
|
+
expect(result.block).toContain(
|
|
609
|
+
"# memory/concepts/summarized-page.md\nA short prose description",
|
|
610
|
+
);
|
|
611
|
+
// Body is NOT in the block — the agent must follow up with a read tool.
|
|
612
|
+
expect(result.block).not.toContain("Long-form body content");
|
|
613
|
+
// Frontmatter is also omitted; the path header carries the identifying
|
|
614
|
+
// information by itself, and edges flow through the activation graph.
|
|
615
|
+
expect(result.block).not.toContain("---\nedges:");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("mixed batch — summary page renders short, fallback page renders full", async () => {
|
|
619
|
+
// Both pages rank into top-K. summarized-page has a summary → short
|
|
620
|
+
// form. frontmatter-demo has no summary → full-page fallback. The
|
|
621
|
+
// single CRITICAL header sits at the top regardless.
|
|
622
|
+
stageTurn([
|
|
623
|
+
{ slug: "summarized-page", denseScore: 0.95 },
|
|
624
|
+
{ slug: "frontmatter-demo", denseScore: 0.85 },
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
const result = await injectMemoryV2Block({
|
|
628
|
+
database: db,
|
|
629
|
+
conversationId: "conv-1",
|
|
630
|
+
currentTurn: 1,
|
|
631
|
+
userMessage: "show me everything",
|
|
632
|
+
assistantMessage: "",
|
|
633
|
+
nowText: "Now",
|
|
634
|
+
messageId: "msg-1",
|
|
635
|
+
config: makeConfig(),
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
expect(result.block).not.toBeNull();
|
|
639
|
+
// CRITICAL header appears exactly once.
|
|
640
|
+
const criticalCount = (
|
|
641
|
+
result.block!.match(/\*\*CRITICAL:\*\* These are page summaries\./g) ?? []
|
|
642
|
+
).length;
|
|
643
|
+
expect(criticalCount).toBe(1);
|
|
644
|
+
// summarized-page → short form (path + summary, no body, no frontmatter).
|
|
645
|
+
expect(result.block).toContain("# memory/concepts/summarized-page.md\nA");
|
|
646
|
+
expect(result.block).not.toContain("Long-form body content");
|
|
647
|
+
// frontmatter-demo → full-page fallback (path + frontmatter + body).
|
|
648
|
+
expect(result.block).toContain(
|
|
649
|
+
"# memory/concepts/frontmatter-demo.md\n---\n",
|
|
650
|
+
);
|
|
651
|
+
expect(result.block).toContain("Demo body content.");
|
|
652
|
+
});
|
|
653
|
+
|
|
560
654
|
test("includes the page frontmatter (edges, ref_files) in each rendered section", async () => {
|
|
561
655
|
// The frontmatter (`edges`, `ref_files`) lives on disk above the page
|
|
562
656
|
// body and is part of the page's content. Injection must reproduce both
|
|
@@ -577,8 +671,12 @@ describe("injectMemoryV2Block", () => {
|
|
|
577
671
|
});
|
|
578
672
|
|
|
579
673
|
expect(result.block).not.toBeNull();
|
|
580
|
-
//
|
|
581
|
-
|
|
674
|
+
// Path header is immediately followed by the frontmatter open delimiter.
|
|
675
|
+
// The fallback path renders the full page (frontmatter + body) when the
|
|
676
|
+
// page has no `summary` field — `frontmatter-demo` predates the field.
|
|
677
|
+
expect(result.block).toContain(
|
|
678
|
+
"# memory/concepts/frontmatter-demo.md\n---\n",
|
|
679
|
+
);
|
|
582
680
|
// Both fields render in YAML block style with their populated values.
|
|
583
681
|
expect(result.block).toContain("edges:\n - alice-vscode");
|
|
584
682
|
expect(result.block).toContain("ref_files:\n - images/demo.jpg");
|
|
@@ -606,8 +704,8 @@ describe("injectMemoryV2Block", () => {
|
|
|
606
704
|
});
|
|
607
705
|
|
|
608
706
|
expect(result.toInject).toEqual(["carol-jazz", "alice-vscode"]);
|
|
609
|
-
const carolIdx = result.block!.indexOf("
|
|
610
|
-
const aliceIdx = result.block!.indexOf("
|
|
707
|
+
const carolIdx = result.block!.indexOf("# memory/concepts/carol-jazz.md");
|
|
708
|
+
const aliceIdx = result.block!.indexOf("# memory/concepts/alice-vscode.md");
|
|
611
709
|
expect(carolIdx).toBeGreaterThan(-1);
|
|
612
710
|
expect(aliceIdx).toBeGreaterThan(-1);
|
|
613
711
|
expect(carolIdx).toBeLessThan(aliceIdx);
|
|
@@ -676,24 +774,22 @@ describe("injectMemoryV2Block", () => {
|
|
|
676
774
|
});
|
|
677
775
|
|
|
678
776
|
// ---------------------------------------------------------------------------
|
|
679
|
-
//
|
|
777
|
+
// Unified pool — skills as `skills/<id>` slugs
|
|
680
778
|
// ---------------------------------------------------------------------------
|
|
681
779
|
|
|
682
|
-
test("renders a skill-only block
|
|
683
|
-
// No concept-page candidates this turn — the
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
],
|
|
696
|
-
);
|
|
780
|
+
test("renders a skill-only block via the skills/ slug prefix", async () => {
|
|
781
|
+
// No concept-page candidates this turn — the only ANN hit is a skill
|
|
782
|
+
// slug. The render path branches on `skills/` prefix: it pulls the
|
|
783
|
+
// entry from the skill-store cache (mocked) and emits the bullet under
|
|
784
|
+
// the `### Skills You Can Use` subsection.
|
|
785
|
+
stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
|
|
786
|
+
stageSkills([
|
|
787
|
+
{
|
|
788
|
+
id: "example-skill-a",
|
|
789
|
+
content:
|
|
790
|
+
'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
|
|
791
|
+
},
|
|
792
|
+
]);
|
|
697
793
|
|
|
698
794
|
const result = await injectMemoryV2Block({
|
|
699
795
|
database: db,
|
|
@@ -706,16 +802,12 @@ describe("injectMemoryV2Block", () => {
|
|
|
706
802
|
config: makeConfig(),
|
|
707
803
|
});
|
|
708
804
|
|
|
709
|
-
expect(result.toInject).toEqual([]);
|
|
805
|
+
expect(result.toInject).toEqual(["skills/example-skill-a"]);
|
|
710
806
|
expect(result.block).not.toBeNull();
|
|
711
|
-
// `block` is the unwrapped inner content; the caller adds the
|
|
712
|
-
// `<memory>...</memory>` wrapper exactly once at injection time.
|
|
713
807
|
expect(result.block).not.toContain("<memory>");
|
|
714
808
|
expect(result.block).not.toContain("</memory>");
|
|
715
809
|
expect(result.block).not.toContain("## What I Remember Right Now");
|
|
716
|
-
|
|
717
|
-
// bullet shape and the unconditional `→ use skill_load to activate` suffix.
|
|
718
|
-
expect(result.block).not.toContain("### alice-vscode");
|
|
810
|
+
expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
|
|
719
811
|
expect(result.block).toContain("### Skills You Can Use");
|
|
720
812
|
expect(result.block).toContain(
|
|
721
813
|
'- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
|
|
@@ -723,19 +815,19 @@ describe("injectMemoryV2Block", () => {
|
|
|
723
815
|
});
|
|
724
816
|
|
|
725
817
|
test("renders concept-page sections before the skills subsection in mixed blocks", async () => {
|
|
726
|
-
// Concept page
|
|
818
|
+
// Concept page hit AND a skill — concept-page sections come first, then
|
|
727
819
|
// the skills subsection.
|
|
728
|
-
stageTurn([
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
);
|
|
820
|
+
stageTurn([
|
|
821
|
+
{ slug: "alice-vscode", denseScore: 0.9 },
|
|
822
|
+
{ slug: "skills/example-skill-a", denseScore: 0.7 },
|
|
823
|
+
]);
|
|
824
|
+
stageSkills([
|
|
825
|
+
{
|
|
826
|
+
id: "example-skill-a",
|
|
827
|
+
content:
|
|
828
|
+
'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
|
|
829
|
+
},
|
|
830
|
+
]);
|
|
739
831
|
|
|
740
832
|
const result = await injectMemoryV2Block({
|
|
741
833
|
database: db,
|
|
@@ -748,55 +840,34 @@ describe("injectMemoryV2Block", () => {
|
|
|
748
840
|
config: makeConfig(),
|
|
749
841
|
});
|
|
750
842
|
|
|
751
|
-
|
|
843
|
+
// Both slugs ranked into top-K and got freshly attached.
|
|
844
|
+
expect(new Set(result.toInject)).toEqual(
|
|
845
|
+
new Set(["alice-vscode", "skills/example-skill-a"]),
|
|
846
|
+
);
|
|
752
847
|
expect(result.block).not.toBeNull();
|
|
753
848
|
|
|
754
|
-
const
|
|
849
|
+
const aliceHeaderIdx = result.block!.indexOf(
|
|
850
|
+
"# memory/concepts/alice-vscode.md",
|
|
851
|
+
);
|
|
755
852
|
const skillsIdx = result.block!.indexOf("### Skills You Can Use");
|
|
756
|
-
expect(
|
|
853
|
+
expect(aliceHeaderIdx).toBeGreaterThan(-1);
|
|
757
854
|
expect(skillsIdx).toBeGreaterThan(-1);
|
|
758
|
-
expect(
|
|
855
|
+
expect(aliceHeaderIdx).toBeLessThan(skillsIdx);
|
|
759
856
|
|
|
760
|
-
// The activation suffix is always appended for skills.
|
|
761
857
|
expect(result.block).toContain(
|
|
762
858
|
'- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
|
|
763
859
|
);
|
|
764
860
|
});
|
|
765
861
|
|
|
766
|
-
test("
|
|
767
|
-
//
|
|
768
|
-
// AND no skill ids.
|
|
769
|
-
stageTurn([]);
|
|
770
|
-
stageSkills([]);
|
|
771
|
-
|
|
772
|
-
const result = await injectMemoryV2Block({
|
|
773
|
-
database: db,
|
|
774
|
-
conversationId: "conv-1",
|
|
775
|
-
currentTurn: 1,
|
|
776
|
-
userMessage: "anything",
|
|
777
|
-
assistantMessage: "",
|
|
778
|
-
nowText: "",
|
|
779
|
-
messageId: "msg-1",
|
|
780
|
-
config: makeConfig(),
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
expect(result.toInject).toEqual([]);
|
|
784
|
-
expect(result.block).toBeNull();
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
test("re-renders the same top-ranked skill on consecutive turns (no dedup)", async () => {
|
|
788
|
-
// Skills are stateless: the same id can appear on back-to-back turns.
|
|
789
|
-
// Stage no concept-page candidates so the block content is purely the
|
|
790
|
-
// skills subsection.
|
|
862
|
+
test("skills participate in everInjected — an attached skill is not re-attached on the next turn", async () => {
|
|
863
|
+
// Turn 1: skill ranks high, gets attached.
|
|
791
864
|
const skillEntry = {
|
|
792
865
|
id: "example-skill-a",
|
|
793
866
|
content:
|
|
794
867
|
'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
|
|
795
868
|
};
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
stageTurn([]);
|
|
799
|
-
stageSkills(["example-skill-a"], [skillEntry]);
|
|
869
|
+
stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
|
|
870
|
+
stageSkills([skillEntry]);
|
|
800
871
|
const result1 = await injectMemoryV2Block({
|
|
801
872
|
database: db,
|
|
802
873
|
conversationId: "conv-1",
|
|
@@ -807,15 +878,14 @@ describe("injectMemoryV2Block", () => {
|
|
|
807
878
|
messageId: "msg-1",
|
|
808
879
|
config: makeConfig(),
|
|
809
880
|
});
|
|
810
|
-
expect(result1.
|
|
881
|
+
expect(result1.toInject).toEqual(["skills/example-skill-a"]);
|
|
811
882
|
expect(result1.block).toContain("### Skills You Can Use");
|
|
812
|
-
expect(result1.block).toContain("example-skill-a");
|
|
813
883
|
|
|
814
|
-
// Turn 2
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
stageTurn([]);
|
|
818
|
-
stageSkills([
|
|
884
|
+
// Turn 2: same skill ranks top again. It is already in `everInjected`, so
|
|
885
|
+
// `toInject` is empty and the block is null — the attachment from turn 1
|
|
886
|
+
// remains visible to the agent via the cached prior user message.
|
|
887
|
+
stageTurn([{ slug: "skills/example-skill-a", denseScore: 0.9 }]);
|
|
888
|
+
stageSkills([skillEntry]);
|
|
819
889
|
const result2 = await injectMemoryV2Block({
|
|
820
890
|
database: db,
|
|
821
891
|
conversationId: "conv-1",
|
|
@@ -826,21 +896,57 @@ describe("injectMemoryV2Block", () => {
|
|
|
826
896
|
messageId: "msg-2",
|
|
827
897
|
config: makeConfig(),
|
|
828
898
|
});
|
|
829
|
-
expect(result2.
|
|
830
|
-
expect(result2.block).
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// The skill content line is identical across the two turns — the renderer
|
|
834
|
-
// is deterministic in `id → entry` lookup and the entry is unchanged.
|
|
835
|
-
const skillLine =
|
|
836
|
-
'- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate';
|
|
837
|
-
expect(result1.block).toContain(skillLine);
|
|
838
|
-
expect(result2.block).toContain(skillLine);
|
|
839
|
-
|
|
840
|
-
// `everInjected` is untouched by the skill pipeline — both turns left it
|
|
841
|
-
// empty (no concept pages were injected).
|
|
899
|
+
expect(result2.toInject).toEqual([]);
|
|
900
|
+
expect(result2.block).toBeNull();
|
|
901
|
+
|
|
842
902
|
const persisted = await hydrate(db, "conv-1");
|
|
843
|
-
expect(persisted!.everInjected).toEqual([
|
|
903
|
+
expect(persisted!.everInjected).toEqual([
|
|
904
|
+
{ slug: "skills/example-skill-a", turn: 1 },
|
|
905
|
+
]);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("skill slugs whose entry is missing from the cache are dropped silently", async () => {
|
|
909
|
+
// The skill ranks into top-K but the in-process cache no longer knows
|
|
910
|
+
// its content (skill uninstalled mid-run). The render path drops it
|
|
911
|
+
// without surfacing it as a `missingSlugs` page-missing event — that
|
|
912
|
+
// status is reserved for on-disk concept pages, not catalog-derived
|
|
913
|
+
// skill entries.
|
|
914
|
+
stageTurn([{ slug: "skills/missing-skill", denseScore: 0.9 }]);
|
|
915
|
+
// No `stageSkills` call — cache stays empty.
|
|
916
|
+
|
|
917
|
+
const result = await injectMemoryV2Block({
|
|
918
|
+
database: db,
|
|
919
|
+
conversationId: "conv-1",
|
|
920
|
+
currentTurn: 1,
|
|
921
|
+
userMessage: "anything",
|
|
922
|
+
assistantMessage: "",
|
|
923
|
+
nowText: "Now",
|
|
924
|
+
messageId: "msg-1",
|
|
925
|
+
config: makeConfig(),
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// `toInject` still records the slug (it ranked into top-K) but the
|
|
929
|
+
// block collapses to null because the only entry was a cache miss.
|
|
930
|
+
expect(result.toInject).toEqual(["skills/missing-skill"]);
|
|
931
|
+
expect(result.block).toBeNull();
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("returns null when both concept pages and skills are empty", async () => {
|
|
935
|
+
stageTurn([]);
|
|
936
|
+
|
|
937
|
+
const result = await injectMemoryV2Block({
|
|
938
|
+
database: db,
|
|
939
|
+
conversationId: "conv-1",
|
|
940
|
+
currentTurn: 1,
|
|
941
|
+
userMessage: "anything",
|
|
942
|
+
assistantMessage: "",
|
|
943
|
+
nowText: "",
|
|
944
|
+
messageId: "msg-1",
|
|
945
|
+
config: makeConfig(),
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
expect(result.toInject).toEqual([]);
|
|
949
|
+
expect(result.block).toBeNull();
|
|
844
950
|
});
|
|
845
951
|
|
|
846
952
|
test("context-load mode renders topNow even when every slug was previously injected", async () => {
|
|
@@ -875,7 +981,7 @@ describe("injectMemoryV2Block", () => {
|
|
|
875
981
|
});
|
|
876
982
|
|
|
877
983
|
expect(result.block).not.toBeNull();
|
|
878
|
-
expect(result.block).toContain("
|
|
984
|
+
expect(result.block).toContain("# memory/concepts/alice-vscode.md");
|
|
879
985
|
// No newly-injected slug — alice was already in everInjected.
|
|
880
986
|
expect(result.toInject).toEqual([]);
|
|
881
987
|
|
|
@@ -911,9 +1017,9 @@ describe("injectMemoryV2Block", () => {
|
|
|
911
1017
|
});
|
|
912
1018
|
|
|
913
1019
|
expect(result.block).not.toBeNull();
|
|
914
|
-
expect(result.block).toContain("
|
|
915
|
-
expect(result.block).toContain("
|
|
916
|
-
expect(result.block).toContain("
|
|
1020
|
+
expect(result.block).toContain("# memory/concepts/alice-vscode.md");
|
|
1021
|
+
expect(result.block).toContain("# memory/concepts/bob-coffee.md");
|
|
1022
|
+
expect(result.block).toContain("# memory/concepts/carol-jazz.md");
|
|
917
1023
|
// The seeded directed edges (alice→bob, bob→alice, frontmatter-demo→alice)
|
|
918
1024
|
// mean alice has two incoming predecessors and bob has one, so directed
|
|
919
1025
|
// spread normalizes alice's activation more aggressively than bob's. The
|
|
@@ -932,39 +1038,6 @@ describe("injectMemoryV2Block", () => {
|
|
|
932
1038
|
expect(persisted!.everInjected).toHaveLength(3);
|
|
933
1039
|
});
|
|
934
1040
|
|
|
935
|
-
test("`top_k_skills: 0` short-circuits to no skills subsection", async () => {
|
|
936
|
-
// Even when the underlying mock would surface skills, the cap at 0 must
|
|
937
|
-
// drop them via `selectSkillInjections.topK = 0` → empty `topNow`.
|
|
938
|
-
stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
|
|
939
|
-
stageSkills(
|
|
940
|
-
["example-skill-a"],
|
|
941
|
-
[
|
|
942
|
-
{
|
|
943
|
-
id: "example-skill-a",
|
|
944
|
-
content:
|
|
945
|
-
'The "Example Skill A" skill (example-skill-a) is available.',
|
|
946
|
-
},
|
|
947
|
-
],
|
|
948
|
-
);
|
|
949
|
-
|
|
950
|
-
const result = await injectMemoryV2Block({
|
|
951
|
-
database: db,
|
|
952
|
-
conversationId: "conv-1",
|
|
953
|
-
currentTurn: 1,
|
|
954
|
-
userMessage: "Alice's editor",
|
|
955
|
-
assistantMessage: "",
|
|
956
|
-
nowText: "Now",
|
|
957
|
-
messageId: "msg-1",
|
|
958
|
-
config: makeConfig({ top_k_skills: 0 }),
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
expect(result.toInject).toEqual(["alice-vscode"]);
|
|
962
|
-
expect(result.block).not.toBeNull();
|
|
963
|
-
expect(result.block).toContain("### alice-vscode");
|
|
964
|
-
expect(result.block).not.toContain("### Skills You Can Use");
|
|
965
|
-
expect(result.block).not.toContain("example-skill-a");
|
|
966
|
-
});
|
|
967
|
-
|
|
968
1041
|
// ---------------------------------------------------------------------------
|
|
969
1042
|
// Activation-log telemetry
|
|
970
1043
|
// ---------------------------------------------------------------------------
|
|
@@ -1013,13 +1086,12 @@ describe("injectMemoryV2Block", () => {
|
|
|
1013
1086
|
status: string;
|
|
1014
1087
|
source: string;
|
|
1015
1088
|
}>;
|
|
1016
|
-
skills: unknown[];
|
|
1017
1089
|
config: { top_k: number };
|
|
1018
1090
|
};
|
|
1019
1091
|
expect(row.conversationId).toBe("conv-1");
|
|
1020
1092
|
expect(row.turn).toBe(2);
|
|
1021
1093
|
expect(row.mode).toBe("per-turn");
|
|
1022
|
-
expect(row.config.top_k).toBe(
|
|
1094
|
+
expect(row.config.top_k).toBe(25);
|
|
1023
1095
|
|
|
1024
1096
|
// The candidate set is the union of fromPrior (alice) and fromAnn
|
|
1025
1097
|
// (alice + carol) → two concept rows.
|
|
@@ -1041,6 +1113,41 @@ describe("injectMemoryV2Block", () => {
|
|
|
1041
1113
|
expect(byslug.get("carol-jazz")!.status).toBe("injected");
|
|
1042
1114
|
});
|
|
1043
1115
|
|
|
1116
|
+
test("activation-log concepts include skill rows under the skills/ prefix", async () => {
|
|
1117
|
+
// Skills participate in the unified telemetry list — they live in the
|
|
1118
|
+
// same `concepts` array, identifiable by the `skills/` slug prefix.
|
|
1119
|
+
stageTurn([
|
|
1120
|
+
{ slug: "alice-vscode", denseScore: 0.9 },
|
|
1121
|
+
{ slug: "skills/example-skill-a", denseScore: 0.7 },
|
|
1122
|
+
]);
|
|
1123
|
+
stageSkills([
|
|
1124
|
+
{
|
|
1125
|
+
id: "example-skill-a",
|
|
1126
|
+
content: "skill content",
|
|
1127
|
+
},
|
|
1128
|
+
]);
|
|
1129
|
+
|
|
1130
|
+
await injectMemoryV2Block({
|
|
1131
|
+
database: db,
|
|
1132
|
+
conversationId: "conv-1",
|
|
1133
|
+
currentTurn: 1,
|
|
1134
|
+
userMessage: "Alice's editor",
|
|
1135
|
+
assistantMessage: "",
|
|
1136
|
+
nowText: "Now",
|
|
1137
|
+
messageId: "msg-1",
|
|
1138
|
+
config: makeConfig(),
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
expect(telemetryState.recordCalls.length).toBe(1);
|
|
1142
|
+
const row = telemetryState.recordCalls[0] as {
|
|
1143
|
+
concepts: Array<{ slug: string; status: string }>;
|
|
1144
|
+
};
|
|
1145
|
+
const slugs = row.concepts.map((c) => c.slug);
|
|
1146
|
+
expect(new Set(slugs)).toEqual(
|
|
1147
|
+
new Set(["alice-vscode", "skills/example-skill-a"]),
|
|
1148
|
+
);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1044
1151
|
test("context-load mode marks every rendered slug as `injected`, never `in_context`", async () => {
|
|
1045
1152
|
// Turn 1 (per-turn): seed alice as injected so the next turn's prior
|
|
1046
1153
|
// `everInjected` includes her — the same setup the per-turn telemetry
|
|
@@ -1173,7 +1280,7 @@ describe("injectMemoryV2Block", () => {
|
|
|
1173
1280
|
expect(telemetryState.recordCalls.length).toBe(0);
|
|
1174
1281
|
expect(result.toInject).toEqual(["alice-vscode"]);
|
|
1175
1282
|
expect(result.block).not.toBeNull();
|
|
1176
|
-
expect(result.block).toContain("
|
|
1283
|
+
expect(result.block).toContain("# memory/concepts/alice-vscode.md");
|
|
1177
1284
|
|
|
1178
1285
|
const persisted = await hydrate(db, "conv-1");
|
|
1179
1286
|
expect(persisted!.everInjected).toEqual([
|