@vellumai/assistant 0.11.3 → 0.11.4-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +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 +139 -37
- 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 +17 -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 +177 -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__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- 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/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -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/api/surfaces.ts +7 -3
- 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 +19 -3
- 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/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- 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 +83 -12
- 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 +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- 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/plugin-api/vision-support.test.ts +1 -1
- 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-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -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/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- 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/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
import { getLogger } from "../../util/logger.js";
|
|
6
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const log = getLogger("migration-144");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert stranded subscription-only `provider: "openai"` fragments to the
|
|
12
|
+
* `chatgpt` routing identity.
|
|
13
|
+
*
|
|
14
|
+
* The ChatGPT-subscription connection row carries `provider: "chatgpt"`
|
|
15
|
+
* (DB migration 366 stamps existing rows). An UNPINNED profile or call-site
|
|
16
|
+
* fragment with `provider: "openai"` resolves by listing active openai
|
|
17
|
+
* connections at dispatch; in a workspace whose only OpenAI access is the
|
|
18
|
+
* subscription, that listing is now empty and every request on the fragment
|
|
19
|
+
* fails with missing_connection. Workspace migration 133 converted fragments
|
|
20
|
+
* that PINNED the subscription row; this converts the unpinned cohort.
|
|
21
|
+
*
|
|
22
|
+
* Conversion requires proof from the workspace DB that the fragment is
|
|
23
|
+
* genuinely stranded: the canonical subscription row exists (matched by
|
|
24
|
+
* name + oauth_subscription auth, valid for both the pre- and post-366 row
|
|
25
|
+
* shapes) and no `provider: "openai"` row exists (an API-key row means
|
|
26
|
+
* openai fragments still resolve). An absent DB or table converts nothing;
|
|
27
|
+
* an open or query failure on an existing DB propagates so the runner
|
|
28
|
+
* records a failed checkpoint and retries on a later boot.
|
|
29
|
+
*
|
|
30
|
+
* A converted fragment gets `provider: "chatgpt"`, and a model outside the
|
|
31
|
+
* Codex subscription set is replaced with `gpt-5.6-terra` (the "chatgpt"
|
|
32
|
+
* identity requires a Codex-servable model at schema validation; on the
|
|
33
|
+
* subscription every model bills the same). An unpinned
|
|
34
|
+
* `llm.defaultProvider` of "openai" converts the same way: it anchors the
|
|
35
|
+
* code-owned default profiles and background call sites, which are equally
|
|
36
|
+
* stranded. Fragments with a `provider_connection` pin, a pinned
|
|
37
|
+
* defaultProvider connectionName, any other provider, or a managed source
|
|
38
|
+
* are left untouched. `llm.default` is not swept: migration 133 drops that
|
|
39
|
+
* legacy blob and runs earlier in the chain.
|
|
40
|
+
*/
|
|
41
|
+
export const convertStrandedSubscriptionOpenaiProfilesMigration: WorkspaceMigration =
|
|
42
|
+
{
|
|
43
|
+
id: "144-convert-stranded-subscription-openai-profiles",
|
|
44
|
+
description:
|
|
45
|
+
'Convert unpinned provider "openai" LLM fragments to the "chatgpt" routing identity in subscription-only workspaces',
|
|
46
|
+
run(workspaceDir: string): void {
|
|
47
|
+
const configPath = join(workspaceDir, "config.json");
|
|
48
|
+
if (!existsSync(configPath)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!isSubscriptionOnlyWorkspace(workspaceDir)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Read outside the parse catch: a transient filesystem error (EIO,
|
|
56
|
+
// EACCES) must reach the runner so the migration retries, while
|
|
57
|
+
// malformed JSON is a permanent state this migration cannot repair.
|
|
58
|
+
const rawText = readFileSync(configPath, "utf-8");
|
|
59
|
+
|
|
60
|
+
let config: Record<string, unknown>;
|
|
61
|
+
try {
|
|
62
|
+
const raw = JSON.parse(rawText);
|
|
63
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
config = raw as Record<string, unknown>;
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const llm = readObject(config.llm);
|
|
72
|
+
if (llm === null) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let changed = false;
|
|
77
|
+
|
|
78
|
+
const profiles = readObject(llm.profiles);
|
|
79
|
+
if (profiles !== null) {
|
|
80
|
+
for (const [name, rawProfile] of Object.entries(profiles)) {
|
|
81
|
+
if (convertFragment(readObject(rawProfile))) {
|
|
82
|
+
changed = true;
|
|
83
|
+
log.info({ profile: name }, "Converted stranded openai profile");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const callSites = readObject(llm.callSites);
|
|
89
|
+
if (callSites !== null) {
|
|
90
|
+
for (const [id, rawConfig] of Object.entries(callSites)) {
|
|
91
|
+
if (convertFragment(readObject(rawConfig))) {
|
|
92
|
+
changed = true;
|
|
93
|
+
log.info(
|
|
94
|
+
{ callSite: id },
|
|
95
|
+
"Converted stranded openai call-site fragment",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const defaultProvider = readObject(llm.defaultProvider);
|
|
102
|
+
if (
|
|
103
|
+
defaultProvider !== null &&
|
|
104
|
+
defaultProvider.provider === "openai" &&
|
|
105
|
+
!isConnectionPin(defaultProvider.connectionName)
|
|
106
|
+
) {
|
|
107
|
+
defaultProvider.provider = "chatgpt";
|
|
108
|
+
// An invalid connectionName leaf (null, empty) would fail
|
|
109
|
+
// DefaultProviderSchema at load, whose catch drops the whole value.
|
|
110
|
+
if ("connectionName" in defaultProvider) {
|
|
111
|
+
delete defaultProvider.connectionName;
|
|
112
|
+
}
|
|
113
|
+
changed = true;
|
|
114
|
+
log.info("Converted stranded openai default provider");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!changed) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Write-then-rename so an interrupted write cannot leave config.json
|
|
122
|
+
// truncated: a torn in-place write would parse as invalid JSON on the
|
|
123
|
+
// retry, which the catch above treats as "nothing to do", letting the
|
|
124
|
+
// runner checkpoint the migration as completed against a corrupt file.
|
|
125
|
+
const tmpPath = `${configPath}.migration-144.tmp`;
|
|
126
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
|
|
127
|
+
renameSync(tmpPath, configPath);
|
|
128
|
+
},
|
|
129
|
+
// The exact-match rewrite is idempotent, so a transient failure (full
|
|
130
|
+
// disk, I/O error) is safe to retry on later startups.
|
|
131
|
+
retryFailedCheckpoint: true,
|
|
132
|
+
down(_workspaceDir: string): void {
|
|
133
|
+
// Forward-only: converting back would restore fragments that cannot
|
|
134
|
+
// resolve a connection.
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Helpers: self-contained per workspace migrations AGENTS.md
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
const SUBSCRIPTION_CONNECTION_NAME = "chatgpt-subscription";
|
|
143
|
+
|
|
144
|
+
const LEGACY_MANAGED_OPENAI_NAME = "openai-managed";
|
|
145
|
+
|
|
146
|
+
const CODEX_SUBSCRIPTION_MODEL_IDS = new Set([
|
|
147
|
+
"gpt-5.6-sol",
|
|
148
|
+
"gpt-5.6-terra",
|
|
149
|
+
"gpt-5.6-luna",
|
|
150
|
+
"gpt-5.5",
|
|
151
|
+
"gpt-5.4",
|
|
152
|
+
"gpt-5.4-mini",
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
const FALLBACK_CODEX_MODEL = "gpt-5.6-terra";
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* True only when the workspace DB proves the stranded condition: the
|
|
159
|
+
* canonical subscription row is present and no openai row exists. Any
|
|
160
|
+
* failure to read counts as unproven.
|
|
161
|
+
*/
|
|
162
|
+
function isSubscriptionOnlyWorkspace(workspaceDir: string): boolean {
|
|
163
|
+
const dbPath = join(workspaceDir, "data", "db", "assistant.db");
|
|
164
|
+
if (!existsSync(dbPath)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// An open or query failure on an existing DB (locked, I/O error, corrupt
|
|
169
|
+
// header) is not proof of anything: swallowing it would checkpoint the
|
|
170
|
+
// migration as completed against unread state. Let it propagate so
|
|
171
|
+
// retryFailedCheckpoint reattempts on a later boot.
|
|
172
|
+
const db = new Database(dbPath, { readonly: true });
|
|
173
|
+
try {
|
|
174
|
+
const tableExists = db
|
|
175
|
+
.query(
|
|
176
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'provider_connections'`,
|
|
177
|
+
)
|
|
178
|
+
.get();
|
|
179
|
+
if (!tableExists) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const subscription = db
|
|
184
|
+
.query(`SELECT auth FROM provider_connections WHERE name = ?`)
|
|
185
|
+
.get(SUBSCRIPTION_CONNECTION_NAME) as { auth: string } | null;
|
|
186
|
+
if (!subscription) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const parsed: unknown = JSON.parse(subscription.auth);
|
|
191
|
+
const authType =
|
|
192
|
+
parsed !== null && typeof parsed === "object"
|
|
193
|
+
? (parsed as { type?: unknown }).type
|
|
194
|
+
: undefined;
|
|
195
|
+
if (authType !== "oauth_subscription") {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Two openai-provider rows do not count as usable openai connections.
|
|
203
|
+
// The canonical row itself stores provider "openai" until DB migration
|
|
204
|
+
// 366 flips it, and this migration checkpoints once; counting it would
|
|
205
|
+
// skip the conversion forever on exactly the upgrade boot that needs it
|
|
206
|
+
// (its subscription auth is already verified above). The legacy
|
|
207
|
+
// "openai-managed" row is a pre-consolidation platform leftover that the
|
|
208
|
+
// connection listings hide (LEGACY_MANAGED_CONNECTION_NAMES in
|
|
209
|
+
// providers/inference/connections.ts); treating it as a live connection
|
|
210
|
+
// would block the repair on installs that still carry it.
|
|
211
|
+
const openaiRow = db
|
|
212
|
+
.query(
|
|
213
|
+
`SELECT 1 FROM provider_connections WHERE provider = 'openai' AND name NOT IN (?, ?)`,
|
|
214
|
+
)
|
|
215
|
+
.get(SUBSCRIPTION_CONNECTION_NAME, LEGACY_MANAGED_OPENAI_NAME);
|
|
216
|
+
return openaiRow === null;
|
|
217
|
+
} finally {
|
|
218
|
+
db.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Only a non-empty string names a pinned connection. The config loader
|
|
224
|
+
* strips `null`, empty, or non-string values as invalid leaves, so runtime
|
|
225
|
+
* treats fragments carrying them as unpinned; the conversion must judge
|
|
226
|
+
* them the same way or those fragments stay stranded.
|
|
227
|
+
*/
|
|
228
|
+
function isConnectionPin(value: unknown): boolean {
|
|
229
|
+
return typeof value === "string" && value.length > 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function convertFragment(fragment: Record<string, unknown> | null): boolean {
|
|
233
|
+
if (fragment === null) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
fragment.provider !== "openai" ||
|
|
238
|
+
isConnectionPin(fragment.provider_connection)
|
|
239
|
+
) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (fragment.source === "managed") {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
fragment.provider = "chatgpt";
|
|
246
|
+
// An invalid pin leaf (null, empty) rode along as unpinned; the chatgpt
|
|
247
|
+
// identity carries no connection reference, so drop it outright.
|
|
248
|
+
if ("provider_connection" in fragment) {
|
|
249
|
+
delete fragment.provider_connection;
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
typeof fragment.model !== "string" ||
|
|
253
|
+
!CODEX_SUBSCRIPTION_MODEL_IDS.has(fragment.model)
|
|
254
|
+
) {
|
|
255
|
+
fragment.model = FALLBACK_CODEX_MODEL;
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function readObject(value: unknown): Record<string, unknown> | null {
|
|
261
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return value as Record<string, unknown>;
|
|
265
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
import { getLogger } from "../../util/logger.js";
|
|
6
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const log = getLogger("migrations/145-collapse-profile-bindings-to-entries");
|
|
9
|
+
|
|
10
|
+
// Fold each profile's `provider_connection` into its `provider` as a
|
|
11
|
+
// connection entry name. Under the entries model the provider field holds
|
|
12
|
+
// the entry name and the row's own provider (its kind) drives dispatch, so
|
|
13
|
+
// an explicit binding needs no second field.
|
|
14
|
+
//
|
|
15
|
+
// Rules per `llm.profiles.<key>`:
|
|
16
|
+
// - Routing-identity providers ("vellum"/"chatgpt") carry no binding by
|
|
17
|
+
// definition: a stray `provider_connection` is deleted verbatim.
|
|
18
|
+
// - A binding whose row EXISTS and whose kind AGREES with the profile's
|
|
19
|
+
// declared provider becomes the provider value (the entry name), and the
|
|
20
|
+
// binding field is deleted. Kind agreement, with the identity kinds
|
|
21
|
+
// mapped the way dispatch maps them: a row of the same provider, a
|
|
22
|
+
// "chatgpt" row for a declared "openai", or a "vellum" row for a declared
|
|
23
|
+
// managed-routable provider.
|
|
24
|
+
// - A dangling binding (row deleted) or a kind-disagreeing binding keeps
|
|
25
|
+
// the declared provider and deletes only the binding, with a structured
|
|
26
|
+
// warn: dispatch's auto-resolution then behaves the way it always has
|
|
27
|
+
// for an unbound vendor profile, and a rewrite would either strand the
|
|
28
|
+
// profile on a nonexistent entry or silently change vendors.
|
|
29
|
+
// - A binding equal to the declared provider (conventional shapes older
|
|
30
|
+
// migrations kept) is a plain delete: the bare vendor value already
|
|
31
|
+
// means the default entry of that kind.
|
|
32
|
+
//
|
|
33
|
+
// Call-site fragments and `llm.defaultProvider.connectionName` are NOT
|
|
34
|
+
// touched: call-site overrides become model-only together with the
|
|
35
|
+
// resolver's tweak blocks (one coherent change, later), and the default
|
|
36
|
+
// provider keeps its closed vendor enum, so its binding needs its own
|
|
37
|
+
// design before it can collapse.
|
|
38
|
+
//
|
|
39
|
+
// The identity mapping and managed-routable set are frozen snapshots
|
|
40
|
+
// (migrations are self-contained): openai/anthropic/gemini/fireworks/
|
|
41
|
+
// together as of 2026-08-10. A provider outside the snapshot takes the
|
|
42
|
+
// conservative keep-provider path.
|
|
43
|
+
|
|
44
|
+
const ROUTING_IDENTITIES = new Set(["vellum", "chatgpt"]);
|
|
45
|
+
const MANAGED_ROUTABLE = new Set([
|
|
46
|
+
"openai",
|
|
47
|
+
"anthropic",
|
|
48
|
+
"gemini",
|
|
49
|
+
"fireworks",
|
|
50
|
+
"together",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// A binding literally named "vellum" or "chatgpt" folds into the ROUTING
|
|
54
|
+
// IDENTITY value, whose model the read-path schema validates (an unroutable
|
|
55
|
+
// pair strips the whole profile on read). Folding is therefore gated on the
|
|
56
|
+
// model being servable by that identity, judged against frozen snapshots of
|
|
57
|
+
// the routing tables as of 2026-08-10; a model outside the snapshot leaves
|
|
58
|
+
// the profile untouched (dispatch keeps honoring the legacy binding).
|
|
59
|
+
const VELLUM_ROUTABLE_MODELS = new Set([
|
|
60
|
+
"claude-fable-5",
|
|
61
|
+
"claude-opus-5",
|
|
62
|
+
"claude-opus-4-8",
|
|
63
|
+
"claude-opus-4-7",
|
|
64
|
+
"claude-opus-4-6",
|
|
65
|
+
"claude-sonnet-5",
|
|
66
|
+
"claude-sonnet-4-6",
|
|
67
|
+
"claude-sonnet-4-5-20250929",
|
|
68
|
+
"claude-opus-4-5-20251101",
|
|
69
|
+
"claude-haiku-4-5-20251001",
|
|
70
|
+
"gpt-5.6-sol",
|
|
71
|
+
"gpt-5.6-terra",
|
|
72
|
+
"gpt-5.6-luna",
|
|
73
|
+
"gpt-5.5",
|
|
74
|
+
"gpt-5.5-pro",
|
|
75
|
+
"gpt-5.4",
|
|
76
|
+
"gpt-5.2",
|
|
77
|
+
"gpt-5.4-mini",
|
|
78
|
+
"gpt-5.4-nano",
|
|
79
|
+
"gemini-3.6-flash",
|
|
80
|
+
"gemini-3.5-flash",
|
|
81
|
+
"gemini-3.5-flash-lite",
|
|
82
|
+
"gemini-3.1-pro-preview",
|
|
83
|
+
"gemini-3.1-pro-preview-customtools",
|
|
84
|
+
"gemini-3-flash-preview",
|
|
85
|
+
"gemini-3.1-flash-lite-preview",
|
|
86
|
+
"gemini-3.1-flash-lite",
|
|
87
|
+
"gemini-2.5-flash",
|
|
88
|
+
"gemini-2.5-flash-lite",
|
|
89
|
+
"gemini-2.5-pro",
|
|
90
|
+
"accounts/fireworks/models/kimi-k3",
|
|
91
|
+
"accounts/fireworks/models/kimi-k2p6",
|
|
92
|
+
"accounts/fireworks/models/glm-5p2",
|
|
93
|
+
"accounts/fireworks/models/minimax-m3",
|
|
94
|
+
"accounts/fireworks/models/minimax-m2p7",
|
|
95
|
+
"accounts/fireworks/models/deepseek-v4-pro",
|
|
96
|
+
"accounts/fireworks/models/deepseek-v4-flash",
|
|
97
|
+
"MiniMaxAI/MiniMax-M3",
|
|
98
|
+
]);
|
|
99
|
+
const CODEX_MODELS = new Set([
|
|
100
|
+
"gpt-5.6-sol",
|
|
101
|
+
"gpt-5.6-terra",
|
|
102
|
+
"gpt-5.6-luna",
|
|
103
|
+
"gpt-5.5",
|
|
104
|
+
"gpt-5.4",
|
|
105
|
+
"gpt-5.4-mini",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
function identitySafeModel(identity: string, model: unknown): boolean {
|
|
109
|
+
if (typeof model !== "string") {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return identity === "vellum"
|
|
113
|
+
? VELLUM_ROUTABLE_MODELS.has(model)
|
|
114
|
+
: CODEX_MODELS.has(model);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const collapseProfileBindingsToEntriesMigration: WorkspaceMigration = {
|
|
118
|
+
id: "145-collapse-profile-bindings-to-entries",
|
|
119
|
+
description:
|
|
120
|
+
"Fold llm.profiles.*.provider_connection into provider as connection entry names",
|
|
121
|
+
retryFailedCheckpoint: true,
|
|
122
|
+
run(workspaceDir: string): void {
|
|
123
|
+
const configPath = join(workspaceDir, "config.json");
|
|
124
|
+
if (!existsSync(configPath)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let config: Record<string, unknown>;
|
|
129
|
+
try {
|
|
130
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
131
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
config = raw as Record<string, unknown>;
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const llm = asObject(config.llm);
|
|
140
|
+
const profiles = llm === null ? null : asObject(llm.profiles);
|
|
141
|
+
if (profiles === null) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const bindings = Object.values(profiles).some((value) => {
|
|
146
|
+
const entry = asObject(value);
|
|
147
|
+
return (
|
|
148
|
+
entry !== null &&
|
|
149
|
+
typeof entry.provider_connection === "string" &&
|
|
150
|
+
entry.provider_connection.length > 0
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
if (!bindings) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Bindings exist, so the rewrite needs the rows. An ABSENT DB file is a
|
|
158
|
+
// real state (a config restored into a fresh workspace has no rows, so
|
|
159
|
+
// every binding is genuinely dangling); an unreadable or unqueryable DB
|
|
160
|
+
// must instead fail the run (retried next boot) rather than checkpoint
|
|
161
|
+
// a pass that would take the destructive dangling path for every
|
|
162
|
+
// binding.
|
|
163
|
+
const rows = readConnectionRows(workspaceDir);
|
|
164
|
+
if (rows === null) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
"provider_connections is not readable; retrying the binding collapse on the next run",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let changed = false;
|
|
171
|
+
for (const [key, value] of Object.entries(profiles)) {
|
|
172
|
+
const entry = asObject(value);
|
|
173
|
+
if (entry === null) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const binding = entry.provider_connection;
|
|
177
|
+
if (typeof binding !== "string" || binding.length === 0) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const provider =
|
|
181
|
+
typeof entry.provider === "string" ? entry.provider : undefined;
|
|
182
|
+
|
|
183
|
+
if (provider !== undefined && ROUTING_IDENTITIES.has(provider)) {
|
|
184
|
+
delete entry.provider_connection;
|
|
185
|
+
changed = true;
|
|
186
|
+
log.info(
|
|
187
|
+
{ profile: key, provider, binding },
|
|
188
|
+
"Deleted stray binding on a routing-identity profile",
|
|
189
|
+
);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (binding === provider) {
|
|
194
|
+
// A row named exactly like its vendor was an explicit pin under the
|
|
195
|
+
// old create route; with several rows of that kind, the bare vendor
|
|
196
|
+
// would auto-resolve differently, so the pin stays until the entry
|
|
197
|
+
// picker can express it. Without such a row the field is a plain
|
|
198
|
+
// conventional shape and the bare vendor already means the default.
|
|
199
|
+
if (rows.has(binding)) {
|
|
200
|
+
log.info(
|
|
201
|
+
{ profile: key, provider },
|
|
202
|
+
"Kept self-named binding (an explicit pin among possible siblings)",
|
|
203
|
+
);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
delete entry.provider_connection;
|
|
207
|
+
changed = true;
|
|
208
|
+
log.info(
|
|
209
|
+
{ profile: key, provider },
|
|
210
|
+
"Deleted self-referential binding (bare vendor means the default entry)",
|
|
211
|
+
);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (ROUTING_IDENTITIES.has(binding)) {
|
|
216
|
+
// Folding writes the identity VALUE itself; gate on the model the
|
|
217
|
+
// identity can serve, or the read-path schema would strip the
|
|
218
|
+
// profile. An unservable model leaves the profile untouched.
|
|
219
|
+
if (identitySafeModel(binding, entry.model)) {
|
|
220
|
+
entry.provider = binding;
|
|
221
|
+
delete entry.provider_connection;
|
|
222
|
+
changed = true;
|
|
223
|
+
log.info(
|
|
224
|
+
{ profile: key, previousProvider: provider, identity: binding },
|
|
225
|
+
"Folded canonical binding into the routing identity",
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
log.warn(
|
|
229
|
+
{ profile: key, provider, binding, reason: "model_not_servable" },
|
|
230
|
+
"Left canonical binding in place; the identity cannot serve this model",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const rowKind = rows.get(binding);
|
|
237
|
+
if (rowKind === undefined) {
|
|
238
|
+
delete entry.provider_connection;
|
|
239
|
+
changed = true;
|
|
240
|
+
log.warn(
|
|
241
|
+
{ profile: key, provider, binding, reason: "row_missing" },
|
|
242
|
+
"Deleted dangling binding; profile keeps its declared provider",
|
|
243
|
+
);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!kindAgrees(rowKind, provider)) {
|
|
248
|
+
delete entry.provider_connection;
|
|
249
|
+
changed = true;
|
|
250
|
+
log.warn(
|
|
251
|
+
{ profile: key, provider, binding, rowKind, reason: "kind_mismatch" },
|
|
252
|
+
"Deleted kind-disagreeing binding; profile keeps its declared provider",
|
|
253
|
+
);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
entry.provider = binding;
|
|
258
|
+
delete entry.provider_connection;
|
|
259
|
+
changed = true;
|
|
260
|
+
log.info(
|
|
261
|
+
{ profile: key, previousProvider: provider, entry: binding },
|
|
262
|
+
"Folded profile binding into provider as an entry name",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (changed) {
|
|
267
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
down(_workspaceDir: string): void {
|
|
271
|
+
// Forward-only.
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
function asObject(value: unknown): Record<string, unknown> | null {
|
|
276
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
277
|
+
? (value as Record<string, unknown>)
|
|
278
|
+
: null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Whether a row of `rowKind` can serve a profile declaring `provider`,
|
|
283
|
+
* mirroring dispatch's identity mapping. An undeclared provider agrees with
|
|
284
|
+
* any row: the entry alone defines the route.
|
|
285
|
+
*/
|
|
286
|
+
function kindAgrees(rowKind: string, provider: string | undefined): boolean {
|
|
287
|
+
if (provider === undefined) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if (rowKind === provider) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (rowKind === "chatgpt") {
|
|
294
|
+
return provider === "openai";
|
|
295
|
+
}
|
|
296
|
+
if (rowKind === "vellum") {
|
|
297
|
+
return MANAGED_ROUTABLE.has(provider);
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Connection name -> provider kind, or null when the DB or table is not
|
|
304
|
+
* readable. The caller fails the run on null: bindings must be judged
|
|
305
|
+
* against real rows, never guessed.
|
|
306
|
+
*/
|
|
307
|
+
function readConnectionRows(workspaceDir: string): Map<string, string> | null {
|
|
308
|
+
const dbPath = join(workspaceDir, "data", "db", "assistant.db");
|
|
309
|
+
if (!existsSync(dbPath)) {
|
|
310
|
+
return new Map();
|
|
311
|
+
}
|
|
312
|
+
let db: Database;
|
|
313
|
+
try {
|
|
314
|
+
db = new Database(dbPath);
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const rows = db
|
|
320
|
+
.query(`SELECT name, provider FROM provider_connections`)
|
|
321
|
+
.all() as Array<{ name: string; provider: string }>;
|
|
322
|
+
return new Map(rows.map((r) => [r.name, r.provider]));
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
} finally {
|
|
326
|
+
db.close();
|
|
327
|
+
}
|
|
328
|
+
}
|