@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
@@ -22,11 +22,38 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
22
22
  // ---------------------------------------------------------------------------
23
23
 
24
24
  let storeReturn = true;
25
+ let deleteReturn: "deleted" | "not-found" | "error" = "not-found";
25
26
  let getReturn: string | undefined = undefined;
26
- const setSecureKeyAsync = mock(
27
- async (_account: string, _value: string) => storeReturn,
28
- );
29
- const getSecureKeyAsync = mock(async (_account: string) => getReturn);
27
+ const vault = new Map<string, string>();
28
+ const ACCESS_KEY = "credential/acp/claude_oauth_token";
29
+ const REFRESH_KEY = "credential/acp/claude_oauth_refresh_token";
30
+ const EXPIRES_KEY = "credential/acp/claude_oauth_expires_at";
31
+ const DIGEST_KEY = "credential/acp/claude_oauth_access_digest";
32
+
33
+ const setSecureKeyAsync = mock(async (account: string, value: string) => {
34
+ if (!storeReturn) {
35
+ return false;
36
+ }
37
+ vault.set(account, value);
38
+ return true;
39
+ });
40
+ const getSecureKeyAsync = mock(async (account: string) => {
41
+ if (vault.has(account)) {
42
+ return vault.get(account);
43
+ }
44
+ // Existing cases that only seed the access token keep using getReturn.
45
+ if (account === ACCESS_KEY) {
46
+ return getReturn;
47
+ }
48
+ return undefined;
49
+ });
50
+ const deleteSecureKeyAsync = mock(async (account: string) => {
51
+ if (deleteReturn === "error") {
52
+ return "error";
53
+ }
54
+ const existed = vault.delete(account);
55
+ return existed ? "deleted" : "not-found";
56
+ });
30
57
 
31
58
  // Spread the real module rather than listing two exports. These cases reach
32
59
  // the marker tables, which pulls persistence into the graph, and anything in
@@ -37,6 +64,7 @@ mock.module("../../security/secure-keys.js", () => ({
37
64
  ...realSecureKeys,
38
65
  setSecureKeyAsync,
39
66
  getSecureKeyAsync,
67
+ deleteSecureKeyAsync,
40
68
  }));
41
69
 
42
70
  const { _setMetadataPath, getCredentialMetadata, upsertCredentialMetadata } =
@@ -69,6 +97,10 @@ const {
69
97
  parseManualClaudeCode,
70
98
  storeAcpClaudeToken,
71
99
  hasAcpClaudeToken,
100
+ persistRefreshedAcpClaudeTokens,
101
+ forgetAcpClaudeRenewalStateIfUnbound,
102
+ clearAcpClaudeRefreshToken,
103
+ isAcpClaudeTokenExpiring,
72
104
  } = await import("../acp-claude-oauth.js");
73
105
 
74
106
  /**
@@ -95,9 +127,12 @@ beforeEach(() => {
95
127
  mkdirSync(TEST_DIR, { recursive: true });
96
128
  _setMetadataPath(join(TEST_DIR, "metadata.json"));
97
129
  storeReturn = true;
130
+ deleteReturn = "not-found";
98
131
  getReturn = undefined;
132
+ vault.clear();
99
133
  setSecureKeyAsync.mockClear();
100
134
  getSecureKeyAsync.mockClear();
135
+ deleteSecureKeyAsync.mockClear();
101
136
  });
102
137
 
103
138
  afterEach(() => {
@@ -204,11 +239,43 @@ describe("storeAcpClaudeToken", () => {
204
239
  test("writes the token to the acp/claude_oauth_token vault field", async () => {
205
240
  await storeAcpClaudeToken("sk-ant-oat-token");
206
241
 
207
- expect(setSecureKeyAsync).toHaveBeenCalledTimes(1);
208
242
  expect(setSecureKeyAsync).toHaveBeenCalledWith(
209
- "credential/acp/claude_oauth_token",
243
+ ACCESS_KEY,
210
244
  "sk-ant-oat-token",
211
245
  );
246
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-token");
247
+ });
248
+
249
+ test("persists refresh token and expiry from a Connect exchange", async () => {
250
+ const before = Date.now();
251
+ await storeAcpClaudeToken({
252
+ accessToken: "sk-ant-oat-connected",
253
+ refreshToken: "refresh-from-exchange",
254
+ expiresIn: 28800,
255
+ });
256
+ const after = Date.now();
257
+
258
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-connected");
259
+ expect(vault.get(REFRESH_KEY)).toBe("refresh-from-exchange");
260
+ expect(vault.get(DIGEST_KEY)).toBe(
261
+ claudeTokenDigest("sk-ant-oat-connected"),
262
+ );
263
+ const expiresAt = Number(vault.get(EXPIRES_KEY));
264
+ expect(expiresAt).toBeGreaterThanOrEqual(before + 28800 * 1000);
265
+ expect(expiresAt).toBeLessThanOrEqual(after + 28800 * 1000);
266
+ });
267
+
268
+ test("clears companion fields when the exchange returns only an access token", async () => {
269
+ vault.set(REFRESH_KEY, "stale-refresh");
270
+ vault.set(EXPIRES_KEY, "1");
271
+ vault.set(DIGEST_KEY, "stale-digest");
272
+
273
+ await storeAcpClaudeToken({ accessToken: "sk-ant-oat-access-only" });
274
+
275
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-access-only");
276
+ expect(vault.has(REFRESH_KEY)).toBe(false);
277
+ expect(vault.has(EXPIRES_KEY)).toBe(false);
278
+ expect(vault.has(DIGEST_KEY)).toBe(false);
212
279
  });
213
280
 
214
281
  test("takes a domain-restricted credential from not-connected to connected", async () => {
@@ -309,6 +376,190 @@ describe("hasAcpClaudeToken", () => {
309
376
 
310
377
  expect(oauthMetadata()).toBeUndefined();
311
378
  });
379
+
380
+ test("reports true for an expired token that still has a refresh token", async () => {
381
+ vault.set(ACCESS_KEY, "sk-ant-oat-expired-renewable");
382
+ vault.set(REFRESH_KEY, "refresh-still-good");
383
+ vault.set(EXPIRES_KEY, String(Date.now() - 1000));
384
+
385
+ expect(await hasAcpClaudeToken()).toBe(true);
386
+ });
387
+
388
+ test("reports false for an expired token with no refresh token", async () => {
389
+ vault.set(ACCESS_KEY, "sk-ant-oat-expired-dead");
390
+ vault.set(EXPIRES_KEY, String(Date.now() - 1000));
391
+
392
+ expect(await hasAcpClaudeToken()).toBe(false);
393
+ });
394
+
395
+ test("reports true for a token with no recorded expiry (legacy access-token-only)", async () => {
396
+ vault.set(ACCESS_KEY, "sk-ant-oat-legacy");
397
+
398
+ expect(await hasAcpClaudeToken()).toBe(true);
399
+ expect(await isAcpClaudeTokenExpiring()).toBe(false);
400
+ });
401
+
402
+ test("reports false when only leftover refresh material remains", async () => {
403
+ vault.set(REFRESH_KEY, "refresh-leftover");
404
+ vault.set(EXPIRES_KEY, String(Date.now() - 1000));
405
+
406
+ expect(await hasAcpClaudeToken()).toBe(false);
407
+ });
408
+
409
+ test("reports true for a pasted token whose leftover expiry describes a previous token", async () => {
410
+ vault.set(ACCESS_KEY, "sk-ant-oat-pasted");
411
+ vault.set(REFRESH_KEY, "refresh-previous");
412
+ vault.set(EXPIRES_KEY, String(Date.now() - 1000));
413
+ vault.set(DIGEST_KEY, claudeTokenDigest("sk-ant-oat-previous"));
414
+
415
+ expect(await hasAcpClaudeToken()).toBe(true);
416
+ });
417
+ });
418
+
419
+ describe("persistRefreshedAcpClaudeTokens", () => {
420
+ test("writes a rotated refresh token and new expiry without touching policy", async () => {
421
+ upsertCredentialMetadata(ACP_SERVICE, OAUTH_FIELD, {
422
+ allowedTools: ["some_other_tool"],
423
+ });
424
+ vault.set(ACCESS_KEY, "sk-ant-oat-current");
425
+ vault.set(REFRESH_KEY, "refresh-expected");
426
+ vault.set(DIGEST_KEY, claudeTokenDigest("sk-ant-oat-current"));
427
+ const before = Date.now();
428
+
429
+ await persistRefreshedAcpClaudeTokens(
430
+ {
431
+ accessToken: "sk-ant-oat-refreshed",
432
+ refreshToken: "refresh-rotated",
433
+ expiresIn: 3600,
434
+ },
435
+ "refresh-expected",
436
+ );
437
+
438
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-refreshed");
439
+ expect(vault.get(REFRESH_KEY)).toBe("refresh-rotated");
440
+ expect(vault.get(DIGEST_KEY)).toBe(
441
+ claudeTokenDigest("sk-ant-oat-refreshed"),
442
+ );
443
+ const expiresAt = Number(vault.get(EXPIRES_KEY));
444
+ expect(expiresAt).toBeGreaterThanOrEqual(before + 3600 * 1000);
445
+ expect(oauthMetadata()?.allowedTools).toEqual(["some_other_tool"]);
446
+ });
447
+
448
+ test("keeps the stored refresh token when the response omits a new one", async () => {
449
+ vault.set(ACCESS_KEY, "sk-ant-oat-current");
450
+ vault.set(REFRESH_KEY, "refresh-kept");
451
+ vault.set(DIGEST_KEY, claudeTokenDigest("sk-ant-oat-current"));
452
+
453
+ await persistRefreshedAcpClaudeTokens(
454
+ {
455
+ accessToken: "sk-ant-oat-refreshed",
456
+ expiresIn: 3600,
457
+ },
458
+ "refresh-kept",
459
+ );
460
+
461
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-refreshed");
462
+ expect(vault.get(REFRESH_KEY)).toBe("refresh-kept");
463
+ });
464
+
465
+ test("refuses to persist when the stored refresh token no longer matches", async () => {
466
+ vault.set(ACCESS_KEY, "sk-ant-oat-current");
467
+ vault.set(REFRESH_KEY, "refresh-current");
468
+ vault.set(EXPIRES_KEY, "999");
469
+
470
+ const persisted = await persistRefreshedAcpClaudeTokens(
471
+ {
472
+ accessToken: "sk-ant-oat-stale",
473
+ refreshToken: "refresh-stale-rotated",
474
+ expiresIn: 3600,
475
+ },
476
+ "refresh-old",
477
+ );
478
+
479
+ expect(persisted).toBe(false);
480
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-current");
481
+ expect(vault.get(REFRESH_KEY)).toBe("refresh-current");
482
+ expect(vault.get(EXPIRES_KEY)).toBe("999");
483
+ });
484
+
485
+ test("refuses to persist when the access token is no longer stored", async () => {
486
+ vault.set(REFRESH_KEY, "refresh-leftover");
487
+ vault.set(EXPIRES_KEY, "111");
488
+
489
+ const persisted = await persistRefreshedAcpClaudeTokens(
490
+ {
491
+ accessToken: "sk-ant-oat-resurrected",
492
+ refreshToken: "refresh-rotated",
493
+ expiresIn: 3600,
494
+ },
495
+ "refresh-leftover",
496
+ );
497
+
498
+ expect(persisted).toBe(false);
499
+ expect(vault.has(ACCESS_KEY)).toBe(false);
500
+ expect(vault.get(REFRESH_KEY)).toBe("refresh-leftover");
501
+ expect(vault.get(EXPIRES_KEY)).toBe("111");
502
+ });
503
+
504
+ test("refuses to persist when the access token is not the one the refresh material was written with", async () => {
505
+ vault.set(ACCESS_KEY, "sk-ant-oat-pasted");
506
+ vault.set(REFRESH_KEY, "refresh-previous");
507
+ vault.set(EXPIRES_KEY, "111");
508
+ vault.set(DIGEST_KEY, claudeTokenDigest("sk-ant-oat-previous"));
509
+
510
+ const persisted = await persistRefreshedAcpClaudeTokens(
511
+ {
512
+ accessToken: "sk-ant-oat-overwritten",
513
+ refreshToken: "refresh-rotated",
514
+ expiresIn: 3600,
515
+ },
516
+ "refresh-previous",
517
+ );
518
+
519
+ expect(persisted).toBe(false);
520
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-pasted");
521
+ expect(vault.get(REFRESH_KEY)).toBe("refresh-previous");
522
+ expect(vault.get(EXPIRES_KEY)).toBe("111");
523
+ });
524
+ });
525
+
526
+ describe("clearAcpClaudeRefreshToken", () => {
527
+ test("leaves the stored refresh token when it no longer matches the rejected one", async () => {
528
+ vault.set(REFRESH_KEY, "refresh-current");
529
+
530
+ await clearAcpClaudeRefreshToken("refresh-old");
531
+
532
+ expect(vault.get(REFRESH_KEY)).toBe("refresh-current");
533
+ });
534
+ });
535
+
536
+ describe("forgetAcpClaudeRenewalStateIfUnbound", () => {
537
+ test("clears leftover renewal fields after the access token was replaced", async () => {
538
+ vault.set(ACCESS_KEY, "sk-ant-oat-pasted");
539
+ vault.set(REFRESH_KEY, "stale-refresh");
540
+ vault.set(EXPIRES_KEY, "111");
541
+ vault.set(DIGEST_KEY, claudeTokenDigest("sk-ant-oat-previous"));
542
+
543
+ await forgetAcpClaudeRenewalStateIfUnbound();
544
+
545
+ expect(vault.get(ACCESS_KEY)).toBe("sk-ant-oat-pasted");
546
+ expect(vault.has(REFRESH_KEY)).toBe(false);
547
+ expect(vault.has(EXPIRES_KEY)).toBe(false);
548
+ expect(vault.has(DIGEST_KEY)).toBe(false);
549
+ });
550
+
551
+ test("keeps renewal fields when they still describe the stored access token", async () => {
552
+ vault.set(ACCESS_KEY, "sk-ant-oat-current");
553
+ vault.set(REFRESH_KEY, "keep-refresh");
554
+ vault.set(EXPIRES_KEY, "222");
555
+ vault.set(DIGEST_KEY, claudeTokenDigest("sk-ant-oat-current"));
556
+
557
+ await forgetAcpClaudeRenewalStateIfUnbound();
558
+
559
+ expect(vault.get(REFRESH_KEY)).toBe("keep-refresh");
560
+ expect(vault.get(EXPIRES_KEY)).toBe("222");
561
+ expect(vault.get(DIGEST_KEY)).toBe(claudeTokenDigest("sk-ant-oat-current"));
562
+ });
312
563
  });
313
564
 
314
565
  describe("storeAcpClaudeToken: retiring the card registry", () => {
@@ -5,6 +5,9 @@
5
5
  import { describe, expect, test } from "bun:test";
6
6
 
7
7
  import {
8
+ ACP_OAUTH_ACCESS_DIGEST_FIELD,
9
+ ACP_OAUTH_EXPIRES_AT_FIELD,
10
+ ACP_OAUTH_REFRESH_TOKEN_FIELD,
8
11
  ACP_OAUTH_TOKEN_FIELD,
9
12
  ACP_SERVICE,
10
13
  AcpCredentialFormatError,
@@ -70,6 +73,9 @@ describe("constants", () => {
70
73
  test("expose the ACP service and OAuth field names", () => {
71
74
  expect(ACP_SERVICE).toBe("acp");
72
75
  expect(ACP_OAUTH_TOKEN_FIELD).toBe("claude_oauth_token");
76
+ expect(ACP_OAUTH_REFRESH_TOKEN_FIELD).toBe("claude_oauth_refresh_token");
77
+ expect(ACP_OAUTH_EXPIRES_AT_FIELD).toBe("claude_oauth_expires_at");
78
+ expect(ACP_OAUTH_ACCESS_DIGEST_FIELD).toBe("claude_oauth_access_digest");
73
79
  });
74
80
  });
75
81
 
@@ -81,6 +87,13 @@ describe("isAcpClaudeOauthField", () => {
81
87
  test("rejects other services and fields", () => {
82
88
  expect(isAcpClaudeOauthField("acp", "openai_api_key")).toBe(false);
83
89
  expect(isAcpClaudeOauthField("acp", "codex_api_key")).toBe(false);
90
+ expect(isAcpClaudeOauthField("acp", "claude_oauth_refresh_token")).toBe(
91
+ false,
92
+ );
93
+ expect(isAcpClaudeOauthField("acp", "claude_oauth_expires_at")).toBe(false);
94
+ expect(isAcpClaudeOauthField("acp", "claude_oauth_access_digest")).toBe(
95
+ false,
96
+ );
84
97
  expect(isAcpClaudeOauthField("sentry", "claude_oauth_token")).toBe(false);
85
98
  expect(isAcpClaudeOauthField("openai", "api_key")).toBe(false);
86
99
  });
@@ -8,9 +8,16 @@
8
8
 
9
9
  import { describe, expect, mock, test } from "bun:test";
10
10
 
11
- import type { AuthMethod, InitializeResponse } from "@agentclientprotocol/sdk";
11
+ import type {
12
+ AuthMethod,
13
+ InitializeResponse,
14
+ SessionConfigOption,
15
+ } from "@agentclientprotocol/sdk";
16
+ import * as acp from "@agentclientprotocol/sdk";
12
17
 
13
18
  import { AcpAgentProcess } from "../agent-process.js";
19
+ import { AcpConfigOptionRefusedError } from "../types.js";
20
+ import { modelOption } from "./helpers/acp-model-option.js";
14
21
 
15
22
  function makeProcess(): AcpAgentProcess {
16
23
  return new AcpAgentProcess(
@@ -160,6 +167,139 @@ describe("AcpAgentProcess loadSession/resumeSession", () => {
160
167
  });
161
168
  });
162
169
 
170
+ describe("AcpAgentProcess config options", () => {
171
+ /** Injects a connection whose session calls report `configOptions`. */
172
+ function stubSessionResponses(
173
+ proc: AcpAgentProcess,
174
+ configOptions: SessionConfigOption[] | null | undefined,
175
+ ): void {
176
+ (proc as unknown as { connection: unknown }).connection = {
177
+ newSession: () =>
178
+ Promise.resolve({ sessionId: "session-1", configOptions }),
179
+ loadSession: () => Promise.resolve({ configOptions }),
180
+ resumeSession: () => Promise.resolve({ configOptions }),
181
+ };
182
+ }
183
+
184
+ test("createSession returns the reported options", async () => {
185
+ const proc = makeProcess();
186
+ const options = [modelOption("sonnet")];
187
+ stubSessionResponses(proc, options);
188
+
189
+ await expect(proc.createSession("/tmp/project")).resolves.toEqual({
190
+ sessionId: "session-1",
191
+ configOptions: options,
192
+ });
193
+ });
194
+
195
+ test("loadSession returns the reported options", async () => {
196
+ const proc = makeProcess();
197
+ const options = [modelOption("sonnet")];
198
+ stubSessionResponses(proc, options);
199
+
200
+ await expect(
201
+ proc.loadSession("session-1", "/tmp/project"),
202
+ ).resolves.toEqual({ configOptions: options });
203
+ });
204
+
205
+ test("resumeSession returns the reported options", async () => {
206
+ const proc = makeProcess();
207
+ const options = [modelOption("opus")];
208
+ stubSessionResponses(proc, options);
209
+
210
+ await expect(
211
+ proc.resumeSession("session-1", "/tmp/project"),
212
+ ).resolves.toEqual({ configOptions: options });
213
+ });
214
+
215
+ test("a null or absent configOptions normalizes to an empty array", async () => {
216
+ const created = makeProcess();
217
+ stubSessionResponses(created, null);
218
+ await expect(created.createSession("/tmp/project")).resolves.toEqual({
219
+ sessionId: "session-1",
220
+ configOptions: [],
221
+ });
222
+
223
+ const loaded = makeProcess();
224
+ stubSessionResponses(loaded, null);
225
+ await expect(
226
+ loaded.loadSession("session-1", "/tmp/project"),
227
+ ).resolves.toEqual({ configOptions: [] });
228
+
229
+ const resumed = makeProcess();
230
+ stubSessionResponses(resumed, undefined);
231
+ await expect(
232
+ resumed.resumeSession("session-1", "/tmp/project"),
233
+ ).resolves.toEqual({ configOptions: [] });
234
+ });
235
+
236
+ test("setConfigOption forwards the value and returns the refreshed set", async () => {
237
+ const proc = makeProcess();
238
+ const calls: unknown[] = [];
239
+ const refreshed = [modelOption("opus")];
240
+ (proc as unknown as { connection: unknown }).connection = {
241
+ setSessionConfigOption: (params: unknown) => {
242
+ calls.push(params);
243
+ return Promise.resolve({ configOptions: refreshed });
244
+ },
245
+ };
246
+
247
+ await expect(
248
+ proc.setConfigOption("session-1", "model", "opus"),
249
+ ).resolves.toEqual(refreshed);
250
+
251
+ expect(calls).toEqual([
252
+ { sessionId: "session-1", configId: "model", value: "opus" },
253
+ ]);
254
+ });
255
+
256
+ test("setConfigOption sends the boolean request variant", async () => {
257
+ const proc = makeProcess();
258
+ const calls: unknown[] = [];
259
+ const refreshed = [modelOption("opus")];
260
+ (proc as unknown as { connection: unknown }).connection = {
261
+ setSessionConfigOption: (params: unknown) => {
262
+ calls.push(params);
263
+ return Promise.resolve({ configOptions: refreshed });
264
+ },
265
+ };
266
+
267
+ await proc.setConfigOption("session-1", "thinking", true);
268
+
269
+ expect(calls).toEqual([
270
+ {
271
+ sessionId: "session-1",
272
+ configId: "thinking",
273
+ type: "boolean",
274
+ value: true,
275
+ },
276
+ ]);
277
+ });
278
+
279
+ test("setConfigOption omits the discriminator for string values", async () => {
280
+ const proc = makeProcess();
281
+ const calls: unknown[] = [];
282
+ (proc as unknown as { connection: unknown }).connection = {
283
+ setSessionConfigOption: (params: unknown) => {
284
+ calls.push(params);
285
+ return Promise.resolve({ configOptions: [modelOption("opus")] });
286
+ },
287
+ };
288
+
289
+ await proc.setConfigOption("session-1", "model", "opus");
290
+
291
+ expect(calls[0]).not.toHaveProperty("type");
292
+ });
293
+
294
+ test("setConfigOption throws when the process is not spawned", async () => {
295
+ const proc = makeProcess();
296
+
297
+ await expect(
298
+ proc.setConfigOption("session-1", "model", "opus"),
299
+ ).rejects.toThrow('ACP agent "test-agent" is not spawned');
300
+ });
301
+ });
302
+
163
303
  describe("AcpAgentProcess auth_required retry", () => {
164
304
  // spawnedEnv is injected the way spawn() builds it ({ ...process.env,
165
305
  // ...config.env }), so the advertised var names use a VELLUM_TEST_ prefix
@@ -196,17 +336,19 @@ describe("AcpAgentProcess auth_required retry", () => {
196
336
  * initialize() so the advertised authMethods are captured.
197
337
  */
198
338
  async function setupAuthProcess(options: {
339
+ command?: string;
199
340
  env?: Record<string, string>;
200
341
  authMethods?: AuthMethod[];
201
342
  /** Rejections to throw from successive newSession calls before succeeding. */
202
343
  newSessionRejections?: unknown[];
203
344
  loadSessionRejections?: unknown[];
204
345
  resumeSessionRejections?: unknown[];
346
+ setConfigOptionRejections?: unknown[];
205
347
  promptRejections?: unknown[];
206
348
  }) {
207
349
  const proc = new AcpAgentProcess(
208
350
  "codex",
209
- { command: "echo", args: [], env: options.env },
351
+ { command: options.command ?? "echo", args: [], env: options.env },
210
352
  () => {
211
353
  throw new Error("client factory should not be called in this test");
212
354
  },
@@ -234,6 +376,10 @@ describe("AcpAgentProcess auth_required retry", () => {
234
376
  options.resumeSessionRejections ?? [],
235
377
  {},
236
378
  );
379
+ const setSessionConfigOption = failThenSucceed(
380
+ options.setConfigOptionRejections ?? [],
381
+ { configOptions: [] },
382
+ );
237
383
  const prompt = failThenSucceed(options.promptRejections ?? [], {
238
384
  stopReason: "end_turn",
239
385
  });
@@ -252,6 +398,7 @@ describe("AcpAgentProcess auth_required retry", () => {
252
398
  newSession,
253
399
  loadSession,
254
400
  resumeSession,
401
+ setSessionConfigOption,
255
402
  prompt,
256
403
  authenticate,
257
404
  };
@@ -264,18 +411,112 @@ describe("AcpAgentProcess auth_required retry", () => {
264
411
  newSession,
265
412
  loadSession,
266
413
  resumeSession,
414
+ setSessionConfigOption,
267
415
  prompt,
268
416
  authenticate,
269
417
  internals,
270
418
  };
271
419
  }
272
420
 
421
+ // An adapter that throws a plain Error reaches the client as a generic
422
+ // "Internal error" carrying the real sentence in `data.details`, which is
423
+ // the shape these fixtures reproduce.
424
+ test("setConfigOption surfaces the adapter's error answer as a refusal", async () => {
425
+ const { proc } = await setupAuthProcess({
426
+ setConfigOptionRejections: [
427
+ new acp.RequestError(-32603, "Internal error", {
428
+ details: "Invalid value for config option model: nope",
429
+ }),
430
+ ],
431
+ });
432
+
433
+ const refusal = await proc
434
+ .setConfigOption("session-1", "model", "nope")
435
+ .catch((err: unknown) => err);
436
+
437
+ expect(refusal).toBeInstanceOf(AcpConfigOptionRefusedError);
438
+ // The adapter's sentence, not the message the SDK framed it in.
439
+ expect((refusal as Error).message).toBe(
440
+ "Invalid value for config option model: nope",
441
+ );
442
+ });
443
+
444
+ test("setConfigOption serializes a payload that names no reason", async () => {
445
+ const { proc } = await setupAuthProcess({
446
+ setConfigOptionRejections: [
447
+ new acp.RequestError(-32603, "Internal error", { code: 7 }),
448
+ ],
449
+ });
450
+
451
+ await expect(
452
+ proc.setConfigOption("session-1", "model", "nope"),
453
+ ).rejects.toThrow('{"code":7}');
454
+ });
455
+
456
+ test("setConfigOption falls back to the message when there is no payload", async () => {
457
+ const { proc } = await setupAuthProcess({
458
+ setConfigOptionRejections: [
459
+ new acp.RequestError(-32603, "Session not found"),
460
+ ],
461
+ });
462
+
463
+ await expect(
464
+ proc.setConfigOption("session-1", "model", "nope"),
465
+ ).rejects.toThrow("Session not found");
466
+ });
467
+
468
+ test.each(["claude-agent-acp", "/usr/local/bin/claude-agent-acp"])(
469
+ "setConfigOption leaves Claude's message-shaped auth failure untyped (%s)",
470
+ async (command) => {
471
+ const expired = new acp.RequestError(-32603, "Internal error", {
472
+ details: "Not logged in",
473
+ });
474
+ const { proc } = await setupAuthProcess({
475
+ command,
476
+ setConfigOptionRejections: [expired],
477
+ });
478
+
479
+ await expect(
480
+ proc.setConfigOption("session-1", "model", "opus"),
481
+ ).rejects.toBe(expired);
482
+ },
483
+ );
484
+
485
+ test("setConfigOption reads Claude's login wording from another adapter as a refusal", async () => {
486
+ const { proc } = await setupAuthProcess({
487
+ command: "codex-acp",
488
+ setConfigOptionRejections: [
489
+ new acp.RequestError(-32603, "Internal error", {
490
+ details: "Not logged in",
491
+ }),
492
+ ],
493
+ });
494
+
495
+ const refusal = await proc
496
+ .setConfigOption("session-1", "model", "gpt-5")
497
+ .catch((err: unknown) => err);
498
+
499
+ expect(refusal).toBeInstanceOf(AcpConfigOptionRefusedError);
500
+ expect((refusal as Error).message).toBe("Not logged in");
501
+ });
502
+
503
+ test("setConfigOption leaves a transport failure untyped", async () => {
504
+ const closed = new Error("ACP connection closed");
505
+ const { proc } = await setupAuthProcess({
506
+ setConfigOptionRejections: [closed],
507
+ });
508
+
509
+ await expect(
510
+ proc.setConfigOption("session-1", "model", "opus"),
511
+ ).rejects.toBe(closed);
512
+ });
513
+
273
514
  test("createSession does not authenticate when newSession succeeds", async () => {
274
515
  const { proc, newSession, authenticate } = await setupAuthProcess({
275
516
  env: { [OPENAI_VAR]: "sk-test" },
276
517
  });
277
518
 
278
- const sessionId = await proc.createSession("/tmp/project");
519
+ const { sessionId } = await proc.createSession("/tmp/project");
279
520
 
280
521
  expect(sessionId).toBe("session-1");
281
522
  expect(newSession).toHaveBeenCalledTimes(1);
@@ -288,7 +529,7 @@ describe("AcpAgentProcess auth_required retry", () => {
288
529
  newSessionRejections: [authRequiredError],
289
530
  });
290
531
 
291
- const sessionId = await proc.createSession("/tmp/project");
532
+ const { sessionId } = await proc.createSession("/tmp/project");
292
533
 
293
534
  expect(sessionId).toBe("session-1");
294
535
  expect(authenticate).toHaveBeenCalledTimes(1);
@@ -387,6 +628,19 @@ describe("AcpAgentProcess auth_required retry", () => {
387
628
  expect(resumeSession).toHaveBeenCalledTimes(2);
388
629
  });
389
630
 
631
+ test("setConfigOption authenticates and retries on auth_required", async () => {
632
+ const { proc, setSessionConfigOption, authenticate } =
633
+ await setupAuthProcess({
634
+ env: { [OPENAI_VAR]: "sk-test" },
635
+ setConfigOptionRejections: [authRequiredError],
636
+ });
637
+
638
+ await proc.setConfigOption("session-1", "model", "opus");
639
+
640
+ expect(authenticate).toHaveBeenCalledWith({ methodId: "openai-api-key" });
641
+ expect(setSessionConfigOption).toHaveBeenCalledTimes(2);
642
+ });
643
+
390
644
  test("prompt authenticates and retries on auth_required, returning the response", async () => {
391
645
  const { proc, prompt, authenticate } = await setupAuthProcess({
392
646
  env: { [OPENAI_VAR]: "sk-test" },