@vellumai/assistant 0.12.1 → 0.12.2-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (548) 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/openapi.yaml +327 -52
  6. package/package.json +3 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/anthropic-provider.test.ts +0 -63
  10. package/src/__tests__/app-compiler.test.ts +6 -0
  11. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  13. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  14. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  15. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  16. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  17. package/src/__tests__/computer-use-tools.test.ts +19 -7
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +28 -29
  20. package/src/__tests__/config-watcher.test.ts +23 -8
  21. package/src/__tests__/connection-model-compat.test.ts +2 -2
  22. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  23. package/src/__tests__/conversation-pairing.test.ts +26 -0
  24. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  25. package/src/__tests__/conversation-queue.test.ts +48 -207
  26. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  27. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  28. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  29. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  30. package/src/__tests__/conversation-title-service.test.ts +14 -10
  31. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  32. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  33. package/src/__tests__/credential-record-retire.test.ts +171 -1
  34. package/src/__tests__/credential-routes.test.ts +212 -3
  35. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  36. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  37. package/src/__tests__/document-sync-tags.test.ts +25 -0
  38. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  39. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  40. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  41. package/src/__tests__/git-porcelain.test.ts +47 -0
  42. package/src/__tests__/heartbeat-service.test.ts +56 -40
  43. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  45. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  46. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  47. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  48. package/src/__tests__/host-file-proxy.test.ts +11 -23
  49. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  50. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  51. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  52. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  53. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  54. package/src/__tests__/mcp-health-check.test.ts +8 -11
  55. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  56. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  57. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  58. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  59. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  60. package/src/__tests__/model-intents.test.ts +3 -0
  61. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  62. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  63. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  64. package/src/__tests__/openai-provider.test.ts +1 -0
  65. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  66. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  67. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  68. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  69. package/src/__tests__/qdrant-manager.test.ts +132 -0
  70. package/src/__tests__/resolve-app.test.ts +135 -0
  71. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  72. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  73. package/src/__tests__/service-account-token.test.ts +247 -0
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  75. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  76. package/src/__tests__/skills.test.ts +9 -4
  77. package/src/__tests__/source-fingerprint.test.ts +184 -0
  78. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  79. package/src/__tests__/subagent-tools.test.ts +3 -2
  80. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  81. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  82. package/src/__tests__/unicode.test.ts +13 -0
  83. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  84. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  85. package/src/__tests__/workspace-git-service.test.ts +26 -0
  86. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  87. package/src/acp/__tests__/agent-process.test.ts +258 -4
  88. package/src/acp/__tests__/auth-required.test.ts +96 -0
  89. package/src/acp/__tests__/client-handler.test.ts +90 -2
  90. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  91. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  92. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  93. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  94. package/src/acp/__tests__/session-manager.test.ts +895 -4
  95. package/src/acp/agent-process.test.ts +4 -1
  96. package/src/acp/agent-process.ts +96 -10
  97. package/src/acp/auth-required.ts +50 -1
  98. package/src/acp/auto-install.test.ts +9 -7
  99. package/src/acp/client-handler.ts +16 -3
  100. package/src/acp/model-config.test.ts +166 -0
  101. package/src/acp/model-config.ts +138 -0
  102. package/src/acp/resolve-agent.test.ts +220 -6
  103. package/src/acp/resolve-agent.ts +88 -23
  104. package/src/acp/session-manager.test.ts +196 -3
  105. package/src/acp/session-manager.ts +441 -18
  106. package/src/acp/types.ts +50 -5
  107. package/src/agent/loop.ts +1 -0
  108. package/src/api/events/acp-session-model-update.test.ts +88 -0
  109. package/src/api/events/acp-session-model-update.ts +46 -0
  110. package/src/api/events/app-files-changed.ts +4 -3
  111. package/src/api/events/notification-intent.ts +2 -0
  112. package/src/api/events/user-message-echo.ts +7 -1
  113. package/src/api/index.ts +6 -0
  114. package/src/api/responses/conversation-message.ts +5 -0
  115. package/src/apps/app-store.ts +1 -1
  116. package/src/apps/resolve-app.ts +81 -0
  117. package/src/apps/source-fingerprint.ts +266 -0
  118. package/src/avatar/identity-avatar.ts +7 -2
  119. package/src/bundler/app-compiler.ts +15 -0
  120. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  121. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  122. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  123. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  124. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  125. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  126. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  127. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  128. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  129. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  130. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  131. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  132. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  133. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  134. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  135. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  136. package/src/calls/barge-in-guard.ts +118 -0
  137. package/src/calls/call-constants.ts +24 -4
  138. package/src/calls/call-controller.ts +499 -199
  139. package/src/calls/call-transport.ts +16 -0
  140. package/src/calls/media-stream-output.ts +32 -0
  141. package/src/calls/media-stream-server.ts +82 -30
  142. package/src/calls/media-stream-stt-session.ts +15 -0
  143. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  144. package/src/calls/resolve-call-tts-provider.ts +33 -2
  145. package/src/calls/tts-call-strategy.ts +9 -5
  146. package/src/calls/types.ts +2 -0
  147. package/src/calls/voice-control-protocol.ts +53 -21
  148. package/src/calls/voice-leg-coordinator.ts +189 -0
  149. package/src/calls/voice-progress-cadence.ts +423 -0
  150. package/src/calls/voice-session-bridge.ts +59 -46
  151. package/src/calls/voice-triage-escalate.ts +175 -56
  152. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  153. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  154. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  155. package/src/cli/commands/apps.help.ts +67 -13
  156. package/src/cli/commands/apps.ts +183 -0
  157. package/src/cli/commands/bash.help.ts +8 -13
  158. package/src/cli/commands/bash.ts +7 -25
  159. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  160. package/src/cli/commands/channels/index.help.ts +70 -2
  161. package/src/cli/commands/channels/index.ts +16 -3
  162. package/src/cli/commands/channels/send.ts +128 -0
  163. package/src/cli/commands/credentials.help.ts +6 -2
  164. package/src/cli/commands/credentials.ts +2 -2
  165. package/src/cli/commands/mcp.help.ts +8 -8
  166. package/src/cli/commands/mcp.ts +1 -1
  167. package/src/cli/commands/roadmap.help.ts +5 -1
  168. package/src/cli/commands/roadmap.ts +1 -1
  169. package/src/cli/lib/bundled-marketplace.json +103 -90
  170. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  171. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  172. package/src/config/__tests__/platform-identity.test.ts +255 -0
  173. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  174. package/src/config/acp-agent-write.ts +43 -0
  175. package/src/config/acp-defaults.test.ts +13 -3
  176. package/src/config/acp-defaults.ts +23 -6
  177. package/src/config/acp-schema.test.ts +81 -0
  178. package/src/config/acp-schema.ts +30 -2
  179. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  180. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  181. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  182. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  183. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  184. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  185. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  186. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  187. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  188. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  189. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  190. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  191. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  192. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  193. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  194. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  195. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  196. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  197. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  198. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  199. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  200. package/src/config/default-profile-catalog.ts +1 -1
  201. package/src/config/env.ts +2 -2
  202. package/src/config/feature-flag-registry.json +24 -12
  203. package/src/config/loader.ts +1 -0
  204. package/src/config/memory-v3-gate.test.ts +31 -0
  205. package/src/config/memory-v3-gate.ts +20 -16
  206. package/src/config/platform-identity.ts +201 -0
  207. package/src/config/platform-rehydration.ts +11 -27
  208. package/src/config/schema.ts +2 -2
  209. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  210. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  211. package/src/config/schemas/calls.ts +1 -28
  212. package/src/config/schemas/live-voice.ts +1 -173
  213. package/src/config/schemas/mcp.ts +1 -1
  214. package/src/config/schemas/memory-retrospective.ts +10 -0
  215. package/src/config/schemas/services.ts +8 -0
  216. package/src/config/schemas/skills.ts +0 -14
  217. package/src/config/schemas/voice.ts +150 -0
  218. package/src/credential-execution/process-manager.test.ts +18 -19
  219. package/src/credential-health/credential-health-service.ts +11 -17
  220. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  221. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  222. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  223. package/src/daemon/app-change-notify.ts +45 -0
  224. package/src/daemon/config-watcher.ts +13 -194
  225. package/src/daemon/conversation-error.ts +3 -2
  226. package/src/daemon/conversation-media-retry.ts +2 -1
  227. package/src/daemon/conversation-process.ts +35 -64
  228. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  229. package/src/daemon/conversation-skill-tools.ts +13 -31
  230. package/src/daemon/conversation-store.ts +1 -1
  231. package/src/daemon/conversation-surfaces.ts +15 -9
  232. package/src/daemon/conversation-tool-setup.ts +37 -43
  233. package/src/daemon/conversation.ts +0 -3
  234. package/src/daemon/dictation-profile-store.ts +2 -1
  235. package/src/daemon/handlers/shared.ts +2 -1
  236. package/src/daemon/handlers/skills.ts +6 -3
  237. package/src/daemon/host-bash-proxy.ts +8 -12
  238. package/src/daemon/host-browser-proxy.ts +15 -1
  239. package/src/daemon/host-cu-proxy.ts +186 -19
  240. package/src/daemon/host-file-proxy.ts +8 -10
  241. package/src/daemon/host-proxy-preactivation.ts +2 -1
  242. package/src/daemon/host-transfer-proxy.ts +14 -18
  243. package/src/daemon/lifecycle.ts +0 -5
  244. package/src/daemon/mcp-reload-service.ts +5 -5
  245. package/src/daemon/message-types/sync.ts +1 -0
  246. package/src/daemon/orphan-reaper.ts +1 -1
  247. package/src/daemon/providers-setup.ts +1 -1
  248. package/src/daemon/reaction-history-render.ts +2 -1
  249. package/src/daemon/shutdown-handlers.ts +1 -9
  250. package/src/daemon/skill-memory-refresh.ts +64 -0
  251. package/src/daemon/tool-side-effects.ts +7 -18
  252. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  253. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  254. package/src/desktop/desktop-chrome-policy.ts +47 -0
  255. package/src/desktop/desktop-chrome-session.ts +37 -0
  256. package/src/desktop/desktop-dependencies.test.ts +102 -0
  257. package/src/desktop/desktop-dependencies.ts +308 -0
  258. package/src/desktop/desktop-feature.test.ts +18 -0
  259. package/src/desktop/desktop-feature.ts +19 -0
  260. package/src/desktop/desktop-panel-config.test.ts +135 -0
  261. package/src/desktop/desktop-panel-config.ts +149 -0
  262. package/src/desktop/desktop-session-manager.test.ts +730 -0
  263. package/src/desktop/desktop-session-manager.ts +850 -0
  264. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  265. package/src/desktop/desktop-stream-bridge.ts +214 -0
  266. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  267. package/src/desktop/desktop-wallpaper.ts +113 -0
  268. package/src/desktop/desktop-window-drag.ts +177 -0
  269. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  270. package/src/desktop/desktop-window-manager-config.ts +238 -0
  271. package/src/desktop/desktop-window-theme.ts +170 -0
  272. package/src/heartbeat/heartbeat-service.ts +21 -3
  273. package/src/home/__tests__/feed-writer.test.ts +22 -0
  274. package/src/home/feed-writer.ts +11 -2
  275. package/src/hooks/types.ts +33 -0
  276. package/src/i18n/AGENTS.md +22 -0
  277. package/src/i18n/__tests__/i18n.test.ts +108 -0
  278. package/src/i18n/index.ts +16 -0
  279. package/src/i18n/locales.ts +93 -0
  280. package/src/i18n/messages.ts +61 -0
  281. package/src/i18n/title.ts +25 -0
  282. package/src/inbound/platform-callback-registration.ts +4 -5
  283. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  284. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  285. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  286. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  287. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  288. package/src/live-voice/live-voice-photo.ts +25 -6
  289. package/src/live-voice/live-voice-session.ts +213 -693
  290. package/src/live-voice/protocol.ts +0 -2
  291. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  292. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  293. package/src/mcp/effective-config.ts +3 -3
  294. package/src/mcp/mcp-header-store.ts +29 -45
  295. package/src/mcp/spec-schema.ts +111 -0
  296. package/src/mcp/startup.ts +8 -4
  297. package/src/mcp/workspace-mcp-config.ts +188 -0
  298. package/src/messaging/provider.ts +10 -3
  299. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  300. package/src/messaging/providers/callback-routing.ts +18 -0
  301. package/src/messaging/providers/channel-transport.ts +56 -0
  302. package/src/messaging/providers/discord/transport.ts +17 -0
  303. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  304. package/src/messaging/providers/slack/adapter.ts +7 -44
  305. package/src/messaging/providers/slack/transport.ts +16 -0
  306. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  307. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  308. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  309. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  310. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  311. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  312. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  313. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  314. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  315. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  316. package/src/notifications/adapters/macos.ts +3 -0
  317. package/src/notifications/assistant-reply-producer.ts +8 -3
  318. package/src/notifications/conversation-pairing.ts +36 -16
  319. package/src/notifications/delivered-post-record.ts +11 -1
  320. package/src/notifications/guardian-feed-projection.ts +125 -5
  321. package/src/notifications/home-feed-side-effect.ts +55 -7
  322. package/src/notifications/schedule-result-producer.ts +66 -22
  323. package/src/oauth/AGENTS.md +2 -0
  324. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  325. package/src/oauth/byo-connection.test.ts +105 -0
  326. package/src/oauth/byo-connection.ts +36 -6
  327. package/src/oauth/connection-resolver.test.ts +73 -0
  328. package/src/oauth/connection-resolver.ts +95 -2
  329. package/src/oauth/scope-utils.ts +24 -0
  330. package/src/oauth/seed-providers.ts +204 -25
  331. package/src/outbound-proxy/certs.ts +3 -5
  332. package/src/permissions/checker.test.ts +11 -6
  333. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  334. package/src/persistence/conversation-crud.ts +71 -63
  335. package/src/persistence/conversation-lineage.ts +12 -4
  336. package/src/persistence/conversation-queries.ts +2 -2
  337. package/src/persistence/conversation-title-placeholders.ts +23 -0
  338. package/src/persistence/conversation-title-service.ts +9 -26
  339. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  340. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  341. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  342. package/src/persistence/message-cursor.ts +78 -0
  343. package/src/persistence/message-reads.ts +30 -0
  344. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  345. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  346. package/src/persistence/schema/memory-core.ts +5 -0
  347. package/src/platform/client.test.ts +4 -2
  348. package/src/platform/client.ts +2 -10
  349. package/src/plugin-api/constants.ts +2 -0
  350. package/src/plugin-api/index.ts +1 -0
  351. package/src/plugin-api/types.ts +2 -0
  352. package/src/plugins/defaults/index.ts +2 -0
  353. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  354. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  355. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  356. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  357. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  358. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  359. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  360. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  361. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  362. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  363. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  364. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  365. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  366. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  367. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  368. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  369. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  370. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  371. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  372. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  373. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  374. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  375. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  376. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  377. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  378. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  379. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  380. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  381. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  382. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  383. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  384. package/src/plugins/defaults/memory/startup.ts +28 -2
  385. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  386. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  387. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  388. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  389. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  390. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  391. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  392. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  393. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  394. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  395. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  396. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  397. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  398. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  399. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  400. package/src/plugins/defaults/memory/worker.ts +6 -0
  401. package/src/plugins/mcp-servers.ts +15 -75
  402. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  403. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  404. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  405. package/src/providers/anthropic/client.ts +0 -47
  406. package/src/providers/gemini/client.ts +9 -23
  407. package/src/providers/gemini-thought-signature.test.ts +63 -0
  408. package/src/providers/gemini-thought-signature.ts +44 -0
  409. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  410. package/src/providers/inference/auth.ts +2 -3
  411. package/src/providers/inference/connection-availability.ts +27 -8
  412. package/src/providers/inference/endpoint-probe.ts +10 -1
  413. package/src/providers/inference/resolve-auth.ts +22 -4
  414. package/src/providers/inference/service-account-token.ts +248 -0
  415. package/src/providers/model-catalog.ts +34 -0
  416. package/src/providers/model-intents.ts +10 -5
  417. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  418. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  419. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  420. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  421. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  422. package/src/providers/openai/chat-completions-provider.ts +212 -79
  423. package/src/providers/openai/codex-models.ts +0 -4
  424. package/src/providers/openai/google-thought-signature.ts +320 -0
  425. package/src/providers/opencode/client.test.ts +73 -4
  426. package/src/providers/opencode/client.ts +60 -3
  427. package/src/providers/openrouter/client.ts +20 -0
  428. package/src/providers/outbound-request-sanitize.ts +96 -0
  429. package/src/providers/retry.ts +30 -19
  430. package/src/providers/unparseable-tool-args.ts +5 -1
  431. package/src/runtime/AGENTS.md +3 -1
  432. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  433. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  434. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  435. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  436. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  437. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  438. package/src/runtime/agent-wake.ts +2 -1
  439. package/src/runtime/assistant-event-hub.ts +21 -3
  440. package/src/runtime/auth/same-actor.ts +38 -3
  441. package/src/runtime/channel-send.ts +316 -0
  442. package/src/runtime/host-observe.ts +11 -1
  443. package/src/runtime/http-server.ts +160 -151
  444. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  445. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  446. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  447. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  448. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  449. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  450. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  451. package/src/runtime/routes/acp-routes.test.ts +15 -0
  452. package/src/runtime/routes/acp-routes.ts +72 -9
  453. package/src/runtime/routes/app-management-routes.ts +73 -0
  454. package/src/runtime/routes/app-routes.ts +1 -1
  455. package/src/runtime/routes/channel-send-routes.ts +85 -0
  456. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  457. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  458. package/src/runtime/routes/conversation-routes.ts +16 -3
  459. package/src/runtime/routes/credential-routes.ts +19 -10
  460. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  461. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  462. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  463. package/src/runtime/routes/events-routes.ts +4 -0
  464. package/src/runtime/routes/host-cu-routes.ts +6 -0
  465. package/src/runtime/routes/index.ts +4 -2
  466. package/src/runtime/routes/log-export-routes.ts +2 -0
  467. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  468. package/src/runtime/routes/migration-routes.ts +6 -20
  469. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  470. package/src/runtime/routes/platform-routes.ts +18 -15
  471. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  472. package/src/runtime/routes/workspace-routes.ts +46 -63
  473. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  474. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  475. package/src/runtime/routes/workspace-utils.ts +17 -0
  476. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  477. package/src/runtime/services/conversation-serializer.ts +16 -3
  478. package/src/runtime/sync/resource-sync-events.ts +2 -1
  479. package/src/schedule/run-script.ts +2 -1
  480. package/src/schedule/schedule-store.ts +9 -2
  481. package/src/schedule/worker.ts +1 -1
  482. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  483. package/src/security/ces-http-record-backend.ts +120 -0
  484. package/src/security/ces-rpc-record-backend.ts +7 -0
  485. package/src/security/untrusted-content.ts +3 -1
  486. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  487. package/src/skills/workspace-skill-md-signature.ts +50 -0
  488. package/src/subagent/consult-context.ts +12 -25
  489. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  490. package/src/tools/acp/list-agents.test.ts +3 -1
  491. package/src/tools/acp/spawn.test.ts +146 -11
  492. package/src/tools/acp/spawn.ts +21 -3
  493. package/src/tools/acp/status.test.ts +94 -0
  494. package/src/tools/acp/status.ts +40 -3
  495. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  496. package/src/tools/browser/browser-execution.ts +1 -1
  497. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  498. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  499. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  500. package/src/tools/browser/network-recorder.ts +2 -1
  501. package/src/tools/capability-offer.test.ts +22 -2
  502. package/src/tools/capability-offer.ts +16 -1
  503. package/src/tools/computer-use/definitions.ts +121 -2
  504. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  505. package/src/tools/credentials/metadata-store.ts +100 -0
  506. package/src/tools/filesystem/search.ts +2 -1
  507. package/src/tools/host-filesystem/edit.test.ts +2 -1
  508. package/src/tools/host-filesystem/edit.ts +2 -2
  509. package/src/tools/host-filesystem/read.test.ts +2 -1
  510. package/src/tools/host-filesystem/read.ts +2 -2
  511. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  512. package/src/tools/host-filesystem/transfer.ts +2 -2
  513. package/src/tools/host-filesystem/write.test.ts +2 -1
  514. package/src/tools/host-filesystem/write.ts +2 -2
  515. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  516. package/src/tools/host-terminal/host-shell.ts +4 -20
  517. package/src/tools/network/web-search.ts +6 -1
  518. package/src/tools/policy-context.ts +6 -7
  519. package/src/tools/skills/scaffold-managed.ts +61 -3
  520. package/src/tools/subagent/spawn.ts +5 -8
  521. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  522. package/src/tools/terminal/sanitized-bash.ts +105 -0
  523. package/src/tools/watch/watch-retro-report.ts +0 -8
  524. package/src/types/host-capabilities.ts +5 -3
  525. package/src/util/__tests__/language-subtag.test.ts +23 -1
  526. package/src/util/ca-bundle.ts +13 -0
  527. package/src/util/language-subtag.ts +16 -0
  528. package/src/util/platform.ts +8 -0
  529. package/src/util/unicode.ts +15 -1
  530. package/src/watch/watch-timeline.ts +2 -2
  531. package/src/workflows/run-manager.ts +2 -1
  532. package/src/workspace/git-porcelain.ts +66 -0
  533. package/src/workspace/git-service.ts +6 -55
  534. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  535. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  536. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  537. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  538. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  539. package/src/workspace/migrations/registry.ts +6 -0
  540. package/src/__tests__/app-source-watcher.test.ts +0 -218
  541. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  542. package/src/__tests__/speaker-identification.test.ts +0 -56
  543. package/src/calls/speaker-identification.ts +0 -233
  544. package/src/daemon/app-source-watcher.ts +0 -218
  545. package/src/messaging/providers/whatsapp/client.ts +0 -29
  546. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  547. package/src/util/fs-watcher-error.ts +0 -36
  548. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -608,22 +608,17 @@ describe("HostBrowserProxy", () => {
608
608
  },
609
609
  ];
610
610
 
611
- const resultPromise = proxy.request(
611
+ const result = await proxy.request(
612
612
  { cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
613
613
  "session-1",
614
614
  undefined,
615
615
  "user-1",
616
616
  );
617
617
 
618
- // Auto-resolution filters out the cross-user candidate, so the
619
- // proxy falls into the existing "no active extension connection"
620
- // rejection — we never broadcast to a different actor's client.
621
- await expect(resultPromise).rejects.toThrow(
622
- "no active extension connection",
623
- );
618
+ expect(result).toMatchObject({ isError: true });
619
+ expect(result.content).toContain("belongs to this user");
624
620
  expect(getPublishedMessages()).toHaveLength(0);
625
621
  });
626
-
627
622
  test("prefers the chrome-extension client over a more-recently-active macOS bridge", async () => {
628
623
  // Mock `listClientsByCapability` returns mockClients in array
629
624
  // order, which mirrors production's `lastActiveAt`-desc ordering.
@@ -720,11 +715,6 @@ describe("HostBrowserProxy", () => {
720
715
  });
721
716
 
722
717
  test("rejects when caller has actor but no host_browser-capable client is connected for that actor", async () => {
723
- // Same-user filter returns empty even though listClientsByCapability
724
- // would return a non-empty list (because that list is for a
725
- // different actor). The unfiltered fallback path runs only when
726
- // the caller has no actor — we don't silently broadcast to anyone
727
- // when the caller IS authenticated to a specific actor.
728
718
  mockClients = [
729
719
  {
730
720
  clientId: "other-user-ext",
@@ -734,16 +724,15 @@ describe("HostBrowserProxy", () => {
734
724
  },
735
725
  ];
736
726
 
737
- const resultPromise = proxy.request(
727
+ const result = await proxy.request(
738
728
  { cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
739
729
  "session-1",
740
730
  undefined,
741
731
  "user-1",
742
732
  );
743
733
 
744
- await expect(resultPromise).rejects.toThrow(
745
- "no active extension connection",
746
- );
734
+ expect(result).toMatchObject({ isError: true });
735
+ expect(result.content).toContain("belongs to this user");
747
736
  expect(getPublishedMessages()).toHaveLength(0);
748
737
  });
749
738
  });
@@ -36,10 +36,36 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
36
36
  },
37
37
  }));
38
38
 
39
+ // Captured `log.info` payloads, so the step-timings line can be asserted on.
40
+ const infoLogs: { payload: Record<string, unknown>; message: string }[] = [];
41
+
42
+ const realLogger = await import("../util/logger.js");
43
+ mock.module("../util/logger.js", () => ({
44
+ ...realLogger,
45
+ getLogger: () => ({
46
+ info: (payload: unknown, message: unknown) => {
47
+ infoLogs.push({
48
+ payload: (payload ?? {}) as Record<string, unknown>,
49
+ message: String(message),
50
+ });
51
+ },
52
+ warn: () => {},
53
+ error: () => {},
54
+ debug: () => {},
55
+ trace: () => {},
56
+ }),
57
+ }));
58
+
39
59
  // Use the REAL pending-interactions module — the proxy self-registers here.
40
60
  const pendingInteractions = await import("../runtime/pending-interactions.js");
41
61
  const { HostCuProxy } = await import("../daemon/host-cu-proxy.js");
42
62
 
63
+ function stepTimingsLogs(): Record<string, unknown>[] {
64
+ return infoLogs
65
+ .filter((entry) => entry.message === "Host CU step timings")
66
+ .map((entry) => entry.payload);
67
+ }
68
+
43
69
  describe("HostCuProxy", () => {
44
70
  let proxy: InstanceType<typeof HostCuProxy>;
45
71
 
@@ -48,6 +74,7 @@ describe("HostCuProxy", () => {
48
74
  sentOptions.length = 0;
49
75
  mockHasClient = false;
50
76
  mockClients = [];
77
+ infoLogs.length = 0;
51
78
  pendingInteractions.clear();
52
79
  proxy = new HostCuProxy(maxSteps);
53
80
  }
@@ -164,6 +191,124 @@ describe("HostCuProxy", () => {
164
191
  });
165
192
  });
166
193
 
194
+ // -------------------------------------------------------------------------
195
+ // Step timings
196
+ // -------------------------------------------------------------------------
197
+
198
+ describe("step timings", () => {
199
+ test("logs helper timings alongside the proxy round trip", async () => {
200
+ setup();
201
+ proxy.recordAction("computer_use_click", { element_id: 42 });
202
+
203
+ const resultPromise = proxy.request(
204
+ "computer_use_click",
205
+ { element_id: 42 },
206
+ "session-1",
207
+ 1,
208
+ );
209
+ const sent = sentMessages[0] as Record<string, unknown>;
210
+
211
+ proxy.processObservation(sent.requestId as string, {
212
+ axTree: "Button [1]",
213
+ executionResult: "Clicked element 42",
214
+ timings: { total: 420, axWalk: 120, capture: 240 },
215
+ });
216
+
217
+ const result = await resultPromise;
218
+ const logs = stepTimingsLogs();
219
+ expect(logs).toHaveLength(1);
220
+ expect(logs[0]).toMatchObject({
221
+ requestId: sent.requestId,
222
+ toolName: "computer_use_click",
223
+ step: 1,
224
+ helper: { total: 420, axWalk: 120, capture: 240 },
225
+ });
226
+ expect(typeof logs[0].roundTripMs).toBe("number");
227
+
228
+ // The timings are observability only: nothing reaches the model.
229
+ expect(result.content).not.toContain("420");
230
+ expect(result.content).not.toContain("roundTripMs");
231
+ });
232
+
233
+ test("labels each line with its own request, not the latest dispatch", async () => {
234
+ setup();
235
+
236
+ // One model response can dispatch several CU tools, and the agent loop
237
+ // runs them concurrently. Dispatch two, then answer the first one last.
238
+ proxy.recordAction("computer_use_click", { element_id: 42 });
239
+ const firstPromise = proxy.request(
240
+ "computer_use_click",
241
+ { element_id: 42 },
242
+ "session-1",
243
+ proxy.stepCount,
244
+ );
245
+ proxy.recordAction("computer_use_type_text", { text: "hello" });
246
+ const secondPromise = proxy.request(
247
+ "computer_use_type_text",
248
+ { text: "hello" },
249
+ "session-1",
250
+ proxy.stepCount,
251
+ );
252
+
253
+ const first = sentMessages[0] as Record<string, unknown>;
254
+ const second = sentMessages[1] as Record<string, unknown>;
255
+
256
+ proxy.processObservation(second.requestId as string, {
257
+ axTree: "Field [2]",
258
+ timings: { total: 200 },
259
+ });
260
+ proxy.processObservation(first.requestId as string, {
261
+ axTree: "Button [1]",
262
+ timings: { total: 400 },
263
+ });
264
+ await Promise.all([firstPromise, secondPromise]);
265
+
266
+ const logs = stepTimingsLogs();
267
+ const firstLog = logs.find((l) => l.requestId === first.requestId);
268
+ const secondLog = logs.find((l) => l.requestId === second.requestId);
269
+
270
+ // The click landed after the type_text, so reading current proxy state
271
+ // would have filed it under computer_use_type_text at step 2.
272
+ expect(firstLog).toMatchObject({
273
+ toolName: "computer_use_click",
274
+ step: 1,
275
+ helper: { total: 400 },
276
+ });
277
+ expect(secondLog).toMatchObject({
278
+ toolName: "computer_use_type_text",
279
+ step: 2,
280
+ helper: { total: 200 },
281
+ });
282
+ });
283
+
284
+ test("an observation without timings resolves normally", async () => {
285
+ setup();
286
+
287
+ const resultPromise = proxy.request(
288
+ "computer_use_click",
289
+ { element_id: 7 },
290
+ "session-1",
291
+ 1,
292
+ );
293
+ const sent = sentMessages[0] as Record<string, unknown>;
294
+
295
+ proxy.processObservation(sent.requestId as string, {
296
+ axTree: "Button [1]",
297
+ executionResult: "Clicked element 7",
298
+ });
299
+
300
+ const result = await resultPromise;
301
+ expect(result.isError).toBe(false);
302
+ expect(result.content).toContain("Clicked element 7");
303
+ expect(result.content).toContain("<ax-tree>");
304
+
305
+ const logs = stepTimingsLogs();
306
+ expect(logs).toHaveLength(1);
307
+ expect(logs[0]).not.toHaveProperty("helper");
308
+ expect(typeof logs[0].roundTripMs).toBe("number");
309
+ });
310
+ });
311
+
167
312
  // -------------------------------------------------------------------------
168
313
  // Timeout
169
314
  // -------------------------------------------------------------------------
@@ -1652,4 +1797,398 @@ describe("HostCuProxy", () => {
1652
1797
  expect(proxy.previousAXTree).toBeUndefined();
1653
1798
  });
1654
1799
  });
1800
+
1801
+ describe("batched actions", () => {
1802
+ const ACTIONS = [
1803
+ { action: "open_app", app_name: "Google Chrome" },
1804
+ { action: "key", key: "cmd+n" },
1805
+ { action: "type_text", text: "example.com" },
1806
+ { action: "key", key: "enter" },
1807
+ ];
1808
+
1809
+ function connect(capabilities: string[]) {
1810
+ mockClients = [
1811
+ { clientId: "mac-1", actorPrincipalId: "user-1", capabilities },
1812
+ ];
1813
+ }
1814
+ function sequence(targetClientId?: string) {
1815
+ return proxy.request(
1816
+ "computer_use_sequence",
1817
+ { actions: ACTIONS, reasoning: "open a new window on a known URL" },
1818
+ "session-1",
1819
+ 1,
1820
+ "open a new window on a known URL",
1821
+ undefined,
1822
+ targetClientId,
1823
+ "user-1",
1824
+ );
1825
+ }
1826
+
1827
+ test("a client without host_cu_sequence is refused and nothing is sent", async () => {
1828
+ setup();
1829
+ connect(["host_cu", "host_cu_window_capture"]);
1830
+ for (const target of [undefined, "mac-1"]) {
1831
+ const result = await sequence(target);
1832
+ expect(result.isError).toBe(true);
1833
+ expect(result.content).toContain(
1834
+ "Batched actions require a desktop app that supports computer_use_sequence",
1835
+ );
1836
+ expect(result.content).toContain("Nothing was run.");
1837
+ }
1838
+ expect(sentMessages).toHaveLength(0);
1839
+ });
1840
+
1841
+ test("no connected client never falls back to an untargeted broadcast", async () => {
1842
+ setup();
1843
+ expect((await sequence()).isError).toBe(true);
1844
+ expect(sentMessages).toHaveLength(0);
1845
+ });
1846
+
1847
+ test("a client with host_cu_sequence gets one request carrying the actions", async () => {
1848
+ setup();
1849
+ connect(["host_cu", "host_cu_sequence"]);
1850
+ const pending = sequence();
1851
+ expect(sentMessages).toHaveLength(1);
1852
+ const sent = sentMessages[0] as {
1853
+ requestId: string;
1854
+ toolName: string;
1855
+ targetClientId: string;
1856
+ input: Record<string, unknown>;
1857
+ };
1858
+ expect(sent.toolName).toBe("computer_use_sequence");
1859
+ expect(sent.targetClientId).toBe("mac-1");
1860
+ expect(sent.input.actions).toEqual(ACTIONS);
1861
+ proxy.processObservation(sent.requestId, {
1862
+ axTree: "Window: New Tab",
1863
+ executionResult: "Ran 4 actions",
1864
+ });
1865
+ const result = await pending;
1866
+ expect(result.isError).toBe(false);
1867
+ expect(result.content).toContain("Ran 4 actions");
1868
+ });
1869
+ });
1870
+
1871
+ describe("screenshot on request", () => {
1872
+ const TREE = 'Window: "Inbox" (Mail)\n [1] button "Reply" at (10, 10)';
1873
+ const OMITTED = "Screenshot omitted";
1874
+
1875
+ function connect() {
1876
+ mockClients = [
1877
+ {
1878
+ clientId: "mac-1",
1879
+ actorPrincipalId: "user-1",
1880
+ capabilities: [
1881
+ "host_cu",
1882
+ "host_cu_window_capture",
1883
+ "host_cu_annotate",
1884
+ ],
1885
+ },
1886
+ ];
1887
+ }
1888
+
1889
+ /** Dispatch one request and return what was sent, without answering it. */
1890
+ function dispatch(
1891
+ input: Record<string, unknown> = { element_id: 1 },
1892
+ toolName = "computer_use_click",
1893
+ ) {
1894
+ const pending = proxy.request(
1895
+ toolName,
1896
+ input,
1897
+ "session-1",
1898
+ 1,
1899
+ undefined,
1900
+ undefined,
1901
+ "mac-1",
1902
+ "user-1",
1903
+ );
1904
+ const sent = sentMessages.at(-1) as {
1905
+ requestId: string;
1906
+ input: Record<string, unknown>;
1907
+ };
1908
+ return { pending, sent };
1909
+ }
1910
+
1911
+ /** Dispatch one request, answer it with `observation`, and return both ends. */
1912
+ async function step(
1913
+ observation: Record<string, unknown>,
1914
+ input?: Record<string, unknown>,
1915
+ toolName?: string,
1916
+ ) {
1917
+ const { pending, sent } = dispatch(input, toolName);
1918
+ proxy.processObservation(sent.requestId, observation);
1919
+ return { sent, result: await pending };
1920
+ }
1921
+
1922
+ function asksForScreenshot(sent: { input: Record<string, unknown> }) {
1923
+ return !Object.hasOwn(sent.input, "includeScreenshot");
1924
+ }
1925
+
1926
+ test("the first observed step attaches and the next unscoped step does not", async () => {
1927
+ setup();
1928
+ connect();
1929
+ const first = await step({ axTree: TREE, screenshot: "img" });
1930
+ expect(first.sent.input).toEqual({ element_id: 1 });
1931
+ expect(first.result.content).not.toContain(OMITTED);
1932
+
1933
+ const second = await step({ axTree: TREE });
1934
+ expect(second.sent.input).toEqual({
1935
+ element_id: 1,
1936
+ includeScreenshot: false,
1937
+ });
1938
+ expect(second.result.content).toContain(OMITTED);
1939
+ expect(second.result.isError).toBe(false);
1940
+ });
1941
+
1942
+ test("the first look is per desktop, so a newly targeted one attaches", async () => {
1943
+ setup();
1944
+ mockClients = [
1945
+ {
1946
+ clientId: "mac-1",
1947
+ actorPrincipalId: "user-1",
1948
+ capabilities: ["host_cu"],
1949
+ },
1950
+ {
1951
+ clientId: "mac-2",
1952
+ actorPrincipalId: "user-1",
1953
+ capabilities: ["host_cu"],
1954
+ },
1955
+ ];
1956
+ const on = (clientId: string) => {
1957
+ const pending = proxy.request(
1958
+ "computer_use_click",
1959
+ { element_id: 1 },
1960
+ "session-1",
1961
+ 1,
1962
+ undefined,
1963
+ undefined,
1964
+ clientId,
1965
+ "user-1",
1966
+ );
1967
+ const sent = sentMessages.at(-1) as {
1968
+ requestId: string;
1969
+ input: Record<string, unknown>;
1970
+ };
1971
+ return { pending, sent };
1972
+ };
1973
+
1974
+ const firstA = on("mac-1");
1975
+ proxy.processObservation(firstA.sent.requestId, {
1976
+ axTree: TREE,
1977
+ screenshot: "img",
1978
+ });
1979
+ await firstA.pending;
1980
+ const secondA = on("mac-1");
1981
+ expect(asksForScreenshot(secondA.sent)).toBe(false);
1982
+ proxy.processObservation(secondA.sent.requestId, { axTree: TREE });
1983
+ await secondA.pending;
1984
+
1985
+ // A different machine the assistant has never seen gets its own look.
1986
+ const firstB = on("mac-2");
1987
+ expect(asksForScreenshot(firstB.sent)).toBe(true);
1988
+ proxy.processObservation(firstB.sent.requestId, {
1989
+ axTree: TREE,
1990
+ screenshot: "img",
1991
+ });
1992
+ await firstB.pending;
1993
+ const secondB = on("mac-2");
1994
+ expect(asksForScreenshot(secondB.sent)).toBe(false);
1995
+ proxy.processObservation(secondB.sent.requestId, { axTree: TREE });
1996
+ await secondB.pending;
1997
+ });
1998
+
1999
+ test("include_screenshot: true attaches and is not forwarded", async () => {
2000
+ setup();
2001
+ connect();
2002
+ await step({ axTree: TREE, screenshot: "img" });
2003
+ const input = { include_screenshot: true };
2004
+ const { sent, result } = await step(
2005
+ { axTree: TREE, screenshot: "img" },
2006
+ input,
2007
+ "computer_use_observe",
2008
+ );
2009
+ expect(sent.input).toEqual({});
2010
+ expect(input).toEqual({ include_screenshot: true });
2011
+ expect(result.content).not.toContain(OMITTED);
2012
+ });
2013
+
2014
+ test("include_screenshot: false is stripped and the step still skips", async () => {
2015
+ setup();
2016
+ connect();
2017
+ await step({ axTree: TREE, screenshot: "img" });
2018
+ const { sent } = await step(
2019
+ { axTree: TREE },
2020
+ { include_screenshot: false },
2021
+ "computer_use_observe",
2022
+ );
2023
+ expect(sent.input).toEqual({ includeScreenshot: false });
2024
+ });
2025
+
2026
+ test("a scoped observe always attaches", async () => {
2027
+ setup();
2028
+ connect();
2029
+ await step({ axTree: TREE, screenshot: "img" });
2030
+ for (const input of [
2031
+ { capture_window_id: 12 },
2032
+ { captureWindowId: 12 },
2033
+ { captureDisplayId: 1 },
2034
+ ]) {
2035
+ const scoped = await step(
2036
+ { axTree: TREE, screenshot: "img" },
2037
+ input,
2038
+ "computer_use_observe",
2039
+ );
2040
+ expect(scoped.sent.input).toEqual(input);
2041
+ }
2042
+ });
2043
+
2044
+ test("a scoped observation does not count as the first look", async () => {
2045
+ setup();
2046
+ connect();
2047
+ await step(
2048
+ { axTree: TREE, screenshot: "img" },
2049
+ { capture_window_id: 12 },
2050
+ "computer_use_observe",
2051
+ );
2052
+ expect(asksForScreenshot((await step({ axTree: TREE })).sent)).toBe(true);
2053
+ });
2054
+
2055
+ test("reset() makes the next step attach again", async () => {
2056
+ setup();
2057
+ connect();
2058
+ await step({ axTree: TREE, screenshot: "img" });
2059
+ expect(asksForScreenshot((await step({ axTree: TREE })).sent)).toBe(
2060
+ false,
2061
+ );
2062
+ proxy.reset();
2063
+ expect(asksForScreenshot((await step({ axTree: TREE })).sent)).toBe(true);
2064
+ });
2065
+
2066
+ test("the omitted line is absent when a requested capture failed", async () => {
2067
+ setup();
2068
+ connect();
2069
+ const failed = await step({ axTree: TREE });
2070
+ expect(asksForScreenshot(failed.sent)).toBe(true);
2071
+ expect(failed.result.content).not.toContain(OMITTED);
2072
+ });
2073
+
2074
+ test("a first look whose capture failed is asked for again", async () => {
2075
+ setup();
2076
+ connect();
2077
+ const failed = await step({ axTree: TREE });
2078
+ expect(asksForScreenshot(failed.sent)).toBe(true);
2079
+ const retry = await step({ axTree: TREE, screenshot: "img" });
2080
+ expect(asksForScreenshot(retry.sent)).toBe(true);
2081
+ expect(asksForScreenshot((await step({ axTree: TREE })).sent)).toBe(
2082
+ false,
2083
+ );
2084
+ });
2085
+
2086
+ test("an observation that lands after a reset leaves the new run's state alone", async () => {
2087
+ setup();
2088
+ connect();
2089
+ const stale = dispatch();
2090
+ proxy.reset();
2091
+ proxy.processObservation(stale.sent.requestId, {
2092
+ axTree: "stale tree from the finished run",
2093
+ screenshot: "img",
2094
+ });
2095
+ await stale.pending;
2096
+ expect(proxy.previousAXTree).toBeUndefined();
2097
+ expect(proxy.consecutiveUnchangedSteps).toBe(0);
2098
+ });
2099
+
2100
+ test("an observation that lands after a reset does not restore the first look", async () => {
2101
+ setup();
2102
+ connect();
2103
+ const stale = dispatch();
2104
+ proxy.reset();
2105
+ proxy.processObservation(stale.sent.requestId, {
2106
+ axTree: TREE,
2107
+ screenshot: "img",
2108
+ });
2109
+ await stale.pending;
2110
+ expect(asksForScreenshot((await step({ axTree: TREE })).sent)).toBe(true);
2111
+ });
2112
+
2113
+ test("a skipped step that fails still carries the omitted line after the failure", async () => {
2114
+ setup();
2115
+ connect();
2116
+ await step({ axTree: TREE, screenshot: "img" });
2117
+ const failed = await step({
2118
+ axTree: TREE,
2119
+ executionError: "Element not found",
2120
+ });
2121
+ expect(asksForScreenshot(failed.sent)).toBe(false);
2122
+ expect(failed.result.isError).toBe(true);
2123
+ expect(failed.result.content).toStartWith(
2124
+ "Action failed: Element not found",
2125
+ );
2126
+ expect(failed.result.content).toContain(OMITTED);
2127
+ });
2128
+
2129
+ test("a refused skipped step with a tree carries the omitted line", async () => {
2130
+ setup();
2131
+ connect();
2132
+ await step({ axTree: TREE, screenshot: "img" });
2133
+ const refused = await step({
2134
+ axTree: TREE,
2135
+ executionError: "BLOCKED: the user is active",
2136
+ });
2137
+ expect(asksForScreenshot(refused.sent)).toBe(false);
2138
+ expect(refused.result.isError).toBe(true);
2139
+ expect(refused.result.content).toContain(OMITTED);
2140
+ });
2141
+
2142
+ test("a skipped step with no tree gets no omitted line", async () => {
2143
+ setup();
2144
+ connect();
2145
+ await step({ axTree: TREE, screenshot: "img" });
2146
+ const empty = await step({ executionResult: "Clicked" });
2147
+ expect(asksForScreenshot(empty.sent)).toBe(false);
2148
+ expect(empty.result.content).not.toContain(OMITTED);
2149
+ });
2150
+
2151
+ test("a helper that ignores the flag keeps its screenshot and gets no omitted line", async () => {
2152
+ setup();
2153
+ connect();
2154
+ await step({ axTree: TREE, screenshot: "img" });
2155
+ const legacy = await step({ axTree: TREE, screenshot: "img" });
2156
+ expect(asksForScreenshot(legacy.sent)).toBe(false);
2157
+ expect(legacy.result.content).not.toContain(OMITTED);
2158
+ expect(legacy.result.contentBlocks).toHaveLength(1);
2159
+ });
2160
+
2161
+ test("a point_at response leaves the first-look state untouched", async () => {
2162
+ setup();
2163
+ connect();
2164
+ const input = { marks: [], include_screenshot: true };
2165
+ const { pending, sent } = dispatch(input, "computer_use_point_at");
2166
+ expect(sent.input).toBe(input);
2167
+ proxy.processObservation(sent.requestId, { axTree: TREE });
2168
+ expect((await pending).content).not.toContain(OMITTED);
2169
+ expect(asksForScreenshot((await step({ axTree: TREE })).sent)).toBe(true);
2170
+ });
2171
+
2172
+ test("concurrent requests each keep their own dispatch decision", async () => {
2173
+ setup();
2174
+ connect();
2175
+ // Both are dispatched before any observation, so both ask for pixels,
2176
+ // even though the first one's observation lands before the second's.
2177
+ const first = dispatch();
2178
+ const second = dispatch();
2179
+ expect(asksForScreenshot(first.sent)).toBe(true);
2180
+ expect(asksForScreenshot(second.sent)).toBe(true);
2181
+ proxy.processObservation(first.sent.requestId, {
2182
+ axTree: TREE,
2183
+ screenshot: "img",
2184
+ });
2185
+ const third = dispatch();
2186
+ expect(asksForScreenshot(third.sent)).toBe(false);
2187
+ proxy.processObservation(third.sent.requestId, { axTree: TREE });
2188
+ proxy.processObservation(second.sent.requestId, { axTree: TREE });
2189
+ await first.pending;
2190
+ expect((await second.pending).content).not.toContain(OMITTED);
2191
+ expect((await third.pending).content).toContain(OMITTED);
2192
+ });
2193
+ });
1655
2194
  });
@@ -274,6 +274,29 @@ describe("HostFileProxy — targetClientId (Phase 2)", () => {
274
274
  });
275
275
  });
276
276
 
277
+ test("selects the most recently active same-user client when several are connected", async () => {
278
+ setup();
279
+ setupMultipleClients(["client-1", "client-2"]);
280
+
281
+ const resultPromise = proxy.request(
282
+ { operation: "read", path: "/tmp/file.txt" },
283
+ "session-1",
284
+ undefined,
285
+ undefined,
286
+ TEST_PRINCIPAL,
287
+ );
288
+
289
+ const sent = sentMessages[0] as Record<string, unknown>;
290
+ expect(sent.targetClientId).toBe("client-1");
291
+ const options = sentMessageOptions[0] as
292
+ | Record<string, unknown>
293
+ | undefined;
294
+ expect(options?.targetClientId).toBe("client-1");
295
+
296
+ proxy.resolve(sent.requestId as string, { content: "ok", isError: false });
297
+ expect((await resultPromise).isError).toBe(false);
298
+ });
299
+
277
300
  // ── targetClientId in cancel (abort signal) ──────────────────────────
278
301
 
279
302
  describe("targetClientId in cancel — abort signal", () => {