@vellumai/assistant 0.11.3 → 0.11.4-staging.1
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 +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -825,6 +825,38 @@ describe("always-candidate frontmatter parsing", () => {
|
|
|
825
825
|
.map(({ id, chars }) => `${id}: ${chars}`);
|
|
826
826
|
expect(overrun).toEqual([]);
|
|
827
827
|
});
|
|
828
|
+
|
|
829
|
+
test("the bundled acp skill's card fits the default budget", async () => {
|
|
830
|
+
// `acp` owns Claude Code and Codex setup, sign-in, and delegation, and the
|
|
831
|
+
// terms that route those turns to it live in this card. The card is
|
|
832
|
+
// hard-truncated at the default budget, so an overrun drops the trailing
|
|
833
|
+
// hints and the whole avoid-when list from what the selector sees.
|
|
834
|
+
const { DEFAULT_CARD_CHARS, buildSkillContent } =
|
|
835
|
+
await import("../plugins/defaults/memory/substrate/skill-content.js");
|
|
836
|
+
const acp = loadSkillCatalog().find((skill) => skill.id === "acp");
|
|
837
|
+
expect(acp).toBeDefined();
|
|
838
|
+
|
|
839
|
+
// A card that overran is cut to exactly the budget, so staying under it is
|
|
840
|
+
// what proves nothing was dropped. `avoid-when` is appended last and is
|
|
841
|
+
// therefore the first content a truncation eats.
|
|
842
|
+
const card = buildSkillContent(acp!);
|
|
843
|
+
expect(card.length).toBeLessThan(DEFAULT_CARD_CHARS);
|
|
844
|
+
expect(card).toContain("Avoid when:");
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("the bundled acp card carries its setup and sign-in vocabulary", async () => {
|
|
848
|
+
// A "configure Claude Code" or "let's do the auth" turn only reaches this
|
|
849
|
+
// skill if the card actually says it covers setup and authentication;
|
|
850
|
+
// delegation-only wording routes those turns to a terminal skill instead.
|
|
851
|
+
const { buildSkillContent } =
|
|
852
|
+
await import("../plugins/defaults/memory/substrate/skill-content.js");
|
|
853
|
+
const acp = loadSkillCatalog().find((skill) => skill.id === "acp");
|
|
854
|
+
const card = buildSkillContent(acp!).toLowerCase();
|
|
855
|
+
|
|
856
|
+
for (const term of ["set up", "install", "authenticate", "connect"]) {
|
|
857
|
+
expect(card).toContain(term);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
828
860
|
});
|
|
829
861
|
|
|
830
862
|
describe("bundled skill categories", () => {
|
|
@@ -184,8 +184,11 @@ import {
|
|
|
184
184
|
clearConversations,
|
|
185
185
|
setConversation,
|
|
186
186
|
} from "../daemon/conversation-registry.js";
|
|
187
|
+
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
187
188
|
import { CallSiteRoutingProvider } from "../providers/call-site-routing.js";
|
|
188
189
|
import { SubagentManager } from "../subagent/manager.js";
|
|
190
|
+
import { mockAuthContext } from "./helpers/mock-actor-context.js";
|
|
191
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
189
192
|
|
|
190
193
|
// Seed the workspace `llm` config per-case. The real loader schema-merges the
|
|
191
194
|
// raw fragment over defaults exactly as the code path reads it.
|
|
@@ -304,26 +307,29 @@ describe("SubagentManager — provider call-site routing", () => {
|
|
|
304
307
|
test("copies parent guardian and auth context into spawned conversation", async () => {
|
|
305
308
|
setLlmConfig(makeLlmFixture());
|
|
306
309
|
|
|
307
|
-
const parentTrustContext = {
|
|
310
|
+
const parentTrustContext: TrustContext = {
|
|
308
311
|
sourceChannel: "vellum",
|
|
309
312
|
trustClass: "guardian",
|
|
310
313
|
guardianPrincipalId: "guardian-1",
|
|
311
314
|
guardianExternalUserId: "guardian-1",
|
|
312
315
|
};
|
|
313
|
-
const parentAuthContext = {
|
|
316
|
+
const parentAuthContext = mockAuthContext({
|
|
314
317
|
subject: "local:self:parent-perms",
|
|
315
318
|
actorPrincipalId: "guardian-1",
|
|
316
|
-
};
|
|
319
|
+
});
|
|
317
320
|
|
|
318
321
|
capturedConversations.length = 0;
|
|
319
322
|
clearConversations();
|
|
320
323
|
const manager = new SubagentManager();
|
|
321
|
-
setConversation(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
setConversation(
|
|
325
|
+
"parent-perms",
|
|
326
|
+
asConversation({
|
|
327
|
+
trustContext: parentTrustContext,
|
|
328
|
+
getAuthContext: () => parentAuthContext,
|
|
329
|
+
assistantId: "self",
|
|
330
|
+
getCurrentSystemPrompt: () => "parent system",
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
327
333
|
|
|
328
334
|
await manager.spawn(
|
|
329
335
|
{
|
|
@@ -354,11 +360,14 @@ describe("SubagentManager — provider call-site routing", () => {
|
|
|
354
360
|
capturedConversations.length = 0;
|
|
355
361
|
clearConversations();
|
|
356
362
|
const manager = new SubagentManager();
|
|
357
|
-
setConversation(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
363
|
+
setConversation(
|
|
364
|
+
"parent-scoped",
|
|
365
|
+
asConversation({
|
|
366
|
+
enabledPlugins: parentScope,
|
|
367
|
+
getAuthContext: () => undefined,
|
|
368
|
+
getCurrentSystemPrompt: () => "parent system",
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
362
371
|
|
|
363
372
|
await manager.spawn(
|
|
364
373
|
{
|
|
@@ -383,11 +392,14 @@ describe("SubagentManager — provider call-site routing", () => {
|
|
|
383
392
|
capturedConversations.length = 0;
|
|
384
393
|
clearConversations();
|
|
385
394
|
const manager = new SubagentManager();
|
|
386
|
-
setConversation(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
395
|
+
setConversation(
|
|
396
|
+
"parent-unscoped",
|
|
397
|
+
asConversation({
|
|
398
|
+
enabledPlugins: null,
|
|
399
|
+
getAuthContext: () => undefined,
|
|
400
|
+
getCurrentSystemPrompt: () => "parent system",
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
391
403
|
|
|
392
404
|
await manager.spawn(
|
|
393
405
|
{
|
|
@@ -207,6 +207,7 @@ import {
|
|
|
207
207
|
setConversation,
|
|
208
208
|
} from "../daemon/conversation-registry.js";
|
|
209
209
|
import { SubagentAbortedError, SubagentManager } from "../subagent/manager.js";
|
|
210
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
210
211
|
|
|
211
212
|
function makeConfig(overrides: Record<string, unknown> = {}) {
|
|
212
213
|
return {
|
|
@@ -229,16 +230,19 @@ function registerFakeParent(parentConversationId: string): {
|
|
|
229
230
|
enqueuedCount: () => number;
|
|
230
231
|
} {
|
|
231
232
|
let enqueued = 0;
|
|
232
|
-
setConversation(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
233
|
+
setConversation(
|
|
234
|
+
parentConversationId,
|
|
235
|
+
asConversation({
|
|
236
|
+
// Accessors read by setUpSubagent when copying trust/auth context.
|
|
237
|
+
trustContext: undefined,
|
|
238
|
+
getAuthContext: () => undefined,
|
|
239
|
+
assistantId: undefined,
|
|
240
|
+
enqueueMessage: () => {
|
|
241
|
+
enqueued += 1;
|
|
242
|
+
return { rejected: false, queued: true, requestId: "req-fake" };
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
242
246
|
return { enqueuedCount: () => enqueued };
|
|
243
247
|
}
|
|
244
248
|
|
|
@@ -331,3 +331,46 @@ describe("queryUnreportedTurnEvents", () => {
|
|
|
331
331
|
expect(() => stampTurnOutcome("no-such-message", "failed")).not.toThrow();
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
|
+
|
|
335
|
+
describe("unfinalized rows and turn accounting", () => {
|
|
336
|
+
test("an in-flight grouped tool-result row is not a real user turn", async () => {
|
|
337
|
+
const conv = createConversation({ conversationType: "standard" });
|
|
338
|
+
const first = await insertUserMessageAt(conv.id, "real question", 1000);
|
|
339
|
+
// The grouped tool-result row: user-role, reserved unfinalized while
|
|
340
|
+
// tool output streams, content a { ref } that does not contain the
|
|
341
|
+
// tool_result text the content exclusions match. Without the finalized
|
|
342
|
+
// predicate it counts as a phantom user turn until the fold.
|
|
343
|
+
const inflight = await insertUserMessageAt(
|
|
344
|
+
conv.id,
|
|
345
|
+
JSON.stringify({ ref: "conversations/conv/inflight/x.jsonl" }),
|
|
346
|
+
2000,
|
|
347
|
+
);
|
|
348
|
+
getDb().run(`UPDATE messages SET finalized = 0 WHERE id = '${inflight}'`);
|
|
349
|
+
const second = await insertUserMessageAt(conv.id, "second question", 3000);
|
|
350
|
+
|
|
351
|
+
const events = queryUnreportedTurnEvents(0, undefined, 100);
|
|
352
|
+
const ids = events
|
|
353
|
+
.filter((e) => e.conversationId === conv.id)
|
|
354
|
+
.map((e) => e.id);
|
|
355
|
+
|
|
356
|
+
expect(ids).toEqual([first, second]);
|
|
357
|
+
const byId = Object.fromEntries(events.map((e) => [e.id, e]));
|
|
358
|
+
expect(byId[first].turnIndex).toBe(1);
|
|
359
|
+
expect(byId[second].turnIndex).toBe(2);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a persisted empty-display user row still counts as a turn", async () => {
|
|
363
|
+
const conv = createConversation({ conversationType: "standard" });
|
|
364
|
+
const real = await insertUserMessageAt(conv.id, "real question", 1000);
|
|
365
|
+
// A hidden send persists its user row with [] content (displayContent "")
|
|
366
|
+
// and still runs a turn, so [] rows must stay in turn accounting.
|
|
367
|
+
const hidden = await insertUserMessageAt(conv.id, "[]", 2000);
|
|
368
|
+
|
|
369
|
+
const events = queryUnreportedTurnEvents(0, undefined, 100);
|
|
370
|
+
const ids = events
|
|
371
|
+
.filter((e) => e.conversationId === conv.id)
|
|
372
|
+
.map((e) => e.id);
|
|
373
|
+
|
|
374
|
+
expect(ids).toEqual([real, hidden]);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -23,9 +23,9 @@ import { tmpdir } from "node:os";
|
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
25
25
|
|
|
26
|
+
import { getUserHooksFor } from "../hooks/registry.js";
|
|
26
27
|
import {
|
|
27
28
|
getCachedUserTools,
|
|
28
|
-
getUserHooksFor,
|
|
29
29
|
resetPluginCacheForTests,
|
|
30
30
|
} from "../plugins/mtime-cache.js";
|
|
31
31
|
import { loadUserPlugins } from "../plugins/user-loader.js";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { registerDefaultPluginInjectors } from "../plugins/defaults/index.js";
|
|
26
26
|
import type { Message } from "../providers/types.js";
|
|
27
27
|
import { getWorkspacePluginsDir } from "../util/platform.js";
|
|
28
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
28
29
|
|
|
29
30
|
// The injector chain is registered by the daemon bootstrap in production; do
|
|
30
31
|
// the same here so `applyRuntimeInjections` walks a non-empty chain.
|
|
@@ -151,15 +152,21 @@ describe("visible_app injection", () => {
|
|
|
151
152
|
];
|
|
152
153
|
|
|
153
154
|
function seedConversation(currentTurnVisibleAppId: string | undefined): void {
|
|
154
|
-
setConversation(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
setConversation(
|
|
156
|
+
CONVERSATION_ID,
|
|
157
|
+
asConversation({
|
|
158
|
+
conversationId: CONVERSATION_ID,
|
|
159
|
+
workingDir: "/sandbox",
|
|
160
|
+
workspaceTopLevelContext: "",
|
|
161
|
+
workspaceTopLevelDirty: false,
|
|
162
|
+
surfaceState: new Map(),
|
|
163
|
+
currentTurnVisibleAppId,
|
|
164
|
+
currentTurnTemporalSnapshot: {
|
|
165
|
+
clientTimezone: null,
|
|
166
|
+
timeSinceLastMessage: null,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
afterEach(() => {
|
|
@@ -18,10 +18,18 @@ import { Glob } from "bun";
|
|
|
18
18
|
* - start the PID-file identity guard — the PID file is a worker's sole
|
|
19
19
|
* tracking handle, so a worker the file stops naming can never be
|
|
20
20
|
* stopped externally and must evict itself.
|
|
21
|
+
*
|
|
22
|
+
* The subset that hosts real agent conversations
|
|
23
|
+
* ({@link CONVERSATION_RUNNING_ENTRYPOINTS}) must additionally register the
|
|
24
|
+
* default plugins' hooks and injectors, since hook dispatch is process-global
|
|
25
|
+
* and a worker that skips it runs conversations with no plugin behavior at all
|
|
26
|
+
* (no image captioning or vision-rejection recovery, no tool-result
|
|
27
|
+
* truncation, no runtime injections).
|
|
21
28
|
*/
|
|
22
29
|
|
|
23
30
|
const DISABLE_CALL = "disableStreamSeqStamping()";
|
|
24
31
|
const PID_GUARD_CALL = "startWorkerPidFileGuard(";
|
|
32
|
+
const PLUGIN_SURFACE_CALL = "registerWorkerPluginSurface(";
|
|
25
33
|
|
|
26
34
|
/** Shape of a shutdown that schedules `process.exit` instead of calling it. */
|
|
27
35
|
const DEFERRED_EXIT = ".finally(() => process.exit";
|
|
@@ -44,6 +52,16 @@ const WORK_START_MARKERS: Record<string, string> = {
|
|
|
44
52
|
"routes/worker.ts": 'server.on("connection"',
|
|
45
53
|
};
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The entrypoints whose work wakes real agent conversations. `monitoring` (a
|
|
57
|
+
* resource sampler) and `routes` (an HTTP route host) run no conversations, so
|
|
58
|
+
* they carry no plugin surface.
|
|
59
|
+
*/
|
|
60
|
+
const CONVERSATION_RUNNING_ENTRYPOINTS: ReadonlySet<string> = new Set([
|
|
61
|
+
"schedule/worker.ts",
|
|
62
|
+
"plugins/defaults/memory/worker.ts",
|
|
63
|
+
]);
|
|
64
|
+
|
|
47
65
|
function findWorkerEntrypoints(): string[] {
|
|
48
66
|
const srcRoot = join(process.cwd(), "src");
|
|
49
67
|
const glob = new Glob("**/worker.ts");
|
|
@@ -137,4 +155,40 @@ describe("worker entrypoint guards", () => {
|
|
|
137
155
|
"worker superseded at startup before it runs orphaned work.",
|
|
138
156
|
).toEqual([]);
|
|
139
157
|
});
|
|
158
|
+
|
|
159
|
+
test("the conversation-running entrypoints are all discovered workers", () => {
|
|
160
|
+
const discovered = new Set(findWorkerEntrypoints());
|
|
161
|
+
const unknown = [...CONVERSATION_RUNNING_ENTRYPOINTS].filter(
|
|
162
|
+
(file) => !discovered.has(file),
|
|
163
|
+
);
|
|
164
|
+
expect(
|
|
165
|
+
unknown,
|
|
166
|
+
"CONVERSATION_RUNNING_ENTRYPOINTS names an entrypoint the glob does " +
|
|
167
|
+
"not find — the worker moved or was renamed.",
|
|
168
|
+
).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("every conversation-running entrypoint registers the default plugin surface before starting work", () => {
|
|
172
|
+
const offenders = [...CONVERSATION_RUNNING_ENTRYPOINTS]
|
|
173
|
+
.sort()
|
|
174
|
+
.filter((file) => {
|
|
175
|
+
const source = readFileSync(join(process.cwd(), "src", file), "utf8");
|
|
176
|
+
const marker = WORK_START_MARKERS[file];
|
|
177
|
+
if (marker == null) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
const registerAt = source.indexOf(PLUGIN_SURFACE_CALL);
|
|
181
|
+
const workAt = source.indexOf(marker);
|
|
182
|
+
return registerAt < 0 || workAt < 0 || registerAt > workAt;
|
|
183
|
+
});
|
|
184
|
+
expect(
|
|
185
|
+
offenders,
|
|
186
|
+
`A worker that wakes agent conversations must call ` +
|
|
187
|
+
`${PLUGIN_SURFACE_CALL}) before its work-start call (see ` +
|
|
188
|
+
"WORK_START_MARKERS). Hook dispatch is process-global, so without it " +
|
|
189
|
+
"the conversations this process runs get no default plugin behavior: " +
|
|
190
|
+
"no vision-rejection recovery, no tool-result truncation, no runtime " +
|
|
191
|
+
"injections.",
|
|
192
|
+
).toEqual([]);
|
|
193
|
+
});
|
|
140
194
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for {@link registerWorkerPluginSurface}, the default-plugin surface
|
|
3
|
+
* that sidecar worker processes install at startup.
|
|
4
|
+
*
|
|
5
|
+
* A worker that wakes real agent conversations shares the daemon's
|
|
6
|
+
* process-global hook and injector registries, so registering the defaults is
|
|
7
|
+
* what gives those conversations the same behavior as daemon conversations.
|
|
8
|
+
* Registration must populate both tables and stay idempotent, and it must do so
|
|
9
|
+
* without running any plugin `init` hook (the memory plugin's init starts the
|
|
10
|
+
* memory jobs worker process itself).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import { getHookEntriesFor } from "../hooks/registry.js";
|
|
16
|
+
import {
|
|
17
|
+
clearInjectorRegistry,
|
|
18
|
+
getRegisteredInjectors,
|
|
19
|
+
} from "../plugins/injector-registry.js";
|
|
20
|
+
import { resetPluginRegistryForTests } from "../plugins/registry.js";
|
|
21
|
+
import { registerWorkerPluginSurface } from "../plugins/worker-plugin-surface.js";
|
|
22
|
+
|
|
23
|
+
async function hookOwnersFor(name: string): Promise<string[]> {
|
|
24
|
+
const entries = await getHookEntriesFor(name);
|
|
25
|
+
return entries.map((entry) => entry.owner.id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("registerWorkerPluginSurface", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
resetPluginRegistryForTests();
|
|
31
|
+
clearInjectorRegistry();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("serves the default plugins' hooks from the process-global registry", async () => {
|
|
35
|
+
expect(await hookOwnersFor("post-model-call")).not.toContain(
|
|
36
|
+
"default-image-fallback",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
registerWorkerPluginSurface();
|
|
40
|
+
|
|
41
|
+
// The vision-rejection recovery that a worker without this surface never
|
|
42
|
+
// ran, plus an oversized-tool-result guard on the other hook surface.
|
|
43
|
+
expect(await hookOwnersFor("post-model-call")).toContain(
|
|
44
|
+
"default-image-fallback",
|
|
45
|
+
);
|
|
46
|
+
expect(await hookOwnersFor("post-tool-use")).toContain(
|
|
47
|
+
"default-tool-result-truncate",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("registers the injector-only defaults' runtime injectors", () => {
|
|
52
|
+
expect(
|
|
53
|
+
getRegisteredInjectors().some((i) => i.name === "unified-turn-context"),
|
|
54
|
+
).toBe(false);
|
|
55
|
+
|
|
56
|
+
registerWorkerPluginSurface();
|
|
57
|
+
|
|
58
|
+
// `default-turn-context` contributes no hooks, so its injections only reach
|
|
59
|
+
// a worker's conversations through the injector registry.
|
|
60
|
+
expect(
|
|
61
|
+
getRegisteredInjectors().some((i) => i.name === "unified-turn-context"),
|
|
62
|
+
).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("is idempotent: a second call neither throws nor duplicates", async () => {
|
|
66
|
+
registerWorkerPluginSurface();
|
|
67
|
+
const hooksAfterFirst = await hookOwnersFor("post-model-call");
|
|
68
|
+
const injectorsAfterFirst = getRegisteredInjectors().map((i) => i.name);
|
|
69
|
+
|
|
70
|
+
expect(() => registerWorkerPluginSurface()).not.toThrow();
|
|
71
|
+
|
|
72
|
+
expect(await hookOwnersFor("post-model-call")).toEqual(hooksAfterFirst);
|
|
73
|
+
expect(getRegisteredInjectors().map((i) => i.name)).toEqual(
|
|
74
|
+
injectorsAfterFirst,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { consolidateVoiceFrontDoorMigration } from "../workspace/migrations/142-consolidate-voice-front-door.js";
|
|
13
|
+
import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
|
|
14
|
+
|
|
15
|
+
let workspaceDir: string;
|
|
16
|
+
let configPath: string;
|
|
17
|
+
let previousDefaultWorkspaceConfigPath: string | undefined;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
previousDefaultWorkspaceConfigPath =
|
|
21
|
+
process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
|
|
22
|
+
delete process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
|
|
23
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-migration-142-test-"));
|
|
24
|
+
configPath = join(workspaceDir, "config.json");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (previousDefaultWorkspaceConfigPath === undefined) {
|
|
29
|
+
delete process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
|
|
30
|
+
} else {
|
|
31
|
+
process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
|
|
32
|
+
previousDefaultWorkspaceConfigPath;
|
|
33
|
+
}
|
|
34
|
+
if (existsSync(workspaceDir)) {
|
|
35
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function writeConfig(config: Record<string, unknown>): void {
|
|
40
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readConfig(): Record<string, unknown> {
|
|
44
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("142-consolidate-voice-front-door migration", () => {
|
|
48
|
+
test("is registered in order", () => {
|
|
49
|
+
expect(consolidateVoiceFrontDoorMigration.id).toBe(
|
|
50
|
+
"142-consolidate-voice-front-door",
|
|
51
|
+
);
|
|
52
|
+
// getLastWorkspaceMigrationId() reports the final entry as the registry
|
|
53
|
+
// ceiling, so ordering matters: this must be registered, and everything
|
|
54
|
+
// after it must carry a higher id. Asserting it stays literally last
|
|
55
|
+
// would break on every migration appended after it.
|
|
56
|
+
const index = WORKSPACE_MIGRATIONS.findIndex(
|
|
57
|
+
(migration) => migration.id === "142-consolidate-voice-front-door",
|
|
58
|
+
);
|
|
59
|
+
expect(index).toBeGreaterThan(-1);
|
|
60
|
+
for (const later of WORKSPACE_MIGRATIONS.slice(index + 1)) {
|
|
61
|
+
expect(later.id > "142").toBe(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("moves progress overrides and removes generated-ack tuning", () => {
|
|
66
|
+
writeConfig({
|
|
67
|
+
llm: {
|
|
68
|
+
callSites: {
|
|
69
|
+
voiceFrontDecision: { profile: "cost-optimized", effort: "low" },
|
|
70
|
+
voiceFrontDoor: { profile: "latency-optimized" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
liveVoice: {
|
|
74
|
+
frontModel: {
|
|
75
|
+
endpointDecisionTimeoutMs: 900,
|
|
76
|
+
ackFirstDeltaTimeoutMs: 2500,
|
|
77
|
+
ackGenerationTimeoutMs: 600,
|
|
78
|
+
progress: { enabled: true },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
consolidateVoiceFrontDoorMigration.run(workspaceDir);
|
|
84
|
+
|
|
85
|
+
const config = readConfig();
|
|
86
|
+
const llm = config.llm as Record<string, unknown>;
|
|
87
|
+
const callSites = llm.callSites as Record<string, unknown>;
|
|
88
|
+
expect(callSites.voiceProgressNarration).toEqual({
|
|
89
|
+
profile: "cost-optimized",
|
|
90
|
+
effort: "low",
|
|
91
|
+
});
|
|
92
|
+
expect(callSites).not.toHaveProperty("voiceFrontDecision");
|
|
93
|
+
expect(callSites.voiceFrontDoor).toEqual({ profile: "latency-optimized" });
|
|
94
|
+
|
|
95
|
+
const liveVoice = config.liveVoice as Record<string, unknown>;
|
|
96
|
+
const frontModel = liveVoice.frontModel as Record<string, unknown>;
|
|
97
|
+
expect(frontModel).not.toHaveProperty("ackFirstDeltaTimeoutMs");
|
|
98
|
+
expect(frontModel).not.toHaveProperty("ackGenerationTimeoutMs");
|
|
99
|
+
expect(frontModel.endpointDecisionTimeoutMs).toBe(900);
|
|
100
|
+
expect(frontModel.progress).toEqual({ enabled: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("keeps an existing progress override when both keys exist", () => {
|
|
104
|
+
writeConfig({
|
|
105
|
+
llm: {
|
|
106
|
+
callSites: {
|
|
107
|
+
voiceFrontDecision: { profile: "cost-optimized" },
|
|
108
|
+
voiceProgressNarration: { profile: "latency-optimized" },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
consolidateVoiceFrontDoorMigration.run(workspaceDir);
|
|
114
|
+
|
|
115
|
+
const config = readConfig();
|
|
116
|
+
const llm = config.llm as Record<string, unknown>;
|
|
117
|
+
const callSites = llm.callSites as Record<string, unknown>;
|
|
118
|
+
expect(callSites.voiceProgressNarration).toEqual({
|
|
119
|
+
profile: "latency-optimized",
|
|
120
|
+
});
|
|
121
|
+
expect(callSites).not.toHaveProperty("voiceFrontDecision");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("migrates saved overrides when a default config overlay is configured", () => {
|
|
125
|
+
process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH = "/tmp/overlay.json";
|
|
126
|
+
writeConfig({
|
|
127
|
+
llm: {
|
|
128
|
+
callSites: {
|
|
129
|
+
voiceFrontDecision: { profile: "cost-optimized" },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
consolidateVoiceFrontDoorMigration.run(workspaceDir);
|
|
135
|
+
|
|
136
|
+
const config = readConfig();
|
|
137
|
+
const llm = config.llm as Record<string, unknown>;
|
|
138
|
+
const callSites = llm.callSites as Record<string, unknown>;
|
|
139
|
+
expect(callSites.voiceProgressNarration).toEqual({
|
|
140
|
+
profile: "cost-optimized",
|
|
141
|
+
});
|
|
142
|
+
expect(callSites).not.toHaveProperty("voiceFrontDecision");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("is idempotent", () => {
|
|
146
|
+
writeConfig({
|
|
147
|
+
llm: {
|
|
148
|
+
callSites: { voiceFrontDecision: { profile: "cost-optimized" } },
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
consolidateVoiceFrontDoorMigration.run(workspaceDir);
|
|
153
|
+
const first = readFileSync(configPath, "utf-8");
|
|
154
|
+
consolidateVoiceFrontDoorMigration.run(workspaceDir);
|
|
155
|
+
|
|
156
|
+
expect(readFileSync(configPath, "utf-8")).toBe(first);
|
|
157
|
+
});
|
|
158
|
+
});
|