@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
@@ -0,0 +1,325 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { Database } from "bun:sqlite";
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import { collapseProfileBindingsToEntriesMigration } from "../workspace/migrations/145-collapse-profile-bindings-to-entries.js";
14
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
15
+
16
+ let workspaceDir: string;
17
+
18
+ function writeConfig(data: Record<string, unknown>): void {
19
+ writeFileSync(
20
+ join(workspaceDir, "config.json"),
21
+ JSON.stringify(data, null, 2) + "\n",
22
+ );
23
+ }
24
+
25
+ function readProfiles(): Record<string, Record<string, unknown>> {
26
+ const raw = JSON.parse(
27
+ readFileSync(join(workspaceDir, "config.json"), "utf-8"),
28
+ ) as { llm?: { profiles?: Record<string, Record<string, unknown>> } };
29
+ return raw.llm?.profiles ?? {};
30
+ }
31
+
32
+ function seedRows(rows: Array<{ name: string; provider: string }>): void {
33
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
34
+ const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
35
+ db.run(`CREATE TABLE IF NOT EXISTS provider_connections (
36
+ name TEXT PRIMARY KEY,
37
+ provider TEXT NOT NULL,
38
+ auth TEXT NOT NULL,
39
+ created_at INTEGER NOT NULL,
40
+ updated_at INTEGER NOT NULL
41
+ )`);
42
+ for (const row of rows) {
43
+ db.query(
44
+ `INSERT INTO provider_connections (name, provider, auth, created_at, updated_at) VALUES (?, ?, '{"type":"api_key"}', 1, 1)`,
45
+ ).run(row.name, row.provider);
46
+ }
47
+ db.close();
48
+ }
49
+
50
+ function run(): void {
51
+ collapseProfileBindingsToEntriesMigration.run(workspaceDir);
52
+ }
53
+
54
+ beforeEach(() => {
55
+ workspaceDir = join(
56
+ tmpdir(),
57
+ `vellum-migration-145-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
58
+ );
59
+ mkdirSync(workspaceDir, { recursive: true });
60
+ });
61
+
62
+ afterEach(() => {
63
+ if (existsSync(workspaceDir)) {
64
+ assertNotLiveDb(workspaceDir);
65
+ rmSync(workspaceDir, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ describe("145-collapse-profile-bindings-to-entries", () => {
70
+ test("folds a kind-agreeing binding into provider as the entry name", () => {
71
+ seedRows([{ name: "anthropic-work", provider: "anthropic" }]);
72
+ writeConfig({
73
+ llm: {
74
+ profiles: {
75
+ work: {
76
+ provider: "anthropic",
77
+ provider_connection: "anthropic-work",
78
+ model: "claude-opus-4-8",
79
+ },
80
+ },
81
+ },
82
+ });
83
+
84
+ run();
85
+
86
+ const work = readProfiles().work;
87
+ expect(work.provider).toBe("anthropic-work");
88
+ expect(work).not.toHaveProperty("provider_connection");
89
+ expect(work.model).toBe("claude-opus-4-8");
90
+ });
91
+
92
+ test("maps identity kinds when judging agreement", () => {
93
+ seedRows([
94
+ { name: "team-managed", provider: "vellum" },
95
+ { name: "chatgpt-subscription", provider: "chatgpt" },
96
+ ]);
97
+ writeConfig({
98
+ llm: {
99
+ profiles: {
100
+ managed: {
101
+ provider: "anthropic",
102
+ provider_connection: "team-managed",
103
+ model: "claude-opus-4-8",
104
+ },
105
+ codex: {
106
+ provider: "openai",
107
+ provider_connection: "chatgpt-subscription",
108
+ model: "gpt-5.5",
109
+ },
110
+ },
111
+ },
112
+ });
113
+
114
+ run();
115
+
116
+ expect(readProfiles().managed.provider).toBe("team-managed");
117
+ expect(readProfiles().codex.provider).toBe("chatgpt-subscription");
118
+ });
119
+
120
+ test("keeps the vendor and drops a dangling binding with a warn", () => {
121
+ seedRows([]);
122
+ writeConfig({
123
+ llm: {
124
+ profiles: {
125
+ stale: {
126
+ provider: "anthropic",
127
+ provider_connection: "deleted-row",
128
+ model: "claude-opus-4-8",
129
+ },
130
+ },
131
+ },
132
+ });
133
+
134
+ run();
135
+
136
+ const stale = readProfiles().stale;
137
+ expect(stale.provider).toBe("anthropic");
138
+ expect(stale).not.toHaveProperty("provider_connection");
139
+ });
140
+
141
+ test("keeps the vendor and drops a kind-disagreeing binding", () => {
142
+ seedRows([{ name: "ollama-local", provider: "ollama" }]);
143
+ writeConfig({
144
+ llm: {
145
+ profiles: {
146
+ conflicted: {
147
+ provider: "anthropic",
148
+ provider_connection: "ollama-local",
149
+ model: "claude-opus-4-8",
150
+ },
151
+ },
152
+ },
153
+ });
154
+
155
+ run();
156
+
157
+ const conflicted = readProfiles().conflicted;
158
+ expect(conflicted.provider).toBe("anthropic");
159
+ expect(conflicted).not.toHaveProperty("provider_connection");
160
+ });
161
+
162
+ test("strips a stray binding from a routing-identity profile", () => {
163
+ seedRows([{ name: "vellum", provider: "vellum" }]);
164
+ writeConfig({
165
+ llm: {
166
+ profiles: {
167
+ managed: {
168
+ provider: "vellum",
169
+ provider_connection: "vellum",
170
+ model: "claude-opus-4-8",
171
+ },
172
+ },
173
+ },
174
+ });
175
+
176
+ run();
177
+
178
+ const managed = readProfiles().managed;
179
+ expect(managed.provider).toBe("vellum");
180
+ expect(managed).not.toHaveProperty("provider_connection");
181
+ });
182
+
183
+ test("deletes a self-referential binding when no row claims that name", () => {
184
+ seedRows([{ name: "anthropic-work", provider: "anthropic" }]);
185
+ writeConfig({
186
+ llm: {
187
+ profiles: {
188
+ plain: {
189
+ provider: "anthropic",
190
+ provider_connection: "anthropic",
191
+ model: "claude-opus-4-8",
192
+ },
193
+ },
194
+ },
195
+ });
196
+
197
+ run();
198
+
199
+ expect(readProfiles().plain.provider).toBe("anthropic");
200
+ expect(readProfiles().plain).not.toHaveProperty("provider_connection");
201
+ });
202
+
203
+ test("an absent DB file means genuinely dangling bindings (restored config)", () => {
204
+ writeConfig({
205
+ llm: {
206
+ profiles: {
207
+ bound: {
208
+ provider: "anthropic",
209
+ provider_connection: "anthropic-work",
210
+ model: "claude-opus-4-8",
211
+ },
212
+ },
213
+ },
214
+ });
215
+ // No DB at all: a config restored into a fresh workspace has no rows.
216
+
217
+ run();
218
+
219
+ const bound = readProfiles().bound;
220
+ expect(bound.provider).toBe("anthropic");
221
+ expect(bound).not.toHaveProperty("provider_connection");
222
+ });
223
+
224
+ test("throws (for retry) when bindings exist but the DB is unqueryable", () => {
225
+ writeConfig({
226
+ llm: {
227
+ profiles: {
228
+ bound: {
229
+ provider: "anthropic",
230
+ provider_connection: "anthropic-work",
231
+ model: "claude-opus-4-8",
232
+ },
233
+ },
234
+ },
235
+ });
236
+ // DB file exists but carries no provider_connections table.
237
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
238
+ new Database(join(workspaceDir, "data", "db", "assistant.db")).close();
239
+
240
+ expect(() => run()).toThrow(/not readable/);
241
+ // Config untouched: no destructive guess was made.
242
+ expect(readProfiles().bound.provider_connection).toBe("anthropic-work");
243
+ expect(
244
+ collapseProfileBindingsToEntriesMigration.retryFailedCheckpoint,
245
+ ).toBe(true);
246
+ });
247
+
248
+ test("folds a canonical vellum binding only for identity-servable models", () => {
249
+ seedRows([{ name: "vellum", provider: "vellum" }]);
250
+ writeConfig({
251
+ llm: {
252
+ profiles: {
253
+ routable: {
254
+ provider: "anthropic",
255
+ provider_connection: "vellum",
256
+ model: "claude-opus-4-8",
257
+ },
258
+ stale: {
259
+ provider: "anthropic",
260
+ provider_connection: "vellum",
261
+ model: "claude-ancient-1",
262
+ },
263
+ },
264
+ },
265
+ });
266
+
267
+ run();
268
+
269
+ const routable = readProfiles().routable;
270
+ expect(routable.provider).toBe("vellum");
271
+ expect(routable).not.toHaveProperty("provider_connection");
272
+ // An unservable model would be stripped by the read-path schema if
273
+ // folded to the identity; the profile stays untouched instead.
274
+ const stale = readProfiles().stale;
275
+ expect(stale.provider).toBe("anthropic");
276
+ expect(stale.provider_connection).toBe("vellum");
277
+ });
278
+
279
+ test("keeps a self-named binding when a row by that name exists", () => {
280
+ seedRows([
281
+ { name: "anthropic", provider: "anthropic" },
282
+ { name: "anthropic-2", provider: "anthropic" },
283
+ ]);
284
+ writeConfig({
285
+ llm: {
286
+ profiles: {
287
+ pinned: {
288
+ provider: "anthropic",
289
+ provider_connection: "anthropic",
290
+ model: "claude-opus-4-8",
291
+ },
292
+ },
293
+ },
294
+ });
295
+
296
+ run();
297
+
298
+ // The explicit pin survives: with sibling rows, the bare vendor would
299
+ // auto-resolve differently.
300
+ expect(readProfiles().pinned.provider_connection).toBe("anthropic");
301
+ });
302
+
303
+ test("no-ops without bindings and is idempotent", () => {
304
+ seedRows([{ name: "anthropic-work", provider: "anthropic" }]);
305
+ writeConfig({
306
+ llm: {
307
+ profiles: {
308
+ work: {
309
+ provider: "anthropic",
310
+ provider_connection: "anthropic-work",
311
+ model: "claude-opus-4-8",
312
+ },
313
+ unbound: { provider: "vellum", model: "claude-opus-4-8" },
314
+ },
315
+ },
316
+ });
317
+
318
+ run();
319
+ const afterFirst = readProfiles();
320
+ run();
321
+
322
+ expect(readProfiles()).toEqual(afterFirst);
323
+ expect(afterFirst.unbound.provider).toBe("vellum");
324
+ });
325
+ });
@@ -59,8 +59,11 @@ beforeEach(() => {
59
59
 
60
60
  describe("CLAUDE_OAUTH_CONFIG", () => {
61
61
  test("matches the verified endpoints, client id, and scope", () => {
62
+ // The single literal pin for the endpoint. Every other test asserts
63
+ // against CLAUDE_OAUTH_CONFIG.authorizeUrl, so an endpoint change edits
64
+ // exactly this one value.
62
65
  expect(CLAUDE_OAUTH_CONFIG.authorizeUrl).toBe(
63
- "https://claude.ai/oauth/authorize",
66
+ "https://claude.com/cai/oauth/authorize",
64
67
  );
65
68
  expect(CLAUDE_OAUTH_CONFIG.tokenExchangeUrl).toBe(
66
69
  "https://platform.claude.com/v1/oauth/token",
@@ -92,8 +95,10 @@ describe("buildClaudeAuthorizeUrl", () => {
92
95
  });
93
96
 
94
97
  const parsed = new URL(url);
98
+ // The property under test is that the builder uses the configured
99
+ // endpoint; the value itself is guarded by the literal pin above.
95
100
  expect(`${parsed.origin}${parsed.pathname}`).toBe(
96
- "https://claude.ai/oauth/authorize",
101
+ CLAUDE_OAUTH_CONFIG.authorizeUrl,
97
102
  );
98
103
 
99
104
  const params = parsed.searchParams;
@@ -106,6 +111,9 @@ describe("buildClaudeAuthorizeUrl", () => {
106
111
  expect(params.get("state")).toBe("state-abc");
107
112
  expect(params.get("code_challenge")).toBe("challenge-123");
108
113
  expect(params.get("code_challenge_method")).toBe("S256");
114
+ // The manual flow's defining param: tells Claude to render `code#state`
115
+ // on the callback page. Claude rejects a manual-redirect grant without it.
116
+ expect(params.get("code")).toBe("true");
109
117
  });
110
118
 
111
119
  test("works with the manual redirect URI too", () => {
@@ -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
+ });