@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
@@ -56,7 +56,9 @@ describe("resolveAcpAgent", () => {
56
56
  return;
57
57
  }
58
58
  expect(result.agent.command).toBe("codex-acp");
59
- expect(result.agent.description).toContain("@agentclientprotocol/codex-acp");
59
+ expect(result.agent.description).toContain(
60
+ "@agentclientprotocol/codex-acp",
61
+ );
60
62
  });
61
63
 
62
64
  test("falls back to default profile for claude when no user entry", () => {
@@ -191,7 +193,7 @@ describe("resolveAcpAgent", () => {
191
193
  return;
192
194
  }
193
195
  expect(result.hint).toBe(
194
- "bun add -g @agentclientprotocol/claude-agent-acp",
196
+ "bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
195
197
  );
196
198
  expect(result.command).toBe("claude-agent-acp");
197
199
  });
@@ -240,7 +242,9 @@ describe("resolveAcpAgent", () => {
240
242
  if (result.reason !== "binary_not_found") {
241
243
  return;
242
244
  }
243
- expect(result.hint).toBe("bun add -g @agentclientprotocol/codex-acp");
245
+ expect(result.hint).toBe(
246
+ "bun add -g @agentclientprotocol/codex-acp@1.10.0",
247
+ );
244
248
  });
245
249
 
246
250
  test("binary preflight honors agent.env.PATH override (matches spawn env)", () => {
@@ -285,6 +289,178 @@ describe("resolveAcpAgent", () => {
285
289
  expect(result.agent.args).toEqual(["--verbose"]);
286
290
  });
287
291
 
292
+ test("the bundled claude profile carries its model", () => {
293
+ config.setConfig({ agents: {} });
294
+
295
+ const result = resolveAcpAgent("claude");
296
+
297
+ expect(result.ok).toBe(true);
298
+ if (!result.ok) {
299
+ return;
300
+ }
301
+ expect(result.agent.model).toBe("opus");
302
+ });
303
+
304
+ test("a user entry that names a model overrides the bundled one", () => {
305
+ config.setConfig({
306
+ agents: {
307
+ claude: { command: "claude-agent-acp", args: [], model: "sonnet" },
308
+ },
309
+ });
310
+
311
+ const result = resolveAcpAgent("claude");
312
+
313
+ expect(result.ok).toBe(true);
314
+ if (!result.ok) {
315
+ return;
316
+ }
317
+ expect(result.agent.model).toBe("sonnet");
318
+ });
319
+
320
+ test("a user entry keeping the profile command and omitting model inherits it", () => {
321
+ config.setConfig({
322
+ agents: {
323
+ claude: { command: "claude-agent-acp", args: ["--my-flag"] },
324
+ },
325
+ });
326
+
327
+ const result = resolveAcpAgent("claude");
328
+
329
+ expect(result.ok).toBe(true);
330
+ if (!result.ok) {
331
+ return;
332
+ }
333
+ expect(result.agent.args).toEqual(["--my-flag"]);
334
+ expect(result.agent.model).toBe("opus");
335
+ });
336
+
337
+ test("a user entry that omits command entirely still inherits the model", () => {
338
+ // The schema admits a command-less entry for a bundled id; the profile's
339
+ // command is inherited with the rest.
340
+ config.setConfig({
341
+ agents: {
342
+ claude: JSON.parse('{"args": ["--my-flag"]}'),
343
+ },
344
+ });
345
+
346
+ const result = resolveAcpAgent("claude");
347
+
348
+ expect(result.ok).toBe(true);
349
+ if (!result.ok) {
350
+ return;
351
+ }
352
+ expect(result.agent.command).toBe("claude-agent-acp");
353
+ expect(result.agent.model).toBe("opus");
354
+ });
355
+
356
+ test("a user entry running the profile adapter by full path still inherits the model", () => {
357
+ config.setConfig({
358
+ agents: {
359
+ claude: { command: "/opt/bin/claude-agent-acp", args: [] },
360
+ },
361
+ });
362
+
363
+ const result = resolveAcpAgent("claude");
364
+
365
+ expect(result.ok).toBe(true);
366
+ if (!result.ok) {
367
+ return;
368
+ }
369
+ expect(result.agent.command).toBe("/opt/bin/claude-agent-acp");
370
+ expect(result.agent.model).toBe("opus");
371
+ });
372
+
373
+ test("a user entry pointing the id at another adapter inherits no model", () => {
374
+ // The bundled aliases are Claude's own vocabulary, so an unrelated
375
+ // adapter under the `claude` id must not be handed `opus`.
376
+ config.setConfig({
377
+ agents: {
378
+ claude: { command: "my-custom-claude", args: ["--my-flag"] },
379
+ },
380
+ });
381
+
382
+ const result = resolveAcpAgent("claude");
383
+
384
+ expect(result.ok).toBe(true);
385
+ if (!result.ok) {
386
+ return;
387
+ }
388
+ expect(result.agent.command).toBe("my-custom-claude");
389
+ expect(result.agent.model).toBeUndefined();
390
+ });
391
+
392
+ test("a user entry naming its own model keeps it across a replaced command", () => {
393
+ config.setConfig({
394
+ agents: {
395
+ claude: {
396
+ command: "my-custom-claude",
397
+ args: [],
398
+ model: "my-fork-large",
399
+ },
400
+ },
401
+ });
402
+
403
+ const result = resolveAcpAgent("claude");
404
+
405
+ expect(result.ok).toBe(true);
406
+ if (!result.ok) {
407
+ return;
408
+ }
409
+ expect(result.agent.command).toBe("my-custom-claude");
410
+ expect(result.agent.model).toBe("my-fork-large");
411
+ });
412
+
413
+ test("a user entry pointing the id at another adapter inherits no description", () => {
414
+ config.setConfig({
415
+ agents: {
416
+ claude: { command: "my-custom-claude", args: [] },
417
+ },
418
+ });
419
+
420
+ const result = resolveAcpAgent("claude");
421
+
422
+ expect(result.ok).toBe(true);
423
+ if (!result.ok) {
424
+ return;
425
+ }
426
+ expect(result.agent.description).toBeUndefined();
427
+ const listed = listAcpAgents().agents.find(
428
+ (entry) => entry.id === "claude",
429
+ );
430
+ expect(listed?.command).toBe("my-custom-claude");
431
+ expect(listed?.description).toBeUndefined();
432
+ });
433
+
434
+ test("a user entry running the profile adapter by full path inherits its description", () => {
435
+ config.setConfig({
436
+ agents: {
437
+ claude: { command: "/opt/bin/claude-agent-acp", args: [] },
438
+ },
439
+ });
440
+
441
+ const result = resolveAcpAgent("claude");
442
+
443
+ expect(result.ok).toBe(true);
444
+ if (!result.ok) {
445
+ return;
446
+ }
447
+ expect(result.agent.description).toContain(
448
+ "@agentclientprotocol/claude-agent-acp",
449
+ );
450
+ });
451
+
452
+ test("a bundled profile with no model leaves the resolved agent without one", () => {
453
+ config.setConfig({ agents: {} });
454
+
455
+ const result = resolveAcpAgent("codex");
456
+
457
+ expect(result.ok).toBe(true);
458
+ if (!result.ok) {
459
+ return;
460
+ }
461
+ expect(result.agent.model).toBeUndefined();
462
+ });
463
+
288
464
  test("resolves a full-path command directly without rewriting it", () => {
289
465
  config.setConfig({
290
466
  agents: {
@@ -332,7 +508,7 @@ describe("resolveAcpAgent - missing binary", () => {
332
508
  }
333
509
  expect(result.command).toBe("claude-agent-acp");
334
510
  expect(result.hint).toBe(
335
- "bun add -g @agentclientprotocol/claude-agent-acp",
511
+ "bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
336
512
  );
337
513
  });
338
514
 
@@ -351,7 +527,7 @@ describe("resolveAcpAgent - missing binary", () => {
351
527
  return;
352
528
  }
353
529
  expect(result.hint).toBe(
354
- "bun add -g @agentclientprotocol/claude-agent-acp",
530
+ "bun add -g @agentclientprotocol/claude-agent-acp@0.75.1",
355
531
  );
356
532
  });
357
533
  });
@@ -430,7 +606,9 @@ describe("listAcpAgents", () => {
430
606
  const codex = result.agents.find((a) => a.id === "codex");
431
607
  expect(codex?.available).toBe(false);
432
608
  expect(codex?.unavailableReason).toBe("'codex-acp' is not on PATH");
433
- expect(codex?.setupHint).toBe("bun add -g @agentclientprotocol/codex-acp");
609
+ expect(codex?.setupHint).toBe(
610
+ "bun add -g @agentclientprotocol/codex-acp@1.10.0",
611
+ );
434
612
  });
435
613
 
436
614
  test("aliases are resolution sugar, not catalog entries", () => {
@@ -489,3 +667,39 @@ describe("listAcpAgents", () => {
489
667
  );
490
668
  });
491
669
  });
670
+
671
+ describe("agent ids that name Object.prototype members", () => {
672
+ // An id read off the prototype chain would resolve as a configured agent
673
+ // that does not exist, and a spawn would start a process for a config
674
+ // nobody wrote.
675
+ for (const id of ["__proto__", "constructor", "prototype"]) {
676
+ test(`"${id}" resolves to nothing`, () => {
677
+ config.setConfig({ agents: {} });
678
+
679
+ const resolved = resolveAcpAgent(id);
680
+ expect(resolved.ok).toBe(false);
681
+ if (resolved.ok) {
682
+ return;
683
+ }
684
+ expect(resolved.reason).toBe("unknown_agent");
685
+ if (resolved.reason !== "unknown_agent") {
686
+ return;
687
+ }
688
+ expect(resolved.available).toEqual(["claude", "codex"]);
689
+ });
690
+ }
691
+
692
+ test("the catalog never lists one, even when config carries the key", () => {
693
+ config.setConfig({
694
+ agents: {
695
+ ...JSON.parse('{"__proto__": {"command": "evil", "args": []}}'),
696
+ claude: { command: "claude-agent-acp", args: [] },
697
+ },
698
+ });
699
+
700
+ expect(listAcpAgents().agents.map((a) => a.id)).toEqual([
701
+ "claude",
702
+ "codex",
703
+ ]);
704
+ });
705
+ });
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Shared resolver for ACP agent ids → agent config + binary preflight.
3
3
  *
4
- * `resolveAcpAgent(id)` merges user-provided `config.acp.agents[id]` (wins on
5
- * overlap) with the bundled `DEFAULT_ACP_AGENT_PROFILES` so common agents like
6
- * `claude` and `codex` Just Work with no per-user config required. Natural
7
- * names ("claude code", "OpenAI Codex") resolve via `AGENT_ID_ALIASES` when the
8
- * raw id misses both maps. The result is a discriminated union covering every
9
- * reason a spawn might fail before we even start the agent process: unknown
10
- * agent id, or binary missing from PATH. Callers (acp_spawn, acp_list_agents,
11
- * and the `/v1/acp/spawn` HTTP route) get a single source of truth and
12
- * matching actionable hints.
4
+ * `resolveAcpAgent(id)` layers user-provided `config.acp.agents[id]` over the
5
+ * bundled `DEFAULT_ACP_AGENT_PROFILES` so common agents like `claude` and
6
+ * `codex` Just Work with no per-user config required; `mergeWithProfile` says
7
+ * what an entry inherits. Natural names ("claude code", "OpenAI Codex")
8
+ * resolve via `AGENT_ID_ALIASES` when the raw id misses both maps. The result
9
+ * is a discriminated union covering every reason a spawn might fail before we
10
+ * even start the agent process: unknown agent id, or binary missing from
11
+ * PATH. Callers (acp_spawn, acp_list_agents, and the `/v1/acp/spawn` HTTP
12
+ * route) get a single source of truth and matching actionable hints.
13
13
  *
14
14
  * The resolver NEVER fetches or runs packages in the (untrusted) task cwd.
15
15
  * When the adapter binary is missing, resolution simply fails with
@@ -25,12 +25,16 @@
25
25
  * `available` / `setupHint` derived from the same binary resolution.
26
26
  */
27
27
 
28
+ import { basename } from "node:path";
29
+
28
30
  import {
31
+ type AcpAgentProfile,
29
32
  DEFAULT_ACP_AGENT_PROFILES,
30
33
  DEFAULT_AGENT_NPM_PACKAGES,
31
34
  } from "../config/acp-defaults.js";
32
- import type { AcpAgentConfig } from "../config/acp-schema.js";
35
+ import type { AcpAgentConfig as ConfiguredAcpAgent } from "../config/acp-schema.js";
33
36
  import { getConfig } from "../config/loader.js";
37
+ import type { AcpAgentConfig } from "./types.js";
34
38
 
35
39
  /**
36
40
  * Whether this agent's entry came from user config (wins over default) or
@@ -134,6 +138,19 @@ const AGENT_ID_ALIASES: Record<string, string> = {
134
138
  openaicodex: "codex",
135
139
  };
136
140
 
141
+ /**
142
+ * Ids that read back as inherited `Object.prototype` members instead of
143
+ * configured agents. `directLookup` and `mergedAgentIds` refuse them so an id
144
+ * from a tool call or an HTTP body can resolve to nothing but a real entry:
145
+ * resolving one would report a configured agent that does not exist, and a
146
+ * spawn would then start a process for a config nobody wrote.
147
+ */
148
+ const PROTOTYPE_SENSITIVE_AGENT_IDS: ReadonlySet<string> = new Set([
149
+ "__proto__",
150
+ "constructor",
151
+ "prototype",
152
+ ]);
153
+
137
154
  /**
138
155
  * Normalize a raw agent id for alias matching: lowercase and strip spaces,
139
156
  * underscores, and hyphens so "Claude Code", "claude-code", and
@@ -166,45 +183,92 @@ export function lookupAcpAgentConfig(id: string): AcpAgentConfig | undefined {
166
183
  }
167
184
 
168
185
  function lookupAgent(
169
- userAgents: Record<string, AcpAgentConfig>,
186
+ userAgents: Record<string, ConfiguredAcpAgent>,
170
187
  id: string,
171
- ): { agent: AcpAgentConfig; source: AcpAgentSource } | undefined {
188
+ ): { agent: AcpAgentConfig; source: AcpAgentSource; id: string } | undefined {
172
189
  const direct = directLookup(userAgents, id);
173
190
  if (direct) {
174
- return direct;
191
+ return { ...direct, id };
175
192
  }
176
- const canonicalId = AGENT_ID_ALIASES[normalizeAgentId(id)];
177
- return canonicalId !== undefined
178
- ? directLookup(userAgents, canonicalId)
193
+ const normalized = normalizeAgentId(id);
194
+ const canonicalId = Object.hasOwn(AGENT_ID_ALIASES, normalized)
195
+ ? AGENT_ID_ALIASES[normalized]
179
196
  : undefined;
197
+ if (canonicalId === undefined) {
198
+ return undefined;
199
+ }
200
+ const aliased = directLookup(userAgents, canonicalId);
201
+ return aliased ? { ...aliased, id: canonicalId } : undefined;
180
202
  }
181
203
 
182
204
  function directLookup(
183
- userAgents: Record<string, AcpAgentConfig>,
205
+ userAgents: Record<string, ConfiguredAcpAgent>,
184
206
  id: string,
185
207
  ): { agent: AcpAgentConfig; source: AcpAgentSource } | undefined {
186
- const userAgent = userAgents[id];
208
+ if (PROTOTYPE_SENSITIVE_AGENT_IDS.has(id)) {
209
+ return undefined;
210
+ }
211
+ const userAgent = Object.hasOwn(userAgents, id) ? userAgents[id] : undefined;
212
+ const defaultAgent = Object.hasOwn(DEFAULT_ACP_AGENT_PROFILES, id)
213
+ ? DEFAULT_ACP_AGENT_PROFILES[id]
214
+ : undefined;
187
215
  if (userAgent) {
188
- return { agent: userAgent, source: "config" };
216
+ const agent = mergeWithProfile(userAgent, defaultAgent);
217
+ return agent ? { agent, source: "config" } : undefined;
189
218
  }
190
- const defaultAgent = DEFAULT_ACP_AGENT_PROFILES[id];
191
219
  if (defaultAgent) {
192
220
  return { agent: defaultAgent, source: "default" };
193
221
  }
194
222
  return undefined;
195
223
  }
196
224
 
225
+ /**
226
+ * Fill a user config entry from the bundled profile for the same id. An entry
227
+ * that runs the profile's adapter (it omits `command`, or names the same
228
+ * binary, by full path or not) inherits the leaves it leaves out: `command`
229
+ * itself, the description, and the `model` a session starts on. `args` is the
230
+ * exception, because the schema defaults it to `[]`: a parsed entry always
231
+ * carries its own, so the profile's never reaches it. An entry that points
232
+ * the id at a different adapter stands alone, like any user-only entry,
233
+ * so a config that reuses the `claude` id for something else never carries
234
+ * Claude's description or has Claude's `opus` sent to it. Zod omits absent
235
+ * optional keys, so an omission never spreads as an undefined override.
236
+ *
237
+ * `undefined` only for an entry with neither a command nor a profile to take
238
+ * one from, a shape the config schema rejects before it gets here.
239
+ */
240
+ function mergeWithProfile(
241
+ userAgent: ConfiguredAcpAgent,
242
+ profile: AcpAgentProfile | undefined,
243
+ ): AcpAgentConfig | undefined {
244
+ const command = userAgent.command;
245
+ if (!profile) {
246
+ return command === undefined ? undefined : { ...userAgent, command };
247
+ }
248
+ // A full path to the bundled binary is still the bundled adapter; the
249
+ // basename is the adapter's identity everywhere else in this module.
250
+ if (
251
+ command !== undefined &&
252
+ basename(command) !== basename(profile.command)
253
+ ) {
254
+ return { ...userAgent, command };
255
+ }
256
+ return { ...profile, ...userAgent, command: command ?? profile.command };
257
+ }
258
+
197
259
  /**
198
260
  * Defaults first (declaration order), then user-only ids. Deduplicated so a
199
261
  * user config that overrides a default doesn't list the id twice.
200
262
  */
201
- function mergedAgentIds(userAgents: Record<string, AcpAgentConfig>): string[] {
263
+ function mergedAgentIds(
264
+ userAgents: Record<string, ConfiguredAcpAgent>,
265
+ ): string[] {
202
266
  return Array.from(
203
267
  new Set([
204
268
  ...Object.keys(DEFAULT_ACP_AGENT_PROFILES),
205
269
  ...Object.keys(userAgents),
206
270
  ]),
207
- );
271
+ ).filter((id) => !PROTOTYPE_SENSITIVE_AGENT_IDS.has(id));
208
272
  }
209
273
 
210
274
  /**
@@ -256,7 +320,8 @@ export function listAcpAgents(): {
256
320
  const config = getConfig();
257
321
  const userAgents = config.acp.agents;
258
322
  const agents: AcpAgentEntry[] = mergedAgentIds(userAgents).map((id) => {
259
- // Non-null: ids come from `mergedAgentIds` so the lookup always resolves.
323
+ // Non-null: ids come from `mergedAgentIds`, and the schema admits a
324
+ // command-less entry only for a bundled id, so the lookup always resolves.
260
325
  const { agent, source } = lookupAgent(userAgents, id)!;
261
326
  // Same binary preflight as `resolveAcpAgent`: available iff the command
262
327
  // is on PATH. A missing binary is auto-installed at spawn time, but the
@@ -1,5 +1,7 @@
1
1
  import { afterEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { RequestError } from "@agentclientprotocol/sdk";
4
+
3
5
  import type { Conversation } from "../daemon/conversation.js";
4
6
  import {
5
7
  deleteConversation,
@@ -39,6 +41,7 @@ mock.module("./prepare-agent-env.js", () => ({
39
41
  }));
40
42
 
41
43
  import { createAbortReason } from "../util/abort-reasons.js";
44
+ import { modelOption } from "./__tests__/helpers/acp-model-option.js";
42
45
  import { VellumAcpClientHandler } from "./client-handler.js";
43
46
  import { AcpSessionManager } from "./session-manager.js";
44
47
 
@@ -90,10 +93,10 @@ function mockConversation(opts?: { enqueueQueued?: boolean }) {
90
93
  }
91
94
 
92
95
  /** Fake AcpAgentProcess covering only the calls firePromptInBackground makes. */
93
- function fakeProcess(prompt: () => Promise<unknown>) {
96
+ function fakeProcess(prompt: () => Promise<unknown>, stderr = "") {
94
97
  return {
95
98
  markStderr: () => 0,
96
- stderrSince: () => "",
99
+ stderrSince: () => stderr,
97
100
  prompt,
98
101
  kill: mock(() => {}),
99
102
  };
@@ -365,6 +368,8 @@ describe("AcpSessionManager auth-required recovery surface", () => {
365
368
  parentToolUseId?: string;
366
369
  cancelled?: boolean;
367
370
  credentialDigest?: string;
371
+ failure?: () => Promise<never>;
372
+ stderr?: string;
368
373
  }) {
369
374
  const manager = new AcpSessionManager(1);
370
375
  const parentId = `parent-${opts.id}`;
@@ -376,7 +381,7 @@ describe("AcpSessionManager auth-required recovery surface", () => {
376
381
  manager,
377
382
  opts.id,
378
383
  parentId,
379
- fakeProcess(authFailure),
384
+ fakeProcess(opts.failure ?? authFailure, opts.stderr),
380
385
  );
381
386
  entry.command = opts.command;
382
387
  (entry as { parentToolUseId?: string }).parentToolUseId =
@@ -512,6 +517,62 @@ describe("AcpSessionManager auth-required recovery surface", () => {
512
517
  expect(r.persistedContent).toBeUndefined();
513
518
  });
514
519
 
520
+ test("claude's prompt-time 401 raises the surface when stderr names another failure", async () => {
521
+ // claude-agent-acp raises it as RequestError.internalError({ errorKind },
522
+ // cliText), and stderr supplies the failure message, so the auth text
523
+ // survives only on the rejection's own message.
524
+ refusedDigests.length = 0;
525
+ const digest = claudeTokenDigest("sk-ant-oat-prompt-401");
526
+ const r = await driveAuthFailure({
527
+ id: "sess-auth-prompt-401",
528
+ command: "claude-agent-acp",
529
+ parentToolUseId: "tool-anchor-prompt-401",
530
+ credentialDigest: digest,
531
+ failure: () =>
532
+ Promise.reject(
533
+ new RequestError(
534
+ -32603,
535
+ "Internal error: Failed to authenticate. API Error: 401 OAuth access token has expired.",
536
+ { errorKind: "authentication_failed" },
537
+ ),
538
+ ),
539
+ stderr: '{"error":{"message":"Something else happened"}}',
540
+ });
541
+
542
+ expect(r.authEvent).toMatchObject({
543
+ acpSessionId: "sess-auth-prompt-401",
544
+ authCode: "acp_claude_auth_required",
545
+ parentToolUseId: "tool-anchor-prompt-401",
546
+ });
547
+ expect(hasAcpConnectCardRaised(r.parentId)).toBe(true);
548
+ expect(refusedDigests).toContain(digest);
549
+ });
550
+
551
+ test("the rejection's own message is checked when its payload names a different reason", async () => {
552
+ refusedDigests.length = 0;
553
+ const digest = claudeTokenDigest("sk-ant-oat-own-message");
554
+ const r = await driveAuthFailure({
555
+ id: "sess-auth-own-message",
556
+ command: "claude-agent-acp",
557
+ parentToolUseId: "tool-anchor-own-message",
558
+ credentialDigest: digest,
559
+ failure: () =>
560
+ Promise.reject(
561
+ new RequestError(
562
+ -32603,
563
+ "Internal error: Failed to authenticate. API Error: 401",
564
+ { details: "Request failed" },
565
+ ),
566
+ ),
567
+ stderr: '{"error":{"message":"Something else happened"}}',
568
+ });
569
+
570
+ expect(r.authEvent).toMatchObject({
571
+ authCode: "acp_claude_auth_required",
572
+ });
573
+ expect(refusedDigests).toContain(digest);
574
+ });
575
+
515
576
  test("a non-claude adapter never raises the surface, even on an auth-shaped failure", async () => {
516
577
  const r = await driveAuthFailure({
517
578
  id: "sess-auth-codex",
@@ -716,4 +777,136 @@ describe("AcpSessionManager.spawn: a stopped turn leaves no agent running", () =
716
777
  expect(proc.kill).toHaveBeenCalled();
717
778
  expect(internals.sessions.size).toBe(0);
718
779
  });
780
+
781
+ /**
782
+ * The model pin is an await of its own, and it sits after the setup
783
+ * recheck. A stop landing inside it gets the same treatment: no spawned
784
+ * event, no model event, no prompt, and no session left behind.
785
+ */
786
+ test("a cancel during the model pin tears the process down and fires nothing", async () => {
787
+ const manager = new AcpSessionManager(1);
788
+ const controller = new AbortController();
789
+ const prompt = mock(() => Promise.resolve({}));
790
+ const setConfigOption = mock(async () => {
791
+ // The user stops the turn while the pin's round trip is open.
792
+ controller.abort(REASON);
793
+ return [modelOption("opus")];
794
+ });
795
+ const proc = {
796
+ ...fakeProcess(prompt),
797
+ spawn: () => {},
798
+ initialize: async () => {},
799
+ createSession: async () => ({
800
+ sessionId: "proto-1",
801
+ configOptions: [modelOption("sonnet")],
802
+ }),
803
+ setConfigOption,
804
+ };
805
+ let injected: ReturnType<typeof injectSession> | undefined;
806
+ const internals = manager as unknown as {
807
+ registerSession: (opts: {
808
+ acpSessionId: string;
809
+ parentConversationId: string;
810
+ }) => unknown;
811
+ sessions: Map<string, unknown>;
812
+ };
813
+ internals.registerSession = (opts) => {
814
+ injected = injectSession(
815
+ manager,
816
+ opts.acpSessionId,
817
+ opts.parentConversationId,
818
+ proc as unknown as ReturnType<typeof fakeProcess>,
819
+ );
820
+ return injected;
821
+ };
822
+
823
+ await expect(
824
+ manager.spawn(
825
+ "claude",
826
+ { command: "noop", args: [] } as never,
827
+ "do the task",
828
+ "/tmp",
829
+ "conv-1",
830
+ () => {},
831
+ { model: "opus" },
832
+ { signal: controller.signal },
833
+ ),
834
+ ).rejects.toThrow();
835
+
836
+ expect(setConfigOption).toHaveBeenCalled();
837
+ expect(injected?.sendToVellum).not.toHaveBeenCalled();
838
+ expect(prompt).not.toHaveBeenCalled();
839
+ expect(proc.kill).toHaveBeenCalled();
840
+ expect(internals.sessions.size).toBe(0);
841
+ });
842
+
843
+ /**
844
+ * A cancel that persists a resumable row frees the id, so a resume can
845
+ * register a fresh entry under it before the stopped spawn's pin settles.
846
+ * The teardown owes that spawn its own process, and owes the replacement
847
+ * its map slot.
848
+ */
849
+ test("a cancel during the model pin leaves a replacement session for the same id alone", async () => {
850
+ const manager = new AcpSessionManager(2);
851
+ const controller = new AbortController();
852
+ const prompt = mock(() => Promise.resolve({}));
853
+ let replacement: ReturnType<typeof injectSession> | undefined;
854
+ let spawnedId: string | undefined;
855
+ const setConfigOption = mock(async () => {
856
+ // The turn is stopped and the id is resumed by another request while
857
+ // the pin's round trip is still open.
858
+ controller.abort(REASON);
859
+ replacement = injectSession(
860
+ manager,
861
+ spawnedId!,
862
+ "conv-2",
863
+ fakeProcess(mock(() => Promise.resolve({}))),
864
+ );
865
+ return [modelOption("opus")];
866
+ });
867
+ const proc = {
868
+ ...fakeProcess(prompt),
869
+ spawn: () => {},
870
+ initialize: async () => {},
871
+ createSession: async () => ({
872
+ sessionId: "proto-1",
873
+ configOptions: [modelOption("sonnet")],
874
+ }),
875
+ setConfigOption,
876
+ };
877
+ const internals = manager as unknown as {
878
+ registerSession: (opts: {
879
+ acpSessionId: string;
880
+ parentConversationId: string;
881
+ }) => unknown;
882
+ sessions: Map<string, unknown>;
883
+ };
884
+ internals.registerSession = (opts) => {
885
+ spawnedId = opts.acpSessionId;
886
+ return injectSession(
887
+ manager,
888
+ opts.acpSessionId,
889
+ opts.parentConversationId,
890
+ proc as unknown as ReturnType<typeof fakeProcess>,
891
+ );
892
+ };
893
+
894
+ await expect(
895
+ manager.spawn(
896
+ "claude",
897
+ { command: "noop", args: [] } as never,
898
+ "do the task",
899
+ "/tmp",
900
+ "conv-1",
901
+ () => {},
902
+ { model: "opus" },
903
+ { signal: controller.signal },
904
+ ),
905
+ ).rejects.toThrow();
906
+
907
+ expect(prompt).not.toHaveBeenCalled();
908
+ expect(proc.kill).toHaveBeenCalled();
909
+ expect(replacement?.process.kill).not.toHaveBeenCalled();
910
+ expect(internals.sessions.get(spawnedId!)).toBe(replacement);
911
+ });
719
912
  });