@vellumai/assistant 0.8.10 → 0.8.11-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -5,7 +5,7 @@ import type { ToolContext } from "../../../types.js";
5
5
  import { CdpError } from "../errors.js";
6
6
 
7
7
  type FakeClient = {
8
- kind: "extension" | "local" | "cdp-inspect";
8
+ kind: "extension" | "local" | "cdp-inspect" | "host-bridge";
9
9
  conversationId: string;
10
10
  send: ReturnType<typeof mock>;
11
11
  dispose: ReturnType<typeof mock>;
@@ -20,6 +20,15 @@ function makeFakeExtensionClient(conversationId: string): FakeClient {
20
20
  };
21
21
  }
22
22
 
23
+ function makeFakeHostBridgeClient(conversationId: string): FakeClient {
24
+ return {
25
+ kind: "host-bridge",
26
+ conversationId,
27
+ send: mock(async () => ({ ok: true, via: "host-bridge" })),
28
+ dispose: mock(() => {}),
29
+ };
30
+ }
31
+
23
32
  function makeFakeLocalClient(conversationId: string): FakeClient {
24
33
  return {
25
34
  kind: "local",
@@ -50,6 +59,17 @@ const createExtensionCdpClientMock = mock(
50
59
  },
51
60
  );
52
61
 
62
+ const createHostBridgeCdpClientMock = mock(
63
+ (
64
+ _proxy: HostBrowserProxy,
65
+ conversationId: string,
66
+ _sourceActorPrincipalId?: string,
67
+ ) => {
68
+ const client = makeFakeHostBridgeClient(conversationId);
69
+ return client;
70
+ },
71
+ );
72
+
53
73
  const createLocalCdpClientMock = mock((conversationId: string) => {
54
74
  const client = makeFakeLocalClient(conversationId);
55
75
  lastLocalClient = client;
@@ -86,12 +106,17 @@ const logDebugCalls: Array<{ args: unknown[] }> = [];
86
106
  // suite stubs out.
87
107
  import * as realCdpInspectClient from "../cdp-inspect-client.js";
88
108
  import * as realExtensionCdpClient from "../extension-cdp-client.js";
109
+ import * as realHostBridgeCdpClient from "../host-bridge-cdp-client.js";
89
110
  import * as realLocalCdpClient from "../local-cdp-client.js";
90
111
 
91
112
  mock.module("../extension-cdp-client.js", () => ({
92
113
  ...realExtensionCdpClient,
93
114
  createExtensionCdpClient: createExtensionCdpClientMock,
94
115
  }));
116
+ mock.module("../host-bridge-cdp-client.js", () => ({
117
+ ...realHostBridgeCdpClient,
118
+ createHostBridgeCdpClient: createHostBridgeCdpClientMock,
119
+ }));
95
120
  mock.module("../local-cdp-client.js", () => ({
96
121
  ...realLocalCdpClient,
97
122
  createLocalCdpClient: createLocalCdpClientMock,
@@ -161,6 +186,10 @@ const {
161
186
  _getDesktopAutoCooldownSince,
162
187
  recordDesktopAutoCooldown,
163
188
  isDesktopAutoCooldownActive,
189
+ _resetHostBridgeCooldown,
190
+ _getHostBridgeCooldownSince,
191
+ recordHostBridgeCooldown,
192
+ isHostBridgeCooldownActive,
164
193
  } = await import("../factory.js");
165
194
 
166
195
  /**
@@ -210,17 +239,40 @@ function makeUnavailableProxy(): HostBrowserProxy {
210
239
  } as unknown as HostBrowserProxy;
211
240
  }
212
241
 
242
+ /**
243
+ * Create a fake HostBrowserProxy with actor-scoped availability, mirroring
244
+ * the real implementation: with an actor, only that actor's clients count;
245
+ * without one, any client counts.
246
+ */
247
+ function makeActorScopedProxy(opts: {
248
+ extensionActors: string[];
249
+ bridgeActors: string[];
250
+ }): HostBrowserProxy {
251
+ const allActors = [...opts.extensionActors, ...opts.bridgeActors];
252
+ return {
253
+ request: mock(async () => ({})),
254
+ isAvailable: (actor?: string) =>
255
+ actor == null ? allActors.length > 0 : allActors.includes(actor),
256
+ hasExtensionClient: (actor?: string) =>
257
+ actor == null
258
+ ? opts.extensionActors.length > 0
259
+ : opts.extensionActors.includes(actor),
260
+ } as unknown as HostBrowserProxy;
261
+ }
262
+
213
263
  describe("getCdpClient", () => {
214
264
  beforeEach(() => {
215
265
  createExtensionCdpClientMock.mockClear();
216
266
  createLocalCdpClientMock.mockClear();
217
267
  createCdpInspectClientMock.mockClear();
268
+ createHostBridgeCdpClientMock.mockClear();
218
269
  lastExtensionClient = undefined;
219
270
  lastLocalClient = undefined;
220
271
  lastCdpInspectClient = undefined;
221
272
  cdpInspectEnabled = false;
222
273
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
223
274
  _resetDesktopAutoCooldown();
275
+ _resetHostBridgeCooldown();
224
276
  logWarnCalls.length = 0;
225
277
  logDebugCalls.length = 0;
226
278
  mockSingletonProxy = null;
@@ -785,6 +837,7 @@ describe("buildCandidateList", () => {
785
837
  cdpInspectEnabled = false;
786
838
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
787
839
  _resetDesktopAutoCooldown();
840
+ _resetHostBridgeCooldown();
788
841
  mockSingletonProxy = null;
789
842
  });
790
843
 
@@ -816,10 +869,11 @@ describe("buildCandidateList", () => {
816
869
  expect(candidates[0].kind).toBe("local");
817
870
  });
818
871
 
819
- test("excludes extension candidate when only macOS SSE bridge is connected", () => {
872
+ test("uses host-bridge candidate when only macOS SSE bridge is connected", () => {
820
873
  // isAvailable() = true but hasExtensionClient() = false: only macOS bridge.
821
- // The macOS bridge routes through localhost:9222 on the host, so it must
822
- // NOT be included under the "extension" candidate kind.
874
+ // The bridge gets its own first-class candidate kind — it must NOT be
875
+ // labelled "extension" (it routes raw CDP through localhost:9222 on the
876
+ // host and cannot serve Vellum.* pseudo-methods).
823
877
  const fakeProxy = makeMacosBridgeOnlyProxy();
824
878
  mockSingletonProxy = fakeProxy;
825
879
  const ctx = makeContext({
@@ -829,6 +883,180 @@ describe("buildCandidateList", () => {
829
883
  const candidates = buildCandidateList(ctx);
830
884
 
831
885
  expect(candidates.every((c) => c.kind !== "extension")).toBe(true);
886
+ expect(candidates[0].kind).toBe("host-bridge");
887
+ expect(candidates[candidates.length - 1].kind).toBe("local");
888
+ });
889
+
890
+ test("host-bridge candidate is absent when an extension is connected", () => {
891
+ mockSingletonProxy = makeAvailableProxy();
892
+ const ctx = makeContext({ conversationId: "candidates-ext-no-bridge" });
893
+
894
+ const candidates = buildCandidateList(ctx);
895
+
896
+ expect(candidates[0].kind).toBe("extension");
897
+ expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
898
+ });
899
+
900
+ test("host-bridge candidate is absent when the proxy is unavailable", () => {
901
+ mockSingletonProxy = makeUnavailableProxy();
902
+ const ctx = makeContext({ conversationId: "candidates-no-bridge" });
903
+
904
+ const candidates = buildCandidateList(ctx);
905
+
906
+ expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
907
+ expect(candidates[0].kind).toBe("local");
908
+ });
909
+
910
+ test("host-bridge candidate is skipped while the bridge cooldown is active", () => {
911
+ mockSingletonProxy = makeMacosBridgeOnlyProxy();
912
+ const ctx = makeContext({ conversationId: "candidates-bridge-cooldown" });
913
+
914
+ recordHostBridgeCooldown();
915
+ const candidates = buildCandidateList(ctx);
916
+
917
+ expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
918
+
919
+ _resetHostBridgeCooldown();
920
+ const after = buildCandidateList(ctx);
921
+ expect(after[0].kind).toBe("host-bridge");
922
+ });
923
+
924
+ test("host-bridge cooldown is independent of the desktop-auto cooldown", () => {
925
+ mockSingletonProxy = makeMacosBridgeOnlyProxy();
926
+ const ctx = makeContext({
927
+ conversationId: "candidates-bridge-cooldown-independent",
928
+ transportInterface: "macos",
929
+ });
930
+
931
+ recordDesktopAutoCooldown();
932
+ const candidates = buildCandidateList(ctx);
933
+
934
+ // Desktop-auto cooldown suppresses cdp-inspect but not the bridge.
935
+ expect(candidates[0].kind).toBe("host-bridge");
936
+ expect(candidates.every((c) => c.kind !== "cdp-inspect")).toBe(true);
937
+ expect(isHostBridgeCooldownActive(30_000)).toBe(false);
938
+ });
939
+
940
+ test("host-bridge cooldown is scoped per actor", () => {
941
+ // One actor's missing debug port must not suppress another actor's
942
+ // only route to their own Chrome on a multi-actor cloud daemon.
943
+ mockSingletonProxy = makeActorScopedProxy({
944
+ extensionActors: [],
945
+ bridgeActors: ["actor-a", "actor-b"],
946
+ });
947
+
948
+ recordHostBridgeCooldown("actor-a");
949
+
950
+ const forA = buildCandidateList(
951
+ makeContext({
952
+ conversationId: "cooldown-actor-a",
953
+ sourceActorPrincipalId: "actor-a",
954
+ }),
955
+ );
956
+ expect(forA.every((c) => c.kind !== "host-bridge")).toBe(true);
957
+
958
+ const forB = buildCandidateList(
959
+ makeContext({
960
+ conversationId: "cooldown-actor-b",
961
+ sourceActorPrincipalId: "actor-b",
962
+ }),
963
+ );
964
+ expect(forB[0].kind).toBe("host-bridge");
965
+
966
+ // Actor-scoped cooldowns do not leak into the no-actor default slot.
967
+ expect(isHostBridgeCooldownActive(30_000)).toBe(false);
968
+ expect(isHostBridgeCooldownActive(30_000, "actor-a")).toBe(true);
969
+ expect(isHostBridgeCooldownActive(30_000, "actor-b")).toBe(false);
970
+ });
971
+
972
+
973
+ test("ordering on a macOS turn with bridge only: host-bridge > cdp-inspect > local", () => {
974
+ mockSingletonProxy = makeMacosBridgeOnlyProxy();
975
+ const ctx = makeContext({
976
+ conversationId: "candidates-bridge-order",
977
+ transportInterface: "macos",
978
+ });
979
+
980
+ const candidates = buildCandidateList(ctx);
981
+
982
+ expect(candidates.map((c) => c.kind)).toEqual([
983
+ "host-bridge",
984
+ "cdp-inspect",
985
+ "local",
986
+ ]);
987
+ });
988
+
989
+ test("host-bridge create() threads proxy + actor and never reads pinned tabs", () => {
990
+ const fakeProxy = makeMacosBridgeOnlyProxy();
991
+ mockSingletonProxy = fakeProxy;
992
+ const ctx = makeContext({
993
+ conversationId: "candidates-bridge-create",
994
+ sourceActorPrincipalId: "actor-1",
995
+ });
996
+
997
+ const candidates = buildCandidateList(ctx);
998
+ expect(candidates[0].kind).toBe("host-bridge");
999
+ candidates[0].create();
1000
+
1001
+ expect(createHostBridgeCdpClientMock).toHaveBeenCalledWith(
1002
+ fakeProxy,
1003
+ "candidates-bridge-create",
1004
+ "actor-1",
1005
+ );
1006
+ });
1007
+
1008
+ test("multi-actor: another actor's extension does not select the extension candidate", () => {
1009
+ // Actor A has an extension; actor B has only the bridge. B's
1010
+ // conversation must get the host-bridge candidate, not an
1011
+ // extension-labelled client that the proxy would route to B's bridge.
1012
+ mockSingletonProxy = makeActorScopedProxy({
1013
+ extensionActors: ["actor-a"],
1014
+ bridgeActors: ["actor-b"],
1015
+ });
1016
+ const ctx = makeContext({
1017
+ conversationId: "candidates-multi-actor-b",
1018
+ sourceActorPrincipalId: "actor-b",
1019
+ });
1020
+
1021
+ const candidates = buildCandidateList(ctx);
1022
+
1023
+ expect(candidates[0].kind).toBe("host-bridge");
1024
+ expect(candidates.every((c) => c.kind !== "extension")).toBe(true);
1025
+ });
1026
+
1027
+ test("multi-actor: the extension owner still gets the extension candidate", () => {
1028
+ mockSingletonProxy = makeActorScopedProxy({
1029
+ extensionActors: ["actor-a"],
1030
+ bridgeActors: ["actor-b"],
1031
+ });
1032
+ const ctx = makeContext({
1033
+ conversationId: "candidates-multi-actor-a",
1034
+ sourceActorPrincipalId: "actor-a",
1035
+ });
1036
+
1037
+ const candidates = buildCandidateList(ctx);
1038
+
1039
+ expect(candidates[0].kind).toBe("extension");
1040
+ expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
1041
+ });
1042
+
1043
+ test("multi-actor: actor with no clients at all gets neither proxy candidate", () => {
1044
+ mockSingletonProxy = makeActorScopedProxy({
1045
+ extensionActors: ["actor-a"],
1046
+ bridgeActors: ["actor-b"],
1047
+ });
1048
+ const ctx = makeContext({
1049
+ conversationId: "candidates-multi-actor-c",
1050
+ sourceActorPrincipalId: "actor-c",
1051
+ });
1052
+
1053
+ const candidates = buildCandidateList(ctx);
1054
+
1055
+ expect(
1056
+ candidates.every(
1057
+ (c) => c.kind !== "extension" && c.kind !== "host-bridge",
1058
+ ),
1059
+ ).toBe(true);
832
1060
  expect(candidates[candidates.length - 1].kind).toBe("local");
833
1061
  });
834
1062
 
@@ -931,12 +1159,14 @@ describe("buildChainedClient failover", () => {
931
1159
  createExtensionCdpClientMock.mockClear();
932
1160
  createLocalCdpClientMock.mockClear();
933
1161
  createCdpInspectClientMock.mockClear();
1162
+ createHostBridgeCdpClientMock.mockClear();
934
1163
  lastExtensionClient = undefined;
935
1164
  lastLocalClient = undefined;
936
1165
  lastCdpInspectClient = undefined;
937
1166
  cdpInspectEnabled = false;
938
1167
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
939
1168
  _resetDesktopAutoCooldown();
1169
+ _resetHostBridgeCooldown();
940
1170
  logWarnCalls.length = 0;
941
1171
  logDebugCalls.length = 0;
942
1172
  mockSingletonProxy = null;
@@ -1030,6 +1260,94 @@ describe("buildChainedClient failover", () => {
1030
1260
  expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
1031
1261
  });
1032
1262
 
1263
+ test("fails over from host-bridge to local and records the bridge cooldown", async () => {
1264
+ mockSingletonProxy = makeMacosBridgeOnlyProxy();
1265
+
1266
+ // Bridge fails like Chrome without a debug port: structured
1267
+ // "unreachable" envelope → transport_error.
1268
+ createHostBridgeCdpClientMock.mockImplementationOnce(
1269
+ (_proxy: HostBrowserProxy, conversationId: string) => {
1270
+ const c = makeFakeHostBridgeClient(conversationId);
1271
+ c.send = mock(async () => {
1272
+ throw new CdpError(
1273
+ "transport_error",
1274
+ "HTTP 404 from http://localhost:9222/json/list",
1275
+ { cdpMethod: "Page.navigate" },
1276
+ );
1277
+ });
1278
+ return c;
1279
+ },
1280
+ );
1281
+
1282
+ const ctx = makeContext({ conversationId: "failover-bridge-to-local" });
1283
+
1284
+ const client = getCdpClient(ctx);
1285
+ const result = await client.send<{ ok: boolean; via: string }>(
1286
+ "Page.navigate",
1287
+ { url: "https://example.com" },
1288
+ );
1289
+
1290
+ expect(result).toEqual({ ok: true, via: "local" });
1291
+ expect(createHostBridgeCdpClientMock).toHaveBeenCalledTimes(1);
1292
+ expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
1293
+ // Cooldown was recorded so the next candidate list skips the bridge.
1294
+ expect(_getHostBridgeCooldownSince()).toBeGreaterThan(0);
1295
+ expect(_getDesktopAutoCooldownSince()).toBe(0);
1296
+ const after = buildCandidateList(ctx);
1297
+ expect(after.every((c) => c.kind !== "host-bridge")).toBe(true);
1298
+ });
1299
+
1300
+ test("host-bridge failover records the cooldown under the failing actor only", async () => {
1301
+ mockSingletonProxy = makeActorScopedProxy({
1302
+ extensionActors: [],
1303
+ bridgeActors: ["actor-a", "actor-b"],
1304
+ });
1305
+ createHostBridgeCdpClientMock.mockImplementationOnce(
1306
+ (_proxy: HostBrowserProxy, conversationId: string) => {
1307
+ const c = makeFakeHostBridgeClient(conversationId);
1308
+ c.send = mock(async () => {
1309
+ throw new CdpError(
1310
+ "transport_error",
1311
+ "HTTP 404 from http://localhost:9222/json/list",
1312
+ { cdpMethod: "Page.navigate" },
1313
+ );
1314
+ });
1315
+ return c;
1316
+ },
1317
+ );
1318
+
1319
+ const client = getCdpClient(
1320
+ makeContext({
1321
+ conversationId: "cooldown-record-actor-a",
1322
+ sourceActorPrincipalId: "actor-a",
1323
+ }),
1324
+ );
1325
+ const result = await client.send<{ ok: boolean; via: string }>(
1326
+ "Page.navigate",
1327
+ { url: "https://example.com" },
1328
+ );
1329
+
1330
+ expect(result).toEqual({ ok: true, via: "local" });
1331
+ expect(_getHostBridgeCooldownSince("actor-a")).toBeGreaterThan(0);
1332
+ expect(_getHostBridgeCooldownSince("actor-b")).toBe(0);
1333
+ expect(_getHostBridgeCooldownSince()).toBe(0);
1334
+ });
1335
+
1336
+ test("sticky host-bridge client rejects tab operations with a clear error", async () => {
1337
+ mockSingletonProxy = makeMacosBridgeOnlyProxy();
1338
+ const ctx = makeContext({ conversationId: "bridge-tab-ops" });
1339
+
1340
+ const client = getCdpClient(ctx);
1341
+ await client.send("Page.navigate", { url: "https://example.com" });
1342
+ expect(client.kind).toBe("host-bridge");
1343
+
1344
+ // The fake bridge client has no listTabs/selectTab/closeTab, so the
1345
+ // chained client surfaces the not-supported transport_error.
1346
+ await expect(client.listTabs()).rejects.toThrow(
1347
+ "extension backend required",
1348
+ );
1349
+ });
1350
+
1033
1351
  test("does NOT fail over on cdp_error -- propagates immediately", async () => {
1034
1352
  cdpInspectEnabled = true;
1035
1353
  const fakeProxy = makeAvailableProxy();
@@ -1245,12 +1563,14 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
1245
1563
  createExtensionCdpClientMock.mockClear();
1246
1564
  createLocalCdpClientMock.mockClear();
1247
1565
  createCdpInspectClientMock.mockClear();
1566
+ createHostBridgeCdpClientMock.mockClear();
1248
1567
  lastExtensionClient = undefined;
1249
1568
  lastLocalClient = undefined;
1250
1569
  lastCdpInspectClient = undefined;
1251
1570
  cdpInspectEnabled = false;
1252
1571
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
1253
1572
  _resetDesktopAutoCooldown();
1573
+ _resetHostBridgeCooldown();
1254
1574
  logWarnCalls.length = 0;
1255
1575
  logDebugCalls.length = 0;
1256
1576
  mockSingletonProxy = null;
@@ -1597,12 +1917,14 @@ describe("pinned-mode selection", () => {
1597
1917
  createExtensionCdpClientMock.mockClear();
1598
1918
  createLocalCdpClientMock.mockClear();
1599
1919
  createCdpInspectClientMock.mockClear();
1920
+ createHostBridgeCdpClientMock.mockClear();
1600
1921
  lastExtensionClient = undefined;
1601
1922
  lastLocalClient = undefined;
1602
1923
  lastCdpInspectClient = undefined;
1603
1924
  cdpInspectEnabled = false;
1604
1925
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
1605
1926
  _resetDesktopAutoCooldown();
1927
+ _resetHostBridgeCooldown();
1606
1928
  logWarnCalls.length = 0;
1607
1929
  logDebugCalls.length = 0;
1608
1930
  mockSingletonProxy = null;
@@ -1811,12 +2133,14 @@ describe("buildPinnedCandidateList", () => {
1811
2133
  createExtensionCdpClientMock.mockClear();
1812
2134
  createLocalCdpClientMock.mockClear();
1813
2135
  createCdpInspectClientMock.mockClear();
2136
+ createHostBridgeCdpClientMock.mockClear();
1814
2137
  lastExtensionClient = undefined;
1815
2138
  lastLocalClient = undefined;
1816
2139
  lastCdpInspectClient = undefined;
1817
2140
  cdpInspectEnabled = false;
1818
2141
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
1819
2142
  _resetDesktopAutoCooldown();
2143
+ _resetHostBridgeCooldown();
1820
2144
  mockSingletonProxy = null;
1821
2145
  });
1822
2146
 
@@ -1892,6 +2216,54 @@ describe("buildPinnedCandidateList", () => {
1892
2216
  expect(cdpErr.message).toContain("no Chrome Extension connected");
1893
2217
  }
1894
2218
  });
2219
+
2220
+ test("host-bridge mode produces single host-bridge candidate when bridge-only", () => {
2221
+ mockSingletonProxy = makeMacosBridgeOnlyProxy();
2222
+ const ctx = makeContext({ conversationId: "bpl-bridge" });
2223
+
2224
+ const candidates = buildPinnedCandidateList(ctx, "host-bridge");
2225
+
2226
+ expect(candidates).toHaveLength(1);
2227
+ expect(candidates[0].kind).toBe("host-bridge");
2228
+ expect(candidates[0].reason).toContain("pinned mode: host-bridge");
2229
+ });
2230
+
2231
+ test("host-bridge mode throws with diagnostics when proxy is unavailable", () => {
2232
+ mockSingletonProxy = makeUnavailableProxy();
2233
+ const ctx = makeContext({ conversationId: "bpl-bridge-absent" });
2234
+
2235
+ try {
2236
+ buildPinnedCandidateList(ctx, "host-bridge");
2237
+ expect(true).toBe(false); // should not reach
2238
+ } catch (err) {
2239
+ expect(err).toBeInstanceOf(CdpError);
2240
+ const cdpErr = err as CdpError;
2241
+ expect(cdpErr.code).toBe("transport_error");
2242
+ expect(cdpErr.attemptDiagnostics![0]).toMatchObject({
2243
+ candidateKind: "host-bridge",
2244
+ stage: "candidate_selection",
2245
+ });
2246
+ }
2247
+ });
2248
+
2249
+ test("host-bridge mode throws when an extension has since connected", () => {
2250
+ // The sticky memo says host-bridge but a Chrome Extension is now
2251
+ // available. Throwing trips acquireCdpClientWithMode's sticky-fallback
2252
+ // retry, which clears the memo and re-runs auto so the conversation
2253
+ // upgrades to the extension candidate.
2254
+ mockSingletonProxy = makeAvailableProxy();
2255
+ const ctx = makeContext({ conversationId: "bpl-bridge-ext-upgrade" });
2256
+
2257
+ try {
2258
+ buildPinnedCandidateList(ctx, "host-bridge");
2259
+ expect(true).toBe(false); // should not reach
2260
+ } catch (err) {
2261
+ expect(err).toBeInstanceOf(CdpError);
2262
+ expect((err as CdpError).message).toContain(
2263
+ "a Chrome Extension is now connected",
2264
+ );
2265
+ }
2266
+ });
1895
2267
  });
1896
2268
 
1897
2269
  // ── Attempt diagnostics & fallback log tests ─────────────────────────────
@@ -1901,12 +2273,14 @@ describe("attempt diagnostics", () => {
1901
2273
  createExtensionCdpClientMock.mockClear();
1902
2274
  createLocalCdpClientMock.mockClear();
1903
2275
  createCdpInspectClientMock.mockClear();
2276
+ createHostBridgeCdpClientMock.mockClear();
1904
2277
  lastExtensionClient = undefined;
1905
2278
  lastLocalClient = undefined;
1906
2279
  lastCdpInspectClient = undefined;
1907
2280
  cdpInspectEnabled = false;
1908
2281
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
1909
2282
  _resetDesktopAutoCooldown();
2283
+ _resetHostBridgeCooldown();
1910
2284
  logWarnCalls.length = 0;
1911
2285
  logDebugCalls.length = 0;
1912
2286
  mockSingletonProxy = null;
@@ -2300,12 +2674,14 @@ describe("macOS host-browser proxy without extension registry", () => {
2300
2674
  createExtensionCdpClientMock.mockClear();
2301
2675
  createLocalCdpClientMock.mockClear();
2302
2676
  createCdpInspectClientMock.mockClear();
2677
+ createHostBridgeCdpClientMock.mockClear();
2303
2678
  lastExtensionClient = undefined;
2304
2679
  lastLocalClient = undefined;
2305
2680
  lastCdpInspectClient = undefined;
2306
2681
  cdpInspectEnabled = false;
2307
2682
  desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
2308
2683
  _resetDesktopAutoCooldown();
2684
+ _resetHostBridgeCooldown();
2309
2685
  logWarnCalls.length = 0;
2310
2686
  logDebugCalls.length = 0;
2311
2687
  mockSingletonProxy = null;
@@ -0,0 +1,107 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { HostBrowserProxy } from "../../../../daemon/host-browser-proxy.js";
4
+ import { CdpError } from "../errors.js";
5
+ import {
6
+ createHostBridgeCdpClient,
7
+ HostBridgeCdpClient,
8
+ } from "../host-bridge-cdp-client.js";
9
+
10
+ type ProxyResult = { content: string; isError: boolean };
11
+
12
+ function fakeProxy(
13
+ handler: (
14
+ input: unknown,
15
+ conversationId: string,
16
+ signal?: AbortSignal,
17
+ ) => Promise<ProxyResult> | ProxyResult,
18
+ ): {
19
+ proxy: HostBrowserProxy;
20
+ request: ReturnType<typeof mock>;
21
+ } {
22
+ const request = mock(
23
+ async (
24
+ input: unknown,
25
+ conversationId: string,
26
+ signal?: AbortSignal,
27
+ ): Promise<ProxyResult> => handler(input, conversationId, signal),
28
+ );
29
+ const proxy = { request } as unknown as HostBrowserProxy;
30
+ return { proxy, request };
31
+ }
32
+
33
+ describe("HostBridgeCdpClient", () => {
34
+ test("kind is 'host-bridge' and exposes conversationId", () => {
35
+ const { proxy } = fakeProxy(async () => ({
36
+ content: "{}",
37
+ isError: false,
38
+ }));
39
+ const client = createHostBridgeCdpClient(proxy, "conv-bridge");
40
+ expect(client).toBeInstanceOf(HostBridgeCdpClient);
41
+ expect(client.kind).toBe("host-bridge");
42
+ expect(client.conversationId).toBe("conv-bridge");
43
+ });
44
+
45
+ test("send forwards raw CDP with no cdpSessionId or targetClientId", async () => {
46
+ const { proxy, request } = fakeProxy(async () => ({
47
+ content: JSON.stringify({ frameId: "frame-1" }),
48
+ isError: false,
49
+ }));
50
+
51
+ const client = createHostBridgeCdpClient(proxy, "conv-1", "actor-1");
52
+ const result = await client.send<{ frameId: string }>("Page.navigate", {
53
+ url: "https://example.com/",
54
+ });
55
+
56
+ expect(result).toEqual({ frameId: "frame-1" });
57
+ expect(request).toHaveBeenCalledTimes(1);
58
+ const call = request.mock.calls[0];
59
+ expect(call?.[0]).toEqual({
60
+ cdpMethod: "Page.navigate",
61
+ cdpParams: { url: "https://example.com/" },
62
+ cdpSessionId: undefined,
63
+ });
64
+ expect(call?.[1]).toBe("conv-1");
65
+ // sourceActorPrincipalId threads through; targetClientId is never set.
66
+ expect(call?.[3]).toBe("actor-1");
67
+ expect(call?.[4]).toBeUndefined();
68
+ });
69
+
70
+ test("bridge 'unreachable' envelope classifies as transport_error (failover-eligible)", async () => {
71
+ const { proxy } = fakeProxy(async () => ({
72
+ content: JSON.stringify({
73
+ code: "unreachable",
74
+ message: "HTTP 404 from http://localhost:9222/json/list",
75
+ }),
76
+ isError: true,
77
+ }));
78
+
79
+ const client = createHostBridgeCdpClient(proxy, "conv-err");
80
+ try {
81
+ await client.send("Page.captureScreenshot", {});
82
+ expect(true).toBe(false); // should not reach
83
+ } catch (err) {
84
+ expect(err).toBeInstanceOf(CdpError);
85
+ expect((err as CdpError).code).toBe("transport_error");
86
+ expect((err as CdpError).message).toContain("HTTP 404");
87
+ }
88
+ });
89
+
90
+ test.each(["listTabs", "selectTab", "closeTab"] as const)(
91
+ "%s throws transport_error locally without touching the proxy",
92
+ async (method) => {
93
+ const { proxy, request } = fakeProxy(async () => ({
94
+ content: "{}",
95
+ isError: false,
96
+ }));
97
+ const client = createHostBridgeCdpClient(proxy, "conv-tabs");
98
+
99
+ const call =
100
+ method === "listTabs" ? client.listTabs() : client[method](1);
101
+ await expect(call).rejects.toThrow(
102
+ `${method} is not supported by the host-bridge backend (extension backend required)`,
103
+ );
104
+ expect(request).not.toHaveBeenCalled();
105
+ },
106
+ );
107
+ });
@@ -79,7 +79,12 @@ describe("cdp-client re-exports", () => {
79
79
  });
80
80
 
81
81
  test("ScopedCdpClient exposes kind and conversationId", () => {
82
- const kinds: CdpClientKind[] = ["local", "extension", "cdp-inspect"];
82
+ const kinds: CdpClientKind[] = [
83
+ "local",
84
+ "extension",
85
+ "cdp-inspect",
86
+ "host-bridge",
87
+ ];
83
88
  for (const kind of kinds) {
84
89
  const scoped: ScopedCdpClient = {
85
90
  kind,