@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
@@ -28,7 +28,6 @@ import { getAllDefaultPluginNames } from "../plugins/defaults/main.js";
28
28
  import { isActivationSession } from "../plugins/defaults/memory/activation-session-store.js";
29
29
  import { isPluginDisabled } from "../plugins/disabled-state.js";
30
30
  import type { Message, ToolDefinition } from "../providers/types.js";
31
- import { assistantEventHub } from "../runtime/assistant-event-hub.js";
32
31
  import { registerConversationSender } from "../tools/browser/browser-screencast.js";
33
32
  import { supportsClientOsForSkillTool } from "../tools/client-os.js";
34
33
  import type { ToolExecutor } from "../tools/executor.js";
@@ -260,6 +259,24 @@ export function createToolExecutor(
260
259
  // see {@link SubagentToolGateMode}): rejects non-allowlisted calls BEFORE
261
260
  // any executor dispatch, so a non-allowlisted tool's executor never runs.
262
261
  // The error tool_result lets the model continue or finish.
262
+ const rejectUnattendedHostTool = (
263
+ toolName: string,
264
+ ): ToolExecutionResult | null => {
265
+ const { transportInterface } = resolveTurnClientOs(ctx);
266
+ const isUnattended = ctx.currentTurnIsNonInteractive ?? ctx.hasNoClient;
267
+ if (
268
+ HOST_TOOL_NAMES.has(toolName) &&
269
+ isUnattended &&
270
+ transportInterface !== "chrome-extension"
271
+ ) {
272
+ return {
273
+ content: `The "${toolName}" tool requires an interactive user turn and cannot run in the background.`,
274
+ isError: true,
275
+ };
276
+ }
277
+ return null;
278
+ };
279
+
263
280
  const rejectNonAllowlistedTool = (
264
281
  toolName: string,
265
282
  ): ToolExecutionResult | null => {
@@ -387,6 +404,10 @@ export function createToolExecutor(
387
404
  if (rejection) {
388
405
  return rejection;
389
406
  }
407
+ const unattendedRejection = rejectUnattendedHostTool(executionName);
408
+ if (unattendedRejection) {
409
+ return unattendedRejection;
410
+ }
390
411
  }
391
412
 
392
413
  if (isDoordashCommand(executionName, executionInput)) {
@@ -550,6 +571,10 @@ export function createToolExecutor(
550
571
  if (innerRejection) {
551
572
  return innerRejection;
552
573
  }
574
+ const unattendedRejection = rejectUnattendedHostTool(toolName);
575
+ if (unattendedRejection) {
576
+ return unattendedRejection;
577
+ }
553
578
 
554
579
  // Per-chat plugin scope: reject the resolved inner tool when it belongs
555
580
  // to a plugin outside the conversation's effective set.
@@ -628,7 +653,6 @@ export const DEFAULT_PREACTIVATED_SKILL_IDS = ["notifications", "subagent"];
628
653
  // ── Conditional tool sets ────────────────────────────────────────────
629
654
 
630
655
  const UI_SURFACE_TOOL_NAMES = new Set(["ui_show", "ui_update", "ui_dismiss"]);
631
- const SLACK_TASK_PROGRESS_UI_TOOL_NAMES = new Set(["ui_show", "ui_update"]);
632
656
  /**
633
657
  * Single source of truth for which tools are host tools and the capability
634
658
  * each one requires from the connected client interface. Adding a tool here
@@ -856,56 +880,26 @@ export function isToolActiveForContext(
856
880
  return supportsChannelReaction(turnChannel);
857
881
  }
858
882
  if (UI_SURFACE_TOOL_NAMES.has(name)) {
859
- if (
860
- channelCapabilities?.channel === "slack" &&
861
- SLACK_TASK_PROGRESS_UI_TOOL_NAMES.has(name)
862
- ) {
863
- return !hasNoClient;
864
- }
865
- return channelCapabilities?.supportsDynamicUi ?? !hasNoClient;
883
+ // Surface calls write conversation content. Background turns persist that
884
+ // content for the next client that opens the conversation, so presence
885
+ // cannot change whether the model receives these definitions.
886
+ return true;
866
887
  }
867
888
  if (HOST_TOOL_NAMES.has(name)) {
868
889
  const capability = HOST_TOOL_TO_CAPABILITY.get(name);
869
890
  const transport = transportInterface;
870
891
 
871
- // Per-capability check is authoritative for structural support: if the
872
- // transport cannot service this capability, the tool is filtered out.
892
+ // A transport that does not implement a capability can invoke it through
893
+ // an eligible same-user client. Client selection happens when the call
894
+ // runs, so the wire schema stays stable across live and background turns.
873
895
  if (transport && capability && !supportsHostProxy(transport, capability)) {
874
- // Cross-client exception: allow host tools whose capabilities have
875
- // cross-client routing infrastructure (Phases 1–3 plus host_browser
876
- // via PR #27489) to be exposed for non-host-proxy transports (e.g.
877
- // "web", "ios") when at least one capable client is connected via
878
- // the event hub. Members of CROSS_CLIENT_EXPOSED_CAPABILITIES
879
- // (host_bash, host_file, host_browser) qualify.
880
- // chrome-extension transport is excluded as a security boundary
881
- // (extension only gets host_browser via its own executor path);
882
- // hasNoClient turns are excluded (no interactive approval UI
883
- // available).
884
- if (
885
- capability &&
896
+ return (
886
897
  CROSS_CLIENT_EXPOSED_CAPABILITIES.has(capability) &&
887
- transport !== "chrome-extension" &&
888
- !hasNoClient &&
889
- assistantEventHub.listClientsByCapability(capability).length > 0
890
- ) {
891
- return true;
892
- }
893
- return false;
894
- }
895
-
896
- // chrome-extension is its own executor — the extension's popup gates
897
- // commands via its own UI, and the transport does not use an SSE-level
898
- // interactive approval channel. hasNoClient is intentionally `true` for
899
- // chrome-extension turns (chrome-extension is not in INTERACTIVE_INTERFACES)
900
- // and must not gate host_browser. Trust the per-capability check.
901
- if (transport === "chrome-extension") {
902
- return true;
898
+ transport !== "chrome-extension"
899
+ );
903
900
  }
904
901
 
905
- // For transports that surface approvals over SSE (macos, backwards-compat
906
- // fallback), deny when no client is present so the guardian auto-approve
907
- // path cannot execute host commands unattended.
908
- return !hasNoClient;
902
+ return true;
909
903
  }
910
904
  if (CLIENT_CAPABILITY_TOOL_NAMES.has(name)) {
911
905
  if (name === "ask_question" && channelCapabilities?.clientOS === "macos") {
@@ -2366,9 +2366,6 @@ export class Conversation {
2366
2366
 
2367
2367
  markStale(): void {
2368
2368
  this.stale = true;
2369
- // Invalidate the cached skill catalog so the next projection picks up
2370
- // filesystem changes (e.g. a skill created during this run).
2371
- this.skillProjectionCache.catalog = undefined;
2372
2369
  }
2373
2370
 
2374
2371
  isStale(): boolean {
@@ -10,6 +10,7 @@ import { join } from "node:path";
10
10
 
11
11
  import { getLogger } from "../util/logger.js";
12
12
  import { getWorkspaceDir } from "../util/platform.js";
13
+ import { safeStringSlice } from "../util/unicode.js";
13
14
 
14
15
  const log = getLogger("dictation-profile-store");
15
16
 
@@ -130,7 +131,7 @@ function validateAndClampConfig(raw: unknown): DictationProfilesConfig {
130
131
  { profileId: profile.id },
131
132
  `stylePrompt exceeds ${MAX_STYLE_PROMPT_LENGTH} chars, truncating`,
132
133
  );
133
- stylePrompt = stylePrompt.slice(0, MAX_STYLE_PROMPT_LENGTH);
134
+ stylePrompt = safeStringSlice(stylePrompt, 0, MAX_STYLE_PROMPT_LENGTH);
134
135
  }
135
136
 
136
137
  // Validate dictionary entries
@@ -32,6 +32,7 @@ import { unwrapExternalContentForDisplay } from "../../security/untrusted-conten
32
32
  import type { CredentialInjectionTemplate } from "../../tools/credentials/policy-types.js";
33
33
  import { getLogger } from "../../util/logger.js";
34
34
  import { joinWithSpacing } from "../../util/text-spacing.js";
35
+ import { safeStringSlice } from "../../util/unicode.js";
35
36
  import { estimateBase64Bytes } from "../assistant-attachments.js";
36
37
  import { conversationSupportsDynamicUi } from "../channel-ui-capability.js";
37
38
  import { findConversation } from "../conversation-registry.js";
@@ -264,7 +265,7 @@ function clampAttachmentText(text: string): string {
264
265
  if (text.length <= HISTORY_ATTACHMENT_TEXT_LIMIT) {
265
266
  return text;
266
267
  }
267
- return `${text.slice(0, HISTORY_ATTACHMENT_TEXT_LIMIT)}<truncated />`;
268
+ return `${safeStringSlice(text, 0, HISTORY_ATTACHMENT_TEXT_LIMIT)}<truncated />`;
268
269
  }
269
270
 
270
271
  interface FileBlockMetadata {
@@ -82,6 +82,7 @@ import {
82
82
  searchSkillsRegistry,
83
83
  } from "../../skills/skillssh-registry.js";
84
84
  import { getWorkspaceSkillsDir } from "../../util/platform.js";
85
+ import { safeStringSlice } from "../../util/unicode.js";
85
86
  import { getConfigWatcher } from "../config-watcher.js";
86
87
  import type {
87
88
  SkillDetailResponse,
@@ -237,9 +238,11 @@ function heuristicDraft(body: string): {
237
238
  const lines = body.split("\n").filter((l) => l.trim());
238
239
  const firstLine = lines[0]?.trim() ?? "";
239
240
  const name =
240
- firstLine.replace(/^#+\s*/, "").slice(0, 100) || "Untitled Skill";
241
+ safeStringSlice(firstLine.replace(/^#+\s*/, ""), 0, 100) ||
242
+ "Untitled Skill";
241
243
  const skillId = toSkillSlug(name) || "untitled-skill";
242
- const description = body.trim().slice(0, 200) || "No description provided";
244
+ const description =
245
+ safeStringSlice(body.trim(), 0, 200) || "No description provided";
243
246
  return { skillId, name, description, emoji: "\u{1F4DD}" };
244
247
  }
245
248
 
@@ -1732,7 +1735,7 @@ export async function draftSkill(params: {
1732
1735
  "- emoji: a single emoji character representing the skill",
1733
1736
  "",
1734
1737
  "Skill body:",
1735
- body.slice(0, 2000),
1738
+ safeStringSlice(body, 0, 2000),
1736
1739
  ].join("\n");
1737
1740
 
1738
1741
  const response = await provider.sendMessage([userMessage(prompt)], {
@@ -1,9 +1,9 @@
1
1
  import { getConfig } from "../config/loader.js";
2
2
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
3
3
  import {
4
- ambiguousSameUserError,
5
4
  enforceSameActorOrErrorResult,
6
- pickSameUserAutoResolve,
5
+ pickMostRecentSameUserClient,
6
+ unavailableSameUserClientError,
7
7
  } from "../runtime/auth/same-actor.js";
8
8
  import * as pendingInteractions from "../runtime/pending-interactions.js";
9
9
  import { formatShellOutput } from "../tools/shared/shell-output.js";
@@ -93,21 +93,17 @@ export class HostBashProxy extends HostProxyBase<
93
93
  }
94
94
  resolvedTargetClientId = input.targetClientId;
95
95
  } else {
96
- // Auto-resolve to the unique same-user client. Reject (rather than
97
- // broadcast) when multiple same-user clients are connected so that
98
- // a single targeted-style request cannot fan out across every one
99
- // of the user's machines. Zero same-user matches falls through to
100
- // the existing untargeted code path.
101
- const resolved = pickSameUserAutoResolve({
96
+ resolvedTargetClientId = pickMostRecentSameUserClient({
102
97
  hub: assistantEventHub,
103
98
  capability: "host_bash",
104
99
  sourceActorPrincipalId,
105
100
  });
106
- if (resolved.kind === "ambiguous") {
107
- return ambiguousSameUserError("host_bash");
101
+ if (
102
+ resolvedTargetClientId === undefined &&
103
+ sourceActorPrincipalId !== undefined
104
+ ) {
105
+ return unavailableSameUserClientError("host_bash");
108
106
  }
109
- resolvedTargetClientId =
110
- resolved.kind === "match" ? resolved.clientId : undefined;
111
107
  }
112
108
 
113
109
  // Targeted requests must be bound to the same authenticated user as the
@@ -5,7 +5,10 @@ import {
5
5
  assistantEventHub,
6
6
  broadcastMessage,
7
7
  } from "../runtime/assistant-event-hub.js";
8
- import { enforceSameActorOrErrorResult } from "../runtime/auth/same-actor.js";
8
+ import {
9
+ enforceSameActorOrErrorResult,
10
+ unavailableSameUserClientError,
11
+ } from "../runtime/auth/same-actor.js";
9
12
  import * as pendingInteractions from "../runtime/pending-interactions.js";
10
13
  import type { ToolExecutionResult } from "../tools/types.js";
11
14
  import { AssistantError, ErrorCode } from "../util/errors.js";
@@ -284,6 +287,17 @@ export class HostBrowserProxy {
284
287
  targetClientId,
285
288
  );
286
289
 
290
+ if (
291
+ preferredClient === undefined &&
292
+ sourceActorPrincipalId !== undefined &&
293
+ targetClientId === undefined
294
+ ) {
295
+ if (isExtensionOnlyMethod(input.cdpMethod)) {
296
+ return Promise.resolve(extensionRequiredResult(input.cdpMethod));
297
+ }
298
+ return Promise.resolve(unavailableSameUserClientError("host_browser"));
299
+ }
300
+
287
301
  // Same-user enforcement: when the caller's actor is known, refuse to
288
302
  // dispatch to a client owned by a different actor. This covers the
289
303
  // cross-client exposure case where a web/iOS turn for actor A would
@@ -42,6 +42,11 @@ const MAX_HISTORY_ENTRIES = 10;
42
42
  const LOOP_DETECTION_WINDOW = 3;
43
43
  const CONSECUTIVE_UNCHANGED_WARNING_THRESHOLD = 2;
44
44
 
45
+ const SEQUENCE_TOOL = "computer_use_sequence";
46
+
47
+ const SCREENSHOT_OMITTED_MESSAGE =
48
+ "Screenshot omitted: the accessibility tree above is current. Pass include_screenshot: true on computer_use_observe to see the screen.";
49
+
45
50
  // computer_use_key combos that change only selection/cursor/clipboard state.
46
51
  // The AX tree models none of these, so they always produce an empty diff —
47
52
  // exempt them from the "NO VISIBLE EFFECT" signal (mirrors computer_use_wait).
@@ -113,6 +118,8 @@ export interface CuObservationResult {
113
118
  executionResult?: string;
114
119
  executionError?: string;
115
120
  userGuidance?: string;
121
+ /** Per-phase helper timings in milliseconds, absent on older helpers. */
122
+ timings?: Record<string, number>;
116
123
  }
117
124
 
118
125
  export interface ActionRecord {
@@ -161,6 +168,40 @@ function actionSignature(record: ActionRecord): string {
161
168
  return `${record.toolName}:${JSON.stringify(record.input)}`;
162
169
  }
163
170
 
171
+ /**
172
+ * The key a desktop's first look is tracked under. An untargeted request has
173
+ * no client id, so it gets a key no real id can take.
174
+ */
175
+ function observedTargetKey(targetClientId: string | undefined): string {
176
+ return targetClientId === undefined
177
+ ? "\u0000untargeted"
178
+ : `client:${targetClientId}`;
179
+ }
180
+
181
+ /** Whether `input` scopes the observation to one window or display. */
182
+ function hasCaptureTarget(input: Record<string, unknown>): boolean {
183
+ return (
184
+ Object.hasOwn(input, "capture_window_id") ||
185
+ Object.hasOwn(input, "captureWindowId") ||
186
+ Object.hasOwn(input, "captureDisplayId")
187
+ );
188
+ }
189
+
190
+ /**
191
+ * Whether the resolved client claimed `capability` on its connection. An
192
+ * untargeted request has no client to vouch for it, so it never qualifies.
193
+ */
194
+ function clientAdvertises(
195
+ targetClientId: string | undefined,
196
+ capability: "host_cu_window_capture" | "host_cu_sequence",
197
+ ): boolean {
198
+ const client =
199
+ targetClientId == null
200
+ ? undefined
201
+ : assistantEventHub.getClientById(targetClientId);
202
+ return client?.capabilities.includes(capability) ?? false;
203
+ }
204
+
164
205
  // ---------------------------------------------------------------------------
165
206
  // HostCuProxy
166
207
  // ---------------------------------------------------------------------------
@@ -172,8 +213,36 @@ export class HostCuProxy {
172
213
  private _previousAXTree: string | undefined;
173
214
  private _consecutiveUnchangedSteps = 0;
174
215
  private _actionHistory: ActionRecord[] = [];
175
- /** Owned request IDs mapped to whether their observation is scoped. */
176
- private _ownedRequests = new Map<string, boolean>();
216
+ /**
217
+ * Desktops an unscoped observation has come back from since the last reset,
218
+ * keyed by `observedTargetKey`. A first look is per desktop: having seen one
219
+ * machine says nothing about another the conversation switches to.
220
+ */
221
+ private _observedTargets = new Set<string>();
222
+ /**
223
+ * Bumped on every reset. A request carries the value it was dispatched
224
+ * under, so an observation that lands after a reset cannot restore state
225
+ * that reset cleared.
226
+ */
227
+ private _resetGeneration = 0;
228
+ /**
229
+ * Owned request IDs mapped to whether their observation is scoped, whether
230
+ * the helper was told to skip the screenshot, and when the request was
231
+ * dispatched. The dispatch time gives the round trip, which is the part of
232
+ * a step the helper's own timings cannot see.
233
+ */
234
+ private _ownedRequests = new Map<
235
+ string,
236
+ {
237
+ scoped: boolean;
238
+ screenshotSkipped: boolean;
239
+ targetKey: string;
240
+ resetGeneration: number;
241
+ dispatchedAt: number;
242
+ toolName: string;
243
+ step: number;
244
+ }
245
+ >();
177
246
 
178
247
  constructor(maxSteps = loadConfig().maxStepsPerSession) {
179
248
  this._maxSteps = maxSteps;
@@ -288,11 +357,7 @@ export class HostCuProxy {
288
357
  }
289
358
  // Never dispatch this option to a legacy executor: it would forward the
290
359
  // unknown snake-case key and silently capture the entire desktop.
291
- const client =
292
- resolvedTargetClientId == null
293
- ? undefined
294
- : assistantEventHub.getClientById(resolvedTargetClientId);
295
- if (!client?.capabilities.includes("host_cu_window_capture")) {
360
+ if (!clientAdvertises(resolvedTargetClientId, "host_cu_window_capture")) {
296
361
  return Promise.resolve({
297
362
  content:
298
363
  "Window-only observation requires a connected client advertising host_cu_window_capture; update the desktop app before retrying. No capture was requested.",
@@ -300,14 +365,40 @@ export class HostCuProxy {
300
365
  });
301
366
  }
302
367
  }
303
- const scopedObservation =
304
- hasWindowTarget ||
305
- Object.hasOwn(input, "captureWindowId") ||
306
- Object.hasOwn(input, "captureDisplayId");
368
+ // An older helper answers an unknown tool name as done, which would end
369
+ // the session with nothing run.
370
+ if (
371
+ toolName === SEQUENCE_TOOL &&
372
+ !clientAdvertises(resolvedTargetClientId, "host_cu_sequence")
373
+ ) {
374
+ return Promise.resolve({
375
+ content:
376
+ "Batched actions require a desktop app that supports computer_use_sequence; update the desktop app, or send the actions one at a time. Nothing was run.",
377
+ isError: true,
378
+ });
379
+ }
380
+ const scopedObservation = hasCaptureTarget(input);
307
381
  if (scopedObservation) {
308
382
  this._previousAXTree = undefined;
309
383
  this._consecutiveUnchangedSteps = 0;
310
384
  }
385
+ const targetKey = observedTargetKey(resolvedTargetClientId);
386
+ // Pointing never reaches the helper's capture path, so its input goes out
387
+ // as given. Every other request carries the screenshot decision made now,
388
+ // at dispatch, and never the model-facing snake_case key.
389
+ let dispatchInput = input;
390
+ let screenshotSkipped = false;
391
+ if (toolName !== POINT_AT_PROXY_TOOL) {
392
+ const { include_screenshot: _includeScreenshot, ...rest } = input;
393
+ screenshotSkipped = !this.shouldAttachScreenshot(
394
+ toolName,
395
+ input,
396
+ targetKey,
397
+ );
398
+ dispatchInput = screenshotSkipped
399
+ ? { ...rest, includeScreenshot: false }
400
+ : rest;
401
+ }
311
402
  const requestId = uuid();
312
403
 
313
404
  return new Promise<ToolExecutionResult>((resolve, reject) => {
@@ -351,7 +442,15 @@ export class HostCuProxy {
351
442
  detachAbort = () => signal.removeEventListener("abort", onAbort);
352
443
  }
353
444
 
354
- this._ownedRequests.set(requestId, scopedObservation);
445
+ this._ownedRequests.set(requestId, {
446
+ scoped: scopedObservation,
447
+ screenshotSkipped,
448
+ targetKey,
449
+ resetGeneration: this._resetGeneration,
450
+ dispatchedAt: Date.now(),
451
+ toolName,
452
+ step: stepNumber,
453
+ });
355
454
 
356
455
  pendingInteractions.register(requestId, {
357
456
  conversationId,
@@ -375,7 +474,7 @@ export class HostCuProxy {
375
474
  requestId,
376
475
  conversationId,
377
476
  toolName,
378
- input,
477
+ input: dispatchInput,
379
478
  stepNumber,
380
479
  reasoning,
381
480
  ...(resolvedTargetClientId != null
@@ -403,7 +502,8 @@ export class HostCuProxy {
403
502
  requestId: string,
404
503
  observation: CuObservationResult,
405
504
  ): ToolExecutionResult | undefined {
406
- const scopedObservation = this._ownedRequests.get(requestId) ?? false;
505
+ const owned = this._ownedRequests.get(requestId);
506
+ const scopedObservation = owned?.scoped ?? false;
407
507
  this._ownedRequests.delete(requestId);
408
508
  const interaction = pendingInteractions.resolve(requestId, "answered");
409
509
  if (!interaction?.rpcResolve) {
@@ -411,9 +511,33 @@ export class HostCuProxy {
411
511
  return undefined;
412
512
  }
413
513
 
514
+ // Label the line from what this request was dispatched with, never from
515
+ // current proxy state. One model response can dispatch several CU tools,
516
+ // and the agent loop runs them concurrently, so a later call can advance
517
+ // the history and the step count before an earlier observation lands.
518
+ // Reading them here would file each measurement under whichever tool was
519
+ // dispatched last, and would mislabel computer_use_point_at, which never
520
+ // records an action at all.
521
+ if (owned) {
522
+ log.info(
523
+ {
524
+ requestId,
525
+ toolName: owned.toolName,
526
+ step: owned.step,
527
+ roundTripMs: Date.now() - owned.dispatchedAt,
528
+ ...(observation.timings ? { helper: observation.timings } : {}),
529
+ },
530
+ "Host CU step timings",
531
+ );
532
+ }
533
+
414
534
  // A targeted snapshot has no comparable action/diff baseline; neither it
415
535
  // nor the first desktop observation after it can imply "no visible effect".
416
- if (scopedObservation) {
536
+ // A response dispatched before the last reset belongs to a finished run,
537
+ // so it must not change any state the new run has started building.
538
+ const fromCurrentRun =
539
+ owned === undefined || owned.resetGeneration === this._resetGeneration;
540
+ if (scopedObservation && fromCurrentRun) {
417
541
  this._previousAXTree = undefined;
418
542
  this._consecutiveUnchangedSteps = 0;
419
543
  }
@@ -421,10 +545,24 @@ export class HostCuProxy {
421
545
  const comparableObservation = scopedObservation
422
546
  ? { ...observation, axDiff: undefined, secondaryWindows: undefined }
423
547
  : observation;
424
- if (!scopedObservation) {
548
+ // Pointing observes nothing, so its response leaves the screen state alone.
549
+ if (
550
+ fromCurrentRun &&
551
+ !scopedObservation &&
552
+ owned?.toolName !== POINT_AT_PROXY_TOOL
553
+ ) {
425
554
  this.updateStateFromObservation(comparableObservation);
555
+ // A desktop has had its first look only once pixels from it arrived,
556
+ // so a failed capture leaves the next request asking again.
557
+ if (owned && observation.screenshot) {
558
+ this._observedTargets.add(owned.targetKey);
559
+ }
426
560
  }
427
- const result = this.formatObservation(comparableObservation, prevAXTree);
561
+ const result = this.formatObservation(
562
+ comparableObservation,
563
+ prevAXTree,
564
+ owned?.screenshotSkipped ?? false,
565
+ );
428
566
  interaction.rpcResolve(result);
429
567
  return result;
430
568
  }
@@ -460,6 +598,27 @@ export class HostCuProxy {
460
598
  this._previousAXTree = undefined;
461
599
  this._consecutiveUnchangedSteps = 0;
462
600
  this._actionHistory = [];
601
+ this._observedTargets.clear();
602
+ this._resetGeneration++;
603
+ }
604
+
605
+ /**
606
+ * Whether the request about to be dispatched should carry a screenshot. The
607
+ * accessibility tree comes back every step. Pixels come back on the first
608
+ * look since the last reset, for a window- or display-scoped capture, and
609
+ * when the model asks for them. Otherwise whether the tree is enough is the
610
+ * model's call.
611
+ */
612
+ private shouldAttachScreenshot(
613
+ toolName: string,
614
+ input: Record<string, unknown>,
615
+ targetKey: string,
616
+ ): boolean {
617
+ return (
618
+ !this._observedTargets.has(targetKey) ||
619
+ hasCaptureTarget(input) ||
620
+ (toolName === "computer_use_observe" && input.include_screenshot === true)
621
+ );
463
622
  }
464
623
 
465
624
  // ---------------------------------------------------------------------------
@@ -474,6 +633,7 @@ export class HostCuProxy {
474
633
  formatObservation(
475
634
  obs: CuObservationResult,
476
635
  previousAXTree?: string,
636
+ screenshotSkipped = false,
477
637
  ): ToolExecutionResult {
478
638
  const prevTree = previousAXTree;
479
639
  const parts: string[] = [];
@@ -552,6 +712,15 @@ export class HostCuProxy {
552
712
  parts.push(...screenshotMeta);
553
713
  }
554
714
 
715
+ // Only a deliberate omission is announced, and only beside a tree the line
716
+ // can point at. A step that asked for pixels and got none is a capture
717
+ // failure, which this line must not disguise. A refused or failed step
718
+ // still carries the line, since that is where asking for pixels helps most.
719
+ const isError = obs.executionError != null;
720
+ if (screenshotSkipped && !obs.screenshot && obs.axTree) {
721
+ parts.push("", SCREENSHOT_OMITTED_MESSAGE);
722
+ }
723
+
555
724
  const content = parts.join("\n").trim() || "Action executed";
556
725
 
557
726
  const contentBlocks: ContentBlock[] = [];
@@ -566,8 +735,6 @@ export class HostCuProxy {
566
735
  });
567
736
  }
568
737
 
569
- const isError = obs.executionError != null;
570
-
571
738
  return {
572
739
  content: isError
573
740
  ? `Action failed: ${obs.executionError}\n\n${content}`
@@ -6,10 +6,10 @@ import {
6
6
  broadcastMessage,
7
7
  } from "../runtime/assistant-event-hub.js";
8
8
  import {
9
- ambiguousSameUserError,
10
9
  enforceSameActorOrErrorResult,
11
- pickSameUserAutoResolve,
10
+ pickMostRecentSameUserClient,
12
11
  snapshotHostProxyActorPrincipalId,
12
+ unavailableSameUserClientError,
13
13
  } from "../runtime/auth/same-actor.js";
14
14
  import * as pendingInteractions from "../runtime/pending-interactions.js";
15
15
  import { readAudioBase64 } from "../tools/shared/filesystem/audio-read.js";
@@ -102,19 +102,17 @@ export class HostFileProxy {
102
102
  });
103
103
  }
104
104
  } else {
105
- // Auto-resolve to the unique same-user client. Reject ambiguous
106
- // (multi-machine) cases so a single targeted-style request cannot
107
- // fan out across the user's machines.
108
- const resolved = pickSameUserAutoResolve({
105
+ resolvedTargetClientId = pickMostRecentSameUserClient({
109
106
  hub: assistantEventHub,
110
107
  capability: "host_file",
111
108
  sourceActorPrincipalId,
112
109
  });
113
- if (resolved.kind === "ambiguous") {
114
- return Promise.resolve(ambiguousSameUserError("host_file"));
110
+ if (
111
+ resolvedTargetClientId === undefined &&
112
+ sourceActorPrincipalId !== undefined
113
+ ) {
114
+ return Promise.resolve(unavailableSameUserClientError("host_file"));
115
115
  }
116
- resolvedTargetClientId =
117
- resolved.kind === "match" ? resolved.clientId : undefined;
118
116
  }
119
117
 
120
118
  // Same-user check: targeted host_file requests must be bound to the same
@@ -105,12 +105,13 @@ export const HOST_PROXY_SKILL_PREACTIVATIONS: ReadonlyArray<{
105
105
  */
106
106
  /**
107
107
  * Capabilities a client asks for on its connection rather than getting from
108
- * what it is. Both ride the host_cu transport and are answered only by a
108
+ * what it is. Each rides the host_cu transport and is answered only by a
109
109
  * macOS build new enough to send the header (`events-routes.ts`), so the
110
110
  * interface alone cannot say whether one is really there.
111
111
  */
112
112
  const NEGOTIATED_CAPABILITIES: ReadonlySet<HostProxyCapability> = new Set([
113
113
  "host_cu_window_capture",
114
+ "host_cu_sequence",
114
115
  "host_cu_annotate",
115
116
  ]);
116
117