@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,161 @@
1
+ /**
2
+ * Pins the two shapes a Claude auth failure travels in, plus the
3
+ * wire-contract literals. Losing either shape silently degrades an actionable
4
+ * "reconnect Claude" failure into an opaque one.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+
9
+ import { AcpAuthRequiredEventSchema } from "../../api/events/acp-auth-required.js";
10
+ import { AcpSessionErrorEventSchema } from "../../api/events/acp-session-error.js";
11
+ import {
12
+ ACP_CLAUDE_AUTH_REQUIRED_CODE,
13
+ AcpAuthRequiredError,
14
+ AUTH_REQUIRED_CODE,
15
+ CLAUDE_ACP_COMMAND,
16
+ isAcpAuthRequired,
17
+ isClaudeAuthFailureMessage,
18
+ } from "../auth-required.js";
19
+
20
+ describe("isAcpAuthRequired", () => {
21
+ test("recognizes the raw JSON-RPC rejection from the agent", () => {
22
+ // What the adapter actually sends: a plain object off the wire, not an
23
+ // instance of any class we control.
24
+ expect(isAcpAuthRequired({ code: AUTH_REQUIRED_CODE })).toBe(true);
25
+ });
26
+
27
+ test("recognizes our own error after the retry path gives up", () => {
28
+ expect(isAcpAuthRequired(new AcpAuthRequiredError("claude", "nope"))).toBe(
29
+ true,
30
+ );
31
+ });
32
+
33
+ test("does not fire on other failures", () => {
34
+ expect(isAcpAuthRequired(new Error("Internal error"))).toBe(false);
35
+ expect(isAcpAuthRequired({ code: -32601 })).toBe(false);
36
+ expect(isAcpAuthRequired(null)).toBe(false);
37
+ expect(isAcpAuthRequired(undefined)).toBe(false);
38
+ expect(isAcpAuthRequired("auth_required")).toBe(false);
39
+ });
40
+
41
+ test("uses the ACP-specified code", () => {
42
+ // Matches the SDK's RequestError.authRequired(); drifting from it would
43
+ // silently stop classifying every auth failure.
44
+ expect(AUTH_REQUIRED_CODE).toBe(-32000);
45
+ });
46
+ });
47
+
48
+ describe("isClaudeAuthFailureMessage", () => {
49
+ test("matches the live rejected-credential failure verbatim", () => {
50
+ // Captured from a real claude-agent-acp run against a revoked token: the
51
+ // adapter relays the CLI's failure as a -32603 internal error carrying
52
+ // this text, NOT as the structured auth_required rejection. This message
53
+ // is the reported bug, so it must classify.
54
+ expect(
55
+ isClaudeAuthFailureMessage(
56
+ "Internal error: Failed to authenticate. API Error: 401 OAuth access token has been revoked.",
57
+ ),
58
+ ).toBe(true);
59
+ });
60
+
61
+ test("is insensitive to the server's variable suffix", () => {
62
+ // The CLI-authored prefix is the stable part; the API's suffix varies.
63
+ expect(
64
+ isClaudeAuthFailureMessage(
65
+ "Failed to authenticate. API Error: 401 OAuth access token has expired. Re-authenticate to continue.",
66
+ ),
67
+ ).toBe(true);
68
+ });
69
+
70
+ test("matches the CLI's other auth phrasings", () => {
71
+ expect(
72
+ isClaudeAuthFailureMessage(
73
+ "Session expired. Please run /login to sign in again.",
74
+ ),
75
+ ).toBe(true);
76
+ expect(isClaudeAuthFailureMessage("Not logged in")).toBe(true);
77
+ });
78
+
79
+ test("does not fire on ordinary failures", () => {
80
+ expect(isClaudeAuthFailureMessage("Internal error")).toBe(false);
81
+ expect(isClaudeAuthFailureMessage("ECONNRESET: connection reset")).toBe(
82
+ false,
83
+ );
84
+ expect(
85
+ isClaudeAuthFailureMessage("Command failed: login.sh: not found"),
86
+ ).toBe(false);
87
+ expect(isClaudeAuthFailureMessage(undefined)).toBe(false);
88
+ expect(isClaudeAuthFailureMessage("")).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe("AcpAuthRequiredError", () => {
93
+ test("is an Error that carries the agent id and its message", () => {
94
+ const err = new AcpAuthRequiredError("claude", "needs a reconnect");
95
+ expect(err).toBeInstanceOf(Error);
96
+ expect(err.name).toBe("AcpAuthRequiredError");
97
+ expect(err.agentId).toBe("claude");
98
+ expect(err.message).toBe("needs a reconnect");
99
+ });
100
+ });
101
+
102
+ describe("event shape is additive-safe for older clients", () => {
103
+ test("acp_session_error rejects unknown keys, which is why the signal is not a field on it", () => {
104
+ // Clients drop a whole event over an unknown key (strict schemas plus a
105
+ // safeParse fallback to `unknown`), so new signals must ride new event
106
+ // types; this pins the strictness that forces that.
107
+ const result = AcpSessionErrorEventSchema.safeParse({
108
+ type: "acp_session_error",
109
+ acpSessionId: "acp-1",
110
+ error: "boom",
111
+ someFutureField: "x",
112
+ });
113
+ expect(result.success).toBe(false);
114
+ });
115
+
116
+ test("acp_session_error still parses in its unchanged shape", () => {
117
+ expect(
118
+ AcpSessionErrorEventSchema.safeParse({
119
+ type: "acp_session_error",
120
+ acpSessionId: "acp-1",
121
+ error: "boom",
122
+ }).success,
123
+ ).toBe(true);
124
+ });
125
+
126
+ test("acp_auth_required carries the code and an optional anchor", () => {
127
+ const parsed = AcpAuthRequiredEventSchema.parse({
128
+ type: "acp_auth_required",
129
+ acpSessionId: "acp-1",
130
+ authCode: ACP_CLAUDE_AUTH_REQUIRED_CODE,
131
+ agent: "claude",
132
+ parentToolUseId: "tool-1",
133
+ });
134
+ expect(parsed.authCode).toBe(ACP_CLAUDE_AUTH_REQUIRED_CODE);
135
+ expect(parsed.parentToolUseId).toBe("tool-1");
136
+
137
+ // The anchor is optional: a run not started by a tool call has none.
138
+ expect(
139
+ AcpAuthRequiredEventSchema.safeParse({
140
+ type: "acp_auth_required",
141
+ acpSessionId: "acp-1",
142
+ authCode: ACP_CLAUDE_AUTH_REQUIRED_CODE,
143
+ agent: "claude",
144
+ }).success,
145
+ ).toBe(true);
146
+ });
147
+ });
148
+
149
+ describe("wire contract", () => {
150
+ test("the client marker matches the web literal", () => {
151
+ // Paired with ACP_CLAUDE_AUTH_REQUIRED_CODE in
152
+ // clients/web/src/domains/chat/utils/acp-connect.ts. A silent rename on
153
+ // either side turns the Connect card off with nothing failing.
154
+ expect(ACP_CLAUDE_AUTH_REQUIRED_CODE).toBe("acp_claude_auth_required");
155
+ });
156
+
157
+ test("the adapter gate matches the resolved command basename", () => {
158
+ // Compared against SessionEntry.command, which is already a basename.
159
+ expect(CLAUDE_ACP_COMMAND).toBe("claude-agent-acp");
160
+ });
161
+ });
@@ -33,7 +33,14 @@ import {
33
33
  * `CLAUDE_CODE_OAUTH_TOKEN` requires.
34
34
  */
35
35
  export const CLAUDE_OAUTH_CONFIG: OAuth2Config = {
36
- authorizeUrl: "https://claude.ai/oauth/authorize",
36
+ // The claude.ai-account authorize endpoint, matching the Claude Code CLI's
37
+ // own CLAUDE_AI_AUTHORIZE_URL. Shared by both flows: the loopback path
38
+ // builds its URL from this in `prepareOAuth2Flow`, the manual path in
39
+ // `buildClaudeAuthorizeUrl`. Only the claude.com/cai host completes a
40
+ // grant; the legacy claude.ai host still renders a working-looking consent
41
+ // screen but rejects every authorize POST, so a wrong host here looks
42
+ // functional until the final click of a real sign-in.
43
+ authorizeUrl: "https://claude.com/cai/oauth/authorize",
37
44
  tokenExchangeUrl: "https://platform.claude.com/v1/oauth/token",
38
45
  clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
39
46
  scopes: ["user:inference"],
@@ -53,12 +60,22 @@ export const CLAUDE_OAUTH_CONFIG: OAuth2Config = {
53
60
  export const CLAUDE_MANUAL_REDIRECT_URI =
54
61
  "https://platform.claude.com/oauth/code/callback";
55
62
 
56
- /** Build the Claude authorize URL for a PKCE flow. */
63
+ /**
64
+ * Build the Claude authorize URL for the MANUAL (paste) PKCE flow.
65
+ *
66
+ * `code=true` tells Claude to render the `code#state` string on the callback
67
+ * page for the user to copy, which is the whole mechanism of the manual flow;
68
+ * Claude rejects a manual-redirect grant without it. The loopback flow
69
+ * performs a real redirect instead and must NOT send it; that URL is built
70
+ * separately in `prepareOAuth2Flow`, which is why this builder can include
71
+ * the param unconditionally.
72
+ */
57
73
  export function buildClaudeAuthorizeUrl(
58
74
  redirectUri: string,
59
75
  pkce: { codeChallenge: string; state: string },
60
76
  ): string {
61
77
  const params = new URLSearchParams({
78
+ code: "true",
62
79
  response_type: "code",
63
80
  client_id: CLAUDE_OAUTH_CONFIG.clientId,
64
81
  redirect_uri: redirectUri,
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { AcpAgentProcess } from "./agent-process.js";
4
+ import { AcpAuthRequiredError, isAcpAuthRequired } from "./auth-required.js";
4
5
  import type { AcpAgentConfig } from "./types.js";
5
6
 
6
7
  const config: AcpAgentConfig = { command: "noop", args: [] };
@@ -114,3 +115,102 @@ describe("AcpAgentProcess.markStderr / stderrSince", () => {
114
115
  expect(proc.stderrSince(mark)).toBe("fresh failure");
115
116
  });
116
117
  });
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // createSession: structured auth_required handling through the real retry path
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Wires a process with a fake protocol connection and advertised auth methods,
125
+ * following the same no-child-process injection style as `feed` above. The
126
+ * retry logic under test (`withAuthRetry`, method selection, the typed throw)
127
+ * runs for real.
128
+ */
129
+ function wireProcess(opts: {
130
+ authMethods: unknown[];
131
+ newSession: () => Promise<unknown>;
132
+ env?: Record<string, string>;
133
+ }): { proc: AcpAgentProcess; authenticateCalls: () => number } {
134
+ const proc = new AcpAgentProcess("test-agent", config, clientFactory);
135
+ let authed = 0;
136
+ (proc as unknown as { connection: unknown }).connection = {
137
+ newSession: opts.newSession,
138
+ authenticate: async () => {
139
+ authed += 1;
140
+ return {};
141
+ },
142
+ };
143
+ (proc as unknown as { initializeResponse: unknown }).initializeResponse = {
144
+ authMethods: opts.authMethods,
145
+ };
146
+ (proc as unknown as { spawnedEnv: unknown }).spawnedEnv = opts.env ?? {};
147
+ return { proc, authenticateCalls: () => authed };
148
+ }
149
+
150
+ const TERMINAL_ONLY_METHODS = [
151
+ { id: "claude-ai-login", name: "Claude Subscription", type: "terminal" },
152
+ ];
153
+
154
+ describe("AcpAgentProcess.createSession auth_required handling", () => {
155
+ test("a structured auth_required rejection with only terminal methods becomes AcpAuthRequiredError", async () => {
156
+ // The wire shape the adapter actually sends: a plain JSON-RPC error
157
+ // object, not an Error subclass. With no satisfiable env_var method the
158
+ // retry path must surface the typed error the session manager and the
159
+ // spawn tool classify on; losing the type here silently degrades the
160
+ // whole recovery surface back into a generic failure.
161
+ const { proc } = wireProcess({
162
+ authMethods: TERMINAL_ONLY_METHODS,
163
+ newSession: () =>
164
+ Promise.reject({ code: -32000, message: "Authentication required" }),
165
+ });
166
+
167
+ const err: unknown = await proc.createSession("/tmp").then(
168
+ () => null,
169
+ (e: unknown) => e,
170
+ );
171
+
172
+ expect(err).toBeInstanceOf(AcpAuthRequiredError);
173
+ expect((err as AcpAuthRequiredError).agentId).toBe("test-agent");
174
+ expect(isAcpAuthRequired(err)).toBe(true);
175
+ });
176
+
177
+ test("a satisfiable env_var method authenticates and retries instead of throwing", async () => {
178
+ let calls = 0;
179
+ const { proc, authenticateCalls } = wireProcess({
180
+ authMethods: [
181
+ {
182
+ id: "api-key",
183
+ name: "Use SOME_KEY",
184
+ type: "env_var",
185
+ vars: [{ name: "SOME_KEY" }],
186
+ },
187
+ ],
188
+ env: { SOME_KEY: "value" },
189
+ newSession: () => {
190
+ calls += 1;
191
+ return calls === 1
192
+ ? Promise.reject({ code: -32000, message: "Authentication required" })
193
+ : Promise.resolve({ sessionId: "session-1" });
194
+ },
195
+ });
196
+
197
+ await expect(proc.createSession("/tmp")).resolves.toBe("session-1");
198
+ expect(authenticateCalls()).toBe(1);
199
+ });
200
+
201
+ test("a non-auth rejection passes through untyped", async () => {
202
+ const { proc } = wireProcess({
203
+ authMethods: TERMINAL_ONLY_METHODS,
204
+ newSession: () =>
205
+ Promise.reject({ code: -32603, message: "Internal error" }),
206
+ });
207
+
208
+ const err: unknown = await proc.createSession("/tmp").then(
209
+ () => null,
210
+ (e: unknown) => e,
211
+ );
212
+
213
+ expect(err).not.toBeInstanceOf(AcpAuthRequiredError);
214
+ expect(isAcpAuthRequired(err)).toBe(false);
215
+ });
216
+ });
@@ -20,16 +20,11 @@ import type {
20
20
  import * as acp from "@agentclientprotocol/sdk";
21
21
 
22
22
  import { getLogger } from "../util/logger.js";
23
+ import { AcpAuthRequiredError, isAcpAuthRequired } from "./auth-required.js";
23
24
  import type { AcpAgentConfig } from "./types.js";
24
25
 
25
26
  const log = getLogger("acp");
26
27
 
27
- /**
28
- * JSON-RPC error code agents use to signal that authentication is required
29
- * (matches the SDK's RequestError.authRequired()).
30
- */
31
- const AUTH_REQUIRED_CODE = -32000;
32
-
33
28
  /**
34
29
  * Rough byte cap for retained stderr. Oldest lines are evicted once the sum of
35
30
  * retained line lengths exceeds this, so the stderr ring (read via stderrSince)
@@ -37,19 +32,6 @@ const AUTH_REQUIRED_CODE = -32000;
37
32
  */
38
33
  const STDERR_RETENTION_BYTES = 4096;
39
34
 
40
- /**
41
- * Detects the ACP auth-required error. Checks the `code` property rather than
42
- * `instanceof acp.RequestError` so plain JSON-RPC error objects are also
43
- * recognized.
44
- */
45
- function isAuthRequiredError(err: unknown): boolean {
46
- return (
47
- typeof err === "object" &&
48
- err !== null &&
49
- (err as { code?: unknown }).code === AUTH_REQUIRED_CODE
50
- );
51
- }
52
-
53
35
  function isEnvVarMethod(
54
36
  method: AuthMethod,
55
37
  ): method is AuthMethodEnvVar & { type: "env_var" } {
@@ -296,7 +278,7 @@ export class AcpAgentProcess {
296
278
  try {
297
279
  return await op();
298
280
  } catch (err) {
299
- if (!isAuthRequiredError(err)) {
281
+ if (!isAcpAuthRequired(err)) {
300
282
  throw err;
301
283
  }
302
284
 
@@ -306,12 +288,23 @@ export class AcpAgentProcess {
306
288
 
307
289
  const method = this.selectEnvVarAuthMethod();
308
290
  if (!method) {
309
- throw new Error(
310
- `ACP agent "${this.agentId}" requires authentication. ` +
311
- `Advertised methods: ${this.describeAuthMethods()}. ` +
312
- "Set the required env var under acp.agents.<id>.env in config.json, " +
313
- "collect it securely via 'assistant credentials prompt --service acp --field <field> --label \"<label>\"', " +
314
- "or complete the agent's own login flow in the workspace.",
291
+ // Typed so the session manager can tell an auth failure from a crash
292
+ // and surface a re-authentication path. The remediation text is split
293
+ // by what the agent advertises: env-var and `credentials prompt`
294
+ // advice only helps when an env_var method exists to satisfy, and for
295
+ // terminal-login-only adapters it sends the user chasing a CLI
296
+ // workaround for a credential the app repairs on its own.
297
+ throw new AcpAuthRequiredError(
298
+ this.agentId,
299
+ this.hasEnvVarAuthMethod()
300
+ ? `ACP agent "${this.agentId}" requires authentication. ` +
301
+ `Advertised methods: ${this.describeAuthMethods()}. ` +
302
+ "Set the required env var under acp.agents.<id>.env in config.json, " +
303
+ "or collect it securely via 'assistant credentials prompt --service acp --field <field> --label \"<label>\"'."
304
+ : `ACP agent "${this.agentId}" requires authentication and its ` +
305
+ "stored credential was not accepted. Advertised methods: " +
306
+ `${this.describeAuthMethods()}. None can be completed headlessly, ` +
307
+ "so the credential has to be reconnected.",
315
308
  );
316
309
  }
317
310
 
@@ -325,6 +318,16 @@ export class AcpAgentProcess {
325
318
  }
326
319
  }
327
320
 
321
+ /**
322
+ * Whether the agent advertises any `env_var` auth method at all, satisfiable
323
+ * or not. Distinguishes "we could not satisfy the env var it wants" from
324
+ * "env vars are not how this agent authenticates", which are different
325
+ * problems with different fixes.
326
+ */
327
+ private hasEnvVarAuthMethod(): boolean {
328
+ return this.authMethods.some(isEnvVarMethod);
329
+ }
330
+
328
331
  /**
329
332
  * Renders the agent's advertised auth methods for error messages, e.g.
330
333
  * `"Login with ChatGPT" (chatgpt), "Use OPENAI_API_KEY" (env var OPENAI_API_KEY)`.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Detection of Claude authentication failures on ACP runs.
3
+ *
4
+ * These failures reach the daemon in two shapes, and both are needed:
5
+ *
6
+ * 1. No credentials at all: the adapter raises a structured ACP
7
+ * `auth_required` rejection (JSON-RPC -32000). {@link isAcpAuthRequired}.
8
+ * 2. Credentials present but rejected (the expired/revoked case): the CLI
9
+ * surfaces the API 401 as an error message it authors, and the adapter
10
+ * relays it as a generic -32603 internal error, NOT as `auth_required`.
11
+ * {@link isClaudeAuthFailureMessage}.
12
+ *
13
+ * A positive detection becomes {@link ACP_CLAUDE_AUTH_REQUIRED_CODE} on the
14
+ * `acp_auth_required` event, which the client turns into the inline
15
+ * "Connect Claude Code" card.
16
+ */
17
+
18
+ export { ACP_CLAUDE_AUTH_REQUIRED_CODE } from "../api/events/acp-auth-required.js";
19
+
20
+ /**
21
+ * JSON-RPC error code agents use to signal that authentication is required
22
+ * (matches the ACP SDK's `RequestError.authRequired()`).
23
+ */
24
+ export const AUTH_REQUIRED_CODE = -32000;
25
+
26
+ /**
27
+ * An agent rejected an operation with `auth_required` and the daemon has no
28
+ * way to satisfy it. A distinct type so the classification survives to the
29
+ * session manager, where the failure becomes an event the UI can act on.
30
+ */
31
+ export class AcpAuthRequiredError extends Error {
32
+ constructor(
33
+ readonly agentId: string,
34
+ message: string,
35
+ ) {
36
+ super(message);
37
+ this.name = "AcpAuthRequiredError";
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Detects the structured auth-required signal in either shape it travels: the
43
+ * raw JSON-RPC rejection from the agent, or our own
44
+ * {@link AcpAuthRequiredError}. Checks the `code` property rather than an
45
+ * `instanceof` so plain JSON-RPC error objects are recognized too.
46
+ */
47
+ export function isAcpAuthRequired(err: unknown): boolean {
48
+ if (err instanceof AcpAuthRequiredError) {
49
+ return true;
50
+ }
51
+ return (
52
+ typeof err === "object" &&
53
+ err !== null &&
54
+ (err as { code?: unknown }).code === AUTH_REQUIRED_CODE
55
+ );
56
+ }
57
+
58
+ /**
59
+ * The Claude CLI's own auth-failure phrasings. Unanchored (no ^/$) because
60
+ * the messages arrive with adapter framing ("Internal error: ...") or as a
61
+ * line `deriveFailureError` picked out of stderr.
62
+ */
63
+ const CLAUDE_AUTH_FAILURE_PATTERNS: readonly RegExp[] = [
64
+ /Failed to authenticate\b/,
65
+ /Please run \/login/,
66
+ /\bNot logged in\b/,
67
+ ];
68
+
69
+ /**
70
+ * Whether a failure message is a Claude authentication failure. Callers gate
71
+ * on the adapter (`claude-agent-acp`) BEFORE consulting this: the patterns
72
+ * are Claude-specific, and the marker built on them promises a repair only
73
+ * the Connect Claude flow can perform.
74
+ */
75
+ export function isClaudeAuthFailureMessage(
76
+ message: string | undefined,
77
+ ): boolean {
78
+ return (
79
+ message != null &&
80
+ CLAUDE_AUTH_FAILURE_PATTERNS.some((pattern) => pattern.test(message))
81
+ );
82
+ }
83
+
84
+ /** The adapter whose auth failures the Connect Claude flow can repair. */
85
+ export const CLAUDE_ACP_COMMAND = "claude-agent-acp";
86
+
87
+ /**
88
+ * Model-facing text accompanying a Claude auth failure wherever the inline
89
+ * Connect card is raised (the tool result for a pre-spawn rejection, the
90
+ * parent notification for a post-spawn one), so the model points at the card
91
+ * instead of inventing a remedy (CLI commands, pasted tokens, or a card that
92
+ * is not there). Bans placement words because card position is a UI detail
93
+ * the model cannot see.
94
+ */
95
+ export const ACP_AUTH_RECOVERY_GUIDANCE =
96
+ "The Claude Code connection needs to be re-authorized. The app shows the " +
97
+ 'user an inline "Connect Claude Code" card. Reply with ONE short sentence: ' +
98
+ "ask them to click Connect to sign in again, and tell them you'll continue " +
99
+ "automatically once they are connected. Do NOT say where the card is; never " +
100
+ 'say "below", "above", "at the bottom", or "here". Do NOT tell them to run ' +
101
+ "`claude setup-token`, paste a token in chat, run credential CLI commands, " +
102
+ "or re-run the agent yourself; the card and auto-continue handle it.";
@@ -5,6 +5,7 @@ import {
5
5
  deleteConversation,
6
6
  setConversation,
7
7
  } from "../daemon/conversation-registry.js";
8
+ import { hasAcpConnectCardRaised } from "./acp-connect-card-state.js";
8
9
  import { VellumAcpClientHandler } from "./client-handler.js";
9
10
  import { AcpSessionManager } from "./session-manager.js";
10
11
 
@@ -310,3 +311,121 @@ describe("AcpSessionManager parent notification", () => {
310
311
  expect((manager.getStatus() as unknown[]).length).toBe(1);
311
312
  });
312
313
  });
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Auth-required recovery surface
317
+ // ---------------------------------------------------------------------------
318
+
319
+ describe("AcpSessionManager auth-required recovery surface", () => {
320
+ /** The live-captured shape of a rejected-credential failure. */
321
+ const authFailure = () =>
322
+ Promise.reject(
323
+ new Error(
324
+ "Internal error: Failed to authenticate. API Error: 401 OAuth access token has been revoked.",
325
+ ),
326
+ );
327
+
328
+ /** Drives an auth-shaped prompt failure through firePromptInBackground. */
329
+ async function driveAuthFailure(opts: {
330
+ id: string;
331
+ command: string;
332
+ parentToolUseId?: string;
333
+ cancelled?: boolean;
334
+ }) {
335
+ const manager = new AcpSessionManager(1);
336
+ const parentId = `parent-${opts.id}`;
337
+ const { conversation, persistUserMessage, loopRan } = mockConversation();
338
+ setConversation(parentId, conversation);
339
+ registered.push(parentId);
340
+
341
+ const entry = injectSession(
342
+ manager,
343
+ opts.id,
344
+ parentId,
345
+ fakeProcess(authFailure),
346
+ );
347
+ entry.command = opts.command;
348
+ (entry as { parentToolUseId?: string }).parentToolUseId =
349
+ opts.parentToolUseId;
350
+ if (opts.cancelled) {
351
+ entry.state.status = "cancelled";
352
+ }
353
+
354
+ await fire(manager, opts.id, entry).catch(() => {});
355
+ if (!opts.cancelled) {
356
+ await loopRan;
357
+ }
358
+
359
+ const events = (
360
+ entry.sendToVellum as ReturnType<typeof mock>
361
+ ).mock.calls.map((c) => c[0] as { type: string } & Record<string, unknown>);
362
+ const firstPersist = persistUserMessage.mock.calls[0] as unknown as
363
+ | [{ content: string }]
364
+ | undefined;
365
+ return {
366
+ parentId,
367
+ authEvent: events.find((e) => e.type === "acp_auth_required"),
368
+ persistedContent: firstPersist?.[0].content,
369
+ };
370
+ }
371
+
372
+ test("claude failure with an anchor raises the full surface: event, registry mark, guidance", async () => {
373
+ const r = await driveAuthFailure({
374
+ id: "sess-auth-anchor",
375
+ command: "claude-agent-acp",
376
+ parentToolUseId: "tool-anchor-1",
377
+ });
378
+
379
+ expect(r.authEvent).toMatchObject({
380
+ acpSessionId: "sess-auth-anchor",
381
+ authCode: "acp_claude_auth_required",
382
+ agent: "claude",
383
+ parentToolUseId: "tool-anchor-1",
384
+ });
385
+ // The credential-prompt route consults this registry to redirect a
386
+ // redundant secure prompt at the card instead of stacking a second one.
387
+ expect(hasAcpConnectCardRaised(r.parentId)).toBe(true);
388
+ expect(r.persistedContent).toContain("Connect Claude Code");
389
+ });
390
+
391
+ test("claude failure without an anchor keeps the plain failure: no event, no mark, no guidance", async () => {
392
+ // No spawning tool call means no transcript row to render the card under;
393
+ // guidance would point the model at a card that cannot appear.
394
+ const r = await driveAuthFailure({
395
+ id: "sess-auth-noanchor",
396
+ command: "claude-agent-acp",
397
+ });
398
+
399
+ expect(r.authEvent).toBeUndefined();
400
+ expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
401
+ expect(r.persistedContent).not.toContain("Connect Claude Code");
402
+ });
403
+
404
+ test("a run the user already stopped raises nothing: no event, no registry mark, no parent turn", async () => {
405
+ // The client never renders a card for a cancelled run, and the
406
+ // prompt-dedup registry is never cleared, so marking it here would
407
+ // suppress the secure token prompt for the daemon's lifetime against a
408
+ // card that does not exist.
409
+ const r = await driveAuthFailure({
410
+ id: "sess-auth-cancelled",
411
+ command: "claude-agent-acp",
412
+ parentToolUseId: "tool-anchor-3",
413
+ cancelled: true,
414
+ });
415
+
416
+ expect(r.authEvent).toBeUndefined();
417
+ expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
418
+ expect(r.persistedContent).toBeUndefined();
419
+ });
420
+
421
+ test("a non-claude adapter never raises the surface, even on an auth-shaped failure", async () => {
422
+ const r = await driveAuthFailure({
423
+ id: "sess-auth-codex",
424
+ command: "codex-acp",
425
+ parentToolUseId: "tool-anchor-2",
426
+ });
427
+
428
+ expect(r.authEvent).toBeUndefined();
429
+ expect(hasAcpConnectCardRaised(r.parentId)).toBe(false);
430
+ });
431
+ });