@vellumai/assistant 0.11.3 → 0.11.4-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) 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 +140 -38
  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 +2 -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 +55 -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__/user-plugin-loader.test.ts +1 -1
  67. package/src/__tests__/visible-app-context.test.ts +16 -9
  68. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  69. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  70. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  71. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  72. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  73. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  74. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  75. package/src/acp/__tests__/auth-required.test.ts +161 -0
  76. package/src/acp/acp-claude-oauth.ts +19 -2
  77. package/src/acp/agent-process.test.ts +100 -0
  78. package/src/acp/agent-process.ts +29 -26
  79. package/src/acp/auth-required.ts +102 -0
  80. package/src/acp/session-manager.test.ts +119 -0
  81. package/src/acp/session-manager.ts +68 -2
  82. package/src/api/events/acp-auth-required.ts +55 -0
  83. package/src/api/index.ts +7 -0
  84. package/src/apps/app-store.ts +3 -0
  85. package/src/bundler/package-resolver.ts +2 -30
  86. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  87. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  88. package/src/calls/call-controller.ts +9 -2
  89. package/src/calls/call-setup-flow.ts +0 -1
  90. package/src/calls/media-stream-stt-session.ts +15 -0
  91. package/src/calls/voice-session-bridge.ts +71 -16
  92. package/src/calls/voice-triage-escalate.ts +104 -2
  93. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  94. package/src/channels/config.ts +13 -0
  95. package/src/channels/plugin-channel-declarations.ts +108 -0
  96. package/src/channels/types.ts +30 -0
  97. package/src/cli/AGENTS.md +5 -2
  98. package/src/cli/commands/credentials.help.ts +2 -2
  99. package/src/cli/commands/inference-providers.ts +1 -1
  100. package/src/cli/commands/mcp.help.ts +13 -4
  101. package/src/cli/commands/mcp.ts +9 -0
  102. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  103. package/src/cli/commands/memory/index.help.ts +43 -1
  104. package/src/cli/commands/memory/memory-v3.ts +64 -0
  105. package/src/cli/commands/stt.help.ts +27 -2
  106. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  107. package/src/cli/lib/bundled-marketplace.json +13 -0
  108. package/src/cli/lib/upgrade-plugin.ts +42 -0
  109. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  110. package/src/config/__tests__/default-provider.test.ts +6 -1
  111. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  112. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  113. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  114. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  115. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  116. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  117. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  118. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  119. package/src/config/call-site-defaults.ts +4 -5
  120. package/src/config/default-profile-catalog.ts +82 -11
  121. package/src/config/default-profile-names.ts +4 -1
  122. package/src/config/default-provider-resolution.ts +4 -0
  123. package/src/config/llm-context-resolution.ts +11 -3
  124. package/src/config/llm-resolver.ts +28 -1
  125. package/src/config/profile-materialization.ts +70 -22
  126. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  127. package/src/config/schemas/call-site-catalog.ts +4 -4
  128. package/src/config/schemas/live-voice.ts +57 -23
  129. package/src/config/schemas/llm.ts +59 -32
  130. package/src/config/schemas/mcp.ts +23 -0
  131. package/src/config/schemas/plugin-updates.ts +6 -2
  132. package/src/config/schemas/stt.ts +1 -0
  133. package/src/context/outbound-sanitize.ts +96 -1
  134. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  135. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  136. package/src/daemon/conversation-agent-loop.ts +17 -6
  137. package/src/daemon/conversation-messaging.ts +5 -1
  138. package/src/daemon/conversation-notifiers.ts +9 -1
  139. package/src/daemon/conversation-process.ts +9 -6
  140. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  141. package/src/daemon/conversation-tool-setup.ts +1 -2
  142. package/src/daemon/conversation.ts +48 -0
  143. package/src/daemon/mcp-reload-service.ts +36 -6
  144. package/src/daemon/process-message.ts +13 -3
  145. package/src/daemon/providers-setup.ts +6 -3
  146. package/src/daemon/trust-context-types.ts +29 -0
  147. package/src/daemon/wake-conversation-ops.ts +3 -2
  148. package/src/documents/document-store.ts +138 -5
  149. package/src/hooks/hook-loader.ts +3 -3
  150. package/src/hooks/registry.ts +50 -6
  151. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  152. package/src/inbound/oauth-callback-url.ts +61 -0
  153. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  154. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  155. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  156. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  157. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  158. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  159. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  160. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  161. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  162. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  163. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  164. package/src/live-voice/live-voice-archive.ts +2 -0
  165. package/src/live-voice/live-voice-metrics.ts +57 -32
  166. package/src/live-voice/live-voice-photo.ts +1 -2
  167. package/src/live-voice/live-voice-session.ts +535 -314
  168. package/src/live-voice/progress-narration.ts +277 -0
  169. package/src/live-voice/protocol.ts +21 -1
  170. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  171. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  172. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  173. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  174. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  175. package/src/mcp/client.ts +16 -11
  176. package/src/mcp/effective-config.ts +113 -0
  177. package/src/mcp/manager.ts +11 -6
  178. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  179. package/src/mcp/mcp-oauth-provider.ts +205 -240
  180. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  181. package/src/monitoring/plugin-auto-update.ts +128 -24
  182. package/src/notifications/signal.ts +1 -0
  183. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  184. package/src/permissions/confirmation-guardian-request.ts +2 -2
  185. package/src/permissions/question-guardian-request.test.ts +14 -6
  186. package/src/permissions/question-guardian-request.ts +1 -2
  187. package/src/persistence/attachments-store.ts +8 -1
  188. package/src/persistence/bookmark-crud.ts +3 -7
  189. package/src/persistence/conversation-attention-store.ts +16 -45
  190. package/src/persistence/conversation-crud.ts +33 -4
  191. package/src/persistence/conversation-lineage.ts +9 -0
  192. package/src/persistence/conversation-queries.ts +108 -41
  193. package/src/persistence/delivery-crud.ts +38 -29
  194. package/src/persistence/external-conversation-store.ts +32 -4
  195. package/src/persistence/llm-request-log-store.ts +4 -10
  196. package/src/persistence/llm-usage-store.ts +8 -3
  197. package/src/persistence/message-reads.test.ts +197 -0
  198. package/src/persistence/message-reads.ts +211 -0
  199. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  200. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  201. package/src/persistence/real-user-turn-filter.ts +27 -3
  202. package/src/persistence/steps.ts +9 -0
  203. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  204. package/src/plugin-api/conversation-turn.ts +168 -5
  205. package/src/plugin-api/index.ts +21 -5
  206. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  207. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  208. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  209. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  210. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  211. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  212. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  213. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  214. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  215. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  216. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  217. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  218. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  219. package/src/plugins/defaults/memory/indexer.ts +3 -1
  220. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  221. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  222. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  223. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  224. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  225. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  226. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  227. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  228. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  229. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  230. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  231. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  232. package/src/plugins/defaults/memory/worker.ts +6 -3
  233. package/src/plugins/external-plugin-loader.ts +47 -0
  234. package/src/plugins/mcp-servers.ts +361 -0
  235. package/src/plugins/mtime-cache.ts +23 -49
  236. package/src/plugins/worker-plugin-surface.ts +33 -0
  237. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  238. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  239. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  240. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  241. package/src/providers/call-site-routing.ts +30 -3
  242. package/src/providers/connection-resolution.ts +194 -11
  243. package/src/providers/inference/auth.ts +6 -0
  244. package/src/providers/inference/connection-availability.ts +24 -2
  245. package/src/providers/inference/connections.ts +2 -0
  246. package/src/providers/model-intents.ts +26 -6
  247. package/src/providers/openai/codex-models.ts +2 -1
  248. package/src/providers/provider-send-message.ts +32 -3
  249. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  250. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  251. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  252. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  253. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  254. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  255. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  256. package/src/providers/speech-to-text/resolve.ts +25 -2
  257. package/src/routes/worker.ts +17 -5
  258. package/src/runtime/access-request-helper.ts +9 -12
  259. package/src/runtime/agent-wake.ts +3 -3
  260. package/src/runtime/pre-first-message-gate.ts +4 -0
  261. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  262. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  263. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  264. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  265. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  266. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  267. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  268. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  269. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  270. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  271. package/src/runtime/routes/channel-route-shared.ts +0 -6
  272. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  273. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  274. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  275. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  276. package/src/runtime/routes/default-provider-routes.ts +15 -0
  277. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  278. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  279. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  280. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  281. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  282. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  283. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  284. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  285. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  286. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  287. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  288. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  289. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  291. package/src/runtime/routes/log-export-routes.ts +3 -0
  292. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  293. package/src/runtime/routes/plugins-routes.ts +21 -3
  294. package/src/runtime/routes/stt-routes.ts +31 -25
  295. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  296. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  297. package/src/runtime/routes/user-route-import.ts +108 -0
  298. package/src/schedule/worker.ts +6 -0
  299. package/src/security/oauth2.ts +6 -22
  300. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  301. package/src/stt/__tests__/types.test.ts +94 -0
  302. package/src/stt/daemon-batch-transcriber.ts +10 -0
  303. package/src/stt/stt-stream-session.ts +8 -4
  304. package/src/stt/types.ts +103 -0
  305. package/src/subagent/manager.ts +1 -3
  306. package/src/subagent/types.ts +7 -6
  307. package/src/tools/acp/spawn.test.ts +97 -0
  308. package/src/tools/acp/spawn.ts +32 -0
  309. package/src/tools/document/document-tool.ts +12 -3
  310. package/src/tools/registry.ts +2 -1
  311. package/src/tools/workflows/run-workflow.ts +1 -2
  312. package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
  313. package/src/tts/reasoning-tag-filter.ts +110 -0
  314. package/src/workspace/byok-default-profile-ensure.ts +61 -18
  315. package/src/workspace/custom-profile-ensure.ts +4 -24
  316. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  317. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  318. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  319. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  320. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  321. package/src/workspace/migrations/registry.ts +8 -0
  322. package/src/workspace/provider-commit-message-generator.ts +7 -5
  323. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  324. package/src/live-voice/front-decision.ts +0 -476
@@ -147,6 +147,11 @@ const { _resetAdapterInstallCacheForTests } =
147
147
  await import("../../acp/auto-install.js");
148
148
  const { ACP_CLAUDE_OAUTH_MISSING_CODE } =
149
149
  await import("../../acp/prepare-agent-env.js");
150
+ const { ACP_CLAUDE_AUTH_REQUIRED_CODE, AcpAuthRequiredError } =
151
+ await import("../../acp/auth-required.js");
152
+ const { hasAcpConnectCardRaised } =
153
+ await import("../../acp/acp-connect-card-state.js");
154
+ const { AcpAgentProcess } = await import("../../acp/agent-process.js");
150
155
 
151
156
  // ---------------------------------------------------------------------------
152
157
  // Helpers
@@ -547,3 +552,95 @@ describe("executeAcpSpawn — CLAUDE_CODE_OAUTH_TOKEN injection", () => {
547
552
  expect(result.errorCode).toBe(ACP_CLAUDE_OAUTH_MISSING_CODE);
548
553
  });
549
554
  });
555
+
556
+ // ---------------------------------------------------------------------------
557
+ // Pre-spawn Claude auth rejection
558
+ // ---------------------------------------------------------------------------
559
+
560
+ describe("pre-spawn Claude auth rejection", () => {
561
+ test("raises the recovery surface: errorCode, card guidance, registry mark", async () => {
562
+ // The adapter can reject session creation with auth_required (a stored
563
+ // credential the server refuses). The error thrown here is produced by
564
+ // the REAL conversion seam, not hand-built: a wire-shaped JSON-RPC
565
+ // auth_required rejection driven through AcpAgentProcess.createSession's
566
+ // actual retry path, so this test fails if that seam stops yielding the
567
+ // typed error the tool boundary maps to the recovery surface.
568
+ const proc = new AcpAgentProcess(
569
+ "claude",
570
+ { command: "claude-agent-acp", args: [] },
571
+ (() => ({})) as never,
572
+ );
573
+ (proc as unknown as { connection: unknown }).connection = {
574
+ newSession: () =>
575
+ Promise.reject({ code: -32000, message: "Authentication required" }),
576
+ };
577
+ (proc as unknown as { initializeResponse: unknown }).initializeResponse = {
578
+ authMethods: [
579
+ {
580
+ id: "claude-ai-login",
581
+ name: "Claude Subscription",
582
+ type: "terminal",
583
+ },
584
+ ],
585
+ };
586
+ (proc as unknown as { spawnedEnv: unknown }).spawnedEnv = {};
587
+ const realSeamError: unknown = await proc.createSession("/tmp").then(
588
+ () => null,
589
+ (e: unknown) => e,
590
+ );
591
+ expect(realSeamError).toBeInstanceOf(AcpAuthRequiredError);
592
+
593
+ spawnMock.mockImplementationOnce(async () => {
594
+ throw realSeamError;
595
+ });
596
+
597
+ const context = { ...makeContext(), conversationId: "conv-prespawn-auth" };
598
+ const result = await executeAcpSpawn(
599
+ { agent: "claude", task: "do something" },
600
+ context,
601
+ );
602
+
603
+ expect(result.isError).toBe(true);
604
+ expect(result.errorCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
605
+ expect(result.content).toContain("Connect Claude Code");
606
+ expect(hasAcpConnectCardRaised("conv-prespawn-auth")).toBe(true);
607
+ });
608
+
609
+ test("a raw wire auth_required object (unwrapped initialize path) gets the same surface", async () => {
610
+ // initialize() cannot ride withAuthRetry (its response advertises the
611
+ // methods the retry selects from), so its rejection arrives as the plain
612
+ // JSON-RPC object rather than the typed error. The gate must recognize
613
+ // both shapes or this path silently flattens to a generic failure.
614
+ spawnMock.mockImplementationOnce(async () => {
615
+ throw { code: -32000, message: "Authentication required" };
616
+ });
617
+
618
+ const context = { ...makeContext(), conversationId: "conv-prespawn-raw" };
619
+ const result = await executeAcpSpawn(
620
+ { agent: "claude", task: "do something" },
621
+ context,
622
+ );
623
+
624
+ expect(result.isError).toBe(true);
625
+ expect(result.errorCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
626
+ expect(result.content).toContain("Connect Claude Code");
627
+ expect(hasAcpConnectCardRaised("conv-prespawn-raw")).toBe(true);
628
+ });
629
+
630
+ test("a non-auth spawn failure keeps the plain flattened result", async () => {
631
+ spawnMock.mockImplementationOnce(async () => {
632
+ throw new Error("adapter exploded");
633
+ });
634
+
635
+ const context = { ...makeContext(), conversationId: "conv-prespawn-boom" };
636
+ const result = await executeAcpSpawn(
637
+ { agent: "claude", task: "do something" },
638
+ context,
639
+ );
640
+
641
+ expect(result.isError).toBe(true);
642
+ expect(result.errorCode).toBeUndefined();
643
+ expect(result.content).toContain("adapter exploded");
644
+ expect(hasAcpConnectCardRaised("conv-prespawn-boom")).toBe(false);
645
+ });
646
+ });
@@ -3,6 +3,12 @@ import { basename } from "node:path";
3
3
  import { z } from "zod";
4
4
 
5
5
  import { markAcpConnectCardRaised } from "../../acp/acp-connect-card-state.js";
6
+ import {
7
+ ACP_AUTH_RECOVERY_GUIDANCE,
8
+ ACP_CLAUDE_AUTH_REQUIRED_CODE,
9
+ CLAUDE_ACP_COMMAND,
10
+ isAcpAuthRequired,
11
+ } from "../../acp/auth-required.js";
6
12
  import { resolveAgentWithAutoInstall } from "../../acp/auto-install.js";
7
13
  import { getAcpSessionManager } from "../../acp/index.js";
8
14
  import {
@@ -147,6 +153,32 @@ export async function executeAcpSpawn(
147
153
 
148
154
  return { content: payload, isError: false };
149
155
  } catch (err) {
156
+ // A pre-spawn rejection of the stored Claude credential (the adapter
157
+ // raises auth_required during session creation) gets the same recovery
158
+ // surface as the missing-token preflight: the errorCode raises the inline
159
+ // Connect card off this failed tool call, the registry mark dedups a
160
+ // redundant secure prompt, and the guidance keeps the model pointed at
161
+ // the card. Adapter-gated: the card repairs Claude credentials only.
162
+ // `isAcpAuthRequired` rather than an instanceof: it also recognizes the
163
+ // raw JSON-RPC auth_required object, which is how a rejection reaches
164
+ // this catch from the one protocol call `withAuthRetry` cannot wrap
165
+ // (`initialize`, whose response is what advertises the auth methods the
166
+ // retry would need).
167
+ if (
168
+ isAcpAuthRequired(err) &&
169
+ basename(agentConfig.command) === CLAUDE_ACP_COMMAND
170
+ ) {
171
+ const message =
172
+ err instanceof Error
173
+ ? err.message
174
+ : String((err as { message?: unknown }).message ?? err);
175
+ markAcpConnectCardRaised(context.conversationId);
176
+ return {
177
+ content: `${message}\n\n${ACP_AUTH_RECOVERY_GUIDANCE}`,
178
+ isError: true,
179
+ errorCode: ACP_CLAUDE_AUTH_REQUIRED_CODE,
180
+ };
181
+ }
150
182
  const msg =
151
183
  err instanceof Error
152
184
  ? err.message
@@ -426,13 +426,22 @@ export function executeDocumentUpdate(
426
426
  };
427
427
  }
428
428
 
429
+ // The store drops append content that merely restates the document's tail,
430
+ // so the client renders what actually landed rather than the submission.
431
+ const applied = result.appliedMarkdown;
432
+ const message = result.duplicateLeadingContentSkipped
433
+ ? applied.length === 0
434
+ ? "Nothing appended: the content is already at the end of the document"
435
+ : "Document content updated (leading content already at the end of the document was skipped)"
436
+ : "Document content updated";
437
+
429
438
  // Send document_editor_update message to update the built-in RTE
430
439
  if (context.sendToClient) {
431
440
  context.sendToClient({
432
441
  type: "document_editor_update",
433
442
  conversationId: context.conversationId,
434
443
  surfaceId,
435
- markdown: content,
444
+ markdown: applied,
436
445
  mode,
437
446
  });
438
447
 
@@ -441,7 +450,7 @@ export function executeDocumentUpdate(
441
450
  success: true,
442
451
  surface_id: surfaceId,
443
452
  mode,
444
- message: "Document content updated",
453
+ message,
445
454
  }),
446
455
  isError: false,
447
456
  };
@@ -458,7 +467,7 @@ export function executeDocumentUpdate(
458
467
  success: true,
459
468
  surface_id: surfaceId,
460
469
  mode,
461
- message: "Document content updated (no client connected to render it)",
470
+ message: `${message} (no client connected to render it)`,
462
471
  }),
463
472
  isError: false,
464
473
  };
@@ -431,7 +431,8 @@ export function unregisterPluginTools(pluginName: string): void {
431
431
  * {@link import("../plugins/mtime-cache.js").getActiveUserPluginTools} and diffs
432
432
  * the result into the registry: plugins that vanished are unregistered, new
433
433
  * plugins register, and a plugin whose per-tool source mtimes moved is
434
- * re-registered. Mirrors how hooks resolve through `getUserHookEntriesFor`.
434
+ * re-registered. Mirrors how hooks resolve through `getUserHookEntriesFor`
435
+ * in `hooks/registry.ts`.
435
436
  *
436
437
  * This is a pure read of the plugin cache — it does NOT reconcile the cache
437
438
  * against the source-versions sentinel, so it never activates a plugin or runs
@@ -55,8 +55,7 @@ export const runWorkflowInputSchema = z.looseObject({
55
55
  */
56
56
  function resolveTrustContext(context: ToolContext): TrustContext {
57
57
  const conversation = findConversation(context.conversationId);
58
- const fromConversation =
59
- conversation?.currentTurnTrustContext ?? conversation?.trustContext;
58
+ const fromConversation = conversation?.getTurnOrRestingTrust();
60
59
  if (fromConversation) {
61
60
  return fromConversation;
62
61
  }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { createReasoningTagFilter } from "../reasoning-tag-filter.js";
4
+
5
+ function run(chunks: string[]): string {
6
+ const filter = createReasoningTagFilter();
7
+ let out = "";
8
+ for (const chunk of chunks) {
9
+ out += filter.push(chunk);
10
+ }
11
+ out += filter.flush();
12
+ return out;
13
+ }
14
+
15
+ describe("ReasoningTagFilter", () => {
16
+ test("passes plain text through unchanged", () => {
17
+ expect(run(["Hello ", "there."])).toBe("Hello there.");
18
+ });
19
+
20
+ test("drops a complete think span within one delta", () => {
21
+ expect(run(["<think>hm, let me reason</think>Answer."])).toBe("Answer.");
22
+ });
23
+
24
+ test("drops a reasoning span crossing many deltas", () => {
25
+ expect(
26
+ run(["<thi", "nk>step one", " step two", "</th", "ink>Spoken reply"]),
27
+ ).toBe("Spoken reply");
28
+ });
29
+
30
+ test("drops thinking-tag variant and is case-insensitive", () => {
31
+ expect(run(["<Thinking>inner</Thinking>ok"])).toBe("ok");
32
+ });
33
+
34
+ test("keeps text before and after the span", () => {
35
+ expect(run(["Sure. <think>secret</think> Done."])).toBe("Sure. Done.");
36
+ });
37
+
38
+ test("handles multiple spans in one turn", () => {
39
+ expect(run(["a<think>x</think>b", "<think>y", "z</think>c"])).toBe("abc");
40
+ });
41
+
42
+ test("an unclosed span at end of stream stays dropped", () => {
43
+ expect(run(["ok<think>never closes"])).toBe("ok");
44
+ });
45
+
46
+ test("a dangling partial open tag is released as literal text at flush", () => {
47
+ expect(run(["price <th"])).toBe("price <th");
48
+ });
49
+
50
+ test("a false partial that disambiguates to text is released", () => {
51
+ expect(run(["a <th", "ree-step plan"])).toBe("a <three-step plan");
52
+ });
53
+
54
+ test("does not touch angle brackets that are not think tags", () => {
55
+ expect(run(["use <b>bold</b> and x < y"])).toBe(
56
+ "use <b>bold</b> and x < y",
57
+ );
58
+ });
59
+
60
+ test("holds back nothing across an empty stream", () => {
61
+ expect(run([""])).toBe("");
62
+ });
63
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Streaming reasoning-tag filter for the TTS pipelines.
3
+ *
4
+ * Some OpenAI-compatible reasoning models emit their chain of thought inline
5
+ * in the content stream as `<think>...</think>` (or `<thinking>...</thinking>`)
6
+ * spans. When a profile has not opted into the chat-completions provider's
7
+ * `parseThinkTags` (which re-routes those spans to `thinking_delta`), the raw
8
+ * reasoning rides `text_delta`, and anything downstream that speaks text
9
+ * deltas would read the model's inner monologue aloud and delay real speech
10
+ * by the full length of the reasoning.
11
+ *
12
+ * This filter is the TTS-boundary guard: voice output must never speak
13
+ * reasoning regardless of profile configuration. It is stateful because a
14
+ * tag or a reasoning span routinely crosses delta boundaries; feed every
15
+ * delta through {@link ReasoningTagFilter.push} and call
16
+ * {@link ReasoningTagFilter.flush} when the turn's stream ends.
17
+ *
18
+ * Display paths are deliberately untouched: `assistant_text_delta` frames
19
+ * keep the raw text, exactly like the markdown handling in
20
+ * `calls/tts-text-sanitizer.ts`.
21
+ */
22
+
23
+ const OPEN_TAGS = ["<think>", "<thinking>"] as const;
24
+ const CLOSE_TAGS = ["</think>", "</thinking>"] as const;
25
+
26
+ /** Longest suffix of `text` that is a proper prefix of any listed tag. */
27
+ function partialTagSuffix(text: string, tags: readonly string[]): number {
28
+ const max = Math.max(...tags.map((tag) => tag.length)) - 1;
29
+ const window = Math.min(max, text.length);
30
+ for (let len = window; len > 0; len -= 1) {
31
+ const suffix = text.slice(text.length - len).toLowerCase();
32
+ if (tags.some((tag) => tag.startsWith(suffix))) {
33
+ return len;
34
+ }
35
+ }
36
+ return 0;
37
+ }
38
+
39
+ function indexOfAny(
40
+ haystack: string,
41
+ tags: readonly string[],
42
+ ): { index: number; tag: string } | null {
43
+ const lower = haystack.toLowerCase();
44
+ let best: { index: number; tag: string } | null = null;
45
+ for (const tag of tags) {
46
+ const index = lower.indexOf(tag);
47
+ if (index >= 0 && (best === null || index < best.index)) {
48
+ best = { index, tag };
49
+ }
50
+ }
51
+ return best;
52
+ }
53
+
54
+ export class ReasoningTagFilter {
55
+ private insideReasoning = false;
56
+ private pending = "";
57
+
58
+ /**
59
+ * Feed one streamed delta; returns the speakable text it releases.
60
+ * Reasoning spans are dropped; a partial tag at the end of the buffer is
61
+ * held back until the next delta disambiguates it.
62
+ */
63
+ push(text: string): string {
64
+ this.pending += text;
65
+ let out = "";
66
+ for (;;) {
67
+ if (this.insideReasoning) {
68
+ const close = indexOfAny(this.pending, CLOSE_TAGS);
69
+ if (close) {
70
+ this.pending = this.pending.slice(close.index + close.tag.length);
71
+ this.insideReasoning = false;
72
+ continue;
73
+ }
74
+ // Everything buffered is reasoning except a possible partial close
75
+ // tag at the end; drop the reasoning, keep the partial.
76
+ const partial = partialTagSuffix(this.pending, CLOSE_TAGS);
77
+ this.pending = partial > 0 ? this.pending.slice(-partial) : "";
78
+ return out;
79
+ }
80
+ const open = indexOfAny(this.pending, OPEN_TAGS);
81
+ if (open) {
82
+ out += this.pending.slice(0, open.index);
83
+ this.pending = this.pending.slice(open.index + open.tag.length);
84
+ this.insideReasoning = true;
85
+ continue;
86
+ }
87
+ const partial = partialTagSuffix(this.pending, OPEN_TAGS);
88
+ const safeLength = this.pending.length - partial;
89
+ out += this.pending.slice(0, safeLength);
90
+ this.pending = partial > 0 ? this.pending.slice(safeLength) : "";
91
+ return out;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * End of stream: release any held-back partial. Outside a reasoning span a
97
+ * dangling "<thin" was ordinary text after all; inside one, the span never
98
+ * closed and its content stays dropped.
99
+ */
100
+ flush(): string {
101
+ const rest = this.insideReasoning ? "" : this.pending;
102
+ this.pending = "";
103
+ this.insideReasoning = false;
104
+ return rest;
105
+ }
106
+ }
107
+
108
+ export function createReasoningTagFilter(): ReasoningTagFilter {
109
+ return new ReasoningTagFilter();
110
+ }
@@ -18,6 +18,7 @@ import {
18
18
  type LLMProvider,
19
19
  type ProfileEntry,
20
20
  } from "../config/schemas/llm.js";
21
+ import { ROUTING_IDENTITY_PROVIDERS } from "../providers/inference/auth.js";
21
22
  import { getLogger } from "../util/logger.js";
22
23
  import { completedProfileBody } from "./custom-profile-ensure.js";
23
24
 
@@ -135,10 +136,26 @@ const ERA_COPY_LABEL_SUFFIX = " (Custom Provider)";
135
136
  /**
136
137
  * Comparison ignores `label` and `status` (user overlay state, preserved via
137
138
  * `userOverlayState`) and `model` (checked separately against the current
138
- * intent resolution and `HISTORICAL_INTENT_MODELS`).
139
+ * intent resolution and `HISTORICAL_INTENT_MODELS`). The CONVENTIONAL
140
+ * `<vendor>-personal` binding is normalized away before comparison (it was
141
+ * machinery-written across several eras); a non-conventional binding is a
142
+ * user selection and keeps blocking conversion.
139
143
  */
140
144
  const IGNORED_COMPARISON_KEYS = new Set(["label", "status", "model"]);
141
145
 
146
+ /**
147
+ * Fold the conventional personal entry name back to its vendor. The entries
148
+ * collapse (migration 145) rewrites a hatch copy's binding into its
149
+ * provider as `<vendor>-personal`; the matcher folds that shape so every
150
+ * era of stored copy keeps converting (the same discipline as
151
+ * `HISTORICAL_INTENT_MODELS`).
152
+ */
153
+ function foldPersonalEntryName(provider: string): string {
154
+ return provider.endsWith("-personal")
155
+ ? provider.slice(0, -"-personal".length)
156
+ : provider;
157
+ }
158
+
142
159
  /**
143
160
  * Per-provider model ids that earlier intent eras pinned onto `custom-*`
144
161
  * copies, verified against `providers/model-intents.ts` git history. Hatch
@@ -156,6 +173,10 @@ const HISTORICAL_INTENT_MODELS: Record<
156
173
  Partial<Record<string, readonly string[]>>
157
174
  > = {
158
175
  balanced: {
176
+ openai: [
177
+ // balanced intent 2026-05-05 (#29755) to the 2026-08-10 gpt-5.6 repoint.
178
+ "gpt-5.4-mini",
179
+ ],
159
180
  fireworks: [
160
181
  // balanced intent 2026-05-05 (#29755) to 2026-05-19 (#31068).
161
182
  "accounts/fireworks/models/kimi-k2p5",
@@ -166,6 +187,10 @@ const HISTORICAL_INTENT_MODELS: Record<
166
187
  ],
167
188
  },
168
189
  "quality-optimized": {
190
+ openai: [
191
+ // quality intent 2026-05-05 (#29755) to the 2026-08-10 gpt-5.6 repoint.
192
+ "gpt-5.4",
193
+ ],
169
194
  anthropic: [
170
195
  // quality intent 2026-05-05 (#29755) to 2026-06-11 (#34498).
171
196
  "claude-opus-4-7",
@@ -259,11 +284,13 @@ export function ensureByokDefaultProfiles(workspaceDir: string): void {
259
284
  const completionBase = LLMConfigBase.safeParse(llm.default ?? {}).data;
260
285
 
261
286
  // The default provider on a BYOK default, or the uniform hatch provider
262
- // across the complete copy set on a vellum default; null converts nothing.
263
- const candidateProvider =
264
- parsedDefault.data.provider !== "vellum"
265
- ? parsedDefault.data.provider
266
- : uniformCopyProvider(profiles);
287
+ // across the complete copy set on a routing-identity default (vellum,
288
+ // chatgpt); null converts nothing.
289
+ const candidateProvider = ROUTING_IDENTITY_PROVIDERS.has(
290
+ parsedDefault.data.provider,
291
+ )
292
+ ? uniformCopyProvider(profiles)
293
+ : parsedDefault.data.provider;
267
294
  const convertibleProvider =
268
295
  candidateProvider !== null && isByokDefaultProviderChoice(candidateProvider)
269
296
  ? candidateProvider
@@ -339,7 +366,11 @@ function isKnownUneditedBody(
339
366
  // (hatch materialized it once; the default provider may have changed
340
367
  // since); equality with the corroborated provider rules out a user
341
368
  // re-provision.
342
- if (convertibleProvider === null || entry.provider !== convertibleProvider) {
369
+ if (
370
+ convertibleProvider === null ||
371
+ typeof entry.provider !== "string" ||
372
+ foldPersonalEntryName(entry.provider) !== convertibleProvider
373
+ ) {
343
374
  return false;
344
375
  }
345
376
  const copyProvider = convertibleProvider;
@@ -347,11 +378,13 @@ function isKnownUneditedBody(
347
378
  if (template === undefined) {
348
379
  return false;
349
380
  }
350
- const materialized = materializeProfile(
351
- template,
352
- copyProvider,
353
- `${copyProvider}-personal`,
354
- ) as Record<string, unknown>;
381
+ // No connection binding: the bare provider means the default entry of
382
+ // that kind, and a stamped binding would re-introduce the collapsed
383
+ // field on disk.
384
+ const materialized = materializeProfile(template, copyProvider) as Record<
385
+ string,
386
+ unknown
387
+ >;
355
388
  if (
356
389
  entry.model !== materialized.model &&
357
390
  !(HISTORICAL_INTENT_MODELS[key][copyProvider] ?? []).includes(entry.model)
@@ -361,11 +394,20 @@ function isKnownUneditedBody(
361
394
  // Completion skips managed-source bodies, so a copy from the era whose
362
395
  // templates wrote `source: "managed"` (#29755 to #29768, 2026-05-05) is
363
396
  // compared as if user-source to keep both sides normalized identically.
397
+ const normalizedEntry: Record<string, unknown> = {
398
+ ...entry,
399
+ provider: copyProvider,
400
+ ...(entry.source === "managed" ? { source: "user" } : {}),
401
+ };
402
+ // The conventional personal binding is machinery-written (hatch stamps,
403
+ // completion re-stamps); normalize it away. Any other binding is user
404
+ // state and stays in the comparison, where the unbound template side
405
+ // makes it block conversion.
406
+ if (normalizedEntry.provider_connection === `${copyProvider}-personal`) {
407
+ delete normalizedEntry.provider_connection;
408
+ }
364
409
  const body = comparableBody(
365
- withCompletionBaked(
366
- entry.source === "managed" ? { ...entry, source: "user" } : entry,
367
- completionBase,
368
- ),
410
+ withCompletionBaked(normalizedEntry, completionBase),
369
411
  );
370
412
  const known = comparableBody(
371
413
  withCompletionBaked(materialized, completionBase),
@@ -392,9 +434,10 @@ function uniformCopyProvider(profiles: Record<string, unknown>): string | null {
392
434
  if (typeof entry.provider !== "string") {
393
435
  return null;
394
436
  }
437
+ const entryProvider = foldPersonalEntryName(entry.provider);
395
438
  if (provider === null) {
396
- provider = entry.provider;
397
- } else if (provider !== entry.provider) {
439
+ provider = entryProvider;
440
+ } else if (provider !== entryProvider) {
398
441
  return null;
399
442
  }
400
443
  }
@@ -3,7 +3,10 @@ import { join } from "node:path";
3
3
  import { isDeepStrictEqual } from "node:util";
4
4
 
5
5
  import { invalidateConfigCache } from "../config/loader.js";
6
- import { completeCustomProfile } from "../config/profile-materialization.js";
6
+ import {
7
+ completeCustomProfile,
8
+ mergePreservingUnknownKeys,
9
+ } from "../config/profile-materialization.js";
7
10
  import { LLMConfigBase, ProfileEntry } from "../config/schemas/llm.js";
8
11
  import { getLogger } from "../util/logger.js";
9
12
 
@@ -113,29 +116,6 @@ export function completedProfileBody(
113
116
  );
114
117
  }
115
118
 
116
- /**
117
- * `{...raw, ...completed}` recursively: completed (schema-known) values win,
118
- * raw keys the schema stripped survive at every depth. (Duplicated in the
119
- * write-path normalization in conversation-query-routes.ts — the two land in
120
- * independent PRs; consolidate when either next changes.)
121
- */
122
- function mergePreservingUnknownKeys(
123
- raw: Record<string, unknown>,
124
- completed: Record<string, unknown>,
125
- ): Record<string, unknown> {
126
- const out: Record<string, unknown> = { ...raw, ...completed };
127
- for (const [key, value] of Object.entries(completed)) {
128
- const rawValue = raw[key];
129
- if (readObject(value) !== null && readObject(rawValue) !== null) {
130
- out[key] = mergePreservingUnknownKeys(
131
- rawValue as Record<string, unknown>,
132
- value as Record<string, unknown>,
133
- );
134
- }
135
- }
136
- return out;
137
- }
138
-
139
119
  function readObject(value: unknown): Record<string, unknown> | null {
140
120
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
141
121
  return null;
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ const OLD_CALLSITE = "voiceFrontDecision";
7
+ const PROGRESS_CALLSITE = "voiceProgressNarration";
8
+ const RETIRED_ACK_KEYS = [
9
+ "ackFirstDeltaTimeoutMs",
10
+ "ackGenerationTimeoutMs",
11
+ ] as const;
12
+
13
+ export const consolidateVoiceFrontDoorMigration: WorkspaceMigration = {
14
+ id: "142-consolidate-voice-front-door",
15
+ description:
16
+ "Rename the progress narration callsite and remove retired generated-ack tuning",
17
+ run(workspaceDir: string): void {
18
+ const configPath = join(workspaceDir, "config.json");
19
+ if (!existsSync(configPath)) {
20
+ return;
21
+ }
22
+
23
+ let config: Record<string, unknown>;
24
+ try {
25
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
26
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
27
+ return;
28
+ }
29
+ config = raw as Record<string, unknown>;
30
+ } catch {
31
+ return;
32
+ }
33
+
34
+ let changed = false;
35
+ const llm = readObject(config.llm);
36
+ const callSites = readObject(llm?.callSites);
37
+ if (callSites && Object.hasOwn(callSites, OLD_CALLSITE)) {
38
+ if (!Object.hasOwn(callSites, PROGRESS_CALLSITE)) {
39
+ callSites[PROGRESS_CALLSITE] = callSites[OLD_CALLSITE];
40
+ }
41
+ delete callSites[OLD_CALLSITE];
42
+ changed = true;
43
+ }
44
+
45
+ const liveVoice = readObject(config.liveVoice);
46
+ const frontModel = readObject(liveVoice?.frontModel);
47
+ if (frontModel) {
48
+ for (const key of RETIRED_ACK_KEYS) {
49
+ if (Object.hasOwn(frontModel, key)) {
50
+ delete frontModel[key];
51
+ changed = true;
52
+ }
53
+ }
54
+ }
55
+
56
+ if (changed) {
57
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
58
+ }
59
+ },
60
+ down(_workspaceDir: string): void {
61
+ // Forward-only.
62
+ },
63
+ };
64
+
65
+ function readObject(value: unknown): Record<string, unknown> | null {
66
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
67
+ return null;
68
+ }
69
+ return value as Record<string, unknown>;
70
+ }