@vellumai/assistant 0.12.0 → 0.12.1-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 (406) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/openapi.yaml +292 -2
  5. package/package.json +1 -1
  6. package/scripts/smoke-desktop-window-theme.ts +186 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  8. package/src/__tests__/app-compiler.test.ts +6 -0
  9. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  10. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  11. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  12. package/src/__tests__/computer-use-tools.test.ts +17 -5
  13. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  14. package/src/__tests__/config-schema.test.ts +28 -29
  15. package/src/__tests__/config-watcher.test.ts +23 -8
  16. package/src/__tests__/connection-model-compat.test.ts +2 -2
  17. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  18. package/src/__tests__/conversation-pairing.test.ts +26 -0
  19. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  20. package/src/__tests__/conversation-queue.test.ts +48 -207
  21. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  22. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  23. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  24. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  25. package/src/__tests__/conversation-title-service.test.ts +14 -10
  26. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  27. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  28. package/src/__tests__/credential-record-retire.test.ts +171 -1
  29. package/src/__tests__/credential-routes.test.ts +212 -3
  30. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  31. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  32. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  33. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  34. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  35. package/src/__tests__/git-porcelain.test.ts +47 -0
  36. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  37. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  38. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  39. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  40. package/src/__tests__/host-file-proxy.test.ts +11 -23
  41. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  42. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  43. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  44. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  45. package/src/__tests__/mcp-health-check.test.ts +8 -11
  46. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  47. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  48. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  49. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  50. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  51. package/src/__tests__/model-intents.test.ts +3 -0
  52. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  53. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  55. package/src/__tests__/resolve-app.test.ts +135 -0
  56. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  57. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  58. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  59. package/src/__tests__/source-fingerprint.test.ts +184 -0
  60. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  61. package/src/__tests__/subagent-tools.test.ts +3 -2
  62. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  63. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  64. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  65. package/src/__tests__/workspace-git-service.test.ts +26 -0
  66. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  67. package/src/acp/__tests__/agent-process.test.ts +258 -4
  68. package/src/acp/__tests__/auth-required.test.ts +96 -0
  69. package/src/acp/__tests__/client-handler.test.ts +90 -2
  70. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  71. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  72. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  73. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  74. package/src/acp/__tests__/session-manager.test.ts +895 -4
  75. package/src/acp/agent-process.test.ts +4 -1
  76. package/src/acp/agent-process.ts +94 -9
  77. package/src/acp/auth-required.ts +50 -1
  78. package/src/acp/auto-install.test.ts +9 -7
  79. package/src/acp/client-handler.ts +16 -3
  80. package/src/acp/model-config.test.ts +166 -0
  81. package/src/acp/model-config.ts +138 -0
  82. package/src/acp/resolve-agent.test.ts +220 -6
  83. package/src/acp/resolve-agent.ts +88 -23
  84. package/src/acp/session-manager.test.ts +196 -3
  85. package/src/acp/session-manager.ts +441 -18
  86. package/src/acp/types.ts +50 -5
  87. package/src/api/events/acp-session-model-update.test.ts +88 -0
  88. package/src/api/events/acp-session-model-update.ts +46 -0
  89. package/src/api/events/app-files-changed.ts +4 -3
  90. package/src/api/index.ts +6 -0
  91. package/src/apps/app-store.ts +1 -1
  92. package/src/apps/resolve-app.ts +81 -0
  93. package/src/apps/source-fingerprint.ts +266 -0
  94. package/src/bundler/app-compiler.ts +15 -0
  95. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  96. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  97. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  98. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  99. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  100. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  101. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  102. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  103. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  104. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  105. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  106. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  107. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  108. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  109. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  110. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  111. package/src/calls/barge-in-guard.ts +118 -0
  112. package/src/calls/call-constants.ts +24 -4
  113. package/src/calls/call-controller.ts +499 -199
  114. package/src/calls/call-transport.ts +16 -0
  115. package/src/calls/media-stream-output.ts +32 -0
  116. package/src/calls/media-stream-server.ts +82 -30
  117. package/src/calls/media-stream-stt-session.ts +15 -0
  118. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  119. package/src/calls/resolve-call-tts-provider.ts +33 -2
  120. package/src/calls/tts-call-strategy.ts +9 -5
  121. package/src/calls/types.ts +2 -0
  122. package/src/calls/voice-control-protocol.ts +53 -21
  123. package/src/calls/voice-leg-coordinator.ts +189 -0
  124. package/src/calls/voice-progress-cadence.ts +423 -0
  125. package/src/calls/voice-session-bridge.ts +59 -46
  126. package/src/calls/voice-triage-escalate.ts +175 -56
  127. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  128. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  129. package/src/cli/commands/apps.help.ts +67 -13
  130. package/src/cli/commands/apps.ts +183 -0
  131. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  132. package/src/cli/commands/channels/index.help.ts +70 -2
  133. package/src/cli/commands/channels/index.ts +16 -3
  134. package/src/cli/commands/channels/send.ts +128 -0
  135. package/src/cli/commands/credentials.help.ts +6 -2
  136. package/src/cli/commands/credentials.ts +2 -2
  137. package/src/cli/commands/mcp.help.ts +8 -8
  138. package/src/cli/commands/mcp.ts +1 -1
  139. package/src/cli/commands/roadmap.help.ts +5 -1
  140. package/src/cli/commands/roadmap.ts +1 -1
  141. package/src/cli/lib/bundled-marketplace.json +103 -90
  142. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  143. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  144. package/src/config/__tests__/platform-identity.test.ts +117 -0
  145. package/src/config/__tests__/platform-rehydration.test.ts +93 -0
  146. package/src/config/acp-agent-write.ts +43 -0
  147. package/src/config/acp-defaults.test.ts +13 -3
  148. package/src/config/acp-defaults.ts +23 -6
  149. package/src/config/acp-schema.test.ts +81 -0
  150. package/src/config/acp-schema.ts +30 -2
  151. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  152. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  153. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  154. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  155. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  156. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  157. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  158. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  159. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  160. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  161. package/src/config/default-profile-catalog.ts +1 -1
  162. package/src/config/env.ts +2 -2
  163. package/src/config/feature-flag-registry.json +8 -12
  164. package/src/config/memory-v3-gate.test.ts +31 -0
  165. package/src/config/memory-v3-gate.ts +20 -16
  166. package/src/config/platform-identity.ts +144 -0
  167. package/src/config/platform-rehydration.ts +75 -21
  168. package/src/config/schema.ts +2 -2
  169. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  170. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  171. package/src/config/schemas/calls.ts +1 -28
  172. package/src/config/schemas/live-voice.ts +1 -173
  173. package/src/config/schemas/mcp.ts +1 -1
  174. package/src/config/schemas/memory-retrospective.ts +10 -0
  175. package/src/config/schemas/skills.ts +0 -14
  176. package/src/config/schemas/voice.ts +150 -0
  177. package/src/credential-execution/process-manager.test.ts +18 -19
  178. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  179. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  180. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  181. package/src/daemon/app-change-notify.ts +45 -0
  182. package/src/daemon/config-watcher.ts +13 -194
  183. package/src/daemon/conversation-process.ts +35 -64
  184. package/src/daemon/conversation-skill-tools.ts +13 -31
  185. package/src/daemon/conversation-store.ts +1 -1
  186. package/src/daemon/conversation-surfaces.ts +10 -8
  187. package/src/daemon/conversation-tool-setup.ts +37 -43
  188. package/src/daemon/conversation.ts +0 -3
  189. package/src/daemon/host-bash-proxy.ts +8 -12
  190. package/src/daemon/host-browser-proxy.ts +15 -1
  191. package/src/daemon/host-file-proxy.ts +8 -10
  192. package/src/daemon/host-transfer-proxy.ts +14 -18
  193. package/src/daemon/lifecycle.ts +0 -5
  194. package/src/daemon/mcp-reload-service.ts +5 -5
  195. package/src/daemon/message-types/sync.ts +1 -0
  196. package/src/daemon/providers-setup.ts +1 -1
  197. package/src/daemon/shutdown-handlers.ts +1 -9
  198. package/src/daemon/skill-memory-refresh.ts +64 -0
  199. package/src/daemon/tool-side-effects.ts +7 -18
  200. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  201. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  202. package/src/desktop/desktop-chrome-policy.ts +42 -0
  203. package/src/desktop/desktop-dependencies.test.ts +102 -0
  204. package/src/desktop/desktop-dependencies.ts +306 -0
  205. package/src/desktop/desktop-feature.test.ts +18 -0
  206. package/src/desktop/desktop-feature.ts +19 -0
  207. package/src/desktop/desktop-panel-config.test.ts +135 -0
  208. package/src/desktop/desktop-panel-config.ts +149 -0
  209. package/src/desktop/desktop-session-manager.test.ts +627 -0
  210. package/src/desktop/desktop-session-manager.ts +814 -0
  211. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  212. package/src/desktop/desktop-stream-bridge.ts +214 -0
  213. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  214. package/src/desktop/desktop-wallpaper.ts +113 -0
  215. package/src/desktop/desktop-window-theme.ts +170 -0
  216. package/src/hooks/types.ts +33 -0
  217. package/src/i18n/AGENTS.md +22 -0
  218. package/src/i18n/__tests__/i18n.test.ts +108 -0
  219. package/src/i18n/index.ts +16 -0
  220. package/src/i18n/locales.ts +93 -0
  221. package/src/i18n/messages.ts +61 -0
  222. package/src/i18n/title.ts +25 -0
  223. package/src/inbound/platform-callback-registration.ts +4 -5
  224. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  225. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  226. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  227. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  228. package/src/live-voice/live-voice-session.ts +213 -693
  229. package/src/live-voice/protocol.ts +0 -2
  230. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  231. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  232. package/src/mcp/effective-config.ts +3 -3
  233. package/src/mcp/mcp-header-store.ts +29 -45
  234. package/src/mcp/spec-schema.ts +111 -0
  235. package/src/mcp/startup.ts +8 -4
  236. package/src/mcp/workspace-mcp-config.ts +188 -0
  237. package/src/messaging/provider.ts +10 -3
  238. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  239. package/src/messaging/providers/callback-routing.ts +18 -0
  240. package/src/messaging/providers/channel-transport.ts +56 -0
  241. package/src/messaging/providers/discord/transport.ts +17 -0
  242. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  243. package/src/messaging/providers/slack/adapter.ts +7 -44
  244. package/src/messaging/providers/slack/transport.ts +16 -0
  245. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  246. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  247. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  248. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  249. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  250. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  251. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  252. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  253. package/src/notifications/assistant-reply-producer.ts +8 -3
  254. package/src/notifications/conversation-pairing.ts +36 -16
  255. package/src/notifications/delivered-post-record.ts +11 -1
  256. package/src/notifications/home-feed-side-effect.ts +55 -7
  257. package/src/outbound-proxy/certs.ts +3 -5
  258. package/src/permissions/checker.test.ts +11 -6
  259. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  260. package/src/persistence/conversation-crud.ts +71 -63
  261. package/src/persistence/conversation-lineage.ts +12 -4
  262. package/src/persistence/conversation-title-placeholders.ts +23 -0
  263. package/src/persistence/conversation-title-service.ts +9 -26
  264. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  265. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  266. package/src/persistence/message-cursor.ts +78 -0
  267. package/src/persistence/message-reads.ts +30 -0
  268. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  269. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  270. package/src/persistence/schema/memory-core.ts +5 -0
  271. package/src/platform/client.test.ts +2 -0
  272. package/src/platform/client.ts +2 -10
  273. package/src/plugin-api/constants.ts +2 -0
  274. package/src/plugin-api/index.ts +1 -0
  275. package/src/plugin-api/types.ts +2 -0
  276. package/src/plugins/defaults/index.ts +2 -0
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  280. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  281. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  282. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  283. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  284. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  285. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  286. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  287. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  288. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  289. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  290. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  291. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  292. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  293. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  294. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  295. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  296. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  297. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  298. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  299. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  300. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  301. package/src/plugins/defaults/memory/startup.ts +6 -1
  302. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  303. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  304. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  305. package/src/plugins/defaults/memory/worker.ts +6 -0
  306. package/src/plugins/mcp-servers.ts +15 -75
  307. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  308. package/src/providers/gemini/client.ts +9 -23
  309. package/src/providers/gemini-thought-signature.test.ts +63 -0
  310. package/src/providers/gemini-thought-signature.ts +44 -0
  311. package/src/providers/model-catalog.ts +34 -0
  312. package/src/providers/model-intents.ts +10 -5
  313. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  314. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  315. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  316. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  317. package/src/providers/openai/chat-completions-provider.ts +192 -78
  318. package/src/providers/openai/codex-models.ts +0 -4
  319. package/src/providers/openai/google-thought-signature.ts +320 -0
  320. package/src/runtime/AGENTS.md +2 -0
  321. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  322. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  323. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  324. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  325. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  326. package/src/runtime/auth/same-actor.ts +38 -3
  327. package/src/runtime/channel-send.ts +316 -0
  328. package/src/runtime/http-server.ts +160 -151
  329. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  330. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  331. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  332. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  333. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  334. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  335. package/src/runtime/routes/acp-routes.test.ts +15 -0
  336. package/src/runtime/routes/acp-routes.ts +72 -9
  337. package/src/runtime/routes/app-management-routes.ts +73 -0
  338. package/src/runtime/routes/app-routes.ts +1 -1
  339. package/src/runtime/routes/channel-send-routes.ts +85 -0
  340. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  341. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  342. package/src/runtime/routes/credential-routes.ts +19 -10
  343. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  344. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  345. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  346. package/src/runtime/routes/index.ts +4 -0
  347. package/src/runtime/routes/log-export-routes.ts +2 -0
  348. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  349. package/src/runtime/routes/migration-routes.ts +8 -21
  350. package/src/runtime/routes/platform-routes.ts +18 -15
  351. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  352. package/src/runtime/services/conversation-serializer.ts +16 -3
  353. package/src/runtime/sync/resource-sync-events.ts +2 -1
  354. package/src/schedule/worker.ts +1 -1
  355. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  356. package/src/security/ces-http-record-backend.ts +120 -0
  357. package/src/security/ces-rpc-record-backend.ts +7 -0
  358. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  359. package/src/skills/workspace-skill-md-signature.ts +50 -0
  360. package/src/subagent/consult-context.ts +12 -25
  361. package/src/tools/acp/list-agents.test.ts +3 -1
  362. package/src/tools/acp/spawn.test.ts +146 -11
  363. package/src/tools/acp/spawn.ts +21 -3
  364. package/src/tools/acp/status.test.ts +94 -0
  365. package/src/tools/acp/status.ts +40 -3
  366. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  367. package/src/tools/capability-offer.test.ts +22 -2
  368. package/src/tools/capability-offer.ts +16 -1
  369. package/src/tools/computer-use/definitions.ts +4 -2
  370. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  371. package/src/tools/credentials/metadata-store.ts +100 -0
  372. package/src/tools/host-filesystem/edit.test.ts +2 -1
  373. package/src/tools/host-filesystem/edit.ts +2 -2
  374. package/src/tools/host-filesystem/read.test.ts +2 -1
  375. package/src/tools/host-filesystem/read.ts +2 -2
  376. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  377. package/src/tools/host-filesystem/transfer.ts +2 -2
  378. package/src/tools/host-filesystem/write.test.ts +2 -1
  379. package/src/tools/host-filesystem/write.ts +2 -2
  380. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  381. package/src/tools/host-terminal/host-shell.ts +4 -20
  382. package/src/tools/policy-context.ts +6 -7
  383. package/src/tools/subagent/spawn.ts +5 -8
  384. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  385. package/src/tools/terminal/safe-env.ts +23 -3
  386. package/src/tools/watch/watch-retro-report.ts +0 -8
  387. package/src/util/__tests__/language-subtag.test.ts +23 -1
  388. package/src/util/ca-bundle.ts +13 -0
  389. package/src/util/language-subtag.ts +16 -0
  390. package/src/util/platform.ts +8 -0
  391. package/src/workspace/git-porcelain.ts +66 -0
  392. package/src/workspace/git-service.ts +5 -54
  393. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  394. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  395. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  396. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  397. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  398. package/src/workspace/migrations/registry.ts +6 -0
  399. package/src/__tests__/app-source-watcher.test.ts +0 -218
  400. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  401. package/src/__tests__/speaker-identification.test.ts +0 -56
  402. package/src/calls/speaker-identification.ts +0 -233
  403. package/src/daemon/app-source-watcher.ts +0 -218
  404. package/src/messaging/providers/whatsapp/client.ts +0 -29
  405. package/src/util/fs-watcher-error.ts +0 -36
  406. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -0,0 +1,395 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, describe, expect, test } from "bun:test";
5
+
6
+ import { newFakeDesktop, newViewer, settle } from "./__tests__/fake-desktop.js";
7
+ import { DesktopDependencyInstaller } from "./desktop-dependencies.js";
8
+ import {
9
+ type DesktopSessionManager,
10
+ type DesktopTcpHandlers,
11
+ type DesktopTcpSocket,
12
+ destroyDesktopSessionManager,
13
+ getDesktopSessionManager,
14
+ } from "./desktop-session-manager.js";
15
+ import { DesktopStreamBridge } from "./desktop-stream-bridge.js";
16
+
17
+ const profileDir = mkdtempSync(join(tmpdir(), "desktop-bridge-test-"));
18
+ afterAll(() => {
19
+ rmSync(profileDir, { recursive: true, force: true });
20
+ });
21
+
22
+ class FakeWs {
23
+ readonly sent: Uint8Array[] = [];
24
+ closeCode: number | null = null;
25
+ closeReason: string | null = null;
26
+ /** What `send` reports back; Bun returns 0 for a dropped frame. */
27
+ sendResult: number | null = null;
28
+
29
+ send(data: Uint8Array): number {
30
+ this.sent.push(data);
31
+ return this.sendResult ?? data.byteLength;
32
+ }
33
+
34
+ close(code?: number, reason?: string): void {
35
+ this.closeCode = code ?? null;
36
+ this.closeReason = reason ?? null;
37
+ }
38
+ }
39
+
40
+ class FakeTcp implements DesktopTcpSocket {
41
+ readonly writes: Uint8Array[] = [];
42
+ ended = false;
43
+ handlers!: DesktopTcpHandlers;
44
+
45
+ write(data: Uint8Array): number {
46
+ this.writes.push(data);
47
+ return data.byteLength;
48
+ }
49
+
50
+ end(): void {
51
+ this.ended = true;
52
+ }
53
+ }
54
+
55
+ function newBridge(
56
+ manager: DesktopSessionManager,
57
+ ensureInstalled: () => Promise<void> = async () => {},
58
+ isEnabled: () => boolean = () => true,
59
+ ) {
60
+ const ws = new FakeWs();
61
+ const tcp = new FakeTcp();
62
+ let release: (() => void) | null = null;
63
+ const gate = new Promise<void>((resolve) => {
64
+ release = resolve;
65
+ });
66
+ const bridge = new DesktopStreamBridge(ws, {
67
+ manager,
68
+ ensureInstalled,
69
+ isEnabled,
70
+ connect: async (_port, handlers) => {
71
+ tcp.handlers = handlers;
72
+ await gate;
73
+ return tcp;
74
+ },
75
+ });
76
+ return { ws, tcp, bridge, connectNow: () => release!() };
77
+ }
78
+
79
+ /** Whether the manager's viewer slot is free, observed through acquire. */
80
+ function slotIsFree(manager: DesktopSessionManager): boolean {
81
+ const { viewer } = newViewer();
82
+ const result = manager.acquireViewerSlot(viewer);
83
+ if (result.ok) {
84
+ manager.releaseViewerSlot(viewer);
85
+ }
86
+ return result.ok;
87
+ }
88
+
89
+ describe("DesktopStreamBridge", () => {
90
+ test("refuses a disabled desktop before installation or process startup", async () => {
91
+ const h = newFakeDesktop({ profileDir });
92
+ let installs = 0;
93
+ const b = newBridge(
94
+ h.manager,
95
+ async () => {
96
+ installs++;
97
+ },
98
+ () => false,
99
+ );
100
+ await b.bridge.start();
101
+ expect(b.ws.closeCode).toBe(4008);
102
+ expect(installs).toBe(0);
103
+ expect(h.spawned).toEqual([]);
104
+ expect(slotIsFree(h.manager)).toBe(true);
105
+ });
106
+
107
+ test("does not start the desktop when disabled during installation", async () => {
108
+ const h = newFakeDesktop({ profileDir });
109
+ let enabled = true;
110
+ const install = Promise.withResolvers<void>();
111
+ const b = newBridge(
112
+ h.manager,
113
+ () => install.promise,
114
+ () => enabled,
115
+ );
116
+ const started = b.bridge.start();
117
+ enabled = false;
118
+ install.resolve();
119
+ await started;
120
+ expect(b.ws.closeCode).toBe(4008);
121
+ expect(slotIsFree(h.manager)).toBe(true);
122
+ expect(b.tcp.handlers).toBeUndefined();
123
+ expect(h.spawned).toEqual([]);
124
+ });
125
+
126
+ for (const direction of ["viewer", "desktop"] as const) {
127
+ test(`blocks ${direction} frames when the flag is disabled after connection`, async () => {
128
+ const h = newFakeDesktop({ profileDir });
129
+ let enabled = true;
130
+ const b = newBridge(
131
+ h.manager,
132
+ async () => {},
133
+ () => enabled,
134
+ );
135
+ b.connectNow();
136
+ await b.bridge.start();
137
+ enabled = false;
138
+ const frame = new Uint8Array([1, 2, 3]);
139
+ if (direction === "viewer") {
140
+ b.bridge.handleClientFrame(frame);
141
+ } else {
142
+ b.tcp.handlers.onData(frame);
143
+ }
144
+ expect(b.ws.closeCode).toBe(4008);
145
+ expect(b.ws.sent).toHaveLength(0);
146
+ expect(b.tcp.writes).toHaveLength(0);
147
+ expect(b.tcp.ended).toBe(true);
148
+ expect(slotIsFree(h.manager)).toBe(true);
149
+ await h.manager.destroy();
150
+ });
151
+ }
152
+
153
+ test("direct viewers share background setup across a timeout and reconnect", async () => {
154
+ const h = newFakeDesktop({ profileDir });
155
+ let ready = false;
156
+ let installs = 0;
157
+ const install = Promise.withResolvers<void>();
158
+ const installer = new DesktopDependencyInstaller({
159
+ supported: () => true,
160
+ ready: () => ready,
161
+ install: async () => {
162
+ installs++;
163
+ await install.promise;
164
+ ready = true;
165
+ },
166
+ notify: async () => {},
167
+ });
168
+ const ensureInstalled = () => installer.ensureReady();
169
+ const first = newBridge(h.manager, ensureInstalled);
170
+ const firstStart = first.bridge.start();
171
+ await settle();
172
+ expect(installs).toBe(1);
173
+ expect(h.spawned).toEqual([]);
174
+
175
+ const busy = newBridge(h.manager, async () => {
176
+ throw new Error("A rejected viewer must not start setup");
177
+ });
178
+ await busy.bridge.start();
179
+ expect(busy.ws.closeCode).toBe(4013);
180
+
181
+ first.bridge.handleClose();
182
+ expect(slotIsFree(h.manager)).toBe(true);
183
+ const retry = newBridge(h.manager, ensureInstalled);
184
+ const retryStart = retry.bridge.start();
185
+ await settle();
186
+ expect(installs).toBe(1);
187
+ expect(h.spawned).toEqual([]);
188
+
189
+ install.resolve();
190
+ retry.connectNow();
191
+ await Promise.all([firstStart, retryStart]);
192
+ expect(retry.ws.closeCode).toBeNull();
193
+ expect(h.spawned.length).toBeGreaterThan(0);
194
+ retry.tcp.handlers.onData(new Uint8Array([1, 2, 3]));
195
+ expect(retry.ws.sent).toEqual([new Uint8Array([1, 2, 3])]);
196
+ expect(first.ws.sent).toEqual([]);
197
+ expect(first.tcp.handlers).toBeUndefined();
198
+ retry.bridge.handleClose();
199
+ });
200
+
201
+ test("setup continues after the last viewer closes without starting a desktop", async () => {
202
+ const h = newFakeDesktop({ profileDir });
203
+ const install = Promise.withResolvers<void>();
204
+ const b = newBridge(h.manager, () => install.promise);
205
+ const started = b.bridge.start();
206
+ b.bridge.handleClose();
207
+ install.resolve();
208
+ await started;
209
+ expect(h.spawned).toEqual([]);
210
+ expect(slotIsFree(h.manager)).toBe(true);
211
+ });
212
+
213
+ test("setup failure releases the slot and permits a successful retry", async () => {
214
+ const h = newFakeDesktop({ profileDir });
215
+ let ready = false;
216
+ let attempts = 0;
217
+ const installer = new DesktopDependencyInstaller({
218
+ supported: () => true,
219
+ ready: () => ready,
220
+ install: async () => {
221
+ if (++attempts === 1) {
222
+ throw new Error("Download failed");
223
+ }
224
+ ready = true;
225
+ },
226
+ notify: async () => {},
227
+ });
228
+ const ensureInstalled = () => installer.ensureReady();
229
+ const first = newBridge(h.manager, ensureInstalled);
230
+ await first.bridge.start();
231
+ expect(first.ws.closeCode).toBe(4011);
232
+ expect(slotIsFree(h.manager)).toBe(true);
233
+ expect(h.spawned).toEqual([]);
234
+
235
+ const retry = newBridge(h.manager, ensureInstalled);
236
+ retry.connectNow();
237
+ await retry.bridge.start();
238
+ expect(attempts).toBe(2);
239
+ expect(retry.ws.closeCode).toBeNull();
240
+ retry.bridge.handleClose();
241
+ });
242
+
243
+ test("shutdown during setup closes the viewer without starting a desktop", async () => {
244
+ const h = newFakeDesktop({ profileDir });
245
+ const install = Promise.withResolvers<void>();
246
+ const b = newBridge(h.manager, () => install.promise);
247
+ const started = b.bridge.start();
248
+ await h.manager.destroy();
249
+ expect(b.ws.closeCode).toBe(1001);
250
+ install.resolve();
251
+ await started;
252
+ expect(h.spawned).toEqual([]);
253
+ expect(b.tcp.handlers).toBeUndefined();
254
+ });
255
+
256
+ test("pumps bytes both ways and flushes frames that beat the VNC socket", async () => {
257
+ const h = newFakeDesktop({ profileDir });
258
+ const b = newBridge(h.manager);
259
+ const started = b.bridge.start();
260
+ await settle();
261
+
262
+ b.bridge.handleClientFrame(new Uint8Array([1, 2, 3]));
263
+ b.bridge.handleClientFrame(new Uint8Array([4]).buffer);
264
+ b.bridge.handleClientFrame("text frames are not RFB");
265
+ expect(b.tcp.writes).toEqual([]);
266
+
267
+ b.connectNow();
268
+ await started;
269
+ expect(b.tcp.writes).toEqual([
270
+ new Uint8Array([1, 2, 3]),
271
+ new Uint8Array([4]),
272
+ ]);
273
+
274
+ b.tcp.handlers.onData(new Uint8Array([9, 9]));
275
+ expect(b.ws.sent).toEqual([new Uint8Array([9, 9])]);
276
+ expect(b.ws.closeCode).toBeNull();
277
+ expect(slotIsFree(h.manager)).toBe(false);
278
+
279
+ b.bridge.handleClose();
280
+ expect(b.tcp.ended).toBe(true);
281
+ expect(slotIsFree(h.manager)).toBe(true);
282
+ });
283
+
284
+ test("a second socket closes 4013 and leaves the first untouched", async () => {
285
+ const h = newFakeDesktop({ profileDir });
286
+ const first = newBridge(h.manager);
287
+ first.connectNow();
288
+ await first.bridge.start();
289
+
290
+ const second = newBridge(h.manager);
291
+ await second.bridge.start();
292
+
293
+ expect(second.ws.closeCode).toBe(4013);
294
+ expect(first.ws.closeCode).toBeNull();
295
+
296
+ second.bridge.handleClose();
297
+ expect(slotIsFree(h.manager)).toBe(false);
298
+ });
299
+
300
+ test("closes 4011 when the desktop fails to start and frees the slot", async () => {
301
+ const h = newFakeDesktop({ profileDir });
302
+ h.setVncReady(false);
303
+ const b = newBridge(h.manager);
304
+ await b.bridge.start();
305
+
306
+ expect(b.ws.closeCode).toBe(4011);
307
+ expect(slotIsFree(h.manager)).toBe(true);
308
+ });
309
+
310
+ test("closes 4011 and frees the slot when the start rejects with a plain error", async () => {
311
+ const h = newFakeDesktop({ profileDir });
312
+ h.manager.ensureDesktopRunning = () =>
313
+ Promise.reject(new Error("unexpected"));
314
+ const b = newBridge(h.manager);
315
+ await b.bridge.start();
316
+
317
+ expect(b.ws.closeCode).toBe(4011);
318
+ expect(b.ws.closeReason).toBe("Desktop failed to start");
319
+ expect(slotIsFree(h.manager)).toBe(true);
320
+ });
321
+
322
+ test("closes 4011 when the desktop dies under the viewer", async () => {
323
+ const h = newFakeDesktop({ profileDir });
324
+ const b = newBridge(h.manager);
325
+ b.connectNow();
326
+ await b.bridge.start();
327
+ await settle();
328
+
329
+ h.child("window-manager").exit(1);
330
+ await settle();
331
+
332
+ expect(b.ws.closeCode).toBe(4011);
333
+ expect(b.ws.closeReason).toContain("window-manager exited");
334
+ expect(b.tcp.ended).toBe(true);
335
+ });
336
+
337
+ test("closes 4011 when the viewer drops a frame", async () => {
338
+ const h = newFakeDesktop({ profileDir });
339
+ const b = newBridge(h.manager);
340
+ b.connectNow();
341
+ await b.bridge.start();
342
+
343
+ b.ws.sendResult = 0;
344
+ b.tcp.handlers.onData(new Uint8Array([1]));
345
+
346
+ expect(b.ws.closeCode).toBe(4011);
347
+ expect(b.ws.closeReason).toBe("Viewer too slow");
348
+ expect(b.tcp.ended).toBe(true);
349
+ expect(slotIsFree(h.manager)).toBe(true);
350
+ });
351
+
352
+ test("closes 1001 for a socket arriving after shutdown began", async () => {
353
+ const h = newFakeDesktop({ profileDir });
354
+ await h.manager.destroy();
355
+ const b = newBridge(h.manager);
356
+ await b.bridge.start();
357
+
358
+ expect(b.ws.closeCode).toBe(1001);
359
+ expect(b.ws.closeReason).toBe("The assistant is shutting down");
360
+ expect(h.spawned).toEqual([]);
361
+ });
362
+
363
+ test("the shared manager is latched by shutdown even when no desktop was ever served", async () => {
364
+ await destroyDesktopSessionManager();
365
+
366
+ const ws = new FakeWs();
367
+ const bridge = new DesktopStreamBridge(ws, {
368
+ isEnabled: () => true,
369
+ connect: async () => {
370
+ throw new Error("must not dial the VNC port");
371
+ },
372
+ });
373
+ await bridge.start();
374
+
375
+ expect(ws.closeCode).toBe(1001);
376
+ // Nothing is spawned: the start is refused before any binary is looked up.
377
+ await expect(
378
+ getDesktopSessionManager().ensureDesktopRunning(),
379
+ ).rejects.toThrow("Desktop ingress is closed");
380
+ });
381
+
382
+ test("closes a live viewer with 1001 when the runtime shuts down", async () => {
383
+ const h = newFakeDesktop({ profileDir, exitOnTerm: true });
384
+ const b = newBridge(h.manager);
385
+ b.connectNow();
386
+ await b.bridge.start();
387
+ await settle();
388
+
389
+ await h.manager.destroy();
390
+
391
+ expect(b.ws.closeCode).toBe(1001);
392
+ expect(b.ws.closeReason).toBe("The assistant is shutting down");
393
+ expect(b.tcp.ended).toBe(true);
394
+ });
395
+ });
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Pumps raw RFB bytes between one `/v1/desktop/stream` WebSocket and the
3
+ * desktop's VNC server on loopback. Every frame is binary RFB data; outcomes
4
+ * are signaled with close codes only (see `DESKTOP_CLOSE`).
5
+ */
6
+
7
+ import { desktopDependencyInstaller } from "./desktop-dependencies.js";
8
+ import {
9
+ connectLoopback,
10
+ DESKTOP_CLOSE,
11
+ DESKTOP_VNC_PORT,
12
+ type DesktopLoss,
13
+ type DesktopSessionManager,
14
+ DesktopStartError,
15
+ type DesktopTcpHandlers,
16
+ type DesktopTcpSocket,
17
+ type DesktopViewer,
18
+ getDesktopSessionManager,
19
+ START_FAILED_LOSS,
20
+ } from "./desktop-session-manager.js";
21
+
22
+ /** Frames a client may send before the VNC socket is up; RFB handshakes are tiny. */
23
+ const MAX_PENDING_BYTES = 1024 * 1024;
24
+
25
+ interface DesktopStreamClientSocket {
26
+ /** Bun's `ServerWebSocket.send`: `0` means the frame was dropped. */
27
+ send(data: Uint8Array): number;
28
+ close(code?: number, reason?: string): void;
29
+ }
30
+
31
+ interface DesktopStreamBridgeOptions {
32
+ readonly isEnabled: () => boolean;
33
+ readonly manager?: DesktopSessionManager;
34
+ readonly ensureInstalled?: () => Promise<void>;
35
+ readonly connect?: (
36
+ port: number,
37
+ handlers: DesktopTcpHandlers,
38
+ ) => Promise<DesktopTcpSocket>;
39
+ }
40
+
41
+ export class DesktopStreamBridge {
42
+ private readonly ws: DesktopStreamClientSocket;
43
+ private readonly isEnabled: () => boolean;
44
+ private readonly manager: DesktopSessionManager;
45
+ private readonly ensureInstalled: () => Promise<void>;
46
+ private readonly connect: NonNullable<DesktopStreamBridgeOptions["connect"]>;
47
+ private readonly viewer: DesktopViewer;
48
+
49
+ private tcp: DesktopTcpSocket | null = null;
50
+ /** Client bytes waiting on the VNC socket, or on backpressure once it is up. */
51
+ private pending: Uint8Array[] = [];
52
+ private pendingBytes = 0;
53
+ private closed = false;
54
+ private ownsSlot = false;
55
+
56
+ constructor(
57
+ ws: DesktopStreamClientSocket,
58
+ options: DesktopStreamBridgeOptions,
59
+ ) {
60
+ this.ws = ws;
61
+ this.isEnabled = options.isEnabled;
62
+ this.manager = options.manager ?? getDesktopSessionManager();
63
+ this.ensureInstalled =
64
+ options.ensureInstalled ??
65
+ (() => desktopDependencyInstaller.ensureReady());
66
+ this.connect = options.connect ?? connectLoopback;
67
+ this.viewer = { onDesktopLost: (loss) => this.lose(loss) };
68
+ }
69
+
70
+ /** Claim the viewer slot, start the desktop, and dial its VNC port. */
71
+ async start(): Promise<void> {
72
+ if (!this.checkEnabled()) {
73
+ return;
74
+ }
75
+ const slot = this.manager.acquireViewerSlot(this.viewer);
76
+ if (!slot.ok) {
77
+ this.lose(slot.loss);
78
+ return;
79
+ }
80
+ this.ownsSlot = true;
81
+
82
+ try {
83
+ // Direct viewers share setup with the modal, including across reconnects.
84
+ await this.ensureInstalled();
85
+ if (!this.checkEnabled()) {
86
+ return;
87
+ }
88
+ await this.manager.ensureDesktopRunning();
89
+ } catch (err) {
90
+ // Usually already closed through onDesktopLost; the error covers a
91
+ // start this viewer joined late and never got the callback for.
92
+ this.lose(
93
+ err instanceof DesktopStartError ? err.loss : START_FAILED_LOSS,
94
+ );
95
+ return;
96
+ }
97
+ if (!this.checkEnabled()) {
98
+ return;
99
+ }
100
+
101
+ let tcp: DesktopTcpSocket;
102
+ try {
103
+ tcp = await this.connect(DESKTOP_VNC_PORT, {
104
+ onData: (data) => {
105
+ if (!this.checkEnabled()) {
106
+ return;
107
+ }
108
+ if (this.ws.send(data) === 0) {
109
+ this.fail(DESKTOP_CLOSE.failed, "Viewer too slow");
110
+ }
111
+ },
112
+ onDrain: () => this.flush(),
113
+ onClose: () =>
114
+ this.fail(DESKTOP_CLOSE.failed, "Desktop connection closed"),
115
+ onError: () =>
116
+ this.fail(DESKTOP_CLOSE.failed, "Desktop connection failed"),
117
+ });
118
+ } catch {
119
+ this.fail(DESKTOP_CLOSE.failed, "Desktop connection failed");
120
+ return;
121
+ }
122
+ if (!this.checkEnabled()) {
123
+ tcp.end();
124
+ return;
125
+ }
126
+ this.tcp = tcp;
127
+ this.flush();
128
+ }
129
+
130
+ handleClientFrame(message: string | Uint8Array | ArrayBuffer): void {
131
+ if (!this.checkEnabled() || typeof message === "string") {
132
+ return;
133
+ }
134
+ const bytes =
135
+ message instanceof ArrayBuffer ? new Uint8Array(message) : message;
136
+ if (this.pendingBytes + bytes.byteLength > MAX_PENDING_BYTES) {
137
+ this.fail(DESKTOP_CLOSE.failed, "Desktop stream backlog exceeded");
138
+ return;
139
+ }
140
+ this.pending.push(bytes);
141
+ this.pendingBytes += bytes.byteLength;
142
+ this.flush();
143
+ }
144
+
145
+ /** The client socket closed; the caller has already seen the close frame. */
146
+ handleClose(): void {
147
+ this.release();
148
+ }
149
+
150
+ private flush(): void {
151
+ if (!this.checkEnabled()) {
152
+ return;
153
+ }
154
+ const tcp = this.tcp;
155
+ if (!tcp) {
156
+ return;
157
+ }
158
+ while (this.pending.length > 0) {
159
+ const chunk = this.pending[0]!;
160
+ const written = tcp.write(chunk);
161
+ if (written < 0) {
162
+ this.fail(DESKTOP_CLOSE.failed, "Desktop connection closed");
163
+ return;
164
+ }
165
+ this.pendingBytes -= written;
166
+ if (written < chunk.byteLength) {
167
+ this.pending[0] = chunk.subarray(written);
168
+ return;
169
+ }
170
+ this.pending.shift();
171
+ }
172
+ }
173
+
174
+ private checkEnabled(): boolean {
175
+ if (this.closed) {
176
+ return false;
177
+ }
178
+ if (!this.isEnabled()) {
179
+ this.fail(
180
+ DESKTOP_CLOSE.unavailable,
181
+ "Desktop is not available on this assistant",
182
+ );
183
+ return false;
184
+ }
185
+ return true;
186
+ }
187
+
188
+ private lose({ code, reason }: DesktopLoss): void {
189
+ this.fail(code, reason);
190
+ }
191
+
192
+ private fail(code: number, reason: string): void {
193
+ if (this.closed) {
194
+ return;
195
+ }
196
+ this.release();
197
+ this.ws.close(code, reason);
198
+ }
199
+
200
+ private release(): void {
201
+ if (this.closed) {
202
+ return;
203
+ }
204
+ this.closed = true;
205
+ this.pending = [];
206
+ this.pendingBytes = 0;
207
+ this.tcp?.end();
208
+ this.tcp = null;
209
+ if (this.ownsSlot) {
210
+ this.ownsSlot = false;
211
+ this.manager.releaseViewerSlot(this.viewer);
212
+ }
213
+ }
214
+ }