@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.
Files changed (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. 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
+ }