@vellumai/assistant 0.12.1 → 0.12.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -9,7 +9,8 @@
9
9
  * 3. `executionError` from the client surfaces as a structured failure.
10
10
  * 4. The pending interaction is unregistered on every path.
11
11
  * 5. No capable client connected: failure rather than a throw.
12
- * 6. `includeScreenshot: false` drops the screenshot from the result.
12
+ * 6. `includeScreenshot: false` drops the screenshot from the result, and
13
+ * is sent to the helper only for an unscoped observation.
13
14
  * 7. Actor binding: another user's client is never selected by default, an
14
15
  * explicit clientId owned by another user is rejected, a caller with no
15
16
  * actor principal reaches nothing, and two same-user clients are
@@ -226,6 +227,44 @@ describe("observeHostScreen", () => {
226
227
  expect(pendingInteractions.getAll()).toHaveLength(0);
227
228
  });
228
229
 
230
+ test("tells the helper when no screenshot is wanted on an unscoped observation, and only then", async () => {
231
+ const skipped = observe({ includeScreenshot: false });
232
+ const skippedRequest = sentMessages.find(
233
+ (m) => m.type === "host_cu_request",
234
+ );
235
+ expect(skippedRequest?.input).toEqual({ includeScreenshot: false });
236
+ await postResult({ requestId: sentRequestId(), axTree: "Window [1]" });
237
+ await skipped;
238
+
239
+ sentMessages.length = 0;
240
+ const wanted = observe();
241
+ const wantedRequest = sentMessages.find(
242
+ (m) => m.type === "host_cu_request",
243
+ );
244
+ expect(wantedRequest?.input).toEqual({});
245
+ await postResult({ requestId: sentRequestId(), axTree: "Window [1]" });
246
+ await wanted;
247
+ });
248
+
249
+ test("a scoped observation sends no opt-out and still drops the screenshot", async () => {
250
+ const observation = observe({
251
+ includeScreenshot: false,
252
+ captureTarget: { kind: "window", windowId: 4211 },
253
+ });
254
+ const request = sentMessages.find((m) => m.type === "host_cu_request");
255
+ expect(request?.input).toEqual({ captureWindowId: 4211 });
256
+
257
+ await postResult({
258
+ requestId: sentRequestId(),
259
+ axTree: "Window [1]",
260
+ screenshot: "base64png",
261
+ screenshotWidthPx: 1200,
262
+ screenshotHeightPx: 800,
263
+ });
264
+
265
+ expect(await observation).toEqual({ ok: true, axTree: "Window [1]" });
266
+ });
267
+
229
268
  test("drops the screenshot when includeScreenshot is false", async () => {
230
269
  const observation = observe({ includeScreenshot: false });
231
270
  await postResult({
@@ -0,0 +1,58 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { RuntimeHttpServer } from "../http-server.js";
4
+ import {
5
+ mintActorToken,
6
+ mintGatewayToken,
7
+ requireHttpAuth,
8
+ upgradeHeaders,
9
+ } from "./runtime-ws-test-utils.js";
10
+
11
+ describe("Live voice through the shared runtime upgrade", () => {
12
+ let restoreAuth: () => void;
13
+ beforeEach(() => {
14
+ restoreAuth = requireHttpAuth();
15
+ });
16
+ afterEach(() => restoreAuth());
17
+
18
+ function upgrade(token: string, guardianPrincipalId?: string) {
19
+ const runtime = new RuntimeHttpServer();
20
+ const server = {
21
+ requestIP: () => ({ address: "127.0.0.1" }),
22
+ upgrade: mock(() => true),
23
+ };
24
+ const query = new URLSearchParams({ token });
25
+ if (guardianPrincipalId !== undefined) {
26
+ query.set("guardianPrincipalId", guardianPrincipalId);
27
+ }
28
+ const response = runtime["handleLiveVoiceUpgrade"](
29
+ new Request(`http://127.0.0.1/v1/live-voice?${query}`, {
30
+ headers: upgradeHeaders,
31
+ }),
32
+ server as unknown as ReturnType<typeof Bun.serve>,
33
+ );
34
+ return { response, server };
35
+ }
36
+
37
+ test("forwards the gateway-admitted guardian identity to the session", () => {
38
+ const { response, server } = upgrade(mintGatewayToken(), "user-123");
39
+ expect(response).toBeUndefined();
40
+ expect(server.upgrade).toHaveBeenCalledWith(expect.any(Request), {
41
+ data: { wsType: "live-voice", guardianPrincipalId: "user-123" },
42
+ });
43
+ });
44
+
45
+ test("accepts gateway clients without a guardian identity", () => {
46
+ const { response, server } = upgrade(mintGatewayToken());
47
+ expect(response).toBeUndefined();
48
+ expect(server.upgrade).toHaveBeenCalledWith(expect.any(Request), {
49
+ data: { wsType: "live-voice" },
50
+ });
51
+ });
52
+
53
+ test("rejects actor tokens even when they supply a guardian identity", () => {
54
+ const { response, server } = upgrade(mintActorToken(), "user-123");
55
+ expect(response?.status).toBe(401);
56
+ expect(server.upgrade).not.toHaveBeenCalled();
57
+ });
58
+ });
@@ -0,0 +1,72 @@
1
+ /** Shared pieces for suites that open WebSockets against `RuntimeHttpServer`. */
2
+
3
+ import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
4
+ import { mintToken } from "../auth/token-service.js";
5
+
6
+ export function mintGatewayToken(): string {
7
+ return mintToken({
8
+ aud: "vellum-daemon",
9
+ sub: "svc:gateway:self",
10
+ scope_profile: "gateway_ingress_v1",
11
+ policy_epoch: CURRENT_POLICY_EPOCH,
12
+ ttlSeconds: 3600,
13
+ });
14
+ }
15
+
16
+ export function mintActorToken(): string {
17
+ return mintToken({
18
+ aud: "vellum-daemon",
19
+ sub: "actor:self:user-123",
20
+ scope_profile: "actor_client_v1",
21
+ policy_epoch: CURRENT_POLICY_EPOCH,
22
+ ttlSeconds: 3600,
23
+ });
24
+ }
25
+
26
+ /** Headers of a WebSocket upgrade sent through plain `fetch`. */
27
+ export const upgradeHeaders = {
28
+ Upgrade: "websocket",
29
+ Connection: "Upgrade",
30
+ "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
31
+ "Sec-WebSocket-Version": "13",
32
+ };
33
+
34
+ export function waitForClose(
35
+ ws: WebSocket,
36
+ timeoutMs = 2000,
37
+ ): Promise<CloseEvent> {
38
+ return new Promise((resolve, reject) => {
39
+ const timer = setTimeout(() => {
40
+ cleanup();
41
+ reject(new Error("Timed out waiting for WebSocket close"));
42
+ }, timeoutMs);
43
+ const cleanup = () => {
44
+ clearTimeout(timer);
45
+ ws.removeEventListener("close", onClose);
46
+ ws.removeEventListener("error", onError);
47
+ };
48
+ const onClose = (event: CloseEvent) => {
49
+ cleanup();
50
+ resolve(event);
51
+ };
52
+ const onError = () => {
53
+ cleanup();
54
+ reject(new Error("WebSocket close failed"));
55
+ };
56
+ ws.addEventListener("close", onClose);
57
+ ws.addEventListener("error", onError);
58
+ });
59
+ }
60
+
61
+ /** Turn real HTTP auth on for a test; the returned function restores the env. */
62
+ export function requireHttpAuth(): () => void {
63
+ const saved = process.env.DISABLE_HTTP_AUTH;
64
+ delete process.env.DISABLE_HTTP_AUTH;
65
+ return () => {
66
+ if (saved === undefined) {
67
+ delete process.env.DISABLE_HTTP_AUTH;
68
+ } else {
69
+ process.env.DISABLE_HTTP_AUTH = saved;
70
+ }
71
+ };
72
+ }
@@ -0,0 +1,59 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { RuntimeHttpServer } from "../http-server.js";
4
+ import {
5
+ mintGatewayToken,
6
+ requireHttpAuth,
7
+ upgradeHeaders,
8
+ } from "./runtime-ws-test-utils.js";
9
+
10
+ describe("Watch capture target through the shared runtime upgrade", () => {
11
+ let restoreAuth: () => void;
12
+ beforeEach(() => {
13
+ restoreAuth = requireHttpAuth();
14
+ });
15
+ afterEach(() => restoreAuth());
16
+
17
+ function upgrade(params: string) {
18
+ const runtime = new RuntimeHttpServer();
19
+ const server = {
20
+ requestIP: () => ({ address: "127.0.0.1" }),
21
+ upgrade: mock(() => true),
22
+ };
23
+ const response = runtime["handleWatchStreamUpgrade"](
24
+ new Request(
25
+ `http://127.0.0.1/v1/watch/stream?token=${mintGatewayToken()}&mimeType=audio/pcm&sampleRate=16000&${params}`,
26
+ { headers: upgradeHeaders },
27
+ ),
28
+ server as unknown as ReturnType<typeof Bun.serve>,
29
+ );
30
+ return { response, server };
31
+ }
32
+
33
+ test.each([
34
+ ["captureWindowId=4242", { kind: "window", windowId: 4242 }],
35
+ ["captureDisplayId=7", { kind: "display", displayId: 7 }],
36
+ ["", undefined],
37
+ ])("preserves the selected target: %s", (params, captureTarget) => {
38
+ const { response, server } = upgrade(params);
39
+ expect(response).toBeUndefined();
40
+ expect(server.upgrade).toHaveBeenCalledWith(expect.any(Request), {
41
+ data: expect.objectContaining({
42
+ wsType: "watch-stream",
43
+ mimeType: "audio/pcm",
44
+ sampleRate: 16000,
45
+ captureTarget,
46
+ }),
47
+ });
48
+ });
49
+
50
+ test.each([
51
+ "captureWindowId=abc",
52
+ "captureDisplayId=-1",
53
+ "captureWindowId=1&captureDisplayId=2",
54
+ ])("refuses an invalid target before upgrading: %s", (params) => {
55
+ const { response, server } = upgrade(params);
56
+ expect(response?.status).toBe(400);
57
+ expect(server.upgrade).not.toHaveBeenCalled();
58
+ });
59
+ });
@@ -126,6 +126,7 @@ import {
126
126
  import type { CompletedBackgroundTool } from "../tools/background-tool-registry.js";
127
127
  import { getLogger } from "../util/logger.js";
128
128
  import { createKeyedSingleFlight } from "../util/single-flight.js";
129
+ import { safeStringSlice } from "../util/unicode.js";
129
130
 
130
131
  const log = getLogger("agent-wake");
131
132
 
@@ -139,7 +140,7 @@ const WAKE_POSTAMBLE =
139
140
 
140
141
  /** Sanitize a value for use as an XML attribute (no quotes/brackets/newlines). */
141
142
  function sanitizeEventAttr(value: string): string {
142
- return value.replace(/[<>"&\r\n]/g, "").slice(0, 200);
143
+ return safeStringSlice(value.replace(/[<>"&\r\n]/g, ""), 0, 200);
143
144
  }
144
145
 
145
146
  /**
@@ -745,9 +745,8 @@ export function broadcastMessage(
745
745
  // A reconnect can overlap an older executor with the same device ID.
746
746
  // Filter at delivery too, not only when HostCuProxy resolves its target.
747
747
  const targetCapability =
748
- msg.type === "host_cu_request" &&
749
- Object.hasOwn(msg.input, "capture_window_id")
750
- ? "host_cu_window_capture"
748
+ msg.type === "host_cu_request"
749
+ ? hostCuRequestCapability(msg.toolName, msg.input)
751
750
  : capabilityForMessageType(msg.type);
752
751
  // Self-echo suppression: a `sync_changed` carrying an `originClientId`
753
752
  // means a specific client just mutated the resource. The hub must not
@@ -782,6 +781,25 @@ export function broadcastMessage(
782
781
  });
783
782
  }
784
783
 
784
+ /**
785
+ * The capability a `host_cu_request` needs from the client that receives it.
786
+ * Window-scoped observation and batched actions are answered only by a helper
787
+ * that claimed them; an older helper would capture the whole desktop or end
788
+ * the session on the unknown tool.
789
+ */
790
+ function hostCuRequestCapability(
791
+ toolName: string,
792
+ input: Record<string, unknown>,
793
+ ): HostProxyCapability {
794
+ if (Object.hasOwn(input, "capture_window_id")) {
795
+ return "host_cu_window_capture";
796
+ }
797
+ if (toolName === "computer_use_sequence") {
798
+ return "host_cu_sequence";
799
+ }
800
+ return "host_cu";
801
+ }
802
+
785
803
  function extractConversationId(msg: AssistantEvent): string | undefined {
786
804
  const record = msg as unknown as Record<string, unknown>;
787
805
  if ("conversationId" in msg && typeof record.conversationId === "string") {
@@ -142,9 +142,7 @@ function detectRejection(
142
142
  ? args.hub.getActorPrincipalIdForClient(args.targetClientId)
143
143
  : (args.targetActorPrincipalId ??
144
144
  (isHttpAuthDisabled() && targetClientId
145
- ? args.hubForMissingTarget?.getActorPrincipalIdForClient(
146
- targetClientId,
147
- )
145
+ ? args.hubForMissingTarget?.getActorPrincipalIdForClient(targetClientId)
148
146
  : undefined));
149
147
 
150
148
  let reason: RejectionReason | undefined;
@@ -264,6 +262,43 @@ export function pickSameUserAutoResolve(args: {
264
262
  return { kind: "ambiguous" };
265
263
  }
266
264
 
265
+ /**
266
+ * Pick the most recently active capable client owned by the turn actor.
267
+ * `listClientsByCapability` is ordered by recent activity, so the first
268
+ * same-user match provides deterministic routing without broadcasting a host
269
+ * request across multiple devices.
270
+ */
271
+ export function pickMostRecentSameUserClient(args: {
272
+ hub: Pick<AssistantEventHub, "listClientsByCapability">;
273
+ capability: HostProxyCapability;
274
+ sourceActorPrincipalId: string | undefined;
275
+ }): string | undefined {
276
+ const { hub, capability, sourceActorPrincipalId } = args;
277
+ if (sourceActorPrincipalId == null) {
278
+ return undefined;
279
+ }
280
+ return hub
281
+ .listClientsByCapability(capability)
282
+ .find((client) => client.actorPrincipalId === sourceActorPrincipalId)
283
+ ?.clientId;
284
+ }
285
+
286
+ /**
287
+ * Error result for an automatic host route with no capable client owned by the
288
+ * actor that initiated the turn.
289
+ */
290
+ export function unavailableSameUserClientError(
291
+ capability: HostProxyCapability,
292
+ ): {
293
+ content: string;
294
+ isError: true;
295
+ } {
296
+ return {
297
+ content: `No connected ${capability} client belongs to this user. Connect a capable client signed in as the same user, then retry.`,
298
+ isError: true,
299
+ };
300
+ }
301
+
267
302
  /**
268
303
  * Actor principal persisted on a host-proxy pending interaction.
269
304
  *
@@ -0,0 +1,316 @@
1
+ /**
2
+ * A model-directed text send to a channel chat, through the channel's
3
+ * transport, recorded after the channel acknowledges it.
4
+ *
5
+ * This is the one send implementation per addressable channel. The
6
+ * messaging tool's channel branch and the `channels send` route both call
7
+ * it, so a send costs the same and leaves the same record whichever door it
8
+ * came through. The transport owns the routing: the caller names a neutral
9
+ * target, the transport resolves it into its own callback context, and
10
+ * nothing here switches on a channel's name.
11
+ *
12
+ * Nothing is written before the channel acknowledges. The record carries
13
+ * every id the text became, so a failed send leaves no row that reads as
14
+ * something the assistant said, and a reaction, edit, or delete naming any
15
+ * chunk resolves to the row; the inbound binding a transport asks for is
16
+ * written after the same acknowledgement, so a failed send does not move
17
+ * where the chat's next message lands. A send into the sending turn's own
18
+ * chat and thread writes no row: its tool call and result already sit in
19
+ * that conversation's history, and a second assistant row beside the tool
20
+ * pair would break history repair.
21
+ */
22
+
23
+ import { type ChannelId, isChannelId } from "../channels/types.js";
24
+ import type {
25
+ ProactiveAddress,
26
+ ProactiveTarget,
27
+ } from "../messaging/providers/channel-transport.js";
28
+ import { getTransportForChannel } from "../messaging/providers/index.js";
29
+ import { resolveProactiveHomeConversation } from "../notifications/conversation-pairing.js";
30
+ import { recordDeliveredChannelPost } from "../notifications/delivered-post-record.js";
31
+ import { getConversation } from "../persistence/conversation-crud.js";
32
+ import { syncMessageToDisk } from "../persistence/conversation-disk-view.js";
33
+ import { getOrCreateConversation } from "../persistence/conversation-key-store.js";
34
+ import { buildScopedConversationKey } from "../persistence/delivery-crud.js";
35
+ import {
36
+ normalizeExternalThreadId,
37
+ upsertOutboundBinding,
38
+ } from "../persistence/external-conversation-store.js";
39
+ import { getLogger } from "../util/logger.js";
40
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
41
+ import { publishConversationListChanged } from "./sync/resource-sync-events.js";
42
+
43
+ const log = getLogger("channel-send");
44
+
45
+ /**
46
+ * The turn a send is made from, as a snapshot taken when the turn started:
47
+ * the channel it arrived on, the chat, and the thread. Read from the turn
48
+ * rather than from the live binding, which a concurrent inbound can rewrite
49
+ * while the send runs. A caller with no turn (a script) names only the
50
+ * conversation it acts for, and the home comparison decides on its own.
51
+ */
52
+ export interface ChannelSendSender {
53
+ readonly conversationId: string;
54
+ readonly executionChannel?: string;
55
+ readonly requesterChatId?: string;
56
+ readonly sourceThreadId?: string;
57
+ }
58
+
59
+ export interface ChannelSendParams {
60
+ readonly channel: string;
61
+ readonly target: ProactiveTarget;
62
+ readonly text: string;
63
+ /** Ask the channel for its rich rendering of the text, where it has one. */
64
+ readonly renderRichly?: boolean;
65
+ /** The assistant the send acts for; absent or `self` is this assistant. */
66
+ readonly assistantId?: string;
67
+ readonly sender?: ChannelSendSender;
68
+ }
69
+
70
+ export interface ChannelSendResult {
71
+ readonly channel: ChannelId;
72
+ /** The chat the post landed in, in the channel's own id space. */
73
+ readonly chatId: string;
74
+ /** The thread the post landed in, absent when it is not in one. */
75
+ readonly threadId?: string;
76
+ /** Every id the channel acknowledged for the text, in send order; never empty. */
77
+ readonly messageIds: readonly string[];
78
+ /** The id of the final post, the one a later edit or withdrawal addresses. */
79
+ readonly lastMessageId: string;
80
+ /** The conversation the post was recorded in, absent when it was not. */
81
+ readonly recordedIn?: string;
82
+ }
83
+
84
+ /** The channel cannot be reached from a named target. */
85
+ export class ChannelNotAddressableError extends Error {
86
+ constructor(
87
+ readonly channel: string,
88
+ reason: string,
89
+ ) {
90
+ super(`Channel "${channel}" ${reason}.`);
91
+ this.name = "ChannelNotAddressableError";
92
+ }
93
+ }
94
+
95
+ /**
96
+ * The transport ran but the channel did not acknowledge the send. A success
97
+ * that names no message id counts: an id is what makes the post addressable
98
+ * afterwards, and without one there is nothing to record.
99
+ */
100
+ export class ChannelSendFailedError extends Error {
101
+ constructor(
102
+ readonly channel: ChannelId,
103
+ reason = "did not acknowledge the send",
104
+ ) {
105
+ super(`Channel "${channel}" ${reason}.`);
106
+ this.name = "ChannelSendFailedError";
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Whether a channel's transport declares it can be addressed from a named
112
+ * chat. A channel with no transport, or one that only answers on an inbound
113
+ * callback, is not.
114
+ */
115
+ export function isProactivelyAddressable(channel: string): boolean {
116
+ return getTransportForChannel(channel)?.addressFor !== undefined;
117
+ }
118
+
119
+ /**
120
+ * Deliver `text` to `target` on `channel` and record what was sent.
121
+ *
122
+ * Throws {@link ChannelNotAddressableError} before any delivery when the
123
+ * channel or the target shape cannot be addressed, and
124
+ * {@link ChannelSendFailedError} when the transport reports no
125
+ * acknowledgement or no message id. A binding or recording failure never
126
+ * fails the send: the message is already out.
127
+ */
128
+ export async function sendChannelText(
129
+ params: ChannelSendParams,
130
+ ): Promise<ChannelSendResult> {
131
+ const { channel, target, text } = params;
132
+ if (!isChannelId(channel)) {
133
+ throw new ChannelNotAddressableError(channel, "is not a channel");
134
+ }
135
+ const transport = getTransportForChannel(channel);
136
+ if (!transport?.addressFor) {
137
+ throw new ChannelNotAddressableError(
138
+ channel,
139
+ "cannot be addressed from a named chat",
140
+ );
141
+ }
142
+ const address = transport.addressFor(target);
143
+ if (!address) {
144
+ throw new ChannelNotAddressableError(channel, "cannot address that chat");
145
+ }
146
+
147
+ const result = await transport.deliver(address.ctx, {
148
+ chatId: address.chatId,
149
+ text,
150
+ ...(params.renderRichly ? { renderRichly: true } : {}),
151
+ ...(params.assistantId ? { assistantId: params.assistantId } : {}),
152
+ });
153
+ if (!result.ok) {
154
+ throw new ChannelSendFailedError(channel);
155
+ }
156
+ const messageIds = result.messageIds ?? (result.ts ? [result.ts] : []);
157
+ const lastMessageId = result.ts ?? messageIds[messageIds.length - 1];
158
+ if (!lastMessageId) {
159
+ throw new ChannelSendFailedError(channel, "acknowledged no message id");
160
+ }
161
+
162
+ const actsForSelf =
163
+ !params.assistantId || params.assistantId === DAEMON_INTERNAL_ASSISTANT_ID;
164
+ if (transport.bindsChatOnProactiveSend && actsForSelf) {
165
+ bindChatForNextInbound(channel, address);
166
+ }
167
+
168
+ const recordedIn = await recordProactivePost({
169
+ channel,
170
+ address,
171
+ text,
172
+ messageIds,
173
+ sender: params.sender,
174
+ });
175
+
176
+ return {
177
+ channel,
178
+ chatId: address.chatId,
179
+ ...(address.threadId ? { threadId: address.threadId } : {}),
180
+ messageIds,
181
+ lastMessageId,
182
+ ...(recordedIn ? { recordedIn } : {}),
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Bind the chat's inbound conversation to the delivered chat and thread, so
188
+ * the next message from there resolves to the conversation ingress uses for
189
+ * it. The key is the one ingress builds, thread-scoped where the channel
190
+ * scopes conversations by thread, so a topic's replies continue the topic's
191
+ * conversation. Runs after acknowledgement: the message is out, so the
192
+ * binding it asks for is owed. Best effort: a binding failure must not fail
193
+ * the send.
194
+ */
195
+ function bindChatForNextInbound(
196
+ channel: ChannelId,
197
+ address: ProactiveAddress,
198
+ ): void {
199
+ try {
200
+ const { conversationId, created } = getOrCreateConversation(
201
+ buildScopedConversationKey(channel, address.chatId, address.threadId),
202
+ );
203
+ // Published before the binding, not after: the conversation exists as
204
+ // soon as it is minted, the record that follows finds it rather than
205
+ // minting it, and a binding that throws must not leave a conversation
206
+ // holding a real message out of the list.
207
+ if (created) {
208
+ publishConversationListChanged("created");
209
+ }
210
+ upsertOutboundBinding({
211
+ conversationId,
212
+ sourceChannel: channel,
213
+ externalChatId: address.chatId,
214
+ externalThreadId: address.threadId ?? null,
215
+ });
216
+ } catch (e) {
217
+ log.warn(
218
+ {
219
+ err: e,
220
+ channel,
221
+ externalChatId: address.chatId,
222
+ externalThreadId: address.threadId,
223
+ },
224
+ "Failed to bind the chat's inbound conversation after the send",
225
+ );
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Record the acknowledged post where the chat's proactive posts live, so
231
+ * the chat's own conversation and `recall` can see what was sent from
232
+ * elsewhere. The row names the thread the post landed in, as the transport
233
+ * resolved it. Returns the conversation it was recorded in, or `undefined`
234
+ * when nothing was recorded: the send went into the sending turn's own chat
235
+ * and thread, or the home is the sender.
236
+ */
237
+ async function recordProactivePost(params: {
238
+ channel: ChannelId;
239
+ address: ProactiveAddress;
240
+ text: string;
241
+ messageIds: readonly string[];
242
+ sender: ChannelSendSender | undefined;
243
+ }): Promise<string | undefined> {
244
+ const { channel, address, sender } = params;
245
+ const [firstId, ...restIds] = params.messageIds;
246
+ if (!firstId) {
247
+ return undefined;
248
+ }
249
+ try {
250
+ if (
251
+ sender &&
252
+ isSendIntoOwnChat(sender, channel, address.chatId, address.threadId)
253
+ ) {
254
+ return undefined;
255
+ }
256
+ const home = await resolveProactiveHomeConversation({
257
+ sourceChannel: channel,
258
+ externalChatId: address.chatId,
259
+ threadId: address.threadId,
260
+ source: "notification",
261
+ conversationType: "background",
262
+ title: `Messages to ${address.chatId}`,
263
+ });
264
+ if (home.conversationId === sender?.conversationId) {
265
+ return undefined;
266
+ }
267
+ const recorded = await recordDeliveredChannelPost({
268
+ conversationId: home.conversationId,
269
+ channel,
270
+ externalChatId: address.chatId,
271
+ ...(address.threadId ? { threadId: address.threadId } : {}),
272
+ text: params.text,
273
+ providerMessageId: firstId,
274
+ ...(restIds.length > 0 ? { additionalProviderMessageIds: restIds } : {}),
275
+ ...(sender ? { crossPostedFrom: sender.conversationId } : {}),
276
+ });
277
+ const homeConversation = getConversation(home.conversationId);
278
+ if (homeConversation) {
279
+ syncMessageToDisk(
280
+ home.conversationId,
281
+ recorded.messageId,
282
+ homeConversation.createdAt,
283
+ );
284
+ }
285
+ return home.conversationId;
286
+ } catch (e) {
287
+ log.warn(
288
+ { err: e, channel, externalChatId: address.chatId },
289
+ "Failed to record the sent message in the chat's conversation",
290
+ );
291
+ return undefined;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * True when the delivered chat and thread are the ones the sending turn
297
+ * arrived in. Thread ids compare in the binding store's normalized form, so
298
+ * an absent thread on both sides matches and a thread-less delivery never
299
+ * matches a turn that arrived in a thread.
300
+ */
301
+ function isSendIntoOwnChat(
302
+ sender: ChannelSendSender,
303
+ channel: ChannelId,
304
+ chatId: string,
305
+ deliveredThreadId: string | undefined,
306
+ ): boolean {
307
+ if (!sender.executionChannel || !sender.requesterChatId) {
308
+ return false;
309
+ }
310
+ return (
311
+ sender.executionChannel === channel &&
312
+ sender.requesterChatId === chatId &&
313
+ normalizeExternalThreadId(sender.sourceThreadId) ===
314
+ normalizeExternalThreadId(deliveredThreadId)
315
+ );
316
+ }
@@ -257,7 +257,17 @@ export async function observeHostScreen(
257
257
  // conversation is attached.
258
258
  conversationId: options.stateKey ?? "",
259
259
  toolName: "computer_use_observe",
260
- input: captureTargetInput(options.captureTarget),
260
+ // An unscoped request tells the helper up front when no
261
+ // screenshot is wanted, so it can skip capturing and uploading
262
+ // one. The helper always captures a scoped observation, so the
263
+ // flag is not sent there. The strip below removes the picture in
264
+ // both cases.
265
+ input: {
266
+ ...captureTargetInput(options.captureTarget),
267
+ ...(includeScreenshot || options.captureTarget !== undefined
268
+ ? {}
269
+ : { includeScreenshot: false }),
270
+ },
261
271
  stepNumber: 1,
262
272
  },
263
273
  undefined,