@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -53,6 +53,17 @@ const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
53
53
  */
54
54
  const DEFAULT_INACTIVITY_TIMEOUT_MS = 30_000;
55
55
 
56
+ /**
57
+ * Default interval (ms) for emitting Deepgram `KeepAlive` control frames
58
+ * during silent stretches. Deepgram's server-side timeout closes the
59
+ * socket if no real audio content arrives for ~10s; raw silence PCM does
60
+ * not reset that timer, only an explicit `{"type":"KeepAlive"}` message
61
+ * does. Sending one every 5s keeps the socket alive through arbitrary
62
+ * pauses (think: 1:1 voice mode while the user is thinking) without any
63
+ * meaningful bandwidth cost.
64
+ */
65
+ const DEFAULT_KEEPALIVE_INTERVAL_MS = 5_000;
66
+
56
67
  /**
57
68
  * Maximum WebSocket bufferedAmount (bytes) before sendAudio applies
58
69
  * backpressure by dropping frames. This prevents unbounded memory growth
@@ -87,6 +98,13 @@ export interface DeepgramRealtimeOptions {
87
98
  connectTimeoutMs?: number;
88
99
  /** Inactivity timeout in milliseconds. Default: 30_000. */
89
100
  inactivityTimeoutMs?: number;
101
+ /**
102
+ * Interval (ms) between Deepgram `KeepAlive` control frames sent during
103
+ * silent stretches. Default: 5_000. Set to 0 to disable (not recommended
104
+ * outside tests — the server-side socket will close after ~10s of
105
+ * silence).
106
+ */
107
+ keepaliveIntervalMs?: number;
90
108
  /** Audio sample rate in Hz (default: 16000). Passed through from the client WebSocket connection. */
91
109
  sampleRate?: number;
92
110
  /**
@@ -222,6 +240,7 @@ export class DeepgramRealtimeTranscriber implements StreamingTranscriber {
222
240
  private readonly baseUrl: string;
223
241
  private readonly connectTimeoutMs: number;
224
242
  private readonly inactivityTimeoutMs: number;
243
+ private readonly keepaliveIntervalMs: number;
225
244
  private readonly sampleRate: number;
226
245
  /**
227
246
  * Whether speaker diarization is requested. Forwarded to the Deepgram
@@ -248,6 +267,13 @@ export class DeepgramRealtimeTranscriber implements StreamingTranscriber {
248
267
  /** Close grace timer handle. */
249
268
  private closeGraceTimer: ReturnType<typeof setTimeout> | null = null;
250
269
 
270
+ /**
271
+ * Periodic keepalive timer. Fires every {@link keepaliveIntervalMs} while
272
+ * the socket is open and emits a Deepgram `KeepAlive` control frame so
273
+ * silent stretches do not trip Deepgram's server-side inactivity close.
274
+ */
275
+ private keepaliveTimer: ReturnType<typeof setInterval> | null = null;
276
+
251
277
  constructor(apiKey: string, options: DeepgramRealtimeOptions = {}) {
252
278
  this.apiKey = apiKey;
253
279
  this.model = options.model ?? DEFAULT_MODEL;
@@ -260,6 +286,8 @@ export class DeepgramRealtimeTranscriber implements StreamingTranscriber {
260
286
  options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
261
287
  this.inactivityTimeoutMs =
262
288
  options.inactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS;
289
+ this.keepaliveIntervalMs =
290
+ options.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
263
291
  this.sampleRate = options.sampleRate ?? 16_000;
264
292
  this.diarize = options.diarize ?? false;
265
293
  }
@@ -329,6 +357,7 @@ export class DeepgramRealtimeTranscriber implements StreamingTranscriber {
329
357
  // the active session lifetime.
330
358
  this.attachSessionHandlers(ws);
331
359
  this.resetInactivityTimer();
360
+ this.startKeepaliveTimer();
332
361
 
333
362
  log.info("Deepgram realtime session opened");
334
363
  }
@@ -644,6 +673,34 @@ export class DeepgramRealtimeTranscriber implements StreamingTranscriber {
644
673
  clearTimeout(this.closeGraceTimer);
645
674
  this.closeGraceTimer = null;
646
675
  }
676
+ if (this.keepaliveTimer !== null) {
677
+ clearInterval(this.keepaliveTimer);
678
+ this.keepaliveTimer = null;
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Start the periodic keepalive timer. Sends a Deepgram `KeepAlive`
684
+ * control frame every {@link keepaliveIntervalMs}; this is the only
685
+ * thing that resets Deepgram's server-side inactivity timer when the
686
+ * stream is carrying silence (raw silence PCM frames do not count).
687
+ *
688
+ * Skipped when {@link keepaliveIntervalMs} is 0 (test override) or the
689
+ * adapter is already closed/stopping.
690
+ */
691
+ private startKeepaliveTimer(): void {
692
+ if (this.closed || this.stopping) return;
693
+ if (this.keepaliveIntervalMs <= 0) return;
694
+ this.keepaliveTimer = setInterval(() => {
695
+ if (this.closed || this.stopping) return;
696
+ const ws = this.ws;
697
+ if (!ws || ws.readyState !== WS_OPEN) return;
698
+ try {
699
+ ws.send(JSON.stringify({ type: "KeepAlive" }));
700
+ } catch (err) {
701
+ log.warn({ err }, "Deepgram KeepAlive send failed");
702
+ }
703
+ }, this.keepaliveIntervalMs);
647
704
  }
648
705
 
649
706
  /**
@@ -497,14 +497,79 @@ describe("XAIRealtimeTranscriber", () => {
497
497
  const transcriber = new XAIRealtimeTranscriber(TEST_API_KEY, {
498
498
  connectTimeoutMs: 1_000,
499
499
  });
500
- const { onEvent } = createEventCollector();
500
+ const { events, onEvent } = createEventCollector();
501
501
 
502
- // First attempt: transport-level error before open.
502
+ // First attempt: transport-level error before open. Real WebSocket
503
+ // implementations commonly chain `error` → `close` on the abandoned
504
+ // socket, so simulate both to catch the regression where the old
505
+ // socket's stray close event corrupts `this.closed` and silently
506
+ // breaks the retry.
507
+ const firstSocket = mockWs;
503
508
  const firstAttempt = transcriber.start(onEvent);
504
- mockWs.simulateError(new Error("ECONNREFUSED"));
509
+ firstSocket.simulateError(new Error("ECONNREFUSED"));
510
+ firstSocket.simulateClose(1006, "abnormal closure");
505
511
  await expect(firstAttempt).rejects.toThrow(/xAI realtime connect error/);
506
512
 
507
- // Second attempt instance must be reusable.
513
+ // The stray close on the abandoned socket must NOT have emitted a
514
+ // `closed` event or marked the transcriber closed — otherwise the
515
+ // retry below would silently no-op on sendAudio.
516
+ expect(events.filter((e) => e.type === "closed")).toHaveLength(0);
517
+
518
+ // Second attempt — instance must be reusable AND fully functional.
519
+ mockWs = new MockWebSocket();
520
+ (globalThis as Record<string, unknown>).WebSocket = class {
521
+ constructor(
522
+ _url: string,
523
+ _options?: { headers?: Record<string, string> },
524
+ ) {
525
+ return mockWs;
526
+ }
527
+ };
528
+
529
+ const secondAttempt = transcriber.start(onEvent);
530
+ mockWs.simulateOpen();
531
+ mockWs.simulateMessage(CREATED_FRAME);
532
+ await expect(secondAttempt).resolves.toBeUndefined();
533
+
534
+ // Confirm the retry session is actually live — sendAudio must reach
535
+ // the new socket (proves `this.closed` wasn't sticky-corrupted).
536
+ transcriber.sendAudio(Buffer.from([0x01, 0x02, 0x03]), "audio/pcm");
537
+ expect(mockWs.sentData).toHaveLength(1);
538
+ });
539
+
540
+ // ─────────────────────────────────────────────────────────────────
541
+ // Regression: a close event on the abandoned socket fired AFTER a
542
+ // handshake-phase rejection must not corrupt `this.closed`. Real
543
+ // WebSocket impls emit `close` asynchronously after `ws.close()` is
544
+ // called, and commonly chain `error` → `close`; if the handshake
545
+ // listeners aren't detached before `forceClose()`, the stray event
546
+ // routes through `handleProviderClose` and flips `this.closed`,
547
+ // silently breaking subsequent retries.
548
+ // ─────────────────────────────────────────────────────────────────
549
+
550
+ test("stray close on abandoned socket after handshake rejection does not break retry", async () => {
551
+ const transcriber = new XAIRealtimeTranscriber(TEST_API_KEY, {
552
+ connectTimeoutMs: 1_000,
553
+ });
554
+ const { events, onEvent } = createEventCollector();
555
+
556
+ // First attempt: close rejects the handshake.
557
+ const firstSocket = mockWs;
558
+ const firstAttempt = transcriber.start(onEvent);
559
+ firstSocket.simulateClose(4001, "unauthorized");
560
+ await expect(firstAttempt).rejects.toThrow(
561
+ /xAI WebSocket closed before handshake/,
562
+ );
563
+
564
+ // Simulate a second close event arriving on the abandoned socket
565
+ // (as `forceClose()` → `ws.close()` would trigger in a real impl).
566
+ firstSocket.simulateClose(1006, "abnormal closure");
567
+
568
+ // The stray event must be a no-op: no `closed` event emitted, and
569
+ // no internal state corruption.
570
+ expect(events.filter((e) => e.type === "closed")).toHaveLength(0);
571
+
572
+ // Retry and confirm the session is fully functional.
508
573
  mockWs = new MockWebSocket();
509
574
  (globalThis as Record<string, unknown>).WebSocket = class {
510
575
  constructor(
@@ -519,6 +584,9 @@ describe("XAIRealtimeTranscriber", () => {
519
584
  mockWs.simulateOpen();
520
585
  mockWs.simulateMessage(CREATED_FRAME);
521
586
  await expect(secondAttempt).resolves.toBeUndefined();
587
+
588
+ transcriber.sendAudio(Buffer.from([0x01, 0x02, 0x03]), "audio/pcm");
589
+ expect(mockWs.sentData).toHaveLength(1);
522
590
  });
523
591
 
524
592
  // ─────────────────────────────────────────────────────────────────
@@ -255,13 +255,17 @@ export class XAIRealtimeTranscriber implements StreamingTranscriber {
255
255
  await new Promise<void>((resolve, reject) => {
256
256
  let settled = false;
257
257
 
258
- const handshakeTimer = setTimeout(() => {
259
- if (settled) return;
260
- settled = true;
261
- this.forceClose();
262
- reject(new Error("xAI realtime connect timeout"));
263
- }, this.connectTimeoutMs);
264
-
258
+ // Listener references, captured so we can detach them from the
259
+ // abandoned socket on every reject/timeout path. Without this,
260
+ // `forceClose()` → `ws.close()` triggers an asynchronous `close`
261
+ // event (and real WebSocket impls commonly chain `error` → `close`)
262
+ // on the old socket. With the listeners still attached, that stray
263
+ // event routes through the `settled === true` branch into
264
+ // `handleProviderClose`, which calls `emitClosedAndCleanup()` and
265
+ // sets `this.closed = true`. A subsequent `start()` then resolves
266
+ // but `sendAudio` / `stop` / timers all no-op because `this.closed`
267
+ // is sticky — retry is silently dead. Detaching the handlers
268
+ // before `forceClose()` closes that window.
265
269
  const settleResolve = () => {
266
270
  if (settled) return;
267
271
  settled = true;
@@ -273,6 +277,9 @@ export class XAIRealtimeTranscriber implements StreamingTranscriber {
273
277
  if (settled) return;
274
278
  settled = true;
275
279
  clearTimeout(handshakeTimer);
280
+ // Detach listeners BEFORE `forceClose()` so stray close/error
281
+ // events on the abandoned socket can't flip `this.closed`.
282
+ detachHandshakeListeners();
276
283
  // Null out this.ws (via forceClose) so the instance can be
277
284
  // reused for a retry. Without this, a subsequent start() call
278
285
  // would throw "start() called twice" even though no session
@@ -288,9 +295,8 @@ export class XAIRealtimeTranscriber implements StreamingTranscriber {
288
295
  const onOpen = () => {
289
296
  ws.removeEventListener("open", onOpen);
290
297
  };
291
- ws.addEventListener("open", onOpen);
292
298
 
293
- ws.addEventListener("message", (ev: { data: unknown }) => {
299
+ const onMessage = (ev: { data: unknown }) => {
294
300
  if (!settled) {
295
301
  if (tryParseHandshakeFrame(ev.data)?.type === "transcript.created") {
296
302
  settleResolve();
@@ -303,9 +309,9 @@ export class XAIRealtimeTranscriber implements StreamingTranscriber {
303
309
  return;
304
310
  }
305
311
  this.handleProviderMessage(ev.data);
306
- });
312
+ };
307
313
 
308
- ws.addEventListener("close", (ev: { code: number; reason: string }) => {
314
+ const onClose = (ev: { code: number; reason: string }) => {
309
315
  if (!settled) {
310
316
  // 401 / 403 on connect arrive as WebSocket close codes 4001 /
311
317
  // 4003 in most runtimes (or 1008 policy-violation in others).
@@ -320,9 +326,9 @@ export class XAIRealtimeTranscriber implements StreamingTranscriber {
320
326
  return;
321
327
  }
322
328
  this.handleProviderClose(ev.code, ev.reason);
323
- });
329
+ };
324
330
 
325
- ws.addEventListener("error", (ev: unknown) => {
331
+ const onError = (ev: unknown) => {
326
332
  if (!settled) {
327
333
  const msg =
328
334
  ev instanceof Error
@@ -334,7 +340,26 @@ export class XAIRealtimeTranscriber implements StreamingTranscriber {
334
340
  return;
335
341
  }
336
342
  this.handleProviderError(ev);
337
- });
343
+ };
344
+
345
+ const detachHandshakeListeners = () => {
346
+ ws.removeEventListener("message", onMessage);
347
+ ws.removeEventListener("close", onClose);
348
+ ws.removeEventListener("error", onError);
349
+ };
350
+
351
+ const handshakeTimer = setTimeout(() => {
352
+ if (settled) return;
353
+ settled = true;
354
+ detachHandshakeListeners();
355
+ this.forceClose();
356
+ reject(new Error("xAI realtime connect timeout"));
357
+ }, this.connectTimeoutMs);
358
+
359
+ ws.addEventListener("open", onOpen);
360
+ ws.addEventListener("message", onMessage);
361
+ ws.addEventListener("close", onClose);
362
+ ws.addEventListener("error", onError);
338
363
  });
339
364
 
340
365
  this.resetInactivityTimer();
@@ -65,7 +65,7 @@ Host browser allows the assistant to proxy CDP (Chrome DevTools Protocol) JSON-R
65
65
 
66
66
  The `chrome-extension` interface in `INTERFACE_IDS` is a non-interactive transport that supports only the `host_browser` capability — it does NOT support `host_bash`, `host_file`, or `host_cu`. This is encoded in `supportsHostProxy(id, capability)`: passing a capability argument returns `true` for `chrome-extension` only when the capability is `host_browser`; the no-arg form returns `false` for `chrome-extension` (so legacy desktop-only call sites that assume full-desktop proxy availability continue to gate correctly).
67
67
 
68
- Unlike the SSE-based host proxies used by the macOS client, `host_browser_request` frames for the chrome-extension interface do NOT travel through `assistantEventHub`. Instead they are routed through the `ChromeExtensionRegistry` singleton (`runtime/chrome-extension-registry.ts`), which tracks active chrome-extension WebSocket connections keyed by `(guardianId, clientInstanceId)`. The registry is populated on WebSocket `open` and drained on `close` inside `http-server.ts`'s `/v1/browser-relay` handlers — see the `wsType === "browser-relay"` branches.
68
+ For the chrome-extension interface, `host_browser_request` frames do NOT travel through `assistantEventHub`. Instead they are routed through the `ChromeExtensionRegistry` singleton (`runtime/chrome-extension-registry.ts`), which tracks active chrome-extension WebSocket connections keyed by `(guardianId, clientInstanceId)`. The registry is populated on WebSocket `open` and drained on `close` inside `http-server.ts`'s `/v1/browser-relay` handlers — see the `wsType === "browser-relay"` branches. For macOS, `host_browser_request` frames travel through `assistantEventHub` (SSE) by default; when the guardian also has an active extension connection, the registry-routed WebSocket sender takes precedence.
69
69
 
70
70
  A single guardian may have multiple parallel extension installs connected at once (two Chrome profiles, two desktops sharing a sync identity). Each install generates a stable `clientInstanceId` on first run, persists it in `chrome.storage.local`, and sends it on every WebSocket handshake as a query param (`clientInstanceId=...`) or header (`x-client-instance-id`). The registry keys inner entries by that id so sibling installs don't evict each other on register/unregister. The default `send(guardianId, msg)` path routes to whichever instance has the most recent activity (`lastActiveAt`); `sendToInstance(guardianId, clientInstanceId, msg)` pins a specific install. Older extension builds that omit the id get a connection-scoped `legacy:<connectionId>` fallback key so they degrade gracefully to single-instance semantics.
71
71
 
@@ -79,11 +79,20 @@ See `docs/browser-use-architecture-phase2.md` for the full wire diagram and comp
79
79
 
80
80
  On macOS-originated turns, the CDP factory (`tools/browser/cdp-client/factory.ts`) evaluates three browser backends in strict priority order. Each candidate is tried lazily; if the first command fails with a transport-level error, the factory falls over to the next candidate. CDP protocol errors (the browser understood the command but rejected it) do NOT trigger failover.
81
81
 
82
- | Priority | Backend | Condition | Source |
83
- | -------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
84
- | 1 | **Extension** | `hostBrowserProxy` present AND `isAvailable()` returns `true` (registry-routed WebSocket is connected) | `ChromeExtensionRegistry` via `resolveHostBrowserSender()` in `conversation-routes.ts` |
85
- | 2 | **cdp-inspect** | (a) `hostBrowser.cdpInspect.enabled` is `true` in config, OR (b) `transportInterface === "macos"` AND `desktopAuto.enabled` is `true` (default) AND the cooldown from a prior failure is not active | Config + `desktopAuto` policy in factory |
86
- | 3 | **Local** | Always present as the final fallback | Playwright sacrificial-profile browser managed by `browserManager` |
82
+ | Priority | Backend | Condition | Transport |
83
+ | -------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
84
+ | 1 | **Extension / host proxy** | `hostBrowserProxy` present AND `isAvailable()` returns `true`. On macOS, the proxy is always provisioned (SSE sender when no extension is present, registry-routed when extension is connected) | WS via `ChromeExtensionRegistry` (extension present) or SSE via `assistantEventHub` (macOS host proxy) |
85
+ | 2 | **cdp-inspect** | (a) `hostBrowser.cdpInspect.enabled` is `true` in config, OR (b) `transportInterface === "macos"` AND `desktopAuto.enabled` is `true` (default) AND the cooldown from a prior failure is not active | Direct CDP WebSocket to `localhost:9222` |
86
+ | 3 | **Local** | Always present as the final fallback | In-process Playwright CDP via `browserManager` |
87
+
88
+ **Transport selection for the extension/host-proxy backend:**
89
+
90
+ The "extension" backend label is a misnomer inherited from the original Phase 2 design where only the Chrome Extension provided host-browser access. In the current architecture, two transports can power this backend:
91
+
92
+ - **Extension WebSocket**: When the `ChromeExtensionRegistry` has an active entry for the guardian, `resolveHostBrowserSender()` returns a registry-routed sender. `host_browser_request` frames travel over the `/v1/browser-relay` WebSocket to the Chrome extension, which executes CDP commands via `chrome.debugger`.
93
+ - **macOS SSE bridge**: When the macOS desktop client is connected but no extension is present, `resolveHostBrowserSender()` returns the `onEvent` SSE hub sender. `host_browser_request` frames travel through `assistantEventHub` to the desktop client's SSE connection. The desktop client executes CDP commands against the local Chrome and POSTs results back to `/v1/host-browser-result`.
94
+
95
+ Both transports use the same `HostBrowserProxy` → `ExtensionCdpClient` pipeline. The `browser_status` output distinguishes the transport via the `details.transport` field: `"extension-ws"` or `"macos-sse"`.
87
96
 
88
97
  **Fallback criteria for cdp-inspect (desktop-auto):**
89
98
 
@@ -98,24 +107,24 @@ On macOS-originated turns, the CDP factory (`tools/browser/cdp-client/factory.ts
98
107
 
99
108
  All CDP-backed browser tools (`browser_navigate`, `browser_snapshot`, `browser_screenshot`, `browser_click`, `browser_type`, `browser_hover`, `browser_scroll`, `browser_press_key`, `browser_select_option`, `browser_wait_for`, `browser_extract`, `browser_fill_credential`, `browser_attach`, `browser_detach`, `browser_close`, `browser_status`) accept an optional `browser_mode` input parameter that overrides the automatic backend selection for that invocation.
100
109
 
101
- | Value | Behavior |
102
- | ---------------- | ------------------------------------------------------------------------ |
103
- | `auto` (default) | Existing priority-ordered fallback: extension -> cdp-inspect -> local |
104
- | `extension` | Pin to chrome-extension backend. Fails immediately if proxy unavailable. |
105
- | `cdp-inspect` | Pin to CDP inspect/debugger backend. Fails if endpoint unreachable. |
106
- | `local` | Pin to local Playwright-managed browser. No fallback. |
107
- | `cdp-debugger` | Alias for `cdp-inspect`. |
108
- | `playwright` | Alias for `local`. |
110
+ | Value | Behavior |
111
+ | ---------------- | ---------------------------------------------------------------------------- |
112
+ | `auto` (default) | Existing priority-ordered fallback: extension -> cdp-inspect -> local |
113
+ | `extension` | Pin to extension/host-proxy backend. Fails immediately if proxy unavailable. |
114
+ | `cdp-inspect` | Pin to CDP inspect/debugger backend. Fails if endpoint unreachable. |
115
+ | `local` | Pin to local Playwright-managed browser. No fallback. |
116
+ | `cdp-debugger` | Alias for `cdp-inspect`. |
117
+ | `playwright` | Alias for `local`. |
109
118
 
110
119
  **Strict pinned-mode semantics**: When `browser_mode` is set to a specific backend (not `auto`), the factory builds exactly one candidate and disables failover. If the pinned backend is unavailable, the tool returns a detailed error including:
111
120
 
112
121
  - The requested mode
113
122
  - An ordered list of attempted backends with exact failure reasons
114
- - A remediation checklist tailored by backend and failure code (e.g. "Ensure Chrome is running with --remote-debugging-port=9222")
123
+ - A remediation checklist tailored by backend, failure code, and transport (e.g. for macOS SSE: "Verify the Vellum desktop app is running"; for extension: "Ensure Chrome is running with the extension paired")
115
124
 
116
125
  **Auto-mode fallback logging**: In auto mode, fallback transitions are logged at `warn` level with structured metadata including the full candidate sequence and per-candidate failure reasons. This ensures fallback events are always observable in production logs.
117
126
 
118
- **Test coverage:** Regression tests for `browser_mode` wiring live in `__tests__/headless-browser-mode.test.ts`. E2E regression tests for backend precedence live in `__tests__/host-browser-e2e-cloud.test.ts` (extension path) and `__tests__/conversation-routes-disk-view.test.ts` (macOS fallback path). Unit tests for pinned candidate construction and failover live in `tools/browser/cdp-client/__tests__/factory.test.ts`.
127
+ **Test coverage:** Regression tests for `browser_mode` wiring live in `__tests__/headless-browser-mode.test.ts`. E2E regression tests for backend precedence live in `__tests__/host-browser-e2e-cloud.test.ts` (extension path and macOS SSE bridge path) and `__tests__/conversation-routes-disk-view.test.ts` (macOS fallback path). Unit tests for pinned candidate construction and failover live in `tools/browser/cdp-client/__tests__/factory.test.ts`. Browser status tests covering macOS host-browser diagnostics live in `tools/browser/__tests__/browser-status.test.ts`.
119
128
 
120
129
  ### Channel approvals (Telegram, Slack)
121
130
 
@@ -26,7 +26,7 @@ import {
26
26
  verifyHostBrowserCapability,
27
27
  } from "../capability-tokens.js";
28
28
  import {
29
- ALLOWED_EXTENSION_ORIGINS,
29
+ getAllowedExtensionOrigins,
30
30
  handleBrowserExtensionPair,
31
31
  NATIVE_HOST_MARKER_HEADER,
32
32
  NATIVE_HOST_MARKER_VALUE,
@@ -55,10 +55,10 @@ const lanPeerServer = mockServer("192.168.1.10");
55
55
  const publicPeerServer = mockServer("203.0.113.50");
56
56
 
57
57
  const ALLOWED_ORIGIN = (() => {
58
- const first = Array.from(ALLOWED_EXTENSION_ORIGINS)[0];
58
+ const first = Array.from(getAllowedExtensionOrigins())[0];
59
59
  if (!first) {
60
60
  throw new Error(
61
- "ALLOWED_EXTENSION_ORIGINS must contain at least one extension origin for tests",
61
+ "getAllowedExtensionOrigins() must contain at least one extension origin for tests",
62
62
  );
63
63
  }
64
64
  return first;