@vellumai/assistant 0.12.1 → 0.12.2-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 (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -78,7 +78,7 @@ describe("ensureAdapterInstalled", () => {
78
78
  expect(args).toEqual([
79
79
  "add",
80
80
  "--global",
81
- "@agentclientprotocol/claude-agent-acp",
81
+ "@agentclientprotocol/claude-agent-acp@0.75.1",
82
82
  ]);
83
83
  });
84
84
 
@@ -198,8 +198,8 @@ describe("ensureAdapterInstalled", () => {
198
198
  (call) => (call[1] as string[])[2],
199
199
  );
200
200
  expect(installedPackages.sort()).toEqual([
201
- "@agentclientprotocol/claude-agent-acp",
202
- "@agentclientprotocol/codex-acp",
201
+ "@agentclientprotocol/claude-agent-acp@0.75.1",
202
+ "@agentclientprotocol/codex-acp@1.10.0",
203
203
  ]);
204
204
  });
205
205
  });
@@ -232,7 +232,7 @@ describe("resolveAgentWithAutoInstall - resolution order", () => {
232
232
  // The resolved command is the REAL binary, not a `bun x` wrapper.
233
233
  expect(result.resolved.agent.command).toBe("claude-agent-acp");
234
234
  expect(result.autoInstalledPackage).toBe(
235
- "@agentclientprotocol/claude-agent-acp",
235
+ "@agentclientprotocol/claude-agent-acp@0.75.1",
236
236
  );
237
237
  expect(result.failureMessage).toBeUndefined();
238
238
  expect(execFileMock).toHaveBeenCalledTimes(1);
@@ -264,12 +264,14 @@ describe("resolveAgentWithAutoInstall - resolution order", () => {
264
264
  return;
265
265
  }
266
266
  expect(result.resolved.agent.command).toBe("codex-acp");
267
- expect(result.autoInstalledPackage).toBe("@agentclientprotocol/codex-acp");
267
+ expect(result.autoInstalledPackage).toBe(
268
+ "@agentclientprotocol/codex-acp@1.10.0",
269
+ );
268
270
  expect(execFileMock).toHaveBeenCalledTimes(1);
269
271
  expect(execFileMock.mock.calls[0][1]).toEqual([
270
272
  "add",
271
273
  "--global",
272
- "@agentclientprotocol/codex-acp",
274
+ "@agentclientprotocol/codex-acp@1.10.0",
273
275
  ]);
274
276
  });
275
277
 
@@ -295,7 +297,7 @@ describe("resolveAgentWithAutoInstall - resolution order", () => {
295
297
  expect(result.resolved.ok).toBe(false);
296
298
  expect(result.failureMessage).toContain("claude-agent-acp is not on PATH");
297
299
  expect(result.failureMessage).toContain(
298
- "bun add -g @agentclientprotocol/claude-agent-acp",
300
+ "bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
299
301
  );
300
302
  expect(result.failureMessage).toContain("network is down");
301
303
  for (const call of execFileMock.mock.calls) {
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Renewal policy for the Claude OAuth credential the ACP spawn path injects as
3
+ * `CLAUDE_CODE_OAUTH_TOKEN`. Storage lives in `acp-claude-oauth.ts`; this
4
+ * module owns only the decision to refresh and the handling of what comes back.
5
+ *
6
+ * Setting `CLAUDE_CODE_OAUTH_TOKEN` tells the Claude Agent SDK to treat the
7
+ * value as a static bearer token: its own credential store and refresh
8
+ * machinery are skipped entirely. The SDK does expose a host-refresh hook,
9
+ * `getOAuthToken`, but `claude-agent-acp` never passes it through and it is
10
+ * absent from the SDK's public typings. With the env-var contract the adapter
11
+ * gives us, the daemon is the only thing that can renew this token.
12
+ *
13
+ * Every failure mode here returns without throwing and lets the spawn proceed
14
+ * with whatever token is stored. A dead token then fails at the adapter as a
15
+ * structured ACP `auth_required`, which raises the Connect card. Renewal is
16
+ * the fast path, not the error path, so it must never be the thing that fails
17
+ * a spawn.
18
+ *
19
+ * When the provider rejects the refresh token itself (revoked, or rotated out
20
+ * from under us), the stored refresh token is dropped. The recorded expiry is
21
+ * kept, because that pair (expired, no way to renew) is what makes
22
+ * `hasAcpClaudeToken()` answer "not connected" and keep the inline Connect
23
+ * card on screen.
24
+ */
25
+
26
+ import {
27
+ isCredentialError,
28
+ RefreshDeduplicator,
29
+ } from "@vellumai/credential-storage";
30
+
31
+ import { refreshOAuth2Token } from "../security/oauth2.js";
32
+ import { getLogger } from "../util/logger.js";
33
+ import {
34
+ CLAUDE_OAUTH_CONFIG,
35
+ clearAcpClaudeRefreshToken,
36
+ forgetAcpClaudeRenewalStateIfUnbound,
37
+ hasStoredAcpClaudeAccessToken,
38
+ isAcpClaudeTokenExpiring,
39
+ persistRefreshedAcpClaudeTokens,
40
+ readAcpClaudeRefreshToken,
41
+ } from "./acp-claude-oauth.js";
42
+ import { ACP_OAUTH_TOKEN_FIELD } from "./acp-credentials.js";
43
+ import { acpSpawnCredentialDenialReason } from "./prepare-agent-env.js";
44
+
45
+ const log = getLogger("acp:claude-token-refresh");
46
+
47
+ /**
48
+ * Single in-flight refresh across concurrent spawns. Anthropic rotates the
49
+ * refresh token on use, so two parallel refreshes would race and one would
50
+ * invalidate the other's token.
51
+ */
52
+ const deduplicator = new RefreshDeduplicator();
53
+ const REFRESH_KEY = "acp:claude";
54
+
55
+ /**
56
+ * Renew the stored Claude access token if it is expiring and a refresh token
57
+ * is available. Returns without throwing in every failure mode. Never returns
58
+ * the token: the plaintext read boundary stays with the credential broker, so
59
+ * callers re-read through it as usual.
60
+ */
61
+ export async function ensureFreshAcpClaudeToken(): Promise<void> {
62
+ // An explicit `allowedTools` that omits `acp_spawn` means the broker will
63
+ // deny the read this renewal exists to feed, so there is nothing to gain by
64
+ // spending a refresh token here. Checking first also keeps a passive spawn
65
+ // from touching a credential the workspace has fenced off.
66
+ if (acpSpawnCredentialDenialReason(ACP_OAUTH_TOKEN_FIELD) !== undefined) {
67
+ return;
68
+ }
69
+ // Companions can outlive a deleted access token. Renewal exists to replace
70
+ // that field, not to mint a new credential after the user removed it.
71
+ if (!(await hasStoredAcpClaudeAccessToken())) {
72
+ return;
73
+ }
74
+ try {
75
+ await forgetAcpClaudeRenewalStateIfUnbound();
76
+ } catch (err) {
77
+ log.debug(
78
+ { err },
79
+ "Could not drop unbound Claude OAuth renewal state before refresh",
80
+ );
81
+ }
82
+ if (!(await isAcpClaudeTokenExpiring())) {
83
+ return;
84
+ }
85
+
86
+ const refreshToken = await readAcpClaudeRefreshToken();
87
+ if (!refreshToken) {
88
+ log.info(
89
+ "Claude OAuth token is expiring and no refresh token is stored; " +
90
+ "the spawn will surface auth_required so the user can reconnect",
91
+ );
92
+ return;
93
+ }
94
+
95
+ try {
96
+ await deduplicator.deduplicate(REFRESH_KEY, () => doRefresh(refreshToken));
97
+ } catch (err) {
98
+ log.debug({ err }, "Claude OAuth token refresh did not complete");
99
+ }
100
+ }
101
+
102
+ async function doRefresh(refreshToken: string): Promise<string> {
103
+ log.info("Refreshing the Claude OAuth token for ACP");
104
+
105
+ let result;
106
+ try {
107
+ result = await refreshOAuth2Token(
108
+ CLAUDE_OAUTH_CONFIG.tokenExchangeUrl,
109
+ CLAUDE_OAUTH_CONFIG.clientId,
110
+ refreshToken,
111
+ // PKCE public client, so no secret. Anthropic's token endpoint takes a
112
+ // JSON body, matching the authorization-code exchange.
113
+ undefined,
114
+ undefined,
115
+ CLAUDE_OAUTH_CONFIG.tokenExchangeBodyFormat,
116
+ );
117
+ } catch (err) {
118
+ if (isCredentialError(err)) {
119
+ log.warn(
120
+ { err },
121
+ "Claude OAuth refresh token was rejected; dropping it so the account reads as needing a reconnect",
122
+ );
123
+ await clearAcpClaudeRefreshToken(refreshToken);
124
+ } else {
125
+ log.warn({ err }, "Claude OAuth token refresh failed transiently");
126
+ }
127
+ throw err;
128
+ }
129
+
130
+ if (!result.accessToken) {
131
+ log.warn(
132
+ "Claude OAuth refresh returned no usable access token; dropping the refresh token so the account reads as needing a reconnect",
133
+ );
134
+ await clearAcpClaudeRefreshToken(refreshToken);
135
+ throw new Error("Claude OAuth refresh returned no access token");
136
+ }
137
+
138
+ const persisted = await persistRefreshedAcpClaudeTokens(
139
+ {
140
+ accessToken: result.accessToken,
141
+ refreshToken: result.refreshToken,
142
+ expiresIn: result.expiresIn,
143
+ },
144
+ refreshToken,
145
+ );
146
+ if (persisted) {
147
+ log.info("Claude OAuth token refreshed");
148
+ }
149
+ return result.accessToken;
150
+ }
@@ -21,6 +21,7 @@ import type {
21
21
  ReleaseTerminalResponse,
22
22
  RequestPermissionRequest,
23
23
  RequestPermissionResponse,
24
+ SessionConfigOption,
24
25
  SessionNotification,
25
26
  TerminalOutputRequest,
26
27
  TerminalOutputResponse,
@@ -103,6 +104,9 @@ export class VellumAcpClientHandler implements Client {
103
104
  private readonly acpSessionId: string,
104
105
  private readonly sendToVellum: (msg: AssistantEvent) => void,
105
106
  private readonly parentConversationId: string,
107
+ private readonly onConfigOptions?: (
108
+ configOptions: SessionConfigOption[],
109
+ ) => void,
106
110
  ) {}
107
111
 
108
112
  /** Forwards an update to Vellum, stamping a contiguous per-session `seq`. */
@@ -182,7 +186,12 @@ export class VellumAcpClientHandler implements Client {
182
186
  async sessionUpdate(params: SessionNotification): Promise<void> {
183
187
  const update = params.update;
184
188
 
185
- if (this.suppressForwarding) {
189
+ // Config options are current state rather than transcript history, so they
190
+ // are applied even while a replay is being suppressed.
191
+ if (
192
+ this.suppressForwarding &&
193
+ update.sessionUpdate !== "config_option_update"
194
+ ) {
186
195
  log.debug(
187
196
  { acpSessionId: this.acpSessionId, updateType: update.sessionUpdate },
188
197
  "Dropping replayed session update during suppression",
@@ -286,10 +295,14 @@ export class VellumAcpClientHandler implements Client {
286
295
  break;
287
296
  }
288
297
 
298
+ case "config_option_update": {
299
+ this.onConfigOptions?.(update.configOptions);
300
+ break;
301
+ }
302
+
289
303
  default: {
290
304
  // Other update types (available_commands_update, current_mode_update,
291
- // config_option_update, session_info_update) are not forwarded to
292
- // Vellum.
305
+ // session_info_update) are not forwarded to Vellum.
293
306
  log.debug(
294
307
  {
295
308
  acpSessionId: this.acpSessionId,
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Tests for the pure ACP model-config helpers: flattening an adapter's model
3
+ * `SessionConfigOption` into publishable model info, and the spawn-time
4
+ * precedence ladder.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+
9
+ import type { SessionConfigOption } from "@agentclientprotocol/sdk";
10
+
11
+ import {
12
+ MODEL_OPTION_MODELS,
13
+ modelOption,
14
+ } from "./__tests__/helpers/acp-model-option.js";
15
+ import { deriveModelInfo, resolveAcpModel } from "./model-config.js";
16
+
17
+ const flatModelOption = modelOption("sonnet");
18
+
19
+ describe("deriveModelInfo", () => {
20
+ test("flattens flat options and reports the current value", () => {
21
+ expect(deriveModelInfo([flatModelOption])).toEqual({
22
+ model: "sonnet",
23
+ modelConfigId: "model",
24
+ availableModels: MODEL_OPTION_MODELS,
25
+ });
26
+ });
27
+
28
+ test("flattens grouped options and carries the group name", () => {
29
+ const grouped: SessionConfigOption = {
30
+ type: "select",
31
+ id: "model",
32
+ name: "Model",
33
+ category: "model",
34
+ currentValue: "opus",
35
+ options: [
36
+ {
37
+ group: "anthropic",
38
+ name: "Anthropic",
39
+ options: [
40
+ { value: "opus", name: "Opus" },
41
+ { value: "haiku", name: "Haiku", description: "Fast" },
42
+ ],
43
+ },
44
+ {
45
+ group: "aliases",
46
+ name: "Aliases",
47
+ options: [{ value: "best", name: "Best available" }],
48
+ },
49
+ ],
50
+ };
51
+
52
+ expect(deriveModelInfo([grouped])).toEqual({
53
+ model: "opus",
54
+ modelConfigId: "model",
55
+ availableModels: [
56
+ { value: "opus", label: "Opus", group: "Anthropic" },
57
+ {
58
+ value: "haiku",
59
+ label: "Haiku",
60
+ description: "Fast",
61
+ group: "Anthropic",
62
+ },
63
+ { value: "best", label: "Best available", group: "Aliases" },
64
+ ],
65
+ });
66
+ });
67
+
68
+ test("matches on id alone when no category is reported", () => {
69
+ const uncategorized: SessionConfigOption = {
70
+ type: "select",
71
+ id: "model",
72
+ name: "Model",
73
+ currentValue: "haiku",
74
+ options: [{ value: "haiku", name: "Haiku" }],
75
+ };
76
+
77
+ const info = deriveModelInfo([uncategorized]);
78
+
79
+ expect(info.modelConfigId).toBe("model");
80
+ expect(info.model).toBe("haiku");
81
+ });
82
+
83
+ test("matches on category alone when the id differs", () => {
84
+ const byCategory: SessionConfigOption = {
85
+ type: "select",
86
+ id: "llm",
87
+ name: "LLM",
88
+ category: "model",
89
+ currentValue: "gpt-5",
90
+ options: [{ value: "gpt-5", name: "GPT-5" }],
91
+ };
92
+
93
+ expect(deriveModelInfo([byCategory]).modelConfigId).toBe("llm");
94
+ });
95
+
96
+ test("ignores a boolean option even when its category is model", () => {
97
+ const booleanOption: SessionConfigOption = {
98
+ type: "boolean",
99
+ id: "model",
100
+ name: "Model",
101
+ category: "model",
102
+ currentValue: true,
103
+ };
104
+
105
+ expect(deriveModelInfo([booleanOption])).toEqual({ availableModels: [] });
106
+ });
107
+
108
+ test("skips non-model select options and picks the first match", () => {
109
+ const mode: SessionConfigOption = {
110
+ type: "select",
111
+ id: "mode",
112
+ name: "Mode",
113
+ category: "mode",
114
+ currentValue: "ask",
115
+ options: [{ value: "ask", name: "Ask" }],
116
+ };
117
+
118
+ expect(deriveModelInfo([mode, flatModelOption]).modelConfigId).toBe(
119
+ "model",
120
+ );
121
+ });
122
+
123
+ test("returns no model info for an empty array", () => {
124
+ expect(deriveModelInfo([])).toEqual({ availableModels: [] });
125
+ });
126
+
127
+ test("returns no model info for null", () => {
128
+ expect(deriveModelInfo(null)).toEqual({ availableModels: [] });
129
+ });
130
+
131
+ test("returns no model info for undefined", () => {
132
+ expect(deriveModelInfo(undefined)).toEqual({ availableModels: [] });
133
+ });
134
+ });
135
+
136
+ describe("resolveAcpModel", () => {
137
+ test("prefers the requested model over the per-agent one", () => {
138
+ expect(
139
+ resolveAcpModel({ requestedModel: "opus", agentModel: "haiku" }),
140
+ ).toBe("opus");
141
+ });
142
+
143
+ test("falls back to the per-agent model", () => {
144
+ expect(resolveAcpModel({ agentModel: "haiku" })).toBe("haiku");
145
+ });
146
+
147
+ test("returns undefined when every rung is unset", () => {
148
+ expect(resolveAcpModel({})).toBeUndefined();
149
+ });
150
+
151
+ test("treats blank and whitespace-only values as unset", () => {
152
+ expect(
153
+ resolveAcpModel({ requestedModel: "\t\n", agentModel: "haiku" }),
154
+ ).toBe("haiku");
155
+ });
156
+
157
+ test("returns undefined when every rung is whitespace", () => {
158
+ expect(
159
+ resolveAcpModel({ requestedModel: " ", agentModel: " " }),
160
+ ).toBeUndefined();
161
+ });
162
+
163
+ test("trims the value it returns", () => {
164
+ expect(resolveAcpModel({ requestedModel: " opus " })).toBe("opus");
165
+ });
166
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Pure helpers for ACP session model selection.
3
+ *
4
+ * ACP 0.25.0 has no `session/set_model`: a model is one `SessionConfigOption`
5
+ * among the set an adapter reports from `session/new`, `session/load`,
6
+ * `session/resume`, and `session/set_config_option`. `deriveModelInfo` turns
7
+ * that raw set into the trio the daemon publishes and persists (current model,
8
+ * selectable options, and the config id to write back through
9
+ * `setSessionConfigOption`), flattening the grouped `options` variant so
10
+ * callers never branch on it.
11
+ *
12
+ * The model option is identified by `type === "select"` and `id === "model"`
13
+ * or `category === "model"` only. Option `name` is a human label an adapter
14
+ * may localize or restyle at will, so it is never matched against.
15
+ *
16
+ * `resolveAcpModel` is the precedence ladder a spawn or a resume walks before
17
+ * it talks to the adapter: an explicit request beats the per-agent default from
18
+ * `acp.agents.<id>.model`, and nothing named there leaves the adapter on its
19
+ * own. Values are adapter-reported aliases (`opus`, `sonnet`), never Assistant
20
+ * catalog ids, and are passed through unvalidated: only the adapter knows what
21
+ * it accepts.
22
+ *
23
+ * Both helpers are synchronous and side-effect free.
24
+ */
25
+
26
+ import type {
27
+ SessionConfigOption,
28
+ SessionConfigSelectGroup,
29
+ SessionConfigSelectOption,
30
+ } from "@agentclientprotocol/sdk";
31
+ import type { z } from "zod";
32
+
33
+ import type { AcpSessionModelUpdateEventSchema } from "../api/events/acp-session-model-update.js";
34
+
35
+ /**
36
+ * A selectable model as reported by the adapter, flattened out of its group.
37
+ * Derived from the event schema so the published wire shape and the shape the
38
+ * daemon carries in memory cannot drift apart.
39
+ */
40
+ export type AcpModelOption = z.infer<
41
+ typeof AcpSessionModelUpdateEventSchema
42
+ >["availableModels"][number];
43
+
44
+ /** What an adapter's config-option set says about the session's model. */
45
+ export type AcpModelInfo = {
46
+ model?: string;
47
+ availableModels: AcpModelOption[];
48
+ modelConfigId?: string;
49
+ };
50
+
51
+ type ResolveAcpModelInput = {
52
+ requestedModel?: string;
53
+ agentModel?: string;
54
+ };
55
+
56
+ type ModelSelectOption = Extract<SessionConfigOption, { type: "select" }>;
57
+
58
+ function isGroup(
59
+ entry: SessionConfigSelectOption | SessionConfigSelectGroup,
60
+ ): entry is SessionConfigSelectGroup {
61
+ return "group" in entry;
62
+ }
63
+
64
+ function toModelOption(
65
+ option: SessionConfigSelectOption,
66
+ group?: string,
67
+ ): AcpModelOption {
68
+ return {
69
+ value: option.value,
70
+ label: option.name,
71
+ ...(option.description ? { description: option.description } : {}),
72
+ ...(group ? { group } : {}),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * The adapter's model selector, if it advertises one. `category` is UX-only
78
+ * per the ACP spec, so a `model` id counts too; non-select options never do.
79
+ */
80
+ function findModelConfigOption(
81
+ configOptions: SessionConfigOption[] | null | undefined,
82
+ ): ModelSelectOption | undefined {
83
+ return configOptions?.find(
84
+ (option): option is ModelSelectOption =>
85
+ option.type === "select" &&
86
+ (option.id === "model" || option.category === "model"),
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Extract current model, selectable models, and the config id to write back.
92
+ * Returns `{ availableModels: [] }` when the adapter advertises no model
93
+ * selector, which is how the whole feature degrades to invisible.
94
+ */
95
+ export function deriveModelInfo(
96
+ configOptions: SessionConfigOption[] | null | undefined,
97
+ ): AcpModelInfo {
98
+ const modelOption = findModelConfigOption(configOptions);
99
+
100
+ if (!modelOption) {
101
+ return { availableModels: [] };
102
+ }
103
+
104
+ const availableModels: AcpModelOption[] = [];
105
+ for (const entry of modelOption.options) {
106
+ if (isGroup(entry)) {
107
+ for (const option of entry.options) {
108
+ availableModels.push(toModelOption(option, entry.name));
109
+ }
110
+ continue;
111
+ }
112
+ availableModels.push(toModelOption(entry));
113
+ }
114
+
115
+ return {
116
+ ...(modelOption.currentValue ? { model: modelOption.currentValue } : {}),
117
+ availableModels,
118
+ modelConfigId: modelOption.id,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Pick the model a session should start on. Blank and whitespace-only values
124
+ * count as unset so an empty config field never pins anything.
125
+ */
126
+ export function resolveAcpModel(
127
+ input: ResolveAcpModelInput,
128
+ ): string | undefined {
129
+ const ladder = [input.requestedModel, input.agentModel];
130
+
131
+ for (const candidate of ladder) {
132
+ const trimmed = candidate?.trim();
133
+ if (trimmed) {
134
+ return trimmed;
135
+ }
136
+ }
137
+ return undefined;
138
+ }
@@ -199,6 +199,26 @@ export function acpSpawnCredentialDenialReason(
199
199
  * mode, including a simply-absent credential, as `{ success: false,
200
200
  * reason }`, so callers choose whether a miss is fatal.
201
201
  */
202
+ /**
203
+ * Vault path for `CLAUDE_CODE_OAUTH_TOKEN`. Renews first so a near-expired
204
+ * stored token is replaced before the broker read. Config overrides never
205
+ * reach here, so this does not spend the vault refresh token on a spawn
206
+ * that will not use it.
207
+ */
208
+ async function injectClaudeOauthFromVault(
209
+ env: Record<string, string>,
210
+ ): Promise<string | undefined> {
211
+ const { ensureFreshAcpClaudeToken } =
212
+ await import("./claude-token-refresh.js");
213
+ await ensureFreshAcpClaudeToken();
214
+ return injectCredential(
215
+ env,
216
+ ACP_OAUTH_TOKEN_FIELD,
217
+ "CLAUDE_CODE_OAUTH_TOKEN",
218
+ ACP_CLAUDE_OAUTH_USAGE_DESCRIPTION,
219
+ );
220
+ }
221
+
202
222
  async function injectCredential(
203
223
  env: Record<string, string>,
204
224
  field: string,
@@ -371,12 +391,7 @@ export async function prepareAgentEnv(
371
391
  }
372
392
  let missReason: string | undefined;
373
393
  if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
374
- missReason = await injectCredential(
375
- env,
376
- ACP_OAUTH_TOKEN_FIELD,
377
- "CLAUDE_CODE_OAUTH_TOKEN",
378
- ACP_CLAUDE_OAUTH_USAGE_DESCRIPTION,
379
- );
394
+ missReason = await injectClaudeOauthFromVault(env);
380
395
  }
381
396
  // Any api-key-shaped value still standing here came from the vault read:
382
397
  // the config override was already dropped above, and the read only runs