@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
|
@@ -147,6 +147,11 @@ const { _resetAdapterInstallCacheForTests } =
|
|
|
147
147
|
await import("../../acp/auto-install.js");
|
|
148
148
|
const { ACP_CLAUDE_OAUTH_MISSING_CODE } =
|
|
149
149
|
await import("../../acp/prepare-agent-env.js");
|
|
150
|
+
const { ACP_CLAUDE_AUTH_REQUIRED_CODE, AcpAuthRequiredError } =
|
|
151
|
+
await import("../../acp/auth-required.js");
|
|
152
|
+
const { hasAcpConnectCardRaised } =
|
|
153
|
+
await import("../../acp/acp-connect-card-state.js");
|
|
154
|
+
const { AcpAgentProcess } = await import("../../acp/agent-process.js");
|
|
150
155
|
|
|
151
156
|
// ---------------------------------------------------------------------------
|
|
152
157
|
// Helpers
|
|
@@ -547,3 +552,95 @@ describe("executeAcpSpawn — CLAUDE_CODE_OAUTH_TOKEN injection", () => {
|
|
|
547
552
|
expect(result.errorCode).toBe(ACP_CLAUDE_OAUTH_MISSING_CODE);
|
|
548
553
|
});
|
|
549
554
|
});
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Pre-spawn Claude auth rejection
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
describe("pre-spawn Claude auth rejection", () => {
|
|
561
|
+
test("raises the recovery surface: errorCode, card guidance, registry mark", async () => {
|
|
562
|
+
// The adapter can reject session creation with auth_required (a stored
|
|
563
|
+
// credential the server refuses). The error thrown here is produced by
|
|
564
|
+
// the REAL conversion seam, not hand-built: a wire-shaped JSON-RPC
|
|
565
|
+
// auth_required rejection driven through AcpAgentProcess.createSession's
|
|
566
|
+
// actual retry path, so this test fails if that seam stops yielding the
|
|
567
|
+
// typed error the tool boundary maps to the recovery surface.
|
|
568
|
+
const proc = new AcpAgentProcess(
|
|
569
|
+
"claude",
|
|
570
|
+
{ command: "claude-agent-acp", args: [] },
|
|
571
|
+
(() => ({})) as never,
|
|
572
|
+
);
|
|
573
|
+
(proc as unknown as { connection: unknown }).connection = {
|
|
574
|
+
newSession: () =>
|
|
575
|
+
Promise.reject({ code: -32000, message: "Authentication required" }),
|
|
576
|
+
};
|
|
577
|
+
(proc as unknown as { initializeResponse: unknown }).initializeResponse = {
|
|
578
|
+
authMethods: [
|
|
579
|
+
{
|
|
580
|
+
id: "claude-ai-login",
|
|
581
|
+
name: "Claude Subscription",
|
|
582
|
+
type: "terminal",
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
(proc as unknown as { spawnedEnv: unknown }).spawnedEnv = {};
|
|
587
|
+
const realSeamError: unknown = await proc.createSession("/tmp").then(
|
|
588
|
+
() => null,
|
|
589
|
+
(e: unknown) => e,
|
|
590
|
+
);
|
|
591
|
+
expect(realSeamError).toBeInstanceOf(AcpAuthRequiredError);
|
|
592
|
+
|
|
593
|
+
spawnMock.mockImplementationOnce(async () => {
|
|
594
|
+
throw realSeamError;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const context = { ...makeContext(), conversationId: "conv-prespawn-auth" };
|
|
598
|
+
const result = await executeAcpSpawn(
|
|
599
|
+
{ agent: "claude", task: "do something" },
|
|
600
|
+
context,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
expect(result.isError).toBe(true);
|
|
604
|
+
expect(result.errorCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
|
|
605
|
+
expect(result.content).toContain("Connect Claude Code");
|
|
606
|
+
expect(hasAcpConnectCardRaised("conv-prespawn-auth")).toBe(true);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("a raw wire auth_required object (unwrapped initialize path) gets the same surface", async () => {
|
|
610
|
+
// initialize() cannot ride withAuthRetry (its response advertises the
|
|
611
|
+
// methods the retry selects from), so its rejection arrives as the plain
|
|
612
|
+
// JSON-RPC object rather than the typed error. The gate must recognize
|
|
613
|
+
// both shapes or this path silently flattens to a generic failure.
|
|
614
|
+
spawnMock.mockImplementationOnce(async () => {
|
|
615
|
+
throw { code: -32000, message: "Authentication required" };
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const context = { ...makeContext(), conversationId: "conv-prespawn-raw" };
|
|
619
|
+
const result = await executeAcpSpawn(
|
|
620
|
+
{ agent: "claude", task: "do something" },
|
|
621
|
+
context,
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
expect(result.isError).toBe(true);
|
|
625
|
+
expect(result.errorCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
|
|
626
|
+
expect(result.content).toContain("Connect Claude Code");
|
|
627
|
+
expect(hasAcpConnectCardRaised("conv-prespawn-raw")).toBe(true);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("a non-auth spawn failure keeps the plain flattened result", async () => {
|
|
631
|
+
spawnMock.mockImplementationOnce(async () => {
|
|
632
|
+
throw new Error("adapter exploded");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const context = { ...makeContext(), conversationId: "conv-prespawn-boom" };
|
|
636
|
+
const result = await executeAcpSpawn(
|
|
637
|
+
{ agent: "claude", task: "do something" },
|
|
638
|
+
context,
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
expect(result.isError).toBe(true);
|
|
642
|
+
expect(result.errorCode).toBeUndefined();
|
|
643
|
+
expect(result.content).toContain("adapter exploded");
|
|
644
|
+
expect(hasAcpConnectCardRaised("conv-prespawn-boom")).toBe(false);
|
|
645
|
+
});
|
|
646
|
+
});
|
package/src/tools/acp/spawn.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { basename } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { markAcpConnectCardRaised } from "../../acp/acp-connect-card-state.js";
|
|
6
|
+
import {
|
|
7
|
+
ACP_AUTH_RECOVERY_GUIDANCE,
|
|
8
|
+
ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
9
|
+
CLAUDE_ACP_COMMAND,
|
|
10
|
+
isAcpAuthRequired,
|
|
11
|
+
} from "../../acp/auth-required.js";
|
|
6
12
|
import { resolveAgentWithAutoInstall } from "../../acp/auto-install.js";
|
|
7
13
|
import { getAcpSessionManager } from "../../acp/index.js";
|
|
8
14
|
import {
|
|
@@ -147,6 +153,32 @@ export async function executeAcpSpawn(
|
|
|
147
153
|
|
|
148
154
|
return { content: payload, isError: false };
|
|
149
155
|
} catch (err) {
|
|
156
|
+
// A pre-spawn rejection of the stored Claude credential (the adapter
|
|
157
|
+
// raises auth_required during session creation) gets the same recovery
|
|
158
|
+
// surface as the missing-token preflight: the errorCode raises the inline
|
|
159
|
+
// Connect card off this failed tool call, the registry mark dedups a
|
|
160
|
+
// redundant secure prompt, and the guidance keeps the model pointed at
|
|
161
|
+
// the card. Adapter-gated: the card repairs Claude credentials only.
|
|
162
|
+
// `isAcpAuthRequired` rather than an instanceof: it also recognizes the
|
|
163
|
+
// raw JSON-RPC auth_required object, which is how a rejection reaches
|
|
164
|
+
// this catch from the one protocol call `withAuthRetry` cannot wrap
|
|
165
|
+
// (`initialize`, whose response is what advertises the auth methods the
|
|
166
|
+
// retry would need).
|
|
167
|
+
if (
|
|
168
|
+
isAcpAuthRequired(err) &&
|
|
169
|
+
basename(agentConfig.command) === CLAUDE_ACP_COMMAND
|
|
170
|
+
) {
|
|
171
|
+
const message =
|
|
172
|
+
err instanceof Error
|
|
173
|
+
? err.message
|
|
174
|
+
: String((err as { message?: unknown }).message ?? err);
|
|
175
|
+
markAcpConnectCardRaised(context.conversationId);
|
|
176
|
+
return {
|
|
177
|
+
content: `${message}\n\n${ACP_AUTH_RECOVERY_GUIDANCE}`,
|
|
178
|
+
isError: true,
|
|
179
|
+
errorCode: ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
150
182
|
const msg =
|
|
151
183
|
err instanceof Error
|
|
152
184
|
? err.message
|
|
@@ -426,13 +426,22 @@ export function executeDocumentUpdate(
|
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
// The store drops append content that merely restates the document's tail,
|
|
430
|
+
// so the client renders what actually landed rather than the submission.
|
|
431
|
+
const applied = result.appliedMarkdown;
|
|
432
|
+
const message = result.duplicateLeadingContentSkipped
|
|
433
|
+
? applied.length === 0
|
|
434
|
+
? "Nothing appended: the content is already at the end of the document"
|
|
435
|
+
: "Document content updated (leading content already at the end of the document was skipped)"
|
|
436
|
+
: "Document content updated";
|
|
437
|
+
|
|
429
438
|
// Send document_editor_update message to update the built-in RTE
|
|
430
439
|
if (context.sendToClient) {
|
|
431
440
|
context.sendToClient({
|
|
432
441
|
type: "document_editor_update",
|
|
433
442
|
conversationId: context.conversationId,
|
|
434
443
|
surfaceId,
|
|
435
|
-
markdown:
|
|
444
|
+
markdown: applied,
|
|
436
445
|
mode,
|
|
437
446
|
});
|
|
438
447
|
|
|
@@ -441,7 +450,7 @@ export function executeDocumentUpdate(
|
|
|
441
450
|
success: true,
|
|
442
451
|
surface_id: surfaceId,
|
|
443
452
|
mode,
|
|
444
|
-
message
|
|
453
|
+
message,
|
|
445
454
|
}),
|
|
446
455
|
isError: false,
|
|
447
456
|
};
|
|
@@ -458,7 +467,7 @@ export function executeDocumentUpdate(
|
|
|
458
467
|
success: true,
|
|
459
468
|
surface_id: surfaceId,
|
|
460
469
|
mode,
|
|
461
|
-
message:
|
|
470
|
+
message: `${message} (no client connected to render it)`,
|
|
462
471
|
}),
|
|
463
472
|
isError: false,
|
|
464
473
|
};
|
package/src/tools/registry.ts
CHANGED
|
@@ -431,7 +431,8 @@ export function unregisterPluginTools(pluginName: string): void {
|
|
|
431
431
|
* {@link import("../plugins/mtime-cache.js").getActiveUserPluginTools} and diffs
|
|
432
432
|
* the result into the registry: plugins that vanished are unregistered, new
|
|
433
433
|
* plugins register, and a plugin whose per-tool source mtimes moved is
|
|
434
|
-
* re-registered. Mirrors how hooks resolve through `getUserHookEntriesFor
|
|
434
|
+
* re-registered. Mirrors how hooks resolve through `getUserHookEntriesFor`
|
|
435
|
+
* in `hooks/registry.ts`.
|
|
435
436
|
*
|
|
436
437
|
* This is a pure read of the plugin cache — it does NOT reconcile the cache
|
|
437
438
|
* against the source-versions sentinel, so it never activates a plugin or runs
|
|
@@ -55,8 +55,7 @@ export const runWorkflowInputSchema = z.looseObject({
|
|
|
55
55
|
*/
|
|
56
56
|
function resolveTrustContext(context: ToolContext): TrustContext {
|
|
57
57
|
const conversation = findConversation(context.conversationId);
|
|
58
|
-
const fromConversation =
|
|
59
|
-
conversation?.currentTurnTrustContext ?? conversation?.trustContext;
|
|
58
|
+
const fromConversation = conversation?.getTurnOrRestingTrust();
|
|
60
59
|
if (fromConversation) {
|
|
61
60
|
return fromConversation;
|
|
62
61
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createReasoningTagFilter } from "../reasoning-tag-filter.js";
|
|
4
|
+
|
|
5
|
+
function run(chunks: string[]): string {
|
|
6
|
+
const filter = createReasoningTagFilter();
|
|
7
|
+
let out = "";
|
|
8
|
+
for (const chunk of chunks) {
|
|
9
|
+
out += filter.push(chunk);
|
|
10
|
+
}
|
|
11
|
+
out += filter.flush();
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("ReasoningTagFilter", () => {
|
|
16
|
+
test("passes plain text through unchanged", () => {
|
|
17
|
+
expect(run(["Hello ", "there."])).toBe("Hello there.");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("drops a complete think span within one delta", () => {
|
|
21
|
+
expect(run(["<think>hm, let me reason</think>Answer."])).toBe("Answer.");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("drops a reasoning span crossing many deltas", () => {
|
|
25
|
+
expect(
|
|
26
|
+
run(["<thi", "nk>step one", " step two", "</th", "ink>Spoken reply"]),
|
|
27
|
+
).toBe("Spoken reply");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("drops thinking-tag variant and is case-insensitive", () => {
|
|
31
|
+
expect(run(["<Thinking>inner</Thinking>ok"])).toBe("ok");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("keeps text before and after the span", () => {
|
|
35
|
+
expect(run(["Sure. <think>secret</think> Done."])).toBe("Sure. Done.");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("handles multiple spans in one turn", () => {
|
|
39
|
+
expect(run(["a<think>x</think>b", "<think>y", "z</think>c"])).toBe("abc");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("an unclosed span at end of stream stays dropped", () => {
|
|
43
|
+
expect(run(["ok<think>never closes"])).toBe("ok");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a dangling partial open tag is released as literal text at flush", () => {
|
|
47
|
+
expect(run(["price <th"])).toBe("price <th");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a false partial that disambiguates to text is released", () => {
|
|
51
|
+
expect(run(["a <th", "ree-step plan"])).toBe("a <three-step plan");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("does not touch angle brackets that are not think tags", () => {
|
|
55
|
+
expect(run(["use <b>bold</b> and x < y"])).toBe(
|
|
56
|
+
"use <b>bold</b> and x < y",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("holds back nothing across an empty stream", () => {
|
|
61
|
+
expect(run([""])).toBe("");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming reasoning-tag filter for the TTS pipelines.
|
|
3
|
+
*
|
|
4
|
+
* Some OpenAI-compatible reasoning models emit their chain of thought inline
|
|
5
|
+
* in the content stream as `<think>...</think>` (or `<thinking>...</thinking>`)
|
|
6
|
+
* spans. When a profile has not opted into the chat-completions provider's
|
|
7
|
+
* `parseThinkTags` (which re-routes those spans to `thinking_delta`), the raw
|
|
8
|
+
* reasoning rides `text_delta`, and anything downstream that speaks text
|
|
9
|
+
* deltas would read the model's inner monologue aloud and delay real speech
|
|
10
|
+
* by the full length of the reasoning.
|
|
11
|
+
*
|
|
12
|
+
* This filter is the TTS-boundary guard: voice output must never speak
|
|
13
|
+
* reasoning regardless of profile configuration. It is stateful because a
|
|
14
|
+
* tag or a reasoning span routinely crosses delta boundaries; feed every
|
|
15
|
+
* delta through {@link ReasoningTagFilter.push} and call
|
|
16
|
+
* {@link ReasoningTagFilter.flush} when the turn's stream ends.
|
|
17
|
+
*
|
|
18
|
+
* Display paths are deliberately untouched: `assistant_text_delta` frames
|
|
19
|
+
* keep the raw text, exactly like the markdown handling in
|
|
20
|
+
* `calls/tts-text-sanitizer.ts`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const OPEN_TAGS = ["<think>", "<thinking>"] as const;
|
|
24
|
+
const CLOSE_TAGS = ["</think>", "</thinking>"] as const;
|
|
25
|
+
|
|
26
|
+
/** Longest suffix of `text` that is a proper prefix of any listed tag. */
|
|
27
|
+
function partialTagSuffix(text: string, tags: readonly string[]): number {
|
|
28
|
+
const max = Math.max(...tags.map((tag) => tag.length)) - 1;
|
|
29
|
+
const window = Math.min(max, text.length);
|
|
30
|
+
for (let len = window; len > 0; len -= 1) {
|
|
31
|
+
const suffix = text.slice(text.length - len).toLowerCase();
|
|
32
|
+
if (tags.some((tag) => tag.startsWith(suffix))) {
|
|
33
|
+
return len;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function indexOfAny(
|
|
40
|
+
haystack: string,
|
|
41
|
+
tags: readonly string[],
|
|
42
|
+
): { index: number; tag: string } | null {
|
|
43
|
+
const lower = haystack.toLowerCase();
|
|
44
|
+
let best: { index: number; tag: string } | null = null;
|
|
45
|
+
for (const tag of tags) {
|
|
46
|
+
const index = lower.indexOf(tag);
|
|
47
|
+
if (index >= 0 && (best === null || index < best.index)) {
|
|
48
|
+
best = { index, tag };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return best;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class ReasoningTagFilter {
|
|
55
|
+
private insideReasoning = false;
|
|
56
|
+
private pending = "";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Feed one streamed delta; returns the speakable text it releases.
|
|
60
|
+
* Reasoning spans are dropped; a partial tag at the end of the buffer is
|
|
61
|
+
* held back until the next delta disambiguates it.
|
|
62
|
+
*/
|
|
63
|
+
push(text: string): string {
|
|
64
|
+
this.pending += text;
|
|
65
|
+
let out = "";
|
|
66
|
+
for (;;) {
|
|
67
|
+
if (this.insideReasoning) {
|
|
68
|
+
const close = indexOfAny(this.pending, CLOSE_TAGS);
|
|
69
|
+
if (close) {
|
|
70
|
+
this.pending = this.pending.slice(close.index + close.tag.length);
|
|
71
|
+
this.insideReasoning = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Everything buffered is reasoning except a possible partial close
|
|
75
|
+
// tag at the end; drop the reasoning, keep the partial.
|
|
76
|
+
const partial = partialTagSuffix(this.pending, CLOSE_TAGS);
|
|
77
|
+
this.pending = partial > 0 ? this.pending.slice(-partial) : "";
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
const open = indexOfAny(this.pending, OPEN_TAGS);
|
|
81
|
+
if (open) {
|
|
82
|
+
out += this.pending.slice(0, open.index);
|
|
83
|
+
this.pending = this.pending.slice(open.index + open.tag.length);
|
|
84
|
+
this.insideReasoning = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const partial = partialTagSuffix(this.pending, OPEN_TAGS);
|
|
88
|
+
const safeLength = this.pending.length - partial;
|
|
89
|
+
out += this.pending.slice(0, safeLength);
|
|
90
|
+
this.pending = partial > 0 ? this.pending.slice(safeLength) : "";
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* End of stream: release any held-back partial. Outside a reasoning span a
|
|
97
|
+
* dangling "<thin" was ordinary text after all; inside one, the span never
|
|
98
|
+
* closed and its content stays dropped.
|
|
99
|
+
*/
|
|
100
|
+
flush(): string {
|
|
101
|
+
const rest = this.insideReasoning ? "" : this.pending;
|
|
102
|
+
this.pending = "";
|
|
103
|
+
this.insideReasoning = false;
|
|
104
|
+
return rest;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createReasoningTagFilter(): ReasoningTagFilter {
|
|
109
|
+
return new ReasoningTagFilter();
|
|
110
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type LLMProvider,
|
|
19
19
|
type ProfileEntry,
|
|
20
20
|
} from "../config/schemas/llm.js";
|
|
21
|
+
import { ROUTING_IDENTITY_PROVIDERS } from "../providers/inference/auth.js";
|
|
21
22
|
import { getLogger } from "../util/logger.js";
|
|
22
23
|
import { completedProfileBody } from "./custom-profile-ensure.js";
|
|
23
24
|
|
|
@@ -135,10 +136,26 @@ const ERA_COPY_LABEL_SUFFIX = " (Custom Provider)";
|
|
|
135
136
|
/**
|
|
136
137
|
* Comparison ignores `label` and `status` (user overlay state, preserved via
|
|
137
138
|
* `userOverlayState`) and `model` (checked separately against the current
|
|
138
|
-
* intent resolution and `HISTORICAL_INTENT_MODELS`).
|
|
139
|
+
* intent resolution and `HISTORICAL_INTENT_MODELS`). The CONVENTIONAL
|
|
140
|
+
* `<vendor>-personal` binding is normalized away before comparison (it was
|
|
141
|
+
* machinery-written across several eras); a non-conventional binding is a
|
|
142
|
+
* user selection and keeps blocking conversion.
|
|
139
143
|
*/
|
|
140
144
|
const IGNORED_COMPARISON_KEYS = new Set(["label", "status", "model"]);
|
|
141
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Fold the conventional personal entry name back to its vendor. The entries
|
|
148
|
+
* collapse (migration 145) rewrites a hatch copy's binding into its
|
|
149
|
+
* provider as `<vendor>-personal`; the matcher folds that shape so every
|
|
150
|
+
* era of stored copy keeps converting (the same discipline as
|
|
151
|
+
* `HISTORICAL_INTENT_MODELS`).
|
|
152
|
+
*/
|
|
153
|
+
function foldPersonalEntryName(provider: string): string {
|
|
154
|
+
return provider.endsWith("-personal")
|
|
155
|
+
? provider.slice(0, -"-personal".length)
|
|
156
|
+
: provider;
|
|
157
|
+
}
|
|
158
|
+
|
|
142
159
|
/**
|
|
143
160
|
* Per-provider model ids that earlier intent eras pinned onto `custom-*`
|
|
144
161
|
* copies, verified against `providers/model-intents.ts` git history. Hatch
|
|
@@ -156,6 +173,10 @@ const HISTORICAL_INTENT_MODELS: Record<
|
|
|
156
173
|
Partial<Record<string, readonly string[]>>
|
|
157
174
|
> = {
|
|
158
175
|
balanced: {
|
|
176
|
+
openai: [
|
|
177
|
+
// balanced intent 2026-05-05 (#29755) to the 2026-08-10 gpt-5.6 repoint.
|
|
178
|
+
"gpt-5.4-mini",
|
|
179
|
+
],
|
|
159
180
|
fireworks: [
|
|
160
181
|
// balanced intent 2026-05-05 (#29755) to 2026-05-19 (#31068).
|
|
161
182
|
"accounts/fireworks/models/kimi-k2p5",
|
|
@@ -166,6 +187,10 @@ const HISTORICAL_INTENT_MODELS: Record<
|
|
|
166
187
|
],
|
|
167
188
|
},
|
|
168
189
|
"quality-optimized": {
|
|
190
|
+
openai: [
|
|
191
|
+
// quality intent 2026-05-05 (#29755) to the 2026-08-10 gpt-5.6 repoint.
|
|
192
|
+
"gpt-5.4",
|
|
193
|
+
],
|
|
169
194
|
anthropic: [
|
|
170
195
|
// quality intent 2026-05-05 (#29755) to 2026-06-11 (#34498).
|
|
171
196
|
"claude-opus-4-7",
|
|
@@ -259,11 +284,13 @@ export function ensureByokDefaultProfiles(workspaceDir: string): void {
|
|
|
259
284
|
const completionBase = LLMConfigBase.safeParse(llm.default ?? {}).data;
|
|
260
285
|
|
|
261
286
|
// The default provider on a BYOK default, or the uniform hatch provider
|
|
262
|
-
// across the complete copy set on a
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
287
|
+
// across the complete copy set on a routing-identity default (vellum,
|
|
288
|
+
// chatgpt); null converts nothing.
|
|
289
|
+
const candidateProvider = ROUTING_IDENTITY_PROVIDERS.has(
|
|
290
|
+
parsedDefault.data.provider,
|
|
291
|
+
)
|
|
292
|
+
? uniformCopyProvider(profiles)
|
|
293
|
+
: parsedDefault.data.provider;
|
|
267
294
|
const convertibleProvider =
|
|
268
295
|
candidateProvider !== null && isByokDefaultProviderChoice(candidateProvider)
|
|
269
296
|
? candidateProvider
|
|
@@ -339,7 +366,11 @@ function isKnownUneditedBody(
|
|
|
339
366
|
// (hatch materialized it once; the default provider may have changed
|
|
340
367
|
// since); equality with the corroborated provider rules out a user
|
|
341
368
|
// re-provision.
|
|
342
|
-
if (
|
|
369
|
+
if (
|
|
370
|
+
convertibleProvider === null ||
|
|
371
|
+
typeof entry.provider !== "string" ||
|
|
372
|
+
foldPersonalEntryName(entry.provider) !== convertibleProvider
|
|
373
|
+
) {
|
|
343
374
|
return false;
|
|
344
375
|
}
|
|
345
376
|
const copyProvider = convertibleProvider;
|
|
@@ -347,11 +378,13 @@ function isKnownUneditedBody(
|
|
|
347
378
|
if (template === undefined) {
|
|
348
379
|
return false;
|
|
349
380
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
381
|
+
// No connection binding: the bare provider means the default entry of
|
|
382
|
+
// that kind, and a stamped binding would re-introduce the collapsed
|
|
383
|
+
// field on disk.
|
|
384
|
+
const materialized = materializeProfile(template, copyProvider) as Record<
|
|
385
|
+
string,
|
|
386
|
+
unknown
|
|
387
|
+
>;
|
|
355
388
|
if (
|
|
356
389
|
entry.model !== materialized.model &&
|
|
357
390
|
!(HISTORICAL_INTENT_MODELS[key][copyProvider] ?? []).includes(entry.model)
|
|
@@ -361,11 +394,20 @@ function isKnownUneditedBody(
|
|
|
361
394
|
// Completion skips managed-source bodies, so a copy from the era whose
|
|
362
395
|
// templates wrote `source: "managed"` (#29755 to #29768, 2026-05-05) is
|
|
363
396
|
// compared as if user-source to keep both sides normalized identically.
|
|
397
|
+
const normalizedEntry: Record<string, unknown> = {
|
|
398
|
+
...entry,
|
|
399
|
+
provider: copyProvider,
|
|
400
|
+
...(entry.source === "managed" ? { source: "user" } : {}),
|
|
401
|
+
};
|
|
402
|
+
// The conventional personal binding is machinery-written (hatch stamps,
|
|
403
|
+
// completion re-stamps); normalize it away. Any other binding is user
|
|
404
|
+
// state and stays in the comparison, where the unbound template side
|
|
405
|
+
// makes it block conversion.
|
|
406
|
+
if (normalizedEntry.provider_connection === `${copyProvider}-personal`) {
|
|
407
|
+
delete normalizedEntry.provider_connection;
|
|
408
|
+
}
|
|
364
409
|
const body = comparableBody(
|
|
365
|
-
withCompletionBaked(
|
|
366
|
-
entry.source === "managed" ? { ...entry, source: "user" } : entry,
|
|
367
|
-
completionBase,
|
|
368
|
-
),
|
|
410
|
+
withCompletionBaked(normalizedEntry, completionBase),
|
|
369
411
|
);
|
|
370
412
|
const known = comparableBody(
|
|
371
413
|
withCompletionBaked(materialized, completionBase),
|
|
@@ -392,9 +434,10 @@ function uniformCopyProvider(profiles: Record<string, unknown>): string | null {
|
|
|
392
434
|
if (typeof entry.provider !== "string") {
|
|
393
435
|
return null;
|
|
394
436
|
}
|
|
437
|
+
const entryProvider = foldPersonalEntryName(entry.provider);
|
|
395
438
|
if (provider === null) {
|
|
396
|
-
provider =
|
|
397
|
-
} else if (provider !==
|
|
439
|
+
provider = entryProvider;
|
|
440
|
+
} else if (provider !== entryProvider) {
|
|
398
441
|
return null;
|
|
399
442
|
}
|
|
400
443
|
}
|
|
@@ -3,7 +3,10 @@ import { join } from "node:path";
|
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
|
|
5
5
|
import { invalidateConfigCache } from "../config/loader.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
completeCustomProfile,
|
|
8
|
+
mergePreservingUnknownKeys,
|
|
9
|
+
} from "../config/profile-materialization.js";
|
|
7
10
|
import { LLMConfigBase, ProfileEntry } from "../config/schemas/llm.js";
|
|
8
11
|
import { getLogger } from "../util/logger.js";
|
|
9
12
|
|
|
@@ -113,29 +116,6 @@ export function completedProfileBody(
|
|
|
113
116
|
);
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
/**
|
|
117
|
-
* `{...raw, ...completed}` recursively: completed (schema-known) values win,
|
|
118
|
-
* raw keys the schema stripped survive at every depth. (Duplicated in the
|
|
119
|
-
* write-path normalization in conversation-query-routes.ts — the two land in
|
|
120
|
-
* independent PRs; consolidate when either next changes.)
|
|
121
|
-
*/
|
|
122
|
-
function mergePreservingUnknownKeys(
|
|
123
|
-
raw: Record<string, unknown>,
|
|
124
|
-
completed: Record<string, unknown>,
|
|
125
|
-
): Record<string, unknown> {
|
|
126
|
-
const out: Record<string, unknown> = { ...raw, ...completed };
|
|
127
|
-
for (const [key, value] of Object.entries(completed)) {
|
|
128
|
-
const rawValue = raw[key];
|
|
129
|
-
if (readObject(value) !== null && readObject(rawValue) !== null) {
|
|
130
|
-
out[key] = mergePreservingUnknownKeys(
|
|
131
|
-
rawValue as Record<string, unknown>,
|
|
132
|
-
value as Record<string, unknown>,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return out;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
119
|
function readObject(value: unknown): Record<string, unknown> | null {
|
|
140
120
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
141
121
|
return null;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const OLD_CALLSITE = "voiceFrontDecision";
|
|
7
|
+
const PROGRESS_CALLSITE = "voiceProgressNarration";
|
|
8
|
+
const RETIRED_ACK_KEYS = [
|
|
9
|
+
"ackFirstDeltaTimeoutMs",
|
|
10
|
+
"ackGenerationTimeoutMs",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export const consolidateVoiceFrontDoorMigration: WorkspaceMigration = {
|
|
14
|
+
id: "142-consolidate-voice-front-door",
|
|
15
|
+
description:
|
|
16
|
+
"Rename the progress narration callsite and remove retired generated-ack tuning",
|
|
17
|
+
run(workspaceDir: string): void {
|
|
18
|
+
const configPath = join(workspaceDir, "config.json");
|
|
19
|
+
if (!existsSync(configPath)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let config: Record<string, unknown>;
|
|
24
|
+
try {
|
|
25
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
26
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
config = raw as Record<string, unknown>;
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let changed = false;
|
|
35
|
+
const llm = readObject(config.llm);
|
|
36
|
+
const callSites = readObject(llm?.callSites);
|
|
37
|
+
if (callSites && Object.hasOwn(callSites, OLD_CALLSITE)) {
|
|
38
|
+
if (!Object.hasOwn(callSites, PROGRESS_CALLSITE)) {
|
|
39
|
+
callSites[PROGRESS_CALLSITE] = callSites[OLD_CALLSITE];
|
|
40
|
+
}
|
|
41
|
+
delete callSites[OLD_CALLSITE];
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const liveVoice = readObject(config.liveVoice);
|
|
46
|
+
const frontModel = readObject(liveVoice?.frontModel);
|
|
47
|
+
if (frontModel) {
|
|
48
|
+
for (const key of RETIRED_ACK_KEYS) {
|
|
49
|
+
if (Object.hasOwn(frontModel, key)) {
|
|
50
|
+
delete frontModel[key];
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (changed) {
|
|
57
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
down(_workspaceDir: string): void {
|
|
61
|
+
// Forward-only.
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function readObject(value: unknown): Record<string, unknown> | null {
|
|
66
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return value as Record<string, unknown>;
|
|
70
|
+
}
|