@vellumai/assistant 0.6.3 → 0.6.4

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 (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -8,135 +8,835 @@ import {
8
8
  createLocalBackend,
9
9
  } from "../../../browser-session/index.js";
10
10
  import { getConfig } from "../../../config/loader.js";
11
+ import { getLogger } from "../../../util/logger.js";
11
12
  import type { ToolContext } from "../../types.js";
12
13
  import { createCdpInspectClient } from "./cdp-inspect-client.js";
13
14
  import { CdpError } from "./errors.js";
14
15
  import { createExtensionCdpClient } from "./extension-cdp-client.js";
15
16
  import { createLocalCdpClient } from "./local-cdp-client.js";
16
- import type { CdpClient, CdpClientKind, ScopedCdpClient } from "./types.js";
17
+ import type {
18
+ AttemptDiagnostic,
19
+ BackendCandidate,
20
+ BrowserMode,
21
+ CdpClient,
22
+ CdpClientKind,
23
+ ScopedCdpClient,
24
+ } from "./types.js";
25
+
26
+ const log = getLogger("cdp-factory");
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Desktop-auto cdp-inspect cooldown tracker
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Module-level timestamp (epoch ms) of the last transport-level failure for
34
+ * a desktop-auto cdp-inspect attempt. While `Date.now() - _desktopAutoCooldownSince`
35
+ * is less than the configured `desktopAuto.cooldownMs`, the factory skips the
36
+ * automatic cdp-inspect candidate and goes straight to the local backend.
37
+ *
38
+ * **Process-global scope**: this is a module-level singleton that affects ALL
39
+ * conversations in the process. A cdp-inspect failure on any conversation
40
+ * suppresses desktop-auto probes for every conversation in this daemon until
41
+ * the cooldown expires. This is intentional -- the local loopback CDP
42
+ * endpoint is per-machine, not per-conversation, so a failure on one
43
+ * conversation implies all others would fail the same way.
44
+ *
45
+ * Reset to 0 when the cooldown expires or when manually cleared via
46
+ * {@link _resetDesktopAutoCooldown} (for testing).
47
+ */
48
+ let _desktopAutoCooldownSince = 0;
49
+
50
+ /**
51
+ * Record a cooldown after a desktop-auto cdp-inspect transport failure.
52
+ * Called by {@link maybeRecordDesktopAutoCooldown} in production; also
53
+ * exported directly for use in tests.
54
+ */
55
+ export function recordDesktopAutoCooldown(): void {
56
+ _desktopAutoCooldownSince = Date.now();
57
+ }
58
+
59
+ /**
60
+ * Whether the desktop-auto cdp-inspect cooldown is currently active.
61
+ * Returns `true` if a failure was recorded and the configured cooldown
62
+ * window has not yet elapsed.
63
+ */
64
+ export function isDesktopAutoCooldownActive(cooldownMs: number): boolean {
65
+ if (_desktopAutoCooldownSince === 0 || cooldownMs <= 0) return false;
66
+ return Date.now() - _desktopAutoCooldownSince < cooldownMs;
67
+ }
68
+
69
+ /**
70
+ * Reset the desktop-auto cooldown state. Exported for testing only.
71
+ */
72
+ export function _resetDesktopAutoCooldown(): void {
73
+ _desktopAutoCooldownSince = 0;
74
+ }
75
+
76
+ /**
77
+ * Get the raw cooldown-since timestamp. Exported for testing only.
78
+ */
79
+ export function _getDesktopAutoCooldownSince(): number {
80
+ return _desktopAutoCooldownSince;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Public API
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * Options for {@link getCdpClient}. All fields are optional — omitting
89
+ * them preserves the existing auto-mode behavior.
90
+ */
91
+ export interface GetCdpClientOptions {
92
+ /**
93
+ * Backend mode preference. When omitted or `"auto"`, the factory
94
+ * uses the existing priority-ordered fallback chain. When set to a
95
+ * specific backend kind, the factory pins to that single backend
96
+ * and disables failover.
97
+ */
98
+ mode?: BrowserMode;
99
+ }
17
100
 
18
101
  /**
19
102
  * Select the appropriate CdpClient implementation for a tool
20
103
  * invocation based on the ToolContext and config. Three backends are
21
104
  * considered in priority order:
22
105
  *
23
- * 1. **Extension** When `context.hostBrowserProxy` is set (macOS
24
- * desktop / cloud-hosted with a chrome-extension bound to the
25
- * conversation), register an extension backend so CDP commands
26
- * ride the host_browser_request / host_browser_result round-trip.
27
- * 2. **cdp-inspect** When the extension is absent and
28
- * `hostBrowser.cdpInspect.enabled` is `true` in config, construct
29
- * a `CdpInspectClient` that attaches to an already-running Chrome
30
- * via the DevTools JSON protocol (`--remote-debugging-port`).
31
- * 3. **Local** Default. Drives Playwright's CDPSession
32
- * against the sacrificial-profile browser managed by
33
- * browserManager.
106
+ * 1. **Extension** -- When `context.hostBrowserProxy` is set AND
107
+ * `hostBrowserProxy.isAvailable()` returns `true` (i.e. the
108
+ * proxy exists and the client is actually connected). This
109
+ * prevents selecting the extension transport when the proxy
110
+ * object exists but the underlying WebSocket is disconnected.
111
+ * 2. **cdp-inspect** -- When `hostBrowser.cdpInspect.enabled` is
112
+ * `true` in config, construct a `CdpInspectClient` that attaches
113
+ * to an already-running Chrome via the DevTools JSON protocol.
114
+ * On macOS, cdp-inspect is also included automatically when
115
+ * `desktopAuto.enabled` is true (the default), even when the
116
+ * top-level `enabled` flag is false.
117
+ * 3. **Local** -- Default. Drives Playwright's CDPSession against
118
+ * the sacrificial-profile browser managed by browserManager.
119
+ *
120
+ * When `options.mode` is set to a specific backend kind, the factory
121
+ * builds exactly one candidate and disables failover. If the pinned
122
+ * backend is unavailable (e.g. pinned `extension` without an
123
+ * available host browser proxy), the factory throws a typed
124
+ * `CdpError` with `transport_error` code and a diagnostic indicating
125
+ * the precondition that was not met.
126
+ *
127
+ * The factory builds an ordered candidate list and returns a
128
+ * {@link ScopedCdpClient} with per-invocation failover semantics:
34
129
  *
35
- * All three paths go through a per-invocation `BrowserSessionManager`
36
- * so the manager is the single choke point for CDP routing, session
37
- * lifetime, and (future) session invalidation handling. The returned
38
- * client is `kind`-tagged so tools can branch on transport — e.g.
39
- * browser_navigate skips Playwright-specific screencast and handoff
40
- * hooks when `kind === "extension"`.
130
+ * - On the first `send()`, the top-ranked candidate is selected and
131
+ * its backend is materialised.
132
+ * - If the first command fails with a **transport-level** error
133
+ * (`transport_error`), the factory tears down the failed backend
134
+ * and retries the same command against the next candidate.
135
+ * - **CDP protocol errors** (`cdp_error`) do NOT trigger failover --
136
+ * they indicate the browser understood the command and rejected it,
137
+ * so hopping transports would not help.
138
+ * - After the first successful CDP command, the backend becomes
139
+ * **sticky** for the remainder of the invocation. Subsequent
140
+ * commands always route through the same backend so multi-command
141
+ * tool flows do not hop transports mid-step.
41
142
  *
42
143
  * IMPORTANT: the returned client is per-invocation. Tools MUST call
43
144
  * `dispose()` in a finally block. Dispose tears down the manager's
44
145
  * session and the underlying CDP client. Disposing an extension-backed
45
- * client does NOT dispose the underlying HostBrowserProxy that is
146
+ * client does NOT dispose the underlying HostBrowserProxy -- that is
46
147
  * owned by the conversation.
47
148
  */
48
- export function getCdpClient(context: ToolContext): ScopedCdpClient {
149
+ export function getCdpClient(
150
+ context: ToolContext,
151
+ options?: GetCdpClientOptions,
152
+ ): ScopedCdpClient {
153
+ const mode: BrowserMode = options?.mode ?? "auto";
154
+ const candidates =
155
+ mode === "auto"
156
+ ? buildCandidateList(context)
157
+ : buildPinnedCandidateList(context, mode);
158
+
159
+ log.debug(
160
+ {
161
+ conversationId: context.conversationId,
162
+ mode,
163
+ candidates: candidates.map((c) => ({ kind: c.kind, reason: c.reason })),
164
+ },
165
+ "CDP factory: built candidate list",
166
+ );
167
+
168
+ return buildChainedClient(context.conversationId, candidates, mode);
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Pinned candidate list construction
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * Build a single-element candidate list for a pinned backend mode.
177
+ * Throws a typed `CdpError` with structured diagnostics when the
178
+ * requested backend's preconditions are not met.
179
+ *
180
+ * Exported for testing.
181
+ */
182
+ export function buildPinnedCandidateList(
183
+ context: ToolContext,
184
+ mode: Exclude<BrowserMode, "auto">,
185
+ ): BackendCandidate[] {
49
186
  const { conversationId, hostBrowserProxy } = context;
50
187
 
51
- // 1. Extension backend — preferred when a chrome-extension is bound.
52
- if (hostBrowserProxy) {
53
- const client = createExtensionCdpClient(hostBrowserProxy, conversationId);
54
- const backend = createExtensionBackend({
55
- isAvailable: () => true,
56
- sendCdp: (command, signal) =>
57
- dispatchThroughClient(client, command, signal),
58
- dispose: () => client.dispose(),
188
+ switch (mode) {
189
+ case "extension": {
190
+ if (!hostBrowserProxy || !hostBrowserProxy.isAvailable()) {
191
+ const reason = !hostBrowserProxy
192
+ ? "no host browser proxy provisioned for this conversation"
193
+ : "host browser proxy exists but is not connected";
194
+ throw new CdpError(
195
+ "transport_error",
196
+ `Pinned mode "extension" unavailable: ${reason}`,
197
+ {
198
+ attemptDiagnostics: [
199
+ {
200
+ candidateKind: "extension",
201
+ inclusionReason: `pinned mode: extension`,
202
+ stage: "candidate_selection",
203
+ errorCode: "transport_error",
204
+ errorMessage: reason,
205
+ },
206
+ ],
207
+ },
208
+ );
209
+ }
210
+ return [
211
+ {
212
+ kind: "extension",
213
+ reason: "pinned mode: extension",
214
+ create() {
215
+ const client = createExtensionCdpClient(
216
+ hostBrowserProxy,
217
+ conversationId,
218
+ );
219
+ const backend = createExtensionBackend({
220
+ isAvailable: () => true,
221
+ sendCdp: (command, signal) =>
222
+ dispatchThroughClient(client, command, signal),
223
+ dispose: () => client.dispose(),
224
+ });
225
+ return { client, backend };
226
+ },
227
+ },
228
+ ];
229
+ }
230
+ case "cdp-inspect": {
231
+ const cdpInspectConfig = getConfig().hostBrowser.cdpInspect;
232
+ return [
233
+ {
234
+ kind: "cdp-inspect",
235
+ reason: "pinned mode: cdp-inspect",
236
+ create() {
237
+ const client = createCdpInspectClient(conversationId, {
238
+ host: cdpInspectConfig.host,
239
+ port: cdpInspectConfig.port,
240
+ discoveryTimeoutMs: cdpInspectConfig.probeTimeoutMs,
241
+ });
242
+ const backend = createCdpInspectBackend({
243
+ isAvailable: () => true,
244
+ sendCdp: (command, signal) =>
245
+ dispatchThroughClient(client, command, signal),
246
+ dispose: () => client.dispose(),
247
+ });
248
+ return { client, backend };
249
+ },
250
+ },
251
+ ];
252
+ }
253
+ case "local": {
254
+ return [
255
+ {
256
+ kind: "local",
257
+ reason: "pinned mode: local",
258
+ create() {
259
+ const client = createLocalCdpClient(conversationId);
260
+ const backend = createLocalBackend({
261
+ isAvailable: () => true,
262
+ sendCdp: (command, signal) =>
263
+ dispatchThroughClient(client, command, signal),
264
+ dispose: () => client.dispose(),
265
+ });
266
+ return { client, backend };
267
+ },
268
+ },
269
+ ];
270
+ }
271
+ default: {
272
+ // Exhaustive check — if new modes are added, TypeScript will
273
+ // flag this as an error.
274
+ const _exhaustive: never = mode;
275
+ throw new Error(`Unknown pinned mode: ${_exhaustive}`);
276
+ }
277
+ }
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Candidate list construction (auto mode)
282
+ // ---------------------------------------------------------------------------
283
+
284
+ /**
285
+ * Build an ordered list of backend candidates from the tool context
286
+ * and config. Candidates are evaluated lazily -- `create()` is only
287
+ * called when the candidate is actually selected.
288
+ *
289
+ * Exported for testing.
290
+ */
291
+ export function buildCandidateList(context: ToolContext): BackendCandidate[] {
292
+ const { conversationId, hostBrowserProxy } = context;
293
+ const candidates: BackendCandidate[] = [];
294
+
295
+ // 1. Extension -- preferred when a chrome-extension is bound AND
296
+ // the proxy reports it is connected. Checking isAvailable()
297
+ // prevents selecting the extension transport when the proxy
298
+ // object exists (e.g. it was provisioned at conversation start)
299
+ // but the client has since disconnected.
300
+ if (hostBrowserProxy && hostBrowserProxy.isAvailable()) {
301
+ candidates.push({
302
+ kind: "extension",
303
+ reason: "hostBrowserProxy present and available",
304
+ create() {
305
+ const client = createExtensionCdpClient(
306
+ hostBrowserProxy,
307
+ conversationId,
308
+ );
309
+ const backend = createExtensionBackend({
310
+ isAvailable: () => true,
311
+ sendCdp: (command, signal) =>
312
+ dispatchThroughClient(client, command, signal),
313
+ dispose: () => client.dispose(),
314
+ });
315
+ return { client, backend };
316
+ },
59
317
  });
60
- return buildManagedClient("extension", conversationId, backend);
318
+ } else if (hostBrowserProxy) {
319
+ log.debug(
320
+ { conversationId },
321
+ "CDP factory: hostBrowserProxy present but not available, skipping extension candidate",
322
+ );
61
323
  }
62
324
 
63
- // 2. cdp-inspect backend opt-in via config when the extension is absent.
325
+ // 2. cdp-inspect -- opt-in via config OR desktop-auto for macOS turns.
64
326
  const cdpInspectConfig = getConfig().hostBrowser.cdpInspect;
65
327
  if (cdpInspectConfig.enabled) {
66
- const client = createCdpInspectClient(conversationId, {
67
- host: cdpInspectConfig.host,
68
- port: cdpInspectConfig.port,
69
- discoveryTimeoutMs: cdpInspectConfig.probeTimeoutMs,
70
- });
71
- const backend = createCdpInspectBackend({
72
- isAvailable: () => true,
73
- sendCdp: (command, signal) =>
74
- dispatchThroughClient(client, command, signal),
75
- dispose: () => client.dispose(),
328
+ // Explicitly enabled in config -- always include regardless of platform.
329
+ candidates.push({
330
+ kind: "cdp-inspect",
331
+ reason: "cdpInspect enabled in config",
332
+ create() {
333
+ const client = createCdpInspectClient(conversationId, {
334
+ host: cdpInspectConfig.host,
335
+ port: cdpInspectConfig.port,
336
+ discoveryTimeoutMs: cdpInspectConfig.probeTimeoutMs,
337
+ });
338
+ const backend = createCdpInspectBackend({
339
+ isAvailable: () => true,
340
+ sendCdp: (command, signal) =>
341
+ dispatchThroughClient(client, command, signal),
342
+ dispose: () => client.dispose(),
343
+ });
344
+ return { client, backend };
345
+ },
76
346
  });
77
- return buildManagedClient("cdp-inspect", conversationId, backend);
347
+ } else if (
348
+ context.transportInterface === "macos" &&
349
+ cdpInspectConfig.desktopAuto.enabled
350
+ ) {
351
+ // macOS desktop-auto: include cdp-inspect as a candidate unless:
352
+ // (a) the hostBrowserProxy exists but is temporarily unavailable
353
+ // (extension transport expected but transiently disconnected --
354
+ // inserting cdp-inspect here would cause a silent takeover), or
355
+ // (b) the cooldown from a recent failure is still active.
356
+ //
357
+ // When no hostBrowserProxy is present at all (extension not
358
+ // provisioned for this conversation), cdp-inspect remains available
359
+ // as a fallback per the desktop-auto contract.
360
+ if (hostBrowserProxy && !hostBrowserProxy.isAvailable()) {
361
+ log.debug(
362
+ { conversationId },
363
+ "CDP factory: desktop-auto cdp-inspect skipped (extension transport expected but temporarily unavailable)",
364
+ );
365
+ } else {
366
+ const { cooldownMs } = cdpInspectConfig.desktopAuto;
367
+ if (isDesktopAutoCooldownActive(cooldownMs)) {
368
+ log.debug(
369
+ {
370
+ conversationId,
371
+ cooldownMs,
372
+ cooldownSince: _desktopAutoCooldownSince,
373
+ },
374
+ "CDP factory: desktop-auto cdp-inspect skipped (cooldown active)",
375
+ );
376
+ } else {
377
+ candidates.push({
378
+ kind: "cdp-inspect",
379
+ reason: "desktopAuto: macOS turn, cdp-inspect auto-attempted",
380
+ create() {
381
+ const client = createCdpInspectClient(conversationId, {
382
+ host: cdpInspectConfig.host,
383
+ port: cdpInspectConfig.port,
384
+ discoveryTimeoutMs: cdpInspectConfig.probeTimeoutMs,
385
+ wsConnectTimeoutMs: cdpInspectConfig.probeTimeoutMs,
386
+ });
387
+ const backend = createCdpInspectBackend({
388
+ isAvailable: () => true,
389
+ sendCdp: (command, signal) =>
390
+ dispatchThroughClient(client, command, signal),
391
+ dispose: () => client.dispose(),
392
+ });
393
+ return { client, backend };
394
+ },
395
+ });
396
+ }
397
+ }
78
398
  }
79
399
 
80
- // 3. Local backend default (Playwright-backed Chromium).
81
- const client = createLocalCdpClient(conversationId);
82
- const backend = createLocalBackend({
83
- isAvailable: () => true,
84
- sendCdp: (command, signal) =>
85
- dispatchThroughClient(client, command, signal),
86
- dispose: () => client.dispose(),
400
+ // 3. Local -- always present as the final fallback.
401
+ candidates.push({
402
+ kind: "local",
403
+ reason: "default Playwright fallback",
404
+ create() {
405
+ const client = createLocalCdpClient(conversationId);
406
+ const backend = createLocalBackend({
407
+ isAvailable: () => true,
408
+ sendCdp: (command, signal) =>
409
+ dispatchThroughClient(client, command, signal),
410
+ dispose: () => client.dispose(),
411
+ });
412
+ return { client, backend };
413
+ },
87
414
  });
88
- return buildManagedClient("local", conversationId, backend);
415
+
416
+ return candidates;
89
417
  }
90
418
 
419
+ // ---------------------------------------------------------------------------
420
+ // Chained client with per-invocation failover
421
+ // ---------------------------------------------------------------------------
422
+
91
423
  /**
92
- * Build a ScopedCdpClient whose `send()` routes through a
93
- * BrowserSessionManager seeded with a single backend + session. This
94
- * lets tool call sites remain backend-agnostic while giving the
95
- * manager a seam for future session-invalidation and multi-target
96
- * routing work.
424
+ * Build a {@link ScopedCdpClient} that walks the candidate list on
425
+ * the first command, failing over on transport-level errors, and
426
+ * becomes sticky after the first successful CDP command.
427
+ *
428
+ * Exported for testing.
97
429
  */
98
- function buildManagedClient(
99
- kind: CdpClientKind,
430
+ export function buildChainedClient(
100
431
  conversationId: string,
101
- backend: BrowserBackend,
432
+ candidates: BackendCandidate[],
433
+ mode: BrowserMode = "auto",
102
434
  ): ScopedCdpClient {
103
- const manager = new BrowserSessionManager({ backends: [backend] });
104
- const session = manager.createSession();
435
+ if (candidates.length === 0) {
436
+ throw new Error("CDP factory: no backend candidates available");
437
+ }
438
+
439
+ /** Active backend state -- populated after first successful command. */
440
+ let active: {
441
+ kind: CdpClientKind;
442
+ manager: BrowserSessionManager;
443
+ sessionId: string;
444
+ } | null = null;
445
+
446
+ /** Set to true after the first successful CDP command. */
447
+ let sticky = false;
448
+
105
449
  let disposed = false;
106
450
 
107
- return {
108
- kind,
451
+ /**
452
+ * Track all materialised backends so dispose() can tear them all
453
+ * down, even ones that were tried and failed before the sticky
454
+ * backend was established.
455
+ */
456
+ const materialisedManagers: BrowserSessionManager[] = [];
457
+
458
+ /**
459
+ * The kind of the currently active (or last attempted) backend.
460
+ * Before the first send this reflects the first candidate; after
461
+ * the sticky backend is established it reflects the chosen kind.
462
+ */
463
+ let currentKind: CdpClientKind = candidates[0].kind;
464
+
465
+ const scopedClient: ScopedCdpClient = {
466
+ get kind(): CdpClientKind {
467
+ return active?.kind ?? currentKind;
468
+ },
109
469
  conversationId,
470
+
110
471
  async send<T = unknown>(
111
472
  method: string,
112
473
  params?: Record<string, unknown>,
113
474
  signal?: AbortSignal,
114
475
  ): Promise<T> {
115
476
  if (disposed) {
116
- const clientName =
117
- kind === "extension"
118
- ? "ExtensionCdpClient"
119
- : kind === "cdp-inspect"
120
- ? "CdpInspectClient"
121
- : "LocalCdpClient";
122
- throw new CdpError("disposed", `${clientName} already disposed`, {
477
+ throw new CdpError("disposed", "CdpClient already disposed", {
123
478
  cdpMethod: method,
124
479
  cdpParams: params,
125
480
  });
126
481
  }
127
- const command: CdpCommand = { method, params };
128
- const envelope = await manager.send(session.id, command, signal);
129
- return unwrapResult<T>(envelope, method, params);
482
+
483
+ // Fast path: backend is already sticky -- route directly.
484
+ if (sticky && active) {
485
+ const command: CdpCommand = { method, params };
486
+ const envelope = await active.manager.send(
487
+ active.sessionId,
488
+ command,
489
+ signal,
490
+ );
491
+ return unwrapResult<T>(envelope, method, params);
492
+ }
493
+
494
+ // Slow path: walk the candidate list with failover.
495
+ return sendWithFailover<T>(
496
+ candidates,
497
+ materialisedManagers,
498
+ method,
499
+ params,
500
+ signal,
501
+ (established) => {
502
+ active = established;
503
+ sticky = true;
504
+ currentKind = established.kind;
505
+ },
506
+ () => disposed,
507
+ conversationId,
508
+ mode,
509
+ );
130
510
  },
511
+
131
512
  dispose(): void {
132
513
  if (disposed) return;
133
514
  disposed = true;
134
- // disposeAll() tears down the per-invocation backend (which in
135
- // turn disposes the underlying CdpClient) and clears the single
136
- // session we created in buildManagedClient.
137
- manager.disposeAll();
515
+ for (const m of materialisedManagers) {
516
+ m.disposeAll();
517
+ }
518
+ materialisedManagers.length = 0;
519
+ active = null;
138
520
  },
139
521
  };
522
+
523
+ return scopedClient;
524
+ }
525
+
526
+ /**
527
+ * Walk the candidate list attempting to execute a single CDP command.
528
+ * Transport-level failures trigger failover to the next candidate;
529
+ * CDP protocol errors propagate immediately.
530
+ *
531
+ * When a desktop-auto cdp-inspect candidate fails with a transport
532
+ * error, the factory records a cooldown so subsequent calls skip the
533
+ * probe until the window expires.
534
+ *
535
+ * In auto mode, each attempted candidate is recorded as an
536
+ * {@link AttemptDiagnostic}. When fallback occurs, a production-visible
537
+ * log is emitted with the full candidate sequence and per-candidate
538
+ * failure reasons. If all candidates are exhausted, the diagnostics
539
+ * are attached to the thrown {@link CdpError}.
540
+ */
541
+ async function sendWithFailover<T>(
542
+ candidates: BackendCandidate[],
543
+ materialisedManagers: BrowserSessionManager[],
544
+ method: string,
545
+ params: Record<string, unknown> | undefined,
546
+ signal: AbortSignal | undefined,
547
+ onEstablished: (active: {
548
+ kind: CdpClientKind;
549
+ manager: BrowserSessionManager;
550
+ sessionId: string;
551
+ }) => void,
552
+ isDisposed: () => boolean,
553
+ conversationId: string,
554
+ mode: BrowserMode,
555
+ ): Promise<T> {
556
+ let lastError: CdpError | undefined;
557
+ const diagnostics: AttemptDiagnostic[] = [];
558
+
559
+ for (let i = 0; i < candidates.length; i++) {
560
+ const candidate = candidates[i];
561
+ if (isDisposed()) {
562
+ throw new CdpError("disposed", "CdpClient already disposed", {
563
+ cdpMethod: method,
564
+ cdpParams: params,
565
+ });
566
+ }
567
+
568
+ log.debug(
569
+ {
570
+ conversationId,
571
+ candidateKind: candidate.kind,
572
+ candidateIndex: i,
573
+ method,
574
+ },
575
+ "CDP factory: attempting candidate",
576
+ );
577
+
578
+ let backend: BrowserBackend;
579
+ try {
580
+ const created = candidate.create();
581
+ backend = created.backend;
582
+ } catch (err) {
583
+ // Backend construction failed -- treat as transport error and
584
+ // try the next candidate.
585
+ const errorMessage = `Backend ${candidate.kind} construction failed: ${err instanceof Error ? err.message : String(err)}`;
586
+ log.debug(
587
+ { conversationId, candidateKind: candidate.kind, err },
588
+ "CDP factory: candidate construction failed, trying next",
589
+ );
590
+ lastError = new CdpError("transport_error", errorMessage, {
591
+ cdpMethod: method,
592
+ cdpParams: params,
593
+ underlying: err,
594
+ });
595
+ diagnostics.push({
596
+ candidateKind: candidate.kind,
597
+ inclusionReason: candidate.reason,
598
+ stage: "construction",
599
+ errorCode: "transport_error",
600
+ errorMessage,
601
+ });
602
+ maybeRecordDesktopAutoCooldown(candidate);
603
+
604
+ // Emit production-visible fallback log in auto mode
605
+ if (mode === "auto" && i < candidates.length - 1) {
606
+ log.warn(
607
+ {
608
+ conversationId,
609
+ failedCandidate: candidate.kind,
610
+ nextCandidate: candidates[i + 1].kind,
611
+ attemptedSoFar: diagnostics.map((d) => ({
612
+ kind: d.candidateKind,
613
+ stage: d.stage,
614
+ errorCode: d.errorCode,
615
+ errorMessage: d.errorMessage,
616
+ })),
617
+ },
618
+ "CDP factory: auto-mode fallback triggered",
619
+ );
620
+ }
621
+ continue;
622
+ }
623
+
624
+ const manager = new BrowserSessionManager({ backends: [backend] });
625
+ materialisedManagers.push(manager);
626
+ const session = manager.createSession();
627
+
628
+ const command: CdpCommand = { method, params };
629
+ let envelope: CdpResult;
630
+ try {
631
+ envelope = await manager.send(session.id, command, signal);
632
+ } catch (err) {
633
+ // Manager-level errors (unknown session, no available backend)
634
+ // are transport-level problems -- try the next candidate.
635
+ const errorMessage = `Backend ${candidate.kind} send threw: ${err instanceof Error ? err.message : String(err)}`;
636
+ log.debug(
637
+ { conversationId, candidateKind: candidate.kind, err },
638
+ "CDP factory: candidate send threw, trying next",
639
+ );
640
+ manager.disposeAll();
641
+ lastError = new CdpError("transport_error", errorMessage, {
642
+ cdpMethod: method,
643
+ cdpParams: params,
644
+ underlying: err,
645
+ });
646
+ diagnostics.push({
647
+ candidateKind: candidate.kind,
648
+ inclusionReason: candidate.reason,
649
+ stage: "send",
650
+ errorCode: "transport_error",
651
+ errorMessage,
652
+ discoveryCode: extractDiscoveryCode(err),
653
+ });
654
+ maybeRecordDesktopAutoCooldown(candidate);
655
+
656
+ // Emit production-visible fallback log in auto mode
657
+ if (mode === "auto" && i < candidates.length - 1) {
658
+ log.warn(
659
+ {
660
+ conversationId,
661
+ failedCandidate: candidate.kind,
662
+ nextCandidate: candidates[i + 1].kind,
663
+ attemptedSoFar: diagnostics.map((d) => ({
664
+ kind: d.candidateKind,
665
+ stage: d.stage,
666
+ errorCode: d.errorCode,
667
+ errorMessage: d.errorMessage,
668
+ })),
669
+ },
670
+ "CDP factory: auto-mode fallback triggered",
671
+ );
672
+ }
673
+ continue;
674
+ }
675
+
676
+ // Inspect the envelope for errors. Transport-level errors trigger
677
+ // failover; CDP protocol errors propagate immediately.
678
+ if (envelope.error) {
679
+ const cdpError = extractCdpError(envelope, method, params);
680
+
681
+ if (isTransportFailover(cdpError) && i < candidates.length - 1) {
682
+ log.debug(
683
+ {
684
+ conversationId,
685
+ candidateKind: candidate.kind,
686
+ errorCode: cdpError.code,
687
+ errorMessage: cdpError.message,
688
+ },
689
+ "CDP factory: transport-level failure, failing over to next candidate",
690
+ );
691
+ manager.disposeAll();
692
+ lastError = cdpError;
693
+ diagnostics.push({
694
+ candidateKind: candidate.kind,
695
+ inclusionReason: candidate.reason,
696
+ stage: "send",
697
+ errorCode: cdpError.code,
698
+ errorMessage: cdpError.message,
699
+ discoveryCode: extractDiscoveryCode(cdpError.underlying),
700
+ });
701
+ maybeRecordDesktopAutoCooldown(candidate);
702
+
703
+ // Emit production-visible fallback log in auto mode
704
+ if (mode === "auto") {
705
+ log.warn(
706
+ {
707
+ conversationId,
708
+ failedCandidate: candidate.kind,
709
+ nextCandidate: candidates[i + 1].kind,
710
+ attemptedSoFar: diagnostics.map((d) => ({
711
+ kind: d.candidateKind,
712
+ stage: d.stage,
713
+ errorCode: d.errorCode,
714
+ errorMessage: d.errorMessage,
715
+ })),
716
+ },
717
+ "CDP factory: auto-mode fallback triggered",
718
+ );
719
+ }
720
+ continue;
721
+ }
722
+
723
+ // Either a CDP protocol error or we've exhausted candidates --
724
+ // propagate the error as-is, attaching diagnostics.
725
+ diagnostics.push({
726
+ candidateKind: candidate.kind,
727
+ inclusionReason: candidate.reason,
728
+ stage: "send",
729
+ errorCode: cdpError.code,
730
+ errorMessage: cdpError.message,
731
+ discoveryCode: extractDiscoveryCode(cdpError.underlying),
732
+ });
733
+ throw new CdpError(cdpError.code, cdpError.message, {
734
+ cdpMethod: cdpError.cdpMethod,
735
+ cdpParams: cdpError.cdpParams,
736
+ underlying: cdpError.underlying,
737
+ attemptDiagnostics: diagnostics.length > 0 ? diagnostics : undefined,
738
+ });
739
+ }
740
+
741
+ // Success! Establish this backend as the sticky choice.
742
+ diagnostics.push({
743
+ candidateKind: candidate.kind,
744
+ inclusionReason: candidate.reason,
745
+ stage: "success",
746
+ });
747
+
748
+ // If there were prior failed candidates in auto mode, log the
749
+ // full sequence for observability.
750
+ if (mode === "auto" && diagnostics.length > 1) {
751
+ log.warn(
752
+ {
753
+ conversationId,
754
+ stickyCandidate: candidate.kind,
755
+ attemptSequence: diagnostics.map((d) => ({
756
+ kind: d.candidateKind,
757
+ stage: d.stage,
758
+ errorCode: d.errorCode,
759
+ errorMessage: d.errorMessage,
760
+ })),
761
+ },
762
+ "CDP factory: auto-mode fallback completed, backend established after retries",
763
+ );
764
+ }
765
+
766
+ log.debug(
767
+ { conversationId, candidateKind: candidate.kind, method },
768
+ "CDP factory: candidate succeeded, backend is now sticky",
769
+ );
770
+ onEstablished({ kind: candidate.kind, manager, sessionId: session.id });
771
+ return envelope.result as T;
772
+ }
773
+
774
+ // All candidates exhausted -- throw the last transport error with
775
+ // full attempt diagnostics attached.
776
+ throw lastError
777
+ ? new CdpError(lastError.code, lastError.message, {
778
+ cdpMethod: lastError.cdpMethod,
779
+ cdpParams: lastError.cdpParams,
780
+ underlying: lastError.underlying,
781
+ attemptDiagnostics: diagnostics.length > 0 ? diagnostics : undefined,
782
+ })
783
+ : new CdpError("transport_error", "All backend candidates exhausted", {
784
+ cdpMethod: method,
785
+ cdpParams: params,
786
+ attemptDiagnostics: diagnostics.length > 0 ? diagnostics : undefined,
787
+ });
788
+ }
789
+
790
+ /**
791
+ * If the failed candidate is a desktop-auto cdp-inspect attempt,
792
+ * record the cooldown so subsequent calls skip the probe.
793
+ */
794
+ function maybeRecordDesktopAutoCooldown(candidate: BackendCandidate): void {
795
+ if (
796
+ candidate.kind === "cdp-inspect" &&
797
+ candidate.reason.startsWith("desktopAuto:")
798
+ ) {
799
+ log.debug(
800
+ "CDP factory: recording desktop-auto cdp-inspect cooldown after transport failure",
801
+ );
802
+ recordDesktopAutoCooldown();
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Determine whether a CdpError should trigger failover to the next
808
+ * candidate. Only transport-level failures are eligible -- CDP
809
+ * protocol errors indicate the browser understood the command and
810
+ * rejected it, so retrying on a different transport would not help.
811
+ */
812
+ function isTransportFailover(err: CdpError): boolean {
813
+ return err.code === "transport_error";
814
+ }
815
+
816
+ // ---------------------------------------------------------------------------
817
+ // Helpers (shared with the old implementation)
818
+ // ---------------------------------------------------------------------------
819
+
820
+ /**
821
+ * Extract a CdpError from a CdpResult envelope that carries an error.
822
+ */
823
+ function extractCdpError(
824
+ envelope: CdpResult,
825
+ method: string,
826
+ params?: Record<string, unknown>,
827
+ ): CdpError {
828
+ if (envelope.error?.data instanceof CdpError) {
829
+ return envelope.error.data;
830
+ }
831
+ return new CdpError(
832
+ "cdp_error",
833
+ envelope.error?.message ?? "Unknown CDP error",
834
+ {
835
+ cdpMethod: method,
836
+ cdpParams: params,
837
+ underlying: envelope.error,
838
+ },
839
+ );
140
840
  }
141
841
 
142
842
  /**
@@ -148,7 +848,7 @@ function buildManagedClient(
148
848
  *
149
849
  * The per-command `command.sessionId` (populated by the manager from
150
850
  * a session's opaque `targetId`) is intentionally not forwarded to
151
- * the underlying CdpClient today both LocalCdpClient and
851
+ * the underlying CdpClient today -- both LocalCdpClient and
152
852
  * ExtensionCdpClient take their CDP sessionId at construction time
153
853
  * and tools run one client per invocation. The seam is preserved so
154
854
  * a future multi-target backend can read it off the CdpCommand.
@@ -163,9 +863,9 @@ async function dispatchThroughClient(
163
863
  return { result };
164
864
  } catch (err) {
165
865
  if (err instanceof CdpError) {
166
- // Preserve the original CdpError so unwrapResult can re-throw it
167
- // verbatim. CdpResult's error channel is opaque to the manager,
168
- // so stashing the instance under `data` is safe.
866
+ // Preserve the original CdpError so extractCdpError can
867
+ // re-throw it verbatim. CdpResult's error channel is opaque
868
+ // to the manager, so stashing the instance under `data` is safe.
169
869
  return {
170
870
  error: {
171
871
  code: -1,
@@ -191,14 +891,24 @@ function unwrapResult<T>(
191
891
  params?: Record<string, unknown>,
192
892
  ): T {
193
893
  if (envelope.error) {
194
- if (envelope.error.data instanceof CdpError) {
195
- throw envelope.error.data;
196
- }
197
- throw new CdpError("cdp_error", envelope.error.message, {
198
- cdpMethod: method,
199
- cdpParams: params,
200
- underlying: envelope.error,
201
- });
894
+ throw extractCdpError(envelope, method, params);
202
895
  }
203
896
  return envelope.result as T;
204
897
  }
898
+
899
+ /**
900
+ * Attempt to extract a discovery-level error code from an underlying
901
+ * error. Some CdpInspectClient errors embed a discovery code (e.g.
902
+ * "ECONNREFUSED", "DISCOVERY_TIMEOUT") that is useful for diagnostics.
903
+ */
904
+ function extractDiscoveryCode(underlying: unknown): string | undefined {
905
+ if (underlying == null) return undefined;
906
+ if (typeof underlying === "object" && "code" in underlying) {
907
+ const code = (underlying as Record<string, unknown>).code;
908
+ if (typeof code === "string") return code;
909
+ }
910
+ if (underlying instanceof Error && "cause" in underlying) {
911
+ return extractDiscoveryCode(underlying.cause);
912
+ }
913
+ return undefined;
914
+ }