@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
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { describe, expect, test } from "bun:test";
8
8
 
9
+ import { RequestError } from "@agentclientprotocol/sdk";
10
+
9
11
  import { AcpAuthRequiredEventSchema } from "../../api/events/acp-auth-required.js";
10
12
  import { AcpSessionErrorEventSchema } from "../../api/events/acp-session-error.js";
11
13
  import {
@@ -15,6 +17,7 @@ import {
15
17
  CLAUDE_ACP_COMMAND,
16
18
  isAcpAuthRequired,
17
19
  isClaudeAuthFailureMessage,
20
+ requestErrorReason,
18
21
  } from "../auth-required.js";
19
22
 
20
23
  describe("isAcpAuthRequired", () => {
@@ -89,6 +92,99 @@ describe("isClaudeAuthFailureMessage", () => {
89
92
  });
90
93
  });
91
94
 
95
+ describe("requestErrorReason", () => {
96
+ test("decodes the sentence the SDK moved into the payload", () => {
97
+ const rejection = new RequestError(-32603, "Internal error", {
98
+ details: "Failed to authenticate. Please run /login",
99
+ });
100
+
101
+ expect(requestErrorReason(rejection)).toBe(
102
+ "Failed to authenticate. Please run /login",
103
+ );
104
+ // The point of decoding: the generic message never matches.
105
+ expect(isClaudeAuthFailureMessage(rejection.message)).toBe(false);
106
+ expect(isClaudeAuthFailureMessage(requestErrorReason(rejection))).toBe(
107
+ true,
108
+ );
109
+ });
110
+
111
+ test("reads the message off a rejection that carries no payload", () => {
112
+ // Straight off the wire: a plain object, not an instance of any class we
113
+ // control, and with the auth text where the SDK leaves a framed one.
114
+ const rejection = {
115
+ code: -32603,
116
+ message: "Failed to authenticate. Please run /login",
117
+ };
118
+
119
+ expect(requestErrorReason(rejection)).toBe(
120
+ "Failed to authenticate. Please run /login",
121
+ );
122
+ expect(isClaudeAuthFailureMessage(requestErrorReason(rejection))).toBe(
123
+ true,
124
+ );
125
+ });
126
+
127
+ test("keeps the rejection's own message when the payload names no reason", () => {
128
+ expect(requestErrorReason(new RequestError(-32603, "boom", { c: 7 }))).toBe(
129
+ "boom",
130
+ );
131
+ expect(
132
+ requestErrorReason(
133
+ RequestError.methodNotFound("session/set_config_option"),
134
+ ),
135
+ ).toBe('"Method not found": session/set_config_option');
136
+ expect(requestErrorReason(new Error("Not logged in"))).toBe(
137
+ "Not logged in",
138
+ );
139
+ });
140
+
141
+ test("keeps a specific rejection message ahead of supplemental details", () => {
142
+ expect(
143
+ requestErrorReason(
144
+ new RequestError(-32603, "Model unavailable", {
145
+ details: "The selected deployment is temporarily unavailable",
146
+ }),
147
+ ),
148
+ ).toBe("Model unavailable");
149
+ });
150
+
151
+ test("serializes the payload behind a bare Internal error that names no reason", () => {
152
+ expect(
153
+ requestErrorReason(new RequestError(-32603, "Internal error", { c: 7 })),
154
+ ).toBe('{"c":7}');
155
+ });
156
+
157
+ test("serializes the payload behind a generic Invalid params", () => {
158
+ // How the agent-side SDK answers a request its schema rejects.
159
+ expect(
160
+ requestErrorReason(
161
+ RequestError.invalidParams({ _errors: ["model: Invalid option"] }),
162
+ ),
163
+ ).toBe('{"_errors":["model: Invalid option"]}');
164
+ });
165
+
166
+ test("decodes claude-agent-acp's prompt-time 401 from its message", () => {
167
+ // RequestError.internalError({ errorKind }, cliText) as the adapter raises
168
+ // it: the CLI's text rides the message and the payload only names a kind.
169
+ const rejection = new RequestError(
170
+ -32603,
171
+ "Internal error: Failed to authenticate. API Error: 401 OAuth access token has expired.",
172
+ { errorKind: "authentication_failed" },
173
+ );
174
+
175
+ expect(requestErrorReason(rejection)).toBe(rejection.message);
176
+ expect(isClaudeAuthFailureMessage(requestErrorReason(rejection))).toBe(
177
+ true,
178
+ );
179
+ });
180
+
181
+ test("stringifies a rejection that is not an object at all", () => {
182
+ expect(requestErrorReason("plain string")).toBe("plain string");
183
+ expect(requestErrorReason(-32603)).toBe("-32603");
184
+ expect(requestErrorReason(null)).toBe("null");
185
+ });
186
+ });
187
+
92
188
  describe("AcpAuthRequiredError", () => {
93
189
  test("is an Error that carries the agent id and its message", () => {
94
190
  const err = new AcpAuthRequiredError("claude", "needs a reconnect");
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Tests for the Claude ACP OAuth renewal policy.
3
+ *
4
+ * Storage is mocked (it belongs to `acp-claude-oauth.ts`, tested separately) so
5
+ * these assert only the decisions this module owns: when to spend the refresh
6
+ * token, what to persist, and what to tear down when the provider rejects it.
7
+ *
8
+ * `isCredentialError` is deliberately not mocked. Whether a failure is
9
+ * permanent or transient is the hinge of the whole module, and asserting it
10
+ * against the real classifier is the only way these tests stay honest about
11
+ * which thrown errors clear a user's stored credential.
12
+ */
13
+
14
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
15
+
16
+ import type { OAuth2TokenResult } from "../../security/oauth2.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Mocks: wired BEFORE importing the module via dynamic import.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ let expiring = false;
23
+ let storedAccessToken = true;
24
+ let storedRefreshToken: string | null = null;
25
+ let refreshImpl: () => Promise<OAuth2TokenResult> = async () => {
26
+ throw new Error("refreshOAuth2Token not stubbed for this test");
27
+ };
28
+
29
+ let spawnDenial: string | undefined = undefined;
30
+ const isAcpClaudeTokenExpiring = mock(async () => expiring);
31
+ const hasStoredAcpClaudeAccessToken = mock(async () => storedAccessToken);
32
+ const forgetAcpClaudeRenewalStateIfUnbound = mock(async () => {});
33
+ const readAcpClaudeRefreshToken = mock(async () => storedRefreshToken);
34
+ const persistRefreshedAcpClaudeTokens = mock(
35
+ async (_tokens: unknown, _expectedRefreshToken: string) => true,
36
+ );
37
+ const clearAcpClaudeRefreshToken = mock(async () => {});
38
+ const acpSpawnCredentialDenialReason = mock((_field: string) => spawnDenial);
39
+ const refreshOAuth2Token = mock(async (..._args: unknown[]) => refreshImpl());
40
+
41
+ mock.module("../acp-claude-oauth.js", () => ({
42
+ CLAUDE_OAUTH_CONFIG: {
43
+ tokenExchangeUrl: "https://platform.claude.com/v1/oauth/token",
44
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
45
+ tokenExchangeBodyFormat: "json",
46
+ },
47
+ isAcpClaudeTokenExpiring,
48
+ hasStoredAcpClaudeAccessToken,
49
+ forgetAcpClaudeRenewalStateIfUnbound,
50
+ readAcpClaudeRefreshToken,
51
+ persistRefreshedAcpClaudeTokens,
52
+ clearAcpClaudeRefreshToken,
53
+ }));
54
+ mock.module("../prepare-agent-env.js", () => ({
55
+ acpSpawnCredentialDenialReason,
56
+ }));
57
+ mock.module("../../security/oauth2.js", () => ({ refreshOAuth2Token }));
58
+
59
+ const { ensureFreshAcpClaudeToken } =
60
+ await import("../claude-token-refresh.js");
61
+
62
+ beforeEach(() => {
63
+ expiring = false;
64
+ storedAccessToken = true;
65
+ storedRefreshToken = null;
66
+ spawnDenial = undefined;
67
+ refreshImpl = async () => {
68
+ throw new Error("refreshOAuth2Token not stubbed for this test");
69
+ };
70
+ isAcpClaudeTokenExpiring.mockClear();
71
+ hasStoredAcpClaudeAccessToken.mockClear();
72
+ forgetAcpClaudeRenewalStateIfUnbound.mockClear();
73
+ readAcpClaudeRefreshToken.mockClear();
74
+ persistRefreshedAcpClaudeTokens.mockClear();
75
+ clearAcpClaudeRefreshToken.mockClear();
76
+ acpSpawnCredentialDenialReason.mockClear();
77
+ refreshOAuth2Token.mockClear();
78
+ });
79
+
80
+ describe("ensureFreshAcpClaudeToken: skip paths", () => {
81
+ test("does nothing while the token is still fresh", async () => {
82
+ expiring = false;
83
+ storedRefreshToken = "refresh-me";
84
+
85
+ await ensureFreshAcpClaudeToken();
86
+
87
+ expect(refreshOAuth2Token).not.toHaveBeenCalled();
88
+ expect(persistRefreshedAcpClaudeTokens).not.toHaveBeenCalled();
89
+ });
90
+
91
+ test("does nothing when expiring with no refresh token stored", async () => {
92
+ expiring = true;
93
+ storedRefreshToken = null;
94
+
95
+ await ensureFreshAcpClaudeToken();
96
+
97
+ expect(refreshOAuth2Token).not.toHaveBeenCalled();
98
+ expect(clearAcpClaudeRefreshToken).not.toHaveBeenCalled();
99
+ });
100
+
101
+ test("drops unbound leftover refresh material before deciding to renew", async () => {
102
+ expiring = false;
103
+ storedAccessToken = true;
104
+
105
+ await ensureFreshAcpClaudeToken();
106
+
107
+ expect(forgetAcpClaudeRenewalStateIfUnbound).toHaveBeenCalledTimes(1);
108
+ expect(refreshOAuth2Token).not.toHaveBeenCalled();
109
+ });
110
+
111
+ test("does not mint a new access token after the stored one was deleted", async () => {
112
+ expiring = true;
113
+ storedAccessToken = false;
114
+ storedRefreshToken = "refresh-leftover";
115
+
116
+ await ensureFreshAcpClaudeToken();
117
+
118
+ expect(refreshOAuth2Token).not.toHaveBeenCalled();
119
+ expect(persistRefreshedAcpClaudeTokens).not.toHaveBeenCalled();
120
+ expect(clearAcpClaudeRefreshToken).not.toHaveBeenCalled();
121
+ });
122
+ });
123
+
124
+ describe("ensureFreshAcpClaudeToken: renewal", () => {
125
+ test("refreshes against Claude's token endpoint and persists the rotated set", async () => {
126
+ expiring = true;
127
+ storedRefreshToken = "refresh-me";
128
+ refreshImpl = async () => ({
129
+ accessToken: "sk-ant-oat-new",
130
+ refreshToken: "refresh-rotated",
131
+ expiresIn: 3600,
132
+ });
133
+
134
+ await ensureFreshAcpClaudeToken();
135
+
136
+ expect(refreshOAuth2Token).toHaveBeenCalledTimes(1);
137
+ const args = refreshOAuth2Token.mock.calls[0];
138
+ expect(args[0]).toBe("https://platform.claude.com/v1/oauth/token");
139
+ expect(args[1]).toBe("9d1c250a-e61b-44d9-88ed-5944d1962f5e");
140
+ expect(args[2]).toBe("refresh-me");
141
+ expect(args[3]).toBeUndefined();
142
+ expect(args[5]).toBe("json");
143
+
144
+ expect(persistRefreshedAcpClaudeTokens).toHaveBeenCalledWith(
145
+ {
146
+ accessToken: "sk-ant-oat-new",
147
+ refreshToken: "refresh-rotated",
148
+ expiresIn: 3600,
149
+ },
150
+ "refresh-me",
151
+ );
152
+ });
153
+
154
+ test("does not treat a skipped stale persist as a failure", async () => {
155
+ expiring = true;
156
+ storedRefreshToken = "refresh-me";
157
+ persistRefreshedAcpClaudeTokens.mockResolvedValueOnce(false);
158
+ refreshImpl = async () => ({
159
+ accessToken: "sk-ant-oat-stale",
160
+ refreshToken: "refresh-rotated",
161
+ expiresIn: 3600,
162
+ });
163
+
164
+ await expect(ensureFreshAcpClaudeToken()).resolves.toBeUndefined();
165
+ expect(clearAcpClaudeRefreshToken).not.toHaveBeenCalled();
166
+ });
167
+
168
+ test("runs one refresh for concurrent spawns", async () => {
169
+ expiring = true;
170
+ storedRefreshToken = "refresh-me";
171
+
172
+ let resolveRefresh!: (v: OAuth2TokenResult) => void;
173
+ const gate = new Promise<OAuth2TokenResult>((resolve) => {
174
+ resolveRefresh = resolve;
175
+ });
176
+ refreshImpl = () => gate;
177
+
178
+ const both = Promise.all([
179
+ ensureFreshAcpClaudeToken(),
180
+ ensureFreshAcpClaudeToken(),
181
+ ]);
182
+ for (let i = 0; i < 20; i++) {
183
+ await Promise.resolve();
184
+ }
185
+ resolveRefresh({ accessToken: "sk-ant-oat-new" });
186
+ await both;
187
+
188
+ expect(refreshOAuth2Token).toHaveBeenCalledTimes(1);
189
+ });
190
+ });
191
+
192
+ describe("ensureFreshAcpClaudeToken: policy", () => {
193
+ test("does not refresh when the spawn policy denies reading the credential", async () => {
194
+ expiring = true;
195
+ storedRefreshToken = "refresh-me";
196
+ spawnDenial = 'Tool "acp_spawn" is not allowed to use this credential';
197
+
198
+ await ensureFreshAcpClaudeToken();
199
+
200
+ expect(refreshOAuth2Token).not.toHaveBeenCalled();
201
+ expect(persistRefreshedAcpClaudeTokens).not.toHaveBeenCalled();
202
+ });
203
+ });
204
+
205
+ describe("ensureFreshAcpClaudeToken: failures", () => {
206
+ test("clears refresh material when the provider rejects the refresh token", async () => {
207
+ expiring = true;
208
+ storedRefreshToken = "revoked";
209
+ refreshImpl = async () => {
210
+ throw new Error("OAuth2 token refresh failed (HTTP 400 invalid_grant)");
211
+ };
212
+
213
+ await ensureFreshAcpClaudeToken();
214
+
215
+ expect(clearAcpClaudeRefreshToken).toHaveBeenCalledWith("revoked");
216
+ expect(persistRefreshedAcpClaudeTokens).not.toHaveBeenCalled();
217
+ });
218
+
219
+ test("clears refresh material when the refresh succeeds with no access token", async () => {
220
+ expiring = true;
221
+ storedRefreshToken = "refresh-me";
222
+ refreshImpl = async () =>
223
+ ({
224
+ accessToken: "",
225
+ refreshToken: "refresh-rotated",
226
+ expiresIn: 3600,
227
+ }) as OAuth2TokenResult;
228
+
229
+ await ensureFreshAcpClaudeToken();
230
+
231
+ expect(clearAcpClaudeRefreshToken).toHaveBeenCalledWith("refresh-me");
232
+ expect(persistRefreshedAcpClaudeTokens).not.toHaveBeenCalled();
233
+ });
234
+
235
+ test("keeps refresh material on a transient failure", async () => {
236
+ expiring = true;
237
+ storedRefreshToken = "refresh-me";
238
+ refreshImpl = async () => {
239
+ throw new Error("fetch failed: ECONNREFUSED");
240
+ };
241
+
242
+ await ensureFreshAcpClaudeToken();
243
+
244
+ expect(clearAcpClaudeRefreshToken).not.toHaveBeenCalled();
245
+ expect(persistRefreshedAcpClaudeTokens).not.toHaveBeenCalled();
246
+ });
247
+
248
+ test("never throws, so a refresh outage cannot fail the spawn", async () => {
249
+ expiring = true;
250
+ storedRefreshToken = "refresh-me";
251
+ refreshImpl = async () => {
252
+ throw new Error("HTTP 401 unauthorized");
253
+ };
254
+
255
+ await expect(ensureFreshAcpClaudeToken()).resolves.toBeUndefined();
256
+ });
257
+ });
@@ -1,9 +1,13 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import type { SessionNotification } from "@agentclientprotocol/sdk";
3
+ import type {
4
+ SessionConfigOption,
5
+ SessionNotification,
6
+ } from "@agentclientprotocol/sdk";
4
7
 
5
8
  import type { AssistantEvent } from "../../api/index.js";
6
9
  import { VellumAcpClientHandler } from "../client-handler.js";
10
+ import { modelOption } from "./helpers/acp-model-option.js";
7
11
 
8
12
  const ACP_SESSION_ID = "acp-session-abc";
9
13
  const PARENT_CONVERSATION_ID = "conv-xyz";
@@ -11,16 +15,21 @@ const PARENT_CONVERSATION_ID = "conv-xyz";
11
15
  function makeHandler(): {
12
16
  handler: VellumAcpClientHandler;
13
17
  sent: AssistantEvent[];
18
+ configOptionUpdates: SessionConfigOption[][];
14
19
  } {
15
20
  const sent: AssistantEvent[] = [];
21
+ const configOptionUpdates: SessionConfigOption[][] = [];
16
22
  const handler = new VellumAcpClientHandler(
17
23
  ACP_SESSION_ID,
18
24
  (msg) => {
19
25
  sent.push(msg);
20
26
  },
21
27
  PARENT_CONVERSATION_ID,
28
+ (configOptions) => {
29
+ configOptionUpdates.push(configOptions);
30
+ },
22
31
  );
23
- return { handler, sent };
32
+ return { handler, sent, configOptionUpdates };
24
33
  }
25
34
 
26
35
  describe("VellumAcpClientHandler.sessionUpdate", () => {
@@ -782,3 +791,82 @@ describe("VellumAcpClientHandler seq + enriched fields", () => {
782
791
  expect(msg.toolTitle).toContain("<redacted");
783
792
  });
784
793
  });
794
+
795
+ describe("VellumAcpClientHandler config_option_update", () => {
796
+ const MODEL_OPTION = modelOption("sonnet");
797
+
798
+ const THOUGHT_OPTION: SessionConfigOption = {
799
+ type: "boolean",
800
+ id: "thinking",
801
+ name: "Extended thinking",
802
+ currentValue: true,
803
+ };
804
+
805
+ function configOptionUpdate(
806
+ configOptions: SessionConfigOption[],
807
+ ): SessionNotification {
808
+ return {
809
+ sessionId: ACP_SESSION_ID,
810
+ update: { sessionUpdate: "config_option_update", configOptions },
811
+ };
812
+ }
813
+
814
+ test("invokes the callback with the full option array", async () => {
815
+ const { handler, sent, configOptionUpdates } = makeHandler();
816
+
817
+ await handler.sessionUpdate(
818
+ configOptionUpdate([MODEL_OPTION, THOUGHT_OPTION]),
819
+ );
820
+
821
+ expect(configOptionUpdates).toEqual([[MODEL_OPTION, THOUGHT_OPTION]]);
822
+ expect(sent).toHaveLength(0);
823
+ });
824
+
825
+ test("callback fires during replay suppression", async () => {
826
+ const { handler, sent, configOptionUpdates } = makeHandler();
827
+
828
+ handler.beginReplaySuppression();
829
+ await handler.sessionUpdate(configOptionUpdate([MODEL_OPTION]));
830
+
831
+ expect(configOptionUpdates).toEqual([[MODEL_OPTION]]);
832
+ expect(sent).toHaveLength(0);
833
+ });
834
+
835
+ test("a handler without the callback tolerates the update", async () => {
836
+ const sent: AssistantEvent[] = [];
837
+ const handler = new VellumAcpClientHandler(
838
+ ACP_SESSION_ID,
839
+ (msg) => {
840
+ sent.push(msg);
841
+ },
842
+ PARENT_CONVERSATION_ID,
843
+ );
844
+
845
+ await handler.sessionUpdate(configOptionUpdate([MODEL_OPTION]));
846
+
847
+ expect(sent).toHaveLength(0);
848
+ });
849
+
850
+ test("the remaining unhandled update types stay unforwarded", async () => {
851
+ const { handler, sent, configOptionUpdates } = makeHandler();
852
+
853
+ await handler.sessionUpdate({
854
+ sessionId: ACP_SESSION_ID,
855
+ update: {
856
+ sessionUpdate: "available_commands_update",
857
+ availableCommands: [{ name: "create_plan", description: "Plan work" }],
858
+ },
859
+ });
860
+ await handler.sessionUpdate({
861
+ sessionId: ACP_SESSION_ID,
862
+ update: { sessionUpdate: "current_mode_update", currentModeId: "ask" },
863
+ });
864
+ await handler.sessionUpdate({
865
+ sessionId: ACP_SESSION_ID,
866
+ update: { sessionUpdate: "session_info_update", title: "Refactor" },
867
+ });
868
+
869
+ expect(sent).toHaveLength(0);
870
+ expect(configOptionUpdates).toHaveLength(0);
871
+ });
872
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared test fixture: the `SessionConfigOption` an adapter advertises for
3
+ * model selection, shaped the way claude-agent-acp reports one (a `select`
4
+ * with id and category `model`).
5
+ */
6
+
7
+ import type { SessionConfigOption } from "@agentclientprotocol/sdk";
8
+
9
+ /** A model selector currently sitting on `currentValue`. */
10
+ export function modelOption(currentValue: string): SessionConfigOption {
11
+ return {
12
+ type: "select",
13
+ id: "model",
14
+ name: "Model",
15
+ category: "model",
16
+ currentValue,
17
+ options: [
18
+ { value: "sonnet", name: "Sonnet" },
19
+ { value: "opus", name: "Opus", description: "Most capable" },
20
+ ],
21
+ };
22
+ }
23
+
24
+ /**
25
+ * A model selector sitting on nothing. `deriveModelInfo` reads an empty
26
+ * `currentValue` as "no model reported", which is the only shape that leaves a
27
+ * resumed session with a selector to pin through.
28
+ */
29
+ export function modelOptionWithoutCurrent(): SessionConfigOption {
30
+ return modelOption("");
31
+ }
32
+
33
+ /**
34
+ * A selector that is not the model one, so a set carrying only this reads as
35
+ * an adapter that advertises no model selection.
36
+ */
37
+ export function nonModelOption(): SessionConfigOption {
38
+ return {
39
+ type: "select",
40
+ id: "mode",
41
+ name: "Mode",
42
+ currentValue: "default",
43
+ options: [{ value: "default", name: "Default" }],
44
+ };
45
+ }
46
+
47
+ /** `modelOption`'s options as `deriveModelInfo` flattens them. */
48
+ export const MODEL_OPTION_MODELS = [
49
+ { value: "sonnet", label: "Sonnet" },
50
+ { value: "opus", label: "Opus", description: "Most capable" },
51
+ ];
@@ -38,7 +38,17 @@ import {
38
38
  getCredentialMetadata,
39
39
  upsertCredentialMetadata,
40
40
  } from "../../tools/credentials/metadata-store.js";
41
- import { ACP_OAUTH_TOKEN_FIELD, ACP_SERVICE } from "../acp-credentials.js";
41
+ import {
42
+ ACP_OAUTH_EXPIRES_AT_FIELD,
43
+ ACP_OAUTH_REFRESH_TOKEN_FIELD,
44
+ ACP_OAUTH_TOKEN_FIELD,
45
+ ACP_SERVICE,
46
+ } from "../acp-credentials.js";
47
+
48
+ const ensureFreshAcpClaudeToken = mock(async () => {});
49
+ mock.module("../claude-token-refresh.js", () => ({
50
+ ensureFreshAcpClaudeToken,
51
+ }));
42
52
 
43
53
  const mockLogWarn = mock((_fields: unknown, _msg: string) => {});
44
54
 
@@ -76,6 +86,7 @@ beforeEach(() => {
76
86
  _resetBackend();
77
87
  _setMetadataPath(join(TEST_DIR, "metadata.json"));
78
88
  mockLogWarn.mockClear();
89
+ ensureFreshAcpClaudeToken.mockClear();
79
90
  });
80
91
 
81
92
  afterEach(() => {
@@ -127,6 +138,54 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
127
138
  });
128
139
 
129
140
  expect(prepared.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("vault-AAA");
141
+ expect(ensureFreshAcpClaudeToken).toHaveBeenCalledTimes(1);
142
+ });
143
+
144
+ test("renews the vault token before a spawn, not when a config override supplies one", async () => {
145
+ await seedVaultToken("vault-unused");
146
+
147
+ const prepared = await prepareAgentEnv({
148
+ command: "claude-agent-acp",
149
+ args: [],
150
+ env: { CLAUDE_CODE_OAUTH_TOKEN: "config-override" },
151
+ });
152
+
153
+ expect(prepared.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("config-override");
154
+ expect(ensureFreshAcpClaudeToken).not.toHaveBeenCalled();
155
+ });
156
+
157
+ test("injects only the access token; refresh material never reaches the child env", async () => {
158
+ const refreshToken = "refresh-must-not-cross-the-child-boundary";
159
+ await seedVaultToken("sk-ant-oat-access");
160
+ await seedVaultValue(ACP_OAUTH_REFRESH_TOKEN_FIELD, refreshToken);
161
+ await seedVaultValue(
162
+ ACP_OAUTH_EXPIRES_AT_FIELD,
163
+ String(Date.now() + 60 * 60 * 1000),
164
+ );
165
+
166
+ const prepared = await prepareAgentEnv({
167
+ command: "claude-agent-acp",
168
+ args: [],
169
+ });
170
+
171
+ expect(prepared.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("sk-ant-oat-access");
172
+ expect(JSON.stringify(prepared.env)).not.toContain(refreshToken);
173
+ expect(prepared.env).not.toHaveProperty("CLAUDE_CODE_OAUTH_REFRESH_TOKEN");
174
+
175
+ let leaked: string | undefined;
176
+ const brokerResult = await credentialBroker.serverUse({
177
+ service: ACP_SERVICE,
178
+ field: ACP_OAUTH_REFRESH_TOKEN_FIELD,
179
+ toolName: ACP_SPAWN_TOOL,
180
+ execute: async (value) => {
181
+ leaked = value;
182
+ },
183
+ });
184
+ expect(brokerResult.success).toBe(false);
185
+ expect(leaked).toBeUndefined();
186
+ expect(brokerResult.success ? undefined : brokerResult.reason).toContain(
187
+ "No credential found for acp/claude_oauth_refresh_token",
188
+ );
130
189
  });
131
190
 
132
191
  test("auto-registers metadata with acp_spawn in allowedTools when none exists", async () => {
@@ -66,7 +66,11 @@ function buildSessionWithFakeProcess(opts: {
66
66
  kill: () => {},
67
67
  spawn: () => {},
68
68
  initialize: () => Promise.resolve(),
69
- createSession: () => Promise.resolve(opts.protocolSessionId),
69
+ createSession: () =>
70
+ Promise.resolve({
71
+ sessionId: opts.protocolSessionId,
72
+ configOptions: [],
73
+ }),
70
74
  cancel: () => Promise.resolve(),
71
75
  markStderr: () => 0,
72
76
  stderrSince: () => "",