@vellumai/assistant 0.8.4 → 0.8.5

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 (438) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/browser-use-architecture-phase2.md +1 -1
  3. package/knip.json +2 -1
  4. package/openapi.yaml +809 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/anthropic-provider.test.ts +34 -37
  7. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  9. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
  11. package/src/__tests__/btw-routes.test.ts +2 -3
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  14. package/src/__tests__/channel-guardian.test.ts +3 -3
  15. package/src/__tests__/checker.test.ts +6 -15
  16. package/src/__tests__/compaction-events.test.ts +1 -0
  17. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  18. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  19. package/src/__tests__/computer-use-tools.test.ts +2 -4
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  21. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
  24. package/src/__tests__/conversation-agent-loop.test.ts +163 -122
  25. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  26. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  27. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  29. package/src/__tests__/conversation-error.test.ts +31 -0
  30. package/src/__tests__/conversation-fork-crud.test.ts +178 -15
  31. package/src/__tests__/conversation-lifecycle.test.ts +52 -11
  32. package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
  33. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  34. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  36. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  37. package/src/__tests__/conversation-store.test.ts +1 -1
  38. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  40. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  42. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  43. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  44. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  45. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  46. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  47. package/src/__tests__/email-html-renderer.test.ts +12 -0
  48. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  49. package/src/__tests__/gemini-provider.test.ts +78 -0
  50. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  51. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  52. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  53. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  54. package/src/__tests__/heartbeat-service.test.ts +4 -0
  55. package/src/__tests__/host-shell-tool.test.ts +1 -1
  56. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  57. package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
  58. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  59. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  60. package/src/__tests__/llm-resolver.test.ts +77 -9
  61. package/src/__tests__/llm-usage-store.test.ts +66 -0
  62. package/src/__tests__/logger.test.ts +89 -0
  63. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  64. package/src/__tests__/media-generate-image.test.ts +31 -0
  65. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  66. package/src/__tests__/model-intents.test.ts +2 -4
  67. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  68. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  69. package/src/__tests__/openai-provider.test.ts +46 -0
  70. package/src/__tests__/openai-responses-provider.test.ts +114 -12
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
  72. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  73. package/src/__tests__/platform.test.ts +2 -2
  74. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  75. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  76. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  77. package/src/__tests__/plugin-types.test.ts +3 -2
  78. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  79. package/src/__tests__/pricing.test.ts +12 -0
  80. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  81. package/src/__tests__/registry.test.ts +2 -8
  82. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  83. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  84. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  85. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  86. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  87. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  88. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  89. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  90. package/src/__tests__/suggestion-routes.test.ts +1 -0
  91. package/src/__tests__/sync-message-contract.test.ts +59 -0
  92. package/src/__tests__/system-prompt.test.ts +145 -131
  93. package/src/__tests__/terminal-tools.test.ts +1 -1
  94. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  95. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  96. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  98. package/src/__tests__/tool-executor.test.ts +9 -62
  99. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  100. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  101. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  102. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  103. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  104. package/src/__tests__/usage-routes.test.ts +3 -0
  105. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  106. package/src/__tests__/workspace-git-service.test.ts +6 -5
  107. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  108. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  109. package/src/acp/prepare-agent-env.ts +78 -0
  110. package/src/acp/session-manager.ts +1 -1
  111. package/src/agent/loop.ts +8 -0
  112. package/src/api/README.md +5 -0
  113. package/src/api/index.ts +4 -0
  114. package/src/api/package.json +10 -0
  115. package/src/background-wake/background-wake-routes.test.ts +233 -0
  116. package/src/background-wake/runtime-registry.ts +24 -0
  117. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  118. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  119. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  120. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  121. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  122. package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
  123. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  124. package/src/cli/commands/browser.ts +247 -0
  125. package/src/cli/commands/domain.ts +91 -41
  126. package/src/cli/commands/inference.ts +93 -40
  127. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  128. package/src/cli/commands/memory-v2.ts +176 -1
  129. package/src/cli/commands/memory-v3-render.ts +344 -0
  130. package/src/cli/commands/memory-v3.ts +316 -0
  131. package/src/cli/program.ts +2 -0
  132. package/src/config/assistant-feature-flags.ts +21 -9
  133. package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
  134. package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
  135. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  136. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  137. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  138. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  139. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  140. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  141. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  142. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  143. package/src/config/bundled-tool-registry.ts +2 -0
  144. package/src/config/call-site-defaults.ts +7 -6
  145. package/src/config/feature-flag-registry.json +16 -0
  146. package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
  147. package/src/config/schemas/call-site-catalog.ts +21 -7
  148. package/src/config/schemas/llm.ts +12 -1
  149. package/src/config/schemas/memory-v2.ts +246 -0
  150. package/src/config/schemas/memory.ts +2 -1
  151. package/src/context/compactor.ts +52 -0
  152. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  153. package/src/conversations/message-consolidation.ts +404 -0
  154. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  155. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  156. package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
  157. package/src/daemon/conversation-agent-loop.ts +126 -76
  158. package/src/daemon/conversation-error.ts +31 -1
  159. package/src/daemon/conversation-lifecycle.ts +27 -22
  160. package/src/daemon/conversation-runtime-assembly.ts +10 -9
  161. package/src/daemon/conversation-tool-setup.ts +63 -3
  162. package/src/daemon/conversation-usage.ts +2 -0
  163. package/src/daemon/conversation.ts +14 -29
  164. package/src/daemon/disk-pressure-guard.ts +14 -2
  165. package/src/daemon/handlers/config-model.test.ts +1 -0
  166. package/src/daemon/handlers/conversations.ts +11 -3
  167. package/src/daemon/host-browser-proxy.ts +5 -5
  168. package/src/daemon/host-cu-proxy.ts +4 -4
  169. package/src/daemon/host-file-proxy.ts +4 -4
  170. package/src/daemon/host-proxy-base.ts +4 -4
  171. package/src/daemon/host-transfer-proxy.ts +10 -10
  172. package/src/daemon/lifecycle.ts +23 -20
  173. package/src/daemon/meet-manifest-loader.ts +1 -7
  174. package/src/daemon/message-types/conversations.ts +6 -9
  175. package/src/daemon/message-types/home.ts +1 -13
  176. package/src/daemon/message-types/messages.ts +6 -14
  177. package/src/daemon/message-types/sync.ts +14 -0
  178. package/src/daemon/shutdown-handlers.ts +24 -5
  179. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  180. package/src/daemon/tool-setup-types.ts +13 -0
  181. package/src/events/relationship-state-updated.ts +25 -0
  182. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  183. package/src/home/home-greeting.ts +0 -9
  184. package/src/home/suggested-prompts.ts +0 -9
  185. package/src/ipc/gateway-flag-listener.ts +123 -0
  186. package/src/ipc/skill-routes/registries.ts +8 -12
  187. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  188. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  189. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  190. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  191. package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
  192. package/src/memory/auto-analysis-enqueue.ts +5 -1
  193. package/src/memory/conversation-crud.ts +71 -70
  194. package/src/memory/conversation-starters-cadence.ts +3 -1
  195. package/src/memory/conversation-title-service.ts +19 -3
  196. package/src/memory/db-async-query.ts +214 -0
  197. package/src/memory/db-init.ts +10 -0
  198. package/src/memory/db-maintenance.ts +30 -21
  199. package/src/memory/graph/bootstrap.ts +8 -1
  200. package/src/memory/graph/capability-seed.ts +7 -3
  201. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  202. package/src/memory/graph/extraction.ts +1 -5
  203. package/src/memory/graph/graph-search.ts +7 -1
  204. package/src/memory/indexer.ts +28 -18
  205. package/src/memory/job-handlers/cleanup.ts +76 -18
  206. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  207. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  208. package/src/memory/jobs-store.ts +14 -0
  209. package/src/memory/jobs-worker.ts +55 -22
  210. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  211. package/src/memory/llm-request-log-source-local.ts +7 -0
  212. package/src/memory/llm-request-log-source.ts +9 -2
  213. package/src/memory/llm-request-log-store.ts +43 -1
  214. package/src/memory/llm-usage-store.ts +24 -0
  215. package/src/memory/memory-retrospective-enqueue.ts +8 -1
  216. package/src/memory/memory-retrospective-job.ts +5 -0
  217. package/src/memory/memory-v2-activation-log-store.ts +15 -6
  218. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  219. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  220. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  221. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  222. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  223. package/src/memory/migrations/index.ts +17 -0
  224. package/src/memory/migrations/registry.ts +33 -0
  225. package/src/memory/schema/conversations.ts +1 -1
  226. package/src/memory/schema/infrastructure.ts +21 -0
  227. package/src/memory/tool-usage-store.ts +36 -8
  228. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  229. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  230. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  231. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  232. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  233. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  234. package/src/memory/v2/__tests__/injection.test.ts +127 -98
  235. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  236. package/src/memory/v2/__tests__/router.test.ts +171 -3
  237. package/src/memory/v2/harness/compare.ts +57 -0
  238. package/src/memory/v2/harness/metrics.ts +124 -0
  239. package/src/memory/v2/harness/oracle.ts +145 -0
  240. package/src/memory/v2/harness/replay-input.ts +224 -0
  241. package/src/memory/v2/harness/retriever.ts +74 -0
  242. package/src/memory/v2/harness/router-retriever.ts +43 -0
  243. package/src/memory/v2/harness/runner.ts +106 -0
  244. package/src/memory/v2/harness/trace.ts +58 -0
  245. package/src/memory/v2/injection.ts +21 -15
  246. package/src/memory/v2/prompts/router.ts +26 -1
  247. package/src/memory/v2/qdrant.ts +14 -2
  248. package/src/memory/v2/router.ts +171 -18
  249. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  250. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  251. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  252. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  253. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  254. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  255. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  256. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  257. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  258. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  259. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  260. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  261. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  262. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  263. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  264. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  265. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  266. package/src/memory/v3/auto-edges.ts +223 -0
  267. package/src/memory/v3/coactivation-store.ts +124 -0
  268. package/src/memory/v3/consolidation-job.ts +323 -0
  269. package/src/memory/v3/edge-learning-job.ts +160 -0
  270. package/src/memory/v3/edges.ts +249 -0
  271. package/src/memory/v3/filter.ts +281 -0
  272. package/src/memory/v3/gate.ts +334 -0
  273. package/src/memory/v3/index-composition.ts +113 -0
  274. package/src/memory/v3/llm-capture.ts +46 -0
  275. package/src/memory/v3/loop.ts +382 -0
  276. package/src/memory/v3/maintenance.ts +144 -0
  277. package/src/memory/v3/prompt-context.ts +33 -0
  278. package/src/memory/v3/prompts/consolidation.ts +458 -0
  279. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  280. package/src/memory/v3/retriever.ts +33 -0
  281. package/src/memory/v3/scouts.ts +420 -0
  282. package/src/memory/v3/shadow-middleware.ts +305 -0
  283. package/src/memory/v3/traversal.ts +206 -0
  284. package/src/memory/v3/tree-index.ts +237 -0
  285. package/src/memory/v3/tree-store.ts +394 -0
  286. package/src/memory/v3/tree-walk.ts +351 -0
  287. package/src/memory/v3/types.ts +65 -0
  288. package/src/memory/v3/validate.ts +300 -0
  289. package/src/notifications/adapters/macos.ts +18 -1
  290. package/src/notifications/adapters/platform.ts +1 -1
  291. package/src/notifications/decision-engine.ts +1 -4
  292. package/src/notifications/emit-signal.ts +29 -49
  293. package/src/permissions/prompter.ts +3 -3
  294. package/src/permissions/question-prompter.ts +5 -2
  295. package/src/permissions/secret-prompter.ts +2 -2
  296. package/src/plugin-api/index.ts +4 -0
  297. package/src/plugin-api/types.ts +7 -33
  298. package/src/plugins/defaults/index.ts +6 -0
  299. package/src/plugins/defaults/injectors.ts +18 -11
  300. package/src/plugins/external-plugin-loader.ts +5 -68
  301. package/src/plugins/types.ts +11 -16
  302. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  303. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  304. package/src/prompts/persona-resolver.ts +36 -21
  305. package/src/prompts/sections.ts +39 -7
  306. package/src/prompts/system-prompt.ts +50 -185
  307. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  308. package/src/prompts/templates/system-sections.ts +230 -8
  309. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  310. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  311. package/src/providers/anthropic/client.ts +32 -66
  312. package/src/providers/call-site-routing.ts +14 -2
  313. package/src/providers/connection-model-compat.ts +38 -0
  314. package/src/providers/connection-resolution.ts +16 -2
  315. package/src/providers/gemini/client.ts +49 -6
  316. package/src/providers/inference/adapter-factory.ts +3 -0
  317. package/src/providers/minimax/client.ts +106 -0
  318. package/src/providers/model-catalog.ts +43 -0
  319. package/src/providers/model-intents.ts +1 -1
  320. package/src/providers/openai/chat-completions-provider.ts +6 -3
  321. package/src/providers/openai/codex-models.ts +18 -0
  322. package/src/providers/openai/responses-provider.ts +78 -21
  323. package/src/providers/provider-send-message.ts +7 -1
  324. package/src/providers/retry.ts +34 -3
  325. package/src/providers/thinking-config.ts +26 -1
  326. package/src/providers/usage-tracking.ts +2 -0
  327. package/src/runtime/AGENTS.md +2 -2
  328. package/src/runtime/agent-wake.ts +1 -0
  329. package/src/runtime/assistant-event-hub.ts +76 -6
  330. package/src/runtime/auth/route-policy.ts +36 -0
  331. package/src/runtime/btw-sidechain.ts +0 -6
  332. package/src/runtime/http-types.ts +0 -2
  333. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  334. package/src/runtime/pending-interactions.ts +0 -1
  335. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
  336. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
  337. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  338. package/src/runtime/routes/acp-routes.test.ts +255 -6
  339. package/src/runtime/routes/acp-routes.ts +8 -1
  340. package/src/runtime/routes/avatar-routes.ts +10 -10
  341. package/src/runtime/routes/background-wake-routes.ts +188 -0
  342. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  343. package/src/runtime/routes/btw-routes.ts +0 -6
  344. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  345. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  346. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  347. package/src/runtime/routes/conversation-query-routes.ts +142 -36
  348. package/src/runtime/routes/conversation-routes.ts +252 -410
  349. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  350. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  351. package/src/runtime/routes/domain-routes.ts +60 -10
  352. package/src/runtime/routes/email-routes.ts +5 -2
  353. package/src/runtime/routes/events-routes.ts +54 -10
  354. package/src/runtime/routes/group-routes.ts +24 -8
  355. package/src/runtime/routes/host-browser-routes.ts +10 -2
  356. package/src/runtime/routes/host-cu-routes.ts +2 -2
  357. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  358. package/src/runtime/routes/index.ts +8 -0
  359. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  360. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  361. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  362. package/src/runtime/routes/memory-item-routes.ts +8 -3
  363. package/src/runtime/routes/memory-v2-routes.ts +215 -5
  364. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  365. package/src/runtime/routes/migration-routes.ts +21 -24
  366. package/src/runtime/routes/plugins-routes.ts +337 -0
  367. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  368. package/src/runtime/routes/secret-routes.ts +25 -5
  369. package/src/runtime/routes/settings-routes.ts +12 -11
  370. package/src/runtime/routes/slack-channel-routes.ts +5 -4
  371. package/src/runtime/routes/workspace-routes.ts +25 -10
  372. package/src/runtime/sync/resource-sync-events.ts +106 -38
  373. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  374. package/src/runtime/sync/sync-publisher.ts +2 -1
  375. package/src/runtime/verification-outbound-actions.ts +73 -1
  376. package/src/telemetry/types.ts +12 -0
  377. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  378. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  379. package/src/tools/acp/spawn.test.ts +119 -0
  380. package/src/tools/acp/spawn.ts +15 -2
  381. package/src/tools/apps/definitions.ts +2 -8
  382. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  383. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  384. package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
  385. package/src/tools/browser/browser-execution.ts +16 -3
  386. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  387. package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
  388. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
  389. package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
  390. package/src/tools/browser/cdp-client/factory.ts +100 -17
  391. package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
  392. package/src/tools/browser/cdp-client/types.ts +65 -0
  393. package/src/tools/browser/pinned-tabs.ts +96 -40
  394. package/src/tools/computer-use/definitions.ts +22 -78
  395. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  396. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  397. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  398. package/src/tools/credentials/vault.ts +3 -9
  399. package/src/tools/document/document-tool.ts +59 -0
  400. package/src/tools/execution-target.ts +21 -23
  401. package/src/tools/executor.ts +6 -1
  402. package/src/tools/filesystem/edit.ts +3 -9
  403. package/src/tools/filesystem/list.ts +3 -9
  404. package/src/tools/filesystem/read.ts +3 -9
  405. package/src/tools/filesystem/write.ts +3 -9
  406. package/src/tools/host-filesystem/edit.ts +3 -9
  407. package/src/tools/host-filesystem/read.ts +3 -9
  408. package/src/tools/host-filesystem/transfer.ts +3 -9
  409. package/src/tools/host-filesystem/write.ts +3 -9
  410. package/src/tools/host-terminal/host-shell.ts +3 -9
  411. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  412. package/src/tools/memory/register.test.ts +1 -1
  413. package/src/tools/memory/register.ts +4 -9
  414. package/src/tools/network/web-fetch.ts +3 -9
  415. package/src/tools/network/web-search.ts +25 -32
  416. package/src/tools/registry.ts +7 -23
  417. package/src/tools/schema-transforms.ts +1 -1
  418. package/src/tools/skills/execute.ts +3 -9
  419. package/src/tools/skills/load.ts +3 -9
  420. package/src/tools/skills/skill-tool-factory.ts +1 -8
  421. package/src/tools/subagent/notify-parent.ts +3 -9
  422. package/src/tools/system/request-permission.ts +3 -9
  423. package/src/tools/terminal/shell.ts +3 -9
  424. package/src/tools/tool-defaults.ts +94 -0
  425. package/src/tools/types.ts +27 -98
  426. package/src/tools/ui-surface/definitions.ts +6 -22
  427. package/src/usage/pricing.ts +23 -0
  428. package/src/usage/types.ts +12 -0
  429. package/src/util/logger.ts +16 -7
  430. package/src/util/platform.ts +7 -2
  431. package/src/util/sqlite3-runtime.ts +65 -0
  432. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  433. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  434. package/src/workspace/migrations/registry.ts +2 -0
  435. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  436. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  437. package/src/daemon/query-complexity-router.ts +0 -75
  438. package/src/prompts/cache-boundary.ts +0 -8
@@ -2,13 +2,12 @@ import { z } from "zod";
2
2
 
3
3
  import { QuestionPrompter } from "../../permissions/question-prompter.js";
4
4
  import { RiskLevel } from "../../permissions/types.js";
5
- import type { ToolDefinition } from "../../providers/types.js";
6
5
  import { broadcastMessage } from "../../runtime/assistant-event-hub.js";
7
6
  import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
8
7
 
9
8
  // ── Input schema ────────────────────────────────────────────────────
10
9
  // Runtime validation lives in Zod; the wire-level definition surfaced
11
- // to the LLM is the hand-written JSON Schema in getDefinition() below.
10
+ // to the LLM is the hand-written JSON Schema in `input_schema` below.
12
11
  // (The codebase does not currently use zod-to-json-schema for tool defs,
13
12
  // so the two are kept in sync manually.)
14
13
 
@@ -109,53 +108,37 @@ const DESCRIPTION = [
109
108
  "context shown beneath the label.",
110
109
  ].join("\n");
111
110
 
111
+ // Shared option-schema fragment used by both the batched `questions[]`
112
+ // shape and the legacy flat `options` field.
113
+ const OPTION_ITEMS_SCHEMA = {
114
+ type: "object",
115
+ properties: {
116
+ id: {
117
+ type: "string",
118
+ description:
119
+ "Stable identifier for this option (returned verbatim in the response).",
120
+ },
121
+ label: {
122
+ type: "string",
123
+ description: "Short human-readable label.",
124
+ },
125
+ description: {
126
+ type: "string",
127
+ description: "Optional one-line context shown beneath the label.",
128
+ },
129
+ },
130
+ required: ["id", "label"],
131
+ } as const;
132
+
112
133
  // ── Tool ────────────────────────────────────────────────────────────
113
134
 
114
135
  export class AskQuestionTool implements Tool {
115
136
  name = "ask_question";
116
137
  description = DESCRIPTION;
117
138
  category = "interaction";
139
+ executionTarget = "sandbox" as const;
118
140
  defaultRiskLevel = RiskLevel.Low;
119
-
120
- // Override hook for tests: lets a test replace the prompter factory
121
- // without monkey-patching the module. Default factory wires the real
122
- // broadcastMessage so the question reaches every connected client.
123
- private prompterFactory: () => Pick<QuestionPrompter, "prompt">;
124
-
125
- constructor(
126
- prompterFactory: () => Pick<QuestionPrompter, "prompt"> = () =>
127
- new QuestionPrompter({ broadcastMessage }),
128
- ) {
129
- this.prompterFactory = prompterFactory;
130
- }
131
-
132
- getDefinition(): ToolDefinition {
133
- // Shared option-schema fragment used by both the batched `questions[]`
134
- // shape and the legacy flat `options` field.
135
- const optionItemsSchema = {
136
- type: "object",
137
- properties: {
138
- id: {
139
- type: "string",
140
- description:
141
- "Stable identifier for this option (returned verbatim in the response).",
142
- },
143
- label: {
144
- type: "string",
145
- description: "Short human-readable label.",
146
- },
147
- description: {
148
- type: "string",
149
- description: "Optional one-line context shown beneath the label.",
150
- },
151
- },
152
- required: ["id", "label"],
153
- } as const;
154
-
155
- return {
156
- name: this.name,
157
- description: this.description,
158
- input_schema: {
141
+ input_schema = {
159
142
  type: "object",
160
143
  properties: {
161
144
  // ── Recommended shape ─────────────────────────────────────
@@ -182,7 +165,7 @@ export class AskQuestionTool implements Tool {
182
165
  maxItems: 4,
183
166
  description:
184
167
  "2–4 structured options. The UI always appends a free-text fallback slot, so do not include a 'something else' option here.",
185
- items: optionItemsSchema,
168
+ items: OPTION_ITEMS_SCHEMA,
186
169
  },
187
170
  freeTextPlaceholder: {
188
171
  type: "string",
@@ -212,7 +195,7 @@ export class AskQuestionTool implements Tool {
212
195
  maxItems: 4,
213
196
  description:
214
197
  "Legacy: 2–4 structured options. Prefer `questions[].options`. The UI always appends a free-text fallback slot, so do not include a 'something else' option here.",
215
- items: optionItemsSchema,
198
+ items: OPTION_ITEMS_SCHEMA,
216
199
  },
217
200
  freeTextPlaceholder: {
218
201
  type: "string",
@@ -222,8 +205,18 @@ export class AskQuestionTool implements Tool {
222
205
  },
223
206
  // No top-level `required` — caller must supply either `questions`
224
207
  // or the legacy flat trio (`question` + `options`). Enforced in Zod.
225
- },
226
208
  };
209
+
210
+ // Override hook for tests: lets a test replace the prompter factory
211
+ // without monkey-patching the module. Default factory wires the real
212
+ // broadcastMessage so the question reaches every connected client.
213
+ private prompterFactory: () => Pick<QuestionPrompter, "prompt">;
214
+
215
+ constructor(
216
+ prompterFactory: () => Pick<QuestionPrompter, "prompt"> = () =>
217
+ new QuestionPrompter({ broadcastMessage }),
218
+ ) {
219
+ this.prompterFactory = prompterFactory;
227
220
  }
228
221
 
229
222
  async execute(
@@ -77,4 +77,74 @@ describe("pinned-tabs", () => {
77
77
  setPinnedTab("conv-a", "");
78
78
  expect(getPinnedTab("conv-a")).toBeUndefined();
79
79
  });
80
+
81
+ describe("cross-client isolation (#31361)", () => {
82
+ test("same tabId on different clients are stored independently", () => {
83
+ setPinnedTab("conv-a", "99", "clientA");
84
+ setPinnedTab("conv-a", "99", "clientB");
85
+ expect(getPinnedTab("conv-a", "clientA")).toBe("99");
86
+ expect(getPinnedTab("conv-a", "clientB")).toBe("99");
87
+ });
88
+
89
+ test("clearPinnedTabByTabId with clientId only clears that client", () => {
90
+ setPinnedTab("conv-a", "99", "clientA");
91
+ setPinnedTab("conv-b", "99", "clientB");
92
+
93
+ const cleared = clearPinnedTabByTabId("99", "clientA");
94
+
95
+ expect(cleared).toBe(1);
96
+ expect(getPinnedTab("conv-a", "clientA")).toBeUndefined();
97
+ expect(getPinnedTab("conv-b", "clientB")).toBe("99"); // clientB's pin survives
98
+ });
99
+
100
+ test("getPinnedTab with matching clientId returns correct tabId", () => {
101
+ setPinnedTab("conv-x", "42", "clientA");
102
+ setPinnedTab("conv-x", "77", "clientB");
103
+
104
+ expect(getPinnedTab("conv-x", "clientA")).toBe("42");
105
+ expect(getPinnedTab("conv-x", "clientB")).toBe("77");
106
+ });
107
+
108
+ test("clearPinnedTabByTabId without clientId clears all matching entries (backward compat)", () => {
109
+ setPinnedTab("conv-a", "99", "clientA");
110
+ setPinnedTab("conv-b", "99", "clientB");
111
+
112
+ const cleared = clearPinnedTabByTabId("99");
113
+
114
+ expect(cleared).toBe(2);
115
+ expect(getPinnedTab("conv-a", "clientA")).toBeUndefined();
116
+ expect(getPinnedTab("conv-b", "clientB")).toBeUndefined();
117
+ });
118
+
119
+ test("clearPinnedTab with clientId only removes that client slot", () => {
120
+ setPinnedTab("conv-a", "42", "clientA");
121
+ setPinnedTab("conv-a", "77", "clientB");
122
+
123
+ clearPinnedTab("conv-a", "clientA");
124
+
125
+ expect(getPinnedTab("conv-a", "clientA")).toBeUndefined();
126
+ expect(getPinnedTab("conv-a", "clientB")).toBe("77");
127
+ });
128
+
129
+ test("clearPinnedTab without clientId removes all slots", () => {
130
+ setPinnedTab("conv-a", "42", "clientA");
131
+ setPinnedTab("conv-a", "77", "clientB");
132
+
133
+ clearPinnedTab("conv-a");
134
+
135
+ expect(getPinnedTab("conv-a", "clientA")).toBeUndefined();
136
+ expect(getPinnedTab("conv-a", "clientB")).toBeUndefined();
137
+ });
138
+
139
+ test("getPinnedTab with unknown clientId falls back to __default__ slot", () => {
140
+ setPinnedTab("conv-a", "42"); // stored in __default__ slot
141
+ expect(getPinnedTab("conv-a", "clientA")).toBe("42");
142
+ });
143
+
144
+ test("getPinnedTab without clientId returns first entry", () => {
145
+ setPinnedTab("conv-a", "42", "clientA");
146
+ const result = getPinnedTab("conv-a");
147
+ expect(result).toBe("42");
148
+ });
149
+ });
80
150
  });
@@ -481,6 +481,15 @@ function wrapWithKindMemo(
481
481
  setCdpSessionId(cdpSessionId: string): void {
482
482
  inner.setCdpSessionId?.(cdpSessionId);
483
483
  },
484
+ listTabs() {
485
+ return inner.listTabs();
486
+ },
487
+ selectTab(tabId: number) {
488
+ return inner.selectTab(tabId);
489
+ },
490
+ closeTab(tabId: number) {
491
+ return inner.closeTab(tabId);
492
+ },
484
493
  };
485
494
  }
486
495
 
@@ -644,7 +653,7 @@ export async function executeBrowserNavigate(
644
653
  const newTab = input.new_tab === true;
645
654
  if (newTab && cdp.kind === "extension") {
646
655
  try {
647
- const result = await cdp.send<{ tabId?: number | string }>(
656
+ const result = await cdp.send<{ tabId?: number | string; clientId?: string }>(
648
657
  "Vellum.createTab",
649
658
  {},
650
659
  context.signal,
@@ -655,6 +664,10 @@ export async function executeBrowserNavigate(
655
664
  : typeof result?.tabId === "string"
656
665
  ? result.tabId
657
666
  : undefined;
667
+ const clientId =
668
+ typeof result?.clientId === "string" && result.clientId.length > 0
669
+ ? result.clientId
670
+ : undefined;
658
671
  if (!tabId) {
659
672
  // Malformed createTab response (no tabId). We're nominally falling
660
673
  // back to active-tab routing — but the live `cdp` instance was
@@ -676,9 +689,9 @@ export async function executeBrowserNavigate(
676
689
  );
677
690
  } else {
678
691
  cdp.setCdpSessionId?.(tabId);
679
- setPinnedTab(context.conversationId, tabId);
692
+ setPinnedTab(context.conversationId, tabId, clientId);
680
693
  log.debug(
681
- { conversationId: context.conversationId, tabId },
694
+ { conversationId: context.conversationId, tabId, clientId },
682
695
  "Opened new tab via --new-tab; pinned subsequent ops to it",
683
696
  );
684
697
  }
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Tests that listTabs, selectTab, and closeTab are properly forwarded through
3
+ * the real buildChainedClient wrapper.
4
+ *
5
+ * Per the test-flat-mocks-trap rule, these tests use the REAL buildChainedClient
6
+ * function with a mock candidate — NOT a flat fake that has the methods defined
7
+ * directly on the scoped client.
8
+ */
9
+
10
+ import { describe, expect, mock, test } from "bun:test";
11
+
12
+ import type { BrowserBackend, CdpCommand, CdpResult } from "../../../../browser-session/types.js";
13
+ import { CdpError } from "../errors.js";
14
+ import type { BackendCandidate, CdpClient, TabInfo } from "../types.js";
15
+
16
+ // Import buildChainedClient directly. Since this test file does not mock
17
+ // any of factory.ts's imports, we import directly.
18
+ const { buildChainedClient } = await import("../factory.js");
19
+
20
+ // ── Helpers ───────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Create a BrowserBackend that delegates CDP sends through a CdpClient.
24
+ * This is the real wiring that factory.ts uses — the backend wraps the
25
+ * client's send method via dispatchThroughClient.
26
+ */
27
+ function makeBackendFromClient(client: CdpClient, kind: "extension" | "local" | "cdp-inspect"): BrowserBackend {
28
+ return {
29
+ kind,
30
+ isAvailable: () => true,
31
+ send: async (command: CdpCommand, signal?: AbortSignal): Promise<CdpResult> => {
32
+ try {
33
+ const result = await client.send(command.method, command.params, signal);
34
+ return { result };
35
+ } catch (err) {
36
+ if (err instanceof CdpError) {
37
+ return {
38
+ error: { code: -1, message: err.message, data: err },
39
+ };
40
+ }
41
+ throw err;
42
+ }
43
+ },
44
+ dispose: () => client.dispose(),
45
+ };
46
+ }
47
+
48
+ interface FakeClientWithTabMethods extends CdpClient {
49
+ kind: "extension";
50
+ conversationId: string;
51
+ listTabsMock: ReturnType<typeof mock>;
52
+ selectTabMock: ReturnType<typeof mock>;
53
+ closeTabMock: ReturnType<typeof mock>;
54
+ }
55
+
56
+ function makeFakeExtensionClientWithTabMethods(
57
+ conversationId: string,
58
+ ): FakeClientWithTabMethods {
59
+ const listTabsMock = mock(async (): Promise<TabInfo[]> => [
60
+ {
61
+ tabId: 1,
62
+ windowId: 10,
63
+ url: "https://example.com",
64
+ title: "Example",
65
+ active: true,
66
+ pinned: false,
67
+ },
68
+ ]);
69
+ const selectTabMock = mock(async (_tabId: number) => ({
70
+ tabId: 42,
71
+ windowId: 10,
72
+ url: "https://example.com",
73
+ title: "Example",
74
+ clientId: "clientA",
75
+ }));
76
+ const closeTabMock = mock(async (_tabId: number) => ({
77
+ closed: true as const,
78
+ tabId: 99,
79
+ }));
80
+
81
+ return {
82
+ kind: "extension",
83
+ conversationId,
84
+ listTabsMock,
85
+ selectTabMock,
86
+ closeTabMock,
87
+ send: mock(async () => ({ ok: true })) as unknown as CdpClient["send"],
88
+ dispose: mock(() => {}),
89
+ listTabs: listTabsMock,
90
+ selectTab: selectTabMock,
91
+ closeTab: closeTabMock,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Build a BackendCandidate that wraps a fake extension client with tab methods.
97
+ */
98
+ function makeCandidateFromClient(
99
+ conversationId: string,
100
+ fakeClient: CdpClient,
101
+ kind: "extension" | "local" | "cdp-inspect",
102
+ ): BackendCandidate {
103
+ return {
104
+ kind,
105
+ reason: `test ${kind} client`,
106
+ create() {
107
+ return {
108
+ client: fakeClient,
109
+ backend: makeBackendFromClient(fakeClient, kind),
110
+ };
111
+ },
112
+ };
113
+ }
114
+
115
+ // ── Helpers for fresh-client (pre-sticky) tests ───────────────────────
116
+
117
+ /**
118
+ * A fake extension client whose `send` method returns Vellum.* pseudo-responses
119
+ * so that fresh-client tab calls can succeed without a prior `send("Runtime.*")`.
120
+ * This simulates how the real ExtensionCdpClient handles Vellum.listTabs etc.
121
+ * through the dispatcher before chrome.debugger.sendCommand.
122
+ */
123
+ function makeFakeExtensionClientForFreshPath(conversationId: string) {
124
+ return {
125
+ kind: "extension" as const,
126
+ conversationId,
127
+ send: mock(
128
+ async (method: string, params?: Record<string, unknown>): Promise<unknown> => {
129
+ if (method === "Vellum.listTabs") {
130
+ return {
131
+ tabs: [
132
+ {
133
+ tabId: 1,
134
+ windowId: 10,
135
+ url: "https://example.com",
136
+ title: "Example",
137
+ active: true,
138
+ pinned: false,
139
+ },
140
+ ],
141
+ };
142
+ }
143
+ if (method === "Vellum.selectTab") {
144
+ return {
145
+ tabId: params?.tabId,
146
+ windowId: 10,
147
+ url: "https://example.com",
148
+ title: "Example",
149
+ clientId: "clientA",
150
+ };
151
+ }
152
+ if (method === "Vellum.closeTab") {
153
+ return { closed: true, tabId: params?.tabId };
154
+ }
155
+ return {};
156
+ },
157
+ ) as unknown as CdpClient["send"],
158
+ dispose: mock(() => {}),
159
+ // These must be present so active?.client.listTabs etc. check passes
160
+ listTabs: mock(async (): Promise<TabInfo[]> => []),
161
+ selectTab: mock(async (_tabId: number) => ({
162
+ tabId: _tabId,
163
+ windowId: 10,
164
+ url: "https://example.com",
165
+ title: "Example",
166
+ clientId: "clientA",
167
+ })),
168
+ closeTab: mock(async (_tabId: number) => ({
169
+ closed: true as const,
170
+ tabId: _tabId,
171
+ })),
172
+ };
173
+ }
174
+
175
+ // ── Tests ─────────────────────────────────────────────────────────────
176
+
177
+ describe("buildChainedClient — tab management methods", () => {
178
+ test("listTabs is forwarded to the underlying client after backend becomes sticky", async () => {
179
+ const conversationId = "conv-tabs-list";
180
+ const fakeClient = makeFakeExtensionClientWithTabMethods(conversationId);
181
+ const candidate = makeCandidateFromClient(conversationId, fakeClient, "extension");
182
+
183
+ const scoped = buildChainedClient(conversationId, [candidate]);
184
+
185
+ // Establish sticky by sending a command first
186
+ await scoped.send("Runtime.evaluate", {});
187
+
188
+ const tabs = await scoped.listTabs();
189
+ expect(tabs).toHaveLength(1);
190
+ expect(tabs[0].tabId).toBe(1);
191
+ expect(tabs[0].active).toBe(true);
192
+ expect(fakeClient.listTabsMock).toHaveBeenCalledTimes(1);
193
+
194
+ scoped.dispose();
195
+ });
196
+
197
+ test("selectTab is forwarded to the underlying client", async () => {
198
+ const conversationId = "conv-tabs-select";
199
+ const fakeClient = makeFakeExtensionClientWithTabMethods(conversationId);
200
+ const candidate = makeCandidateFromClient(conversationId, fakeClient, "extension");
201
+
202
+ const scoped = buildChainedClient(conversationId, [candidate]);
203
+
204
+ // Establish sticky
205
+ await scoped.send("Runtime.evaluate", {});
206
+
207
+ const result = await scoped.selectTab(42);
208
+ expect(result.tabId).toBe(42);
209
+ expect(result.clientId).toBe("clientA");
210
+ expect(fakeClient.selectTabMock).toHaveBeenCalledWith(42);
211
+
212
+ scoped.dispose();
213
+ });
214
+
215
+ test("closeTab is forwarded to the underlying client", async () => {
216
+ const conversationId = "conv-tabs-close";
217
+ const fakeClient = makeFakeExtensionClientWithTabMethods(conversationId);
218
+ const candidate = makeCandidateFromClient(conversationId, fakeClient, "extension");
219
+
220
+ const scoped = buildChainedClient(conversationId, [candidate]);
221
+
222
+ // Establish sticky
223
+ await scoped.send("Runtime.evaluate", {});
224
+
225
+ const result = await scoped.closeTab(99);
226
+ expect(result.closed).toBe(true);
227
+ expect(result.tabId).toBe(99);
228
+ expect(fakeClient.closeTabMock).toHaveBeenCalledWith(99);
229
+
230
+ scoped.dispose();
231
+ });
232
+
233
+ test("listTabs throws transport_error when backend does not support it", async () => {
234
+ const conversationId = "conv-tabs-no-list";
235
+ // A local client without listTabs/selectTab/closeTab
236
+ const noTabsClient: CdpClient & {
237
+ kind: "local";
238
+ conversationId: string;
239
+ } = {
240
+ kind: "local",
241
+ conversationId,
242
+ send: mock(async () => ({})) as unknown as CdpClient["send"],
243
+ dispose: mock(() => {}),
244
+ };
245
+ const candidate = makeCandidateFromClient(
246
+ conversationId,
247
+ noTabsClient,
248
+ "local",
249
+ );
250
+
251
+ const scoped = buildChainedClient(conversationId, [candidate]);
252
+
253
+ // Establish sticky
254
+ await scoped.send("Runtime.evaluate", {});
255
+
256
+ let caught: unknown;
257
+ try {
258
+ await scoped.listTabs();
259
+ } catch (err) {
260
+ caught = err;
261
+ }
262
+ expect(caught).toBeInstanceOf(CdpError);
263
+ expect((caught as CdpError).code).toBe("transport_error");
264
+
265
+ scoped.dispose();
266
+ });
267
+
268
+ test("selectTab throws transport_error when backend does not support it", async () => {
269
+ const conversationId = "conv-tabs-no-select";
270
+ const noTabsClient: CdpClient & {
271
+ kind: "local";
272
+ conversationId: string;
273
+ } = {
274
+ kind: "local",
275
+ conversationId,
276
+ send: mock(async () => ({})) as unknown as CdpClient["send"],
277
+ dispose: mock(() => {}),
278
+ };
279
+ const candidate = makeCandidateFromClient(
280
+ conversationId,
281
+ noTabsClient,
282
+ "local",
283
+ );
284
+
285
+ const scoped = buildChainedClient(conversationId, [candidate]);
286
+
287
+ // Establish sticky
288
+ await scoped.send("Runtime.evaluate", {});
289
+
290
+ let caught: unknown;
291
+ try {
292
+ await scoped.selectTab(42);
293
+ } catch (err) {
294
+ caught = err;
295
+ }
296
+ expect(caught).toBeInstanceOf(CdpError);
297
+ expect((caught as CdpError).code).toBe("transport_error");
298
+
299
+ scoped.dispose();
300
+ });
301
+
302
+ test("closeTab throws transport_error when backend does not support it", async () => {
303
+ const conversationId = "conv-tabs-no-close";
304
+ const noTabsClient: CdpClient & {
305
+ kind: "local";
306
+ conversationId: string;
307
+ } = {
308
+ kind: "local",
309
+ conversationId,
310
+ send: mock(async () => ({})) as unknown as CdpClient["send"],
311
+ dispose: mock(() => {}),
312
+ };
313
+ const candidate = makeCandidateFromClient(
314
+ conversationId,
315
+ noTabsClient,
316
+ "local",
317
+ );
318
+
319
+ const scoped = buildChainedClient(conversationId, [candidate]);
320
+
321
+ // Establish sticky
322
+ await scoped.send("Runtime.evaluate", {});
323
+
324
+ let caught: unknown;
325
+ try {
326
+ await scoped.closeTab(77);
327
+ } catch (err) {
328
+ caught = err;
329
+ }
330
+ expect(caught).toBeInstanceOf(CdpError);
331
+ expect((caught as CdpError).code).toBe("transport_error");
332
+
333
+ scoped.dispose();
334
+ });
335
+ });
336
+
337
+ describe("buildChainedClient — fresh-client tab calls (no prior send)", () => {
338
+ test("listTabs on a fresh client triggers failover walk and returns tabs", async () => {
339
+ const conversationId = "conv-fresh-list";
340
+ const fakeClient = makeFakeExtensionClientForFreshPath(conversationId);
341
+ const candidate = makeCandidateFromClient(conversationId, fakeClient, "extension");
342
+ const scoped = buildChainedClient(conversationId, [candidate]);
343
+
344
+ // Call listTabs WITHOUT establishing sticky — this is the production scenario
345
+ const tabs = await scoped.listTabs();
346
+ expect(tabs).toHaveLength(1);
347
+ expect(tabs[0].tabId).toBe(1);
348
+ expect(tabs[0].active).toBe(true);
349
+
350
+ scoped.dispose();
351
+ });
352
+
353
+ test("selectTab on a fresh client triggers failover walk and returns tab info", async () => {
354
+ const conversationId = "conv-fresh-select";
355
+ const fakeClient = makeFakeExtensionClientForFreshPath(conversationId);
356
+ const candidate = makeCandidateFromClient(conversationId, fakeClient, "extension");
357
+ const scoped = buildChainedClient(conversationId, [candidate]);
358
+
359
+ const result = await scoped.selectTab(42);
360
+ expect(result.tabId).toBe(42);
361
+ expect(result.clientId).toBe("clientA");
362
+
363
+ scoped.dispose();
364
+ });
365
+
366
+ test("closeTab on a fresh client triggers failover walk and returns closed status", async () => {
367
+ const conversationId = "conv-fresh-close";
368
+ const fakeClient = makeFakeExtensionClientForFreshPath(conversationId);
369
+ const candidate = makeCandidateFromClient(conversationId, fakeClient, "extension");
370
+ const scoped = buildChainedClient(conversationId, [candidate]);
371
+
372
+ const result = await scoped.closeTab(99);
373
+ expect(result.closed).toBe(true);
374
+ expect(result.tabId).toBe(99);
375
+
376
+ scoped.dispose();
377
+ });
378
+
379
+ test("listTabs on fresh non-extension client throws transport_error after sticky established", async () => {
380
+ const conversationId = "conv-fresh-no-list";
381
+ const noTabsClient: CdpClient & { kind: "local"; conversationId: string } = {
382
+ kind: "local",
383
+ conversationId,
384
+ send: mock(async () => ({})) as unknown as CdpClient["send"],
385
+ dispose: mock(() => {}),
386
+ };
387
+ const candidate = makeCandidateFromClient(conversationId, noTabsClient, "local");
388
+ const scoped = buildChainedClient(conversationId, [candidate]);
389
+
390
+ // No prior send — fresh client, non-extension backend
391
+ let caught: unknown;
392
+ try {
393
+ await scoped.listTabs();
394
+ } catch (err) {
395
+ caught = err;
396
+ }
397
+ expect(caught).toBeInstanceOf(CdpError);
398
+ expect((caught as CdpError).code).toBe("transport_error");
399
+
400
+ scoped.dispose();
401
+ });
402
+ });
@@ -89,6 +89,9 @@ describe("cdp-client re-exports", () => {
89
89
  },
90
90
  dispose: () => {},
91
91
  setCdpSessionId: () => {},
92
+ listTabs: async () => [],
93
+ selectTab: async (_tabId: number) => ({ tabId: _tabId }),
94
+ closeTab: async (_tabId: number) => ({ closed: true as const, tabId: _tabId }),
92
95
  };
93
96
  expect(scoped.kind).toBe(kind);
94
97
  expect(scoped.conversationId).toBe("conv-123");
@@ -710,6 +710,18 @@ export class CdpInspectClient implements ScopedCdpClient {
710
710
  // no-op
711
711
  }
712
712
 
713
+ async listTabs(): Promise<never> {
714
+ throw new CdpError("transport_error", "listTabs is not supported by the cdp-inspect backend (extension backend required)");
715
+ }
716
+
717
+ async selectTab(_tabId: number): Promise<never> {
718
+ throw new CdpError("transport_error", "selectTab is not supported by the cdp-inspect backend (extension backend required)");
719
+ }
720
+
721
+ async closeTab(_tabId: number): Promise<never> {
722
+ throw new CdpError("transport_error", "closeTab is not supported by the cdp-inspect backend (extension backend required)");
723
+ }
724
+
713
725
  dispose(): void {
714
726
  if (this.disposed) return;
715
727
  this.disposed = true;