@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
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Tests for the default `history-repair` plugin's `user-prompt-submit` hook.
2
+ * Tests for the default `history-repair` plugin's hooks.
3
3
  *
4
- * Covers:
4
+ * `user-prompt-submit`:
5
5
  * - The hook normalizes `latestMessages` exactly as `repairHistory` would for
6
6
  * the three documented repair classes (orphan tool_result, missing
7
7
  * tool_result, consecutive same-role messages) and is a no-op for a
@@ -11,6 +11,20 @@
11
11
  * - Chain ordering: because defaults register first, the default repair hook
12
12
  * runs ahead of a later-registered user hook, which therefore observes an
13
13
  * already-normalized history.
14
+ *
15
+ * `post-model-call` (error-recovery):
16
+ * - On a provider rejection carrying a repairable ordering error, the hook
17
+ * deep-repairs the messages, marks the per-conversation bound, and asks the
18
+ * loop to continue.
19
+ * - Bounded to one pass per turn via that bound: a second consecutive ordering
20
+ * rejection is left to surface (the bound is cleared by the `stop` hook, not
21
+ * here).
22
+ * - Ignores non-ordering errors, tool-bearing turns, and finalized (non-error)
23
+ * replies — none of which it ever touches the bound for.
24
+ *
25
+ * `stop` (terminal cleanup):
26
+ * - Clears the repair bound unconditionally on the definitive terminal stop, so
27
+ * the next turn always repairs afresh no matter how the turn ended.
14
28
  */
15
29
 
16
30
  import { beforeEach, describe, expect, test } from "bun:test";
@@ -18,10 +32,22 @@ import { beforeEach, describe, expect, test } from "bun:test";
18
32
  import { HOOKS } from "../plugin-api/constants.js";
19
33
  import type {
20
34
  PluginLogger,
35
+ PostModelCallContext,
36
+ StopContext,
21
37
  UserPromptSubmitContext,
22
38
  } from "../plugin-api/types.js";
39
+ import postModelCall from "../plugins/defaults/history-repair/hooks/post-model-call.js";
40
+ import stop from "../plugins/defaults/history-repair/hooks/stop.js";
23
41
  import userPromptSubmit from "../plugins/defaults/history-repair/hooks/user-prompt-submit.js";
24
- import { repairHistory } from "../plugins/defaults/history-repair/terminal.js";
42
+ import {
43
+ isOrderingRepairAttempted,
44
+ markOrderingRepairAttempted,
45
+ resetRepairStateStoreForTests,
46
+ } from "../plugins/defaults/history-repair/repair-state-store.js";
47
+ import {
48
+ deepRepairHistory,
49
+ repairHistory,
50
+ } from "../plugins/defaults/history-repair/terminal.js";
25
51
  import { defaultHistoryRepairPlugin } from "../plugins/defaults/index.js";
26
52
  import { runHook } from "../plugins/pipeline.js";
27
53
  import {
@@ -30,6 +56,10 @@ import {
30
56
  } from "../plugins/registry.js";
31
57
  import type { Message } from "../providers/types.js";
32
58
 
59
+ /** Provider rejection text matched by `isRepairableOrderingError`. */
60
+ const ORDERING_ERROR_MESSAGE =
61
+ "messages: tool_use ids must have a corresponding tool_result";
62
+
33
63
  const noopLogger: PluginLogger = {
34
64
  info: () => {},
35
65
  warn: () => {},
@@ -42,6 +72,8 @@ function makeCtx(messages: Message[]): UserPromptSubmitContext {
42
72
  conversationId: "conv-test",
43
73
  userMessageId: "msg-test",
44
74
  requestId: "req-test",
75
+ modelProfileKey: null,
76
+ isNonInteractive: false,
45
77
  prompt: "",
46
78
  originalMessages: messages,
47
79
  latestMessages: messages,
@@ -162,3 +194,196 @@ describe("history-repair user-prompt-submit hook — via runHook", () => {
162
194
  expect(observed as Message[] | null).toEqual(expected.messages);
163
195
  });
164
196
  });
197
+
198
+ // ─── post-model-call hook (error-recovery) ───────────────────────────────────
199
+
200
+ /** An ordering-violating history: a leading assistant turn deep-repair strips. */
201
+ function orderingViolatingHistory(): Message[] {
202
+ return [
203
+ { role: "assistant", content: [{ type: "text", text: "orphaned" }] },
204
+ { role: "user", content: [{ type: "text", text: "Hello" }] },
205
+ ];
206
+ }
207
+
208
+ function makePostModelCallCtx(
209
+ overrides: Partial<PostModelCallContext> = {},
210
+ ): PostModelCallContext {
211
+ return {
212
+ conversationId: "conv-pmc",
213
+ callSite: "mainAgent",
214
+ content: [],
215
+ messages: [],
216
+ stopReason: null,
217
+ decision: "stop",
218
+ logger: noopLogger,
219
+ ...overrides,
220
+ };
221
+ }
222
+
223
+ describe("history-repair post-model-call hook — direct", () => {
224
+ beforeEach(() => {
225
+ resetRepairStateStoreForTests();
226
+ });
227
+
228
+ test("repairable ordering error → deep-repairs and continues", async () => {
229
+ // GIVEN a provider rejection carrying a repairable ordering error over a
230
+ // history with a leading assistant turn.
231
+ const messages = orderingViolatingHistory();
232
+ const expected = deepRepairHistory(messages).messages;
233
+ const ctx = makePostModelCallCtx({
234
+ messages,
235
+ error: new Error(ORDERING_ERROR_MESSAGE),
236
+ });
237
+
238
+ // WHEN the hook runs.
239
+ await postModelCall(ctx);
240
+
241
+ // THEN it rewrites the messages to the deep-repaired history and asks the
242
+ // loop to retry.
243
+ expect(ctx.decision).toBe("continue");
244
+ expect(ctx.messages).toEqual(expected);
245
+ expect(isOrderingRepairAttempted(ctx.conversationId)).toBe(true);
246
+ });
247
+
248
+ test("second consecutive ordering rejection is left to surface", async () => {
249
+ // GIVEN a turn whose first ordering rejection already triggered a repair.
250
+ const conversationId = "conv-bounded";
251
+ markOrderingRepairAttempted(conversationId);
252
+ const messages = orderingViolatingHistory();
253
+ const ctx = makePostModelCallCtx({
254
+ conversationId,
255
+ messages,
256
+ error: new Error(ORDERING_ERROR_MESSAGE),
257
+ });
258
+
259
+ // WHEN a second ordering rejection reaches the hook.
260
+ await postModelCall(ctx);
261
+
262
+ // THEN it does not repair again — the error surfaces.
263
+ expect(ctx.decision).toBe("stop");
264
+ expect(ctx.messages).toEqual(messages);
265
+
266
+ // AND the bound stays marked — the hook never clears it; the `stop` hook
267
+ // clears it at the turn boundary.
268
+ expect(isOrderingRepairAttempted(conversationId)).toBe(true);
269
+ });
270
+
271
+ test("a non-ordering error while continuing is a no-op", async () => {
272
+ // GIVEN this turn already repaired an ordering rejection (bound marked),
273
+ // then an earlier hook recovered a different error and set the decision to
274
+ // continue.
275
+ const conversationId = "conv-cross-hook";
276
+ markOrderingRepairAttempted(conversationId);
277
+ const ctx = makePostModelCallCtx({
278
+ conversationId,
279
+ decision: "continue",
280
+ error: new Error("image dimensions exceed max allowed size"),
281
+ });
282
+
283
+ // WHEN the history-repair hook runs after that earlier hook.
284
+ await postModelCall(ctx);
285
+
286
+ // THEN it leaves the in-flight continue and the bound alone — it only acts
287
+ // on a repairable ordering rejection.
288
+ expect(ctx.decision).toBe("continue");
289
+ expect(isOrderingRepairAttempted(conversationId)).toBe(true);
290
+ });
291
+
292
+ test("a mid-turn tool-bearing turn is a no-op", async () => {
293
+ // GIVEN this turn already repaired an ordering rejection (bound marked),
294
+ // then the model returned a tool-bearing turn the loop continues on its
295
+ // own.
296
+ const conversationId = "conv-tool-turn";
297
+ markOrderingRepairAttempted(conversationId);
298
+ const ctx = makePostModelCallCtx({
299
+ conversationId,
300
+ content: [{ type: "tool_use", id: "tu_1", name: "bash", input: {} }],
301
+ });
302
+
303
+ // WHEN the hook runs over the tool-bearing turn.
304
+ await postModelCall(ctx);
305
+
306
+ // THEN it leaves the decision and the bound alone — there is no provider
307
+ // rejection to act on.
308
+ expect(ctx.decision).toBe("stop");
309
+ expect(isOrderingRepairAttempted(conversationId)).toBe(true);
310
+ });
311
+
312
+ test("non-ordering error is left untouched", async () => {
313
+ // GIVEN a provider rejection that is not an ordering violation.
314
+ const messages = orderingViolatingHistory();
315
+ const ctx = makePostModelCallCtx({
316
+ messages,
317
+ error: new Error("rate limit exceeded"),
318
+ });
319
+
320
+ // WHEN the hook runs.
321
+ await postModelCall(ctx);
322
+
323
+ // THEN it defers — the decision and history are unchanged.
324
+ expect(ctx.decision).toBe("stop");
325
+ expect(ctx.messages).toEqual(messages);
326
+ });
327
+
328
+ test("finalized (non-error) reply is ignored", async () => {
329
+ // GIVEN a finalized reply — the model returned content, no error.
330
+ const messages = orderingViolatingHistory();
331
+ const ctx = makePostModelCallCtx({
332
+ messages,
333
+ content: [{ type: "text", text: "done" }],
334
+ stopReason: "end_turn",
335
+ });
336
+
337
+ // WHEN the hook runs.
338
+ await postModelCall(ctx);
339
+
340
+ // THEN the model response is left untouched — repair only applies to
341
+ // ordering rejections.
342
+ expect(ctx.decision).toBe("stop");
343
+ expect(ctx.messages).toEqual(messages);
344
+ });
345
+ });
346
+
347
+ // ─── stop hook (terminal cleanup) ────────────────────────────────────────────
348
+
349
+ function makeStopCtx(overrides: Partial<StopContext> = {}): StopContext {
350
+ return {
351
+ conversationId: "conv-stop",
352
+ messages: [],
353
+ exitReason: "no_tool_calls",
354
+ logger: noopLogger,
355
+ ...overrides,
356
+ };
357
+ }
358
+
359
+ describe("history-repair stop hook — direct", () => {
360
+ beforeEach(() => {
361
+ resetRepairStateStoreForTests();
362
+ });
363
+
364
+ test("a terminal stop clears the repair bound", async () => {
365
+ // GIVEN a turn marked a repair-retry.
366
+ const conversationId = "conv-backstop";
367
+ markOrderingRepairAttempted(conversationId);
368
+
369
+ // WHEN the terminal stop hook runs.
370
+ await stop(makeStopCtx({ conversationId }));
371
+
372
+ // THEN the bound is cleared so the next turn repairs afresh.
373
+ expect(isOrderingRepairAttempted(conversationId)).toBe(false);
374
+ });
375
+
376
+ test("clears the bound regardless of how the turn ended", async () => {
377
+ // GIVEN a turn marked a repair-retry that ends on an abort rather than a
378
+ // finalized reply.
379
+ const conversationId = "conv-abort";
380
+ markOrderingRepairAttempted(conversationId);
381
+
382
+ // WHEN the terminal stop hook runs for that exit.
383
+ await stop(makeStopCtx({ conversationId, exitReason: "aborted_pre_call" }));
384
+
385
+ // THEN the bound is still cleared — `stop` is the definitive terminal, so
386
+ // the next turn always repairs afresh.
387
+ expect(isOrderingRepairAttempted(conversationId)).toBe(false);
388
+ });
389
+ });
@@ -372,6 +372,49 @@ describe("HostAppControlProxy", () => {
372
372
  proxy.dispose();
373
373
  });
374
374
 
375
+ test("non-start tool with mismatched target client is rejected", async () => {
376
+ mockActorMap.set("client-A", "user-1");
377
+ mockActorMap.set("client-B", "user-1");
378
+ const proxy = new HostAppControlProxy("conv-1");
379
+ const startCtrl = new AbortController();
380
+
381
+ const pStart = proxy.request(
382
+ "app_control_start",
383
+ { tool: "start", app: "com.example.editor" },
384
+ "conv-1",
385
+ startCtrl.signal,
386
+ "user-1",
387
+ "client-A",
388
+ );
389
+ proxy.resolve(
390
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
391
+ payload({ pngBase64: PNG_A }),
392
+ );
393
+ await pStart;
394
+ sentMessages.length = 0;
395
+
396
+ const result = await proxy.request(
397
+ "app_control_type",
398
+ {
399
+ tool: "type",
400
+ app: "com.example.editor",
401
+ text: "hello",
402
+ },
403
+ "conv-1",
404
+ new AbortController().signal,
405
+ "user-1",
406
+ "client-B",
407
+ );
408
+
409
+ expect(result.isError).toBe(true);
410
+ expect(result.content).toContain("client-A");
411
+ expect(result.content).toContain("client-B");
412
+ expect(result.content).toContain("app_control_start");
413
+ expect(sentMessages).toHaveLength(0);
414
+
415
+ proxy.dispose();
416
+ });
417
+
375
418
  test("non-start tool with case-different but otherwise matching app is allowed", async () => {
376
419
  const proxy = new HostAppControlProxy("conv-1");
377
420
  const startCtrl = new AbortController();
@@ -1297,6 +1340,7 @@ describe("HostAppControlProxy", () => {
1297
1340
  _setActiveAppControlSession({
1298
1341
  conversationId: "conv-1",
1299
1342
  app: "com.example.app",
1343
+ targetClientId: "client-A",
1300
1344
  });
1301
1345
 
1302
1346
  const resultPromise = proxy.request(
@@ -1332,6 +1376,7 @@ describe("HostAppControlProxy", () => {
1332
1376
  _setActiveAppControlSession({
1333
1377
  conversationId: "conv-1",
1334
1378
  app: "com.example.app",
1379
+ targetClientId: "client-A",
1335
1380
  });
1336
1381
 
1337
1382
  const resultPromise = proxy.request(
@@ -44,6 +44,8 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
44
44
  mockClients.find((c) => c.capabilities.includes(cap)),
45
45
  listClientsByCapability: (cap: string) =>
46
46
  mockClients.filter((c) => c.capabilities.includes(cap)),
47
+ listClientsByInterface: (interfaceId: string) =>
48
+ mockClients.filter((c) => c.interfaceId === interfaceId),
47
49
  getActorPrincipalIdForClient: (clientId: string) =>
48
50
  mockClients.find((c) => c.clientId === clientId)?.actorPrincipalId,
49
51
  },
@@ -278,6 +280,53 @@ describe("HostBrowserProxy", () => {
278
280
  });
279
281
  });
280
282
 
283
+ describe("actor-scoped availability", () => {
284
+ const EXT_A: MockClient = {
285
+ clientId: "ext-a",
286
+ interfaceId: "chrome-extension",
287
+ actorPrincipalId: "actor-a",
288
+ capabilities: ["host_browser"],
289
+ };
290
+ const BRIDGE_B: MockClient = {
291
+ clientId: "bridge-b",
292
+ interfaceId: "macos",
293
+ actorPrincipalId: "actor-b",
294
+ capabilities: ["host_browser"],
295
+ };
296
+
297
+ test("isAvailable(actor) only counts that actor's clients", () => {
298
+ mockClients = [EXT_A, BRIDGE_B];
299
+ expect(proxy.isAvailable("actor-a")).toBe(true);
300
+ expect(proxy.isAvailable("actor-b")).toBe(true);
301
+ expect(proxy.isAvailable("actor-c")).toBe(false);
302
+ // Legacy no-actor form still counts any client.
303
+ expect(proxy.isAvailable()).toBe(true);
304
+ });
305
+
306
+ test("hasExtensionClient(actor) ignores other actors' extensions", () => {
307
+ mockClients = [EXT_A, BRIDGE_B];
308
+ expect(proxy.hasExtensionClient("actor-a")).toBe(true);
309
+ // actor-b has only the bridge — actor-a's extension must not count.
310
+ expect(proxy.hasExtensionClient("actor-b")).toBe(false);
311
+ expect(proxy.hasExtensionClient()).toBe(true);
312
+ });
313
+
314
+ test("hasExtensionClient(actor) is false for a client without an actor binding", () => {
315
+ // Strict matching mirrors resolveTargetClient: a legacy extension
316
+ // connection without an actor is not dispatchable for an
317
+ // actor-authenticated caller, so it must not count as available.
318
+ mockClients = [
319
+ {
320
+ clientId: "legacy-ext",
321
+ interfaceId: "chrome-extension",
322
+ capabilities: ["host_browser"],
323
+ },
324
+ ];
325
+ expect(proxy.hasExtensionClient("actor-a")).toBe(false);
326
+ expect(proxy.hasExtensionClient()).toBe(true);
327
+ });
328
+ });
329
+
281
330
  describe("dispose", () => {
282
331
  test("rejects all pending requests and emits cancels", async () => {
283
332
  const p1 = proxy.request(
@@ -493,11 +542,12 @@ describe("HostBrowserProxy", () => {
493
542
  expect(getPublishedMessages()).toHaveLength(0);
494
543
  });
495
544
 
496
- test("picks the most-recently-active same-actor client among multiple transports", async () => {
545
+ test("prefers the chrome-extension client over a more-recently-active macOS bridge", async () => {
497
546
  // Mock `listClientsByCapability` returns mockClients in array
498
547
  // order, which mirrors production's `lastActiveAt`-desc ordering.
499
- // The first same-actor entry wins regardless of transport; LLMs
500
- // pin a specific transport via `target_client_id`.
548
+ // The macOS bridge is listed first (most recent heartbeat), but
549
+ // chrome-extension clients are deterministically preferred for
550
+ // auto-resolution; LLMs pin the bridge via `target_client_id`.
501
551
  mockClients = [
502
552
  {
503
553
  clientId: "macos-client",
@@ -525,7 +575,7 @@ describe("HostBrowserProxy", () => {
525
575
  const requestId = sent.requestId as string;
526
576
 
527
577
  const pending = pendingInteractions.get(requestId);
528
- expect(pending?.targetClientId).toBe("macos-client");
578
+ expect(pending?.targetClientId).toBe("ext-client");
529
579
 
530
580
  resolveResult(requestId, { content: "ok", isError: false });
531
581
  await resultPromise;
@@ -739,7 +789,7 @@ describe("HostBrowserProxy", () => {
739
789
  expect(getPublishedMessages()).toHaveLength(0);
740
790
  });
741
791
 
742
- test("no targetClientId auto-resolves to the most-recently-active same-actor client", async () => {
792
+ test("no targetClientId auto-resolves with extension preferred over the bridge", async () => {
743
793
  mockClients = [
744
794
  {
745
795
  clientId: "macos-client",
@@ -755,9 +805,9 @@ describe("HostBrowserProxy", () => {
755
805
  },
756
806
  ];
757
807
 
758
- // No targetClientId — falls through to the first same-actor
759
- // entry by lastActiveAt-desc. Mock array order is the proxy's
760
- // input contract.
808
+ // No targetClientId — auto-resolution partitions chrome-extension
809
+ // clients ahead of other transports, then picks the first
810
+ // same-actor entry (lastActiveAt-desc within each group).
761
811
  const resultPromise = proxy.request(
762
812
  { cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
763
813
  "session-1",
@@ -769,10 +819,205 @@ describe("HostBrowserProxy", () => {
769
819
  const requestId = (getPublishedMessages()[0] as Record<string, unknown>)
770
820
  .requestId as string;
771
821
  const pending = pendingInteractions.get(requestId);
772
- expect(pending?.targetClientId).toBe("macos-client");
822
+ expect(pending?.targetClientId).toBe("ext-client");
773
823
 
774
824
  resolveResult(requestId, { content: "ok", isError: false });
775
825
  await resultPromise;
776
826
  });
777
827
  });
828
+
829
+ // ---------------------------------------------------------------------------
830
+ // Method-aware routing
831
+ //
832
+ // `Vellum.*` pseudo-methods are implemented only by the Chrome
833
+ // extension dispatcher; the macOS bridge speaks raw CDP against
834
+ // localhost:9222 and fails on all of them. Auto-resolution therefore
835
+ // restricts pseudo-methods to chrome-extension clients and prefers
836
+ // the extension over the bridge for raw CDP, instead of racing on
837
+ // heartbeat-driven lastActiveAt ordering.
838
+ // ---------------------------------------------------------------------------
839
+
840
+ describe("method-aware routing", () => {
841
+ const MACOS_CLIENT: MockClient = {
842
+ clientId: "macos-client",
843
+ interfaceId: "macos",
844
+ actorPrincipalId: "user-1",
845
+ capabilities: ["host_browser"],
846
+ };
847
+ const EXT_CLIENT: MockClient = {
848
+ clientId: "ext-client",
849
+ interfaceId: "chrome-extension",
850
+ actorPrincipalId: "user-1",
851
+ capabilities: ["host_browser"],
852
+ };
853
+
854
+ function expectExtensionRequired(result: {
855
+ content: string;
856
+ isError: boolean;
857
+ }): void {
858
+ expect(result.isError).toBe(true);
859
+ const parsed = JSON.parse(result.content) as {
860
+ code: string;
861
+ message: string;
862
+ };
863
+ expect(parsed.code).toBe("extension_required");
864
+ expect(parsed.message).toContain("Chrome extension");
865
+ }
866
+
867
+ test("pseudo-method routes to the extension even when the bridge is more recently active", async () => {
868
+ mockClients = [MACOS_CLIENT, EXT_CLIENT];
869
+
870
+ const resultPromise = proxy.request(
871
+ { cdpMethod: "Vellum.listTabs", cdpParams: {} },
872
+ "session-1",
873
+ undefined,
874
+ "user-1",
875
+ );
876
+
877
+ expect(getPublishedMessages()).toHaveLength(1);
878
+ const requestId = (getPublishedMessages()[0] as Record<string, unknown>)
879
+ .requestId as string;
880
+ expect(pendingInteractions.get(requestId)?.targetClientId).toBe(
881
+ "ext-client",
882
+ );
883
+
884
+ resolveResult(requestId, { content: '{"tabs":[]}', isError: false });
885
+ await resultPromise;
886
+ });
887
+
888
+ test("pseudo-method with only the macOS bridge connected resolves extension_required without broadcasting", async () => {
889
+ mockClients = [MACOS_CLIENT];
890
+
891
+ const result = await proxy.request(
892
+ { cdpMethod: "Vellum.selectTab", cdpParams: { tabId: 42 } },
893
+ "session-1",
894
+ undefined,
895
+ "user-1",
896
+ );
897
+
898
+ expectExtensionRequired(result);
899
+ expect(getPublishedMessages()).toHaveLength(0);
900
+ });
901
+
902
+ test("pseudo-method with no clients resolves extension_required instead of the legacy rejection", async () => {
903
+ mockClients = [];
904
+
905
+ const result = await proxy.request(
906
+ { cdpMethod: "Vellum.createTab", cdpParams: {} },
907
+ "session-1",
908
+ );
909
+
910
+ expectExtensionRequired(result);
911
+ expect(getPublishedMessages()).toHaveLength(0);
912
+ });
913
+
914
+ test("explicit targetClientId at the macOS bridge + pseudo-method resolves extension_required naming the interface", async () => {
915
+ mockClients = [MACOS_CLIENT, EXT_CLIENT];
916
+
917
+ const result = await proxy.request(
918
+ { cdpMethod: "Vellum.closeTab", cdpParams: { tabId: 7 } },
919
+ "session-1",
920
+ undefined,
921
+ "user-1",
922
+ "macos-client",
923
+ );
924
+
925
+ expectExtensionRequired(result);
926
+ const parsed = JSON.parse(result.content) as { message: string };
927
+ expect(parsed.message).toContain('"macos"');
928
+ expect(getPublishedMessages()).toHaveLength(0);
929
+ });
930
+
931
+ test("explicit targetClientId at the extension + pseudo-method dispatches normally", async () => {
932
+ mockClients = [MACOS_CLIENT, EXT_CLIENT];
933
+
934
+ const resultPromise = proxy.request(
935
+ { cdpMethod: "Vellum.closeTab", cdpParams: { tabId: 7 } },
936
+ "session-1",
937
+ undefined,
938
+ "user-1",
939
+ "ext-client",
940
+ );
941
+
942
+ expect(getPublishedMessages()).toHaveLength(1);
943
+ const requestId = (getPublishedMessages()[0] as Record<string, unknown>)
944
+ .requestId as string;
945
+ expect(pendingInteractions.get(requestId)?.targetClientId).toBe(
946
+ "ext-client",
947
+ );
948
+
949
+ resolveResult(requestId, {
950
+ content: '{"closed":true,"tabId":7}',
951
+ isError: false,
952
+ });
953
+ await resultPromise;
954
+ });
955
+
956
+ test("same-actor gate wins over the pseudo-method check for cross-actor explicit targets", async () => {
957
+ mockClients = [
958
+ {
959
+ clientId: "other-user-ext",
960
+ interfaceId: "chrome-extension",
961
+ actorPrincipalId: "user-2",
962
+ capabilities: ["host_browser"],
963
+ },
964
+ ];
965
+
966
+ const result = await proxy.request(
967
+ { cdpMethod: "Vellum.listTabs", cdpParams: {} },
968
+ "session-1",
969
+ undefined,
970
+ "user-1",
971
+ "other-user-ext",
972
+ );
973
+
974
+ expect(result.isError).toBe(true);
975
+ expect(result.content).toContain("Submitting actor does not match");
976
+ expect(result.content).not.toContain("extension_required");
977
+ expect(getPublishedMessages()).toHaveLength(0);
978
+ });
979
+
980
+ test("pseudo-method where the only extension belongs to another actor resolves extension_required", async () => {
981
+ mockClients = [
982
+ {
983
+ clientId: "other-user-ext",
984
+ interfaceId: "chrome-extension",
985
+ actorPrincipalId: "user-2",
986
+ capabilities: ["host_browser"],
987
+ },
988
+ MACOS_CLIENT,
989
+ ];
990
+
991
+ const result = await proxy.request(
992
+ { cdpMethod: "Vellum.listTabs", cdpParams: {} },
993
+ "session-1",
994
+ undefined,
995
+ "user-1",
996
+ );
997
+
998
+ expectExtensionRequired(result);
999
+ expect(getPublishedMessages()).toHaveLength(0);
1000
+ });
1001
+
1002
+ test("raw CDP with only the bridge connected still routes to the bridge", async () => {
1003
+ mockClients = [MACOS_CLIENT];
1004
+
1005
+ const resultPromise = proxy.request(
1006
+ { cdpMethod: "Page.captureScreenshot", cdpParams: {} },
1007
+ "session-1",
1008
+ undefined,
1009
+ "user-1",
1010
+ );
1011
+
1012
+ expect(getPublishedMessages()).toHaveLength(1);
1013
+ const requestId = (getPublishedMessages()[0] as Record<string, unknown>)
1014
+ .requestId as string;
1015
+ expect(pendingInteractions.get(requestId)?.targetClientId).toBe(
1016
+ "macos-client",
1017
+ );
1018
+
1019
+ resolveResult(requestId, { content: '{"data":"..."}', isError: false });
1020
+ await resultPromise;
1021
+ });
1022
+ });
778
1023
  });
@@ -237,6 +237,7 @@ describe("identity routes — health endpoint", () => {
237
237
  expect(body.memory).toBeDefined();
238
238
  expect(body.cpu).toBeDefined();
239
239
  expect(body.migrations).toBeDefined();
240
+ expect(body.capabilities).toEqual({ memoryOptOut: true });
240
241
 
241
242
  // Profiler should either be absent or show enabled: false
242
243
  if ("profiler" in body) {