@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -95,7 +95,7 @@ afterAll(() => {
95
95
 
96
96
  const handleHostBashResult = ROUTES.find(
97
97
  (r) => r.endpoint === "host-bash-result",
98
- )!.handler;
98
+ )!.handler as (args: Record<string, unknown>) => Promise<unknown>;
99
99
 
100
100
  // ── Helpers ──────────────────────────────────────────────────────────
101
101
 
@@ -244,22 +244,20 @@ describe("handleHostBashResult", () => {
244
244
  ).toThrow(ForbiddenError);
245
245
  });
246
246
 
247
- test("interaction is NOT resolved on cross-actor 403 (still pending)", () => {
247
+ test("interaction is NOT resolved on cross-actor 403 (still pending)", async () => {
248
248
  const requestId = "req-actor-mismatch-stays";
249
249
  clientActorPrincipals.set("client-abc", "principal-victim");
250
250
  registerPending(requestId, { targetClientId: "client-abc" });
251
251
 
252
- try {
253
- handleHostBashResult({
254
- body: bashBody(requestId),
255
- headers: {
256
- "x-vellum-client-id": "client-abc",
257
- "x-vellum-actor-principal-id": "principal-attacker",
258
- },
259
- });
260
- } catch {
252
+ await handleHostBashResult({
253
+ body: bashBody(requestId),
254
+ headers: {
255
+ "x-vellum-client-id": "client-abc",
256
+ "x-vellum-actor-principal-id": "principal-attacker",
257
+ },
258
+ }).catch(() => {
261
259
  // expected
262
- }
260
+ });
263
261
 
264
262
  expect(resolvedIds).not.toContain(requestId);
265
263
  expect(pendingStore.has(requestId)).toBe(true);
@@ -278,19 +276,17 @@ describe("handleHostBashResult", () => {
278
276
  ).toThrow(ForbiddenError);
279
277
  });
280
278
 
281
- test("interaction is NOT resolved when submitting actor is missing (still pending)", () => {
279
+ test("interaction is NOT resolved when submitting actor is missing (still pending)", async () => {
282
280
  const requestId = "req-actor-missing-stays";
283
281
  clientActorPrincipals.set("client-abc", "principal-victim");
284
282
  registerPending(requestId, { targetClientId: "client-abc" });
285
283
 
286
- try {
287
- handleHostBashResult({
288
- body: bashBody(requestId),
289
- headers: { "x-vellum-client-id": "client-abc" },
290
- });
291
- } catch {
284
+ await handleHostBashResult({
285
+ body: bashBody(requestId),
286
+ headers: { "x-vellum-client-id": "client-abc" },
287
+ }).catch(() => {
292
288
  // expected
293
- }
289
+ });
294
290
 
295
291
  expect(resolvedIds).not.toContain(requestId);
296
292
  expect(pendingStore.has(requestId)).toBe(true);
@@ -358,15 +354,13 @@ describe("handleHostBashResult", () => {
358
354
  ).toThrow(BadRequestError);
359
355
  });
360
356
 
361
- test("interaction is NOT resolved on 400 (still pending)", () => {
357
+ test("interaction is NOT resolved on 400 (still pending)", async () => {
362
358
  const requestId = "req-targeted-no-header-stays";
363
359
  registerPending(requestId, { targetClientId: "client-abc" });
364
360
 
365
- try {
366
- handleHostBashResult({ body: bashBody(requestId) });
367
- } catch {
361
+ await handleHostBashResult({ body: bashBody(requestId) }).catch(() => {
368
362
  // expected
369
- }
363
+ });
370
364
 
371
365
  expect(resolvedIds).not.toContain(requestId);
372
366
  expect(pendingStore.has(requestId)).toBe(true);
@@ -388,19 +382,17 @@ describe("handleHostBashResult", () => {
388
382
  ).toThrow(ForbiddenError);
389
383
  });
390
384
 
391
- test("ForbiddenError message names both the submitting and expected client", () => {
385
+ test("ForbiddenError message names both the submitting and expected client", async () => {
392
386
  const requestId = "req-targeted-mismatch-msg";
393
387
  registerPending(requestId, { targetClientId: "client-abc" });
394
388
 
395
389
  let caught: unknown;
396
- try {
397
- handleHostBashResult({
398
- body: bashBody(requestId),
399
- headers: { "x-vellum-client-id": "client-xyz" },
400
- });
401
- } catch (e) {
390
+ await handleHostBashResult({
391
+ body: bashBody(requestId),
392
+ headers: { "x-vellum-client-id": "client-xyz" },
393
+ }).catch((e: unknown) => {
402
394
  caught = e;
403
- }
395
+ });
404
396
 
405
397
  expect(caught).toBeInstanceOf(ForbiddenError);
406
398
  const msg = (caught as ForbiddenError).message;
@@ -408,18 +400,16 @@ describe("handleHostBashResult", () => {
408
400
  expect(msg).toContain("client-abc");
409
401
  });
410
402
 
411
- test("interaction is NOT resolved on 403 (still pending)", () => {
403
+ test("interaction is NOT resolved on 403 (still pending)", async () => {
412
404
  const requestId = "req-targeted-mismatch-stays";
413
405
  registerPending(requestId, { targetClientId: "client-abc" });
414
406
 
415
- try {
416
- handleHostBashResult({
417
- body: bashBody(requestId),
418
- headers: { "x-vellum-client-id": "client-xyz" },
419
- });
420
- } catch {
407
+ await handleHostBashResult({
408
+ body: bashBody(requestId),
409
+ headers: { "x-vellum-client-id": "client-xyz" },
410
+ }).catch(() => {
421
411
  // expected
422
- }
412
+ });
423
413
 
424
414
  expect(resolvedIds).not.toContain(requestId);
425
415
  expect(pendingStore.has(requestId)).toBe(true);
@@ -39,7 +39,7 @@ afterAll(() => {
39
39
 
40
40
  const handleHostBrowserResult = ROUTES.find(
41
41
  (r) => r.endpoint === "host-browser-result",
42
- )!.handler;
42
+ )!.handler as (args: Record<string, unknown>) => Promise<unknown>;
43
43
 
44
44
  // ── Tests ────────────────────────────────────────────────────────────
45
45
 
@@ -212,7 +212,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
212
212
  ).toThrow(BadRequestError);
213
213
  });
214
214
 
215
- test("interaction is NOT consumed on 400 (still pending)", () => {
215
+ test("interaction is NOT consumed on 400 (still pending)", async () => {
216
216
  const requestId = "browser-req-targeted-no-header-stays";
217
217
  pendingInteractions.register(requestId, {
218
218
  conversationId: "conv-1",
@@ -221,13 +221,11 @@ describe("handleHostBrowserResult — same-actor guard", () => {
221
221
  targetActorPrincipalId: "user-1",
222
222
  });
223
223
 
224
- try {
225
- handleHostBrowserResult({
226
- body: { requestId, content: "ok", isError: false },
227
- });
228
- } catch {
224
+ await handleHostBrowserResult({
225
+ body: { requestId, content: "ok", isError: false },
226
+ }).catch(() => {
229
227
  // expected
230
- }
228
+ });
231
229
 
232
230
  expect(pendingInteractions.get(requestId)).toBeDefined();
233
231
  });
@@ -256,7 +254,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
256
254
  ).toThrow(ForbiddenError);
257
255
  });
258
256
 
259
- test("ForbiddenError message names both submitting and expected client", () => {
257
+ test("ForbiddenError message names both submitting and expected client", async () => {
260
258
  const requestId = "browser-req-targeted-mismatch-msg";
261
259
  pendingInteractions.register(requestId, {
262
260
  conversationId: "conv-1",
@@ -267,7 +265,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
267
265
 
268
266
  let caught: unknown;
269
267
  try {
270
- handleHostBrowserResult({
268
+ await handleHostBrowserResult({
271
269
  body: { requestId, content: "ok", isError: false },
272
270
  headers: {
273
271
  "x-vellum-client-id": "client-B",
@@ -284,7 +282,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
284
282
  expect(msg).toContain("client-B");
285
283
  });
286
284
 
287
- test("interaction is NOT consumed on 403 (still pending)", () => {
285
+ test("interaction is NOT consumed on 403 (still pending)", async () => {
288
286
  const requestId = "browser-req-targeted-mismatch-stays";
289
287
  pendingInteractions.register(requestId, {
290
288
  conversationId: "conv-1",
@@ -293,17 +291,15 @@ describe("handleHostBrowserResult — same-actor guard", () => {
293
291
  targetActorPrincipalId: "user-1",
294
292
  });
295
293
 
296
- try {
297
- handleHostBrowserResult({
298
- body: { requestId, content: "ok", isError: false },
299
- headers: {
300
- "x-vellum-client-id": "client-B",
301
- "x-vellum-actor-principal-id": "user-1",
302
- },
303
- });
304
- } catch {
294
+ await handleHostBrowserResult({
295
+ body: { requestId, content: "ok", isError: false },
296
+ headers: {
297
+ "x-vellum-client-id": "client-B",
298
+ "x-vellum-actor-principal-id": "user-1",
299
+ },
300
+ }).catch(() => {
305
301
  // expected
306
- }
302
+ });
307
303
 
308
304
  expect(pendingInteractions.get(requestId)).toBeDefined();
309
305
  });
@@ -369,7 +365,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
369
365
  ).toThrow(ForbiddenError);
370
366
  });
371
367
 
372
- test("interaction NOT consumed on actor-mismatch 403", () => {
368
+ test("interaction NOT consumed on actor-mismatch 403", async () => {
373
369
  const requestId = "browser-req-actor-mismatch-stays";
374
370
  pendingInteractions.register(requestId, {
375
371
  conversationId: "conv-1",
@@ -378,17 +374,15 @@ describe("handleHostBrowserResult — same-actor guard", () => {
378
374
  targetActorPrincipalId: "user-1",
379
375
  });
380
376
 
381
- try {
382
- handleHostBrowserResult({
383
- body: { requestId, content: "ok", isError: false },
384
- headers: {
385
- "x-vellum-client-id": "client-A",
386
- "x-vellum-actor-principal-id": "user-2",
387
- },
388
- });
389
- } catch {
377
+ await handleHostBrowserResult({
378
+ body: { requestId, content: "ok", isError: false },
379
+ headers: {
380
+ "x-vellum-client-id": "client-A",
381
+ "x-vellum-actor-principal-id": "user-2",
382
+ },
383
+ }).catch(() => {
390
384
  // expected
391
- }
385
+ });
392
386
 
393
387
  expect(pendingInteractions.get(requestId)).toBeDefined();
394
388
  });
@@ -477,6 +477,305 @@ describe("HostCuProxy", () => {
477
477
  expect(result2.content).not.toContain("NO VISIBLE EFFECT");
478
478
  });
479
479
 
480
+ test("skips unchanged warning and counter after a selection-only key (cmd+a)", async () => {
481
+ setup();
482
+
483
+ // Establish previous AX tree
484
+ const p1 = proxy.request(
485
+ "computer_use_click",
486
+ { element_id: 1 },
487
+ "session-1",
488
+ 1,
489
+ );
490
+ proxy.recordAction("computer_use_click", { element_id: 1 });
491
+ const sent1 = sentMessages[0] as Record<string, unknown>;
492
+ proxy.processObservation(sent1.requestId as string, {
493
+ axTree: "TextField [1]",
494
+ });
495
+ await p1;
496
+
497
+ // cmd+a only changes selection — AX tree can't show it, so empty diff
498
+ // is expected and must NOT warn or bump the unchanged counter.
499
+ const p2 = proxy.request(
500
+ "computer_use_key",
501
+ { key: "cmd+a" },
502
+ "session-1",
503
+ 2,
504
+ );
505
+ proxy.recordAction("computer_use_key", { key: "cmd+a" });
506
+ const sent2 = sentMessages[1] as Record<string, unknown>;
507
+ proxy.processObservation(sent2.requestId as string, {
508
+ axTree: "TextField [1]",
509
+ // No axDiff — selection change is invisible in the AX tree
510
+ });
511
+ const result2 = await p2;
512
+ expect(result2.content).not.toContain("NO VISIBLE EFFECT");
513
+ expect(proxy.consecutiveUnchangedSteps).toBe(0);
514
+ });
515
+
516
+ test("exempts key combos regardless of spacing, case, alias, or modifier order", async () => {
517
+ // All of these normalize to an exempt combo the mac helper would execute
518
+ // identically: spaced, uppercase, alt->option alias, command->cmd alias,
519
+ // and reversed modifier order.
520
+ const variants = ["cmd + a", "CMD+A", "alt+tab", "command+c", "tab+shift"];
521
+
522
+ for (const [i, variant] of variants.entries()) {
523
+ setup();
524
+
525
+ // Establish previous AX tree
526
+ const p1 = proxy.request(
527
+ "computer_use_click",
528
+ { element_id: 1 },
529
+ `session-${i}`,
530
+ 1,
531
+ );
532
+ proxy.recordAction("computer_use_click", { element_id: 1 });
533
+ const sent1 = sentMessages[0] as Record<string, unknown>;
534
+ proxy.processObservation(sent1.requestId as string, {
535
+ axTree: "TextField [1]",
536
+ });
537
+ await p1;
538
+
539
+ const p2 = proxy.request(
540
+ "computer_use_key",
541
+ { key: variant },
542
+ `session-${i}`,
543
+ 2,
544
+ );
545
+ proxy.recordAction("computer_use_key", { key: variant });
546
+ const sent2 = sentMessages[1] as Record<string, unknown>;
547
+ proxy.processObservation(sent2.requestId as string, {
548
+ axTree: "TextField [1]",
549
+ // No axDiff — invisible-by-design change
550
+ });
551
+ const result2 = await p2;
552
+ expect(result2.content).not.toContain("NO VISIBLE EFFECT");
553
+ expect(proxy.consecutiveUnchangedSteps).toBe(0);
554
+ }
555
+ });
556
+
557
+ test("exempt key clears the no-effect streak between non-exempt no-ops", async () => {
558
+ setup();
559
+
560
+ // Establish previous AX tree
561
+ const p1 = proxy.request(
562
+ "computer_use_click",
563
+ { element_id: 1 },
564
+ "session-1",
565
+ 1,
566
+ );
567
+ proxy.recordAction("computer_use_click", { element_id: 1 });
568
+ const sent1 = sentMessages[0] as Record<string, unknown>;
569
+ proxy.processObservation(sent1.requestId as string, {
570
+ axTree: "TextField [1]",
571
+ });
572
+ await p1;
573
+
574
+ // Non-exempt no-diff click — streak = 1
575
+ const p2 = proxy.request(
576
+ "computer_use_click",
577
+ { element_id: 1 },
578
+ "session-1",
579
+ 2,
580
+ );
581
+ proxy.recordAction("computer_use_click", { element_id: 1 });
582
+ const sent2 = sentMessages[1] as Record<string, unknown>;
583
+ proxy.processObservation(sent2.requestId as string, {
584
+ axTree: "TextField [1]",
585
+ });
586
+ await p2;
587
+ expect(proxy.consecutiveUnchangedSteps).toBe(1);
588
+
589
+ // Exempt cmd+a no-diff — must CLEAR the streak, not just skip it
590
+ const p3 = proxy.request(
591
+ "computer_use_key",
592
+ { key: "cmd+a" },
593
+ "session-1",
594
+ 3,
595
+ );
596
+ proxy.recordAction("computer_use_key", { key: "cmd+a" });
597
+ const sent3 = sentMessages[2] as Record<string, unknown>;
598
+ proxy.processObservation(sent3.requestId as string, {
599
+ axTree: "TextField [1]",
600
+ });
601
+ await p3;
602
+ expect(proxy.consecutiveUnchangedSteps).toBe(0);
603
+
604
+ // Next non-exempt no-diff click — streak = 1 again, NOT a "2 consecutive"
605
+ // escalation bridged across the exempt keypress.
606
+ const p4 = proxy.request(
607
+ "computer_use_click",
608
+ { element_id: 1 },
609
+ "session-1",
610
+ 4,
611
+ );
612
+ proxy.recordAction("computer_use_click", { element_id: 1 });
613
+ const sent4 = sentMessages[3] as Record<string, unknown>;
614
+ proxy.processObservation(sent4.requestId as string, {
615
+ axTree: "TextField [1]",
616
+ });
617
+ const result4 = await p4;
618
+ expect(proxy.consecutiveUnchangedSteps).toBe(1);
619
+ expect(result4.content).not.toContain("2 consecutive");
620
+ });
621
+
622
+ test("still warns for a non-exempt key (enter) with no diff", async () => {
623
+ setup();
624
+
625
+ // Establish previous AX tree
626
+ const p1 = proxy.request(
627
+ "computer_use_click",
628
+ { element_id: 1 },
629
+ "session-1",
630
+ 1,
631
+ );
632
+ proxy.recordAction("computer_use_click", { element_id: 1 });
633
+ const sent1 = sentMessages[0] as Record<string, unknown>;
634
+ proxy.processObservation(sent1.requestId as string, {
635
+ axTree: "Button [1]",
636
+ });
637
+ await p1;
638
+
639
+ // enter is not in the exempt set — an empty diff should still warn.
640
+ const p2 = proxy.request(
641
+ "computer_use_key",
642
+ { key: "enter" },
643
+ "session-1",
644
+ 2,
645
+ );
646
+ proxy.recordAction("computer_use_key", { key: "enter" });
647
+ const sent2 = sentMessages[1] as Record<string, unknown>;
648
+ proxy.processObservation(sent2.requestId as string, {
649
+ axTree: "Button [1]",
650
+ });
651
+ const result2 = await p2;
652
+ expect(result2.content).toContain("NO VISIBLE EFFECT");
653
+ expect(proxy.consecutiveUnchangedSteps).toBe(1);
654
+ });
655
+
656
+ test("still warns for a non-exempt action (click) with no diff", async () => {
657
+ setup();
658
+
659
+ // Establish previous AX tree
660
+ const p1 = proxy.request(
661
+ "computer_use_click",
662
+ { element_id: 1 },
663
+ "session-1",
664
+ 1,
665
+ );
666
+ proxy.recordAction("computer_use_click", { element_id: 1 });
667
+ const sent1 = sentMessages[0] as Record<string, unknown>;
668
+ proxy.processObservation(sent1.requestId as string, {
669
+ axTree: "Button [1]",
670
+ });
671
+ await p1;
672
+
673
+ const p2 = proxy.request(
674
+ "computer_use_click",
675
+ { element_id: 1 },
676
+ "session-1",
677
+ 2,
678
+ );
679
+ proxy.recordAction("computer_use_click", { element_id: 1 });
680
+ const sent2 = sentMessages[1] as Record<string, unknown>;
681
+ proxy.processObservation(sent2.requestId as string, {
682
+ axTree: "Button [1]",
683
+ });
684
+ const result2 = await p2;
685
+ expect(result2.content).toContain("NO VISIBLE EFFECT");
686
+ expect(proxy.consecutiveUnchangedSteps).toBe(1);
687
+ });
688
+
689
+ test("loop detection still fires for repeated identical exempt keys", () => {
690
+ setup();
691
+
692
+ // Even though `up` is exempt from the unchanged-effect warning, repeating
693
+ // the same key must still be caught by loop detection.
694
+ proxy.recordAction("computer_use_key", { key: "up" });
695
+ proxy.recordAction("computer_use_key", { key: "up" });
696
+ proxy.recordAction("computer_use_key", { key: "up" });
697
+
698
+ const result = proxy.formatObservation({
699
+ axTree: "Button [1]",
700
+ });
701
+
702
+ expect(result.content).toContain(
703
+ "WARNING: You've repeated the same action (computer_use_key) 3 times",
704
+ );
705
+ expect(result.content).not.toContain("NO VISIBLE EFFECT");
706
+ });
707
+
708
+ test("loop detection fires for the same exempt combo spelled differently", () => {
709
+ setup();
710
+
711
+ // Equivalent spellings the mac helper executes identically must count as
712
+ // the same action — otherwise a stuck session could repeat cmd+a forever
713
+ // (no-effect warning is suppressed for exempt keys, so loop detection is
714
+ // the only remaining guard).
715
+ proxy.recordAction("computer_use_key", { key: "cmd+a" });
716
+ proxy.recordAction("computer_use_key", { key: "command+a" });
717
+ proxy.recordAction("computer_use_key", { key: "cmd + a" });
718
+
719
+ const result = proxy.formatObservation({
720
+ axTree: "Button [1]",
721
+ });
722
+
723
+ expect(result.content).toContain(
724
+ "WARNING: You've repeated the same action (computer_use_key) 3 times",
725
+ );
726
+ });
727
+
728
+ test("loop detection ignores the same combo sent to different clients", () => {
729
+ setup();
730
+
731
+ // Same key, different target desktops — not a repeat on one UI, so loop
732
+ // detection must NOT fire. Routing fields are kept in the signature.
733
+ proxy.recordAction("computer_use_key", {
734
+ key: "cmd+a",
735
+ target_client_id: "client-a",
736
+ });
737
+ proxy.recordAction("computer_use_key", {
738
+ key: "cmd+a",
739
+ target_client_id: "client-b",
740
+ });
741
+ proxy.recordAction("computer_use_key", {
742
+ key: "cmd+a",
743
+ target_client_id: "client-c",
744
+ });
745
+
746
+ const result = proxy.formatObservation({
747
+ axTree: "Button [1]",
748
+ });
749
+
750
+ expect(result.content).not.toContain("WARNING: You've repeated");
751
+ });
752
+
753
+ test("loop detection fires for the same combo+client across spellings", () => {
754
+ setup();
755
+
756
+ // Same target client, equivalent spellings — still a repeat on one UI.
757
+ proxy.recordAction("computer_use_key", {
758
+ key: "cmd+a",
759
+ target_client_id: "client-a",
760
+ });
761
+ proxy.recordAction("computer_use_key", {
762
+ key: "command+a",
763
+ target_client_id: "client-a",
764
+ });
765
+ proxy.recordAction("computer_use_key", {
766
+ key: "cmd + a",
767
+ target_client_id: "client-a",
768
+ });
769
+
770
+ const result = proxy.formatObservation({
771
+ axTree: "Button [1]",
772
+ });
773
+
774
+ expect(result.content).toContain(
775
+ "WARNING: You've repeated the same action (computer_use_key) 3 times",
776
+ );
777
+ });
778
+
480
779
  test("resets consecutive count when diff is present", async () => {
481
780
  setup();
482
781
 
@@ -104,7 +104,7 @@ afterAll(() => {
104
104
 
105
105
  const handleHostCuResult = ROUTES.find(
106
106
  (r: { endpoint: string }) => r.endpoint === "host-cu-result",
107
- )!.handler;
107
+ )!.handler as (args: Record<string, unknown>) => Promise<unknown>;
108
108
 
109
109
  // ── Helpers ──────────────────────────────────────────────────────────────────
110
110
 
@@ -226,15 +226,13 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
226
226
  ).toThrow(BadRequestError);
227
227
  });
228
228
 
229
- test("interaction is NOT resolved on 400 (still pending)", () => {
229
+ test("interaction is NOT resolved on 400 (still pending)", async () => {
230
230
  const requestId = "req-cu-targeted-no-header-stays";
231
231
  registerPending(requestId, { targetClientId: "client-A" });
232
232
 
233
- try {
234
- handleHostCuResult({ body: cuBody(requestId) });
235
- } catch {
233
+ await handleHostCuResult({ body: cuBody(requestId) }).catch(() => {
236
234
  // expected
237
- }
235
+ });
238
236
 
239
237
  expect(resolvedIds).not.toContain(requestId);
240
238
  expect(pendingStore.has(requestId)).toBe(true);
@@ -256,19 +254,17 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
256
254
  ).toThrow(ForbiddenError);
257
255
  });
258
256
 
259
- test("ForbiddenError message names both submitting and expected client", () => {
257
+ test("ForbiddenError message names both submitting and expected client", async () => {
260
258
  const requestId = "req-cu-targeted-mismatch-msg";
261
259
  registerPending(requestId, { targetClientId: "client-A" });
262
260
 
263
261
  let caught: unknown;
264
- try {
265
- handleHostCuResult({
266
- body: cuBody(requestId),
267
- headers: { "x-vellum-client-id": "client-B" },
268
- });
269
- } catch (e) {
262
+ await handleHostCuResult({
263
+ body: cuBody(requestId),
264
+ headers: { "x-vellum-client-id": "client-B" },
265
+ }).catch((e: unknown) => {
270
266
  caught = e;
271
- }
267
+ });
272
268
 
273
269
  expect(caught).toBeInstanceOf(ForbiddenError);
274
270
  const msg = (caught as ForbiddenError).message;
@@ -276,18 +272,16 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
276
272
  expect(msg).toContain("client-A");
277
273
  });
278
274
 
279
- test("interaction is NOT consumed on 403 (pendingInteractions.get still returns it)", () => {
275
+ test("interaction is NOT consumed on 403 (pendingInteractions.get still returns it)", async () => {
280
276
  const requestId = "req-cu-targeted-mismatch-stays";
281
277
  registerPending(requestId, { targetClientId: "client-A" });
282
278
 
283
- try {
284
- handleHostCuResult({
285
- body: cuBody(requestId),
286
- headers: { "x-vellum-client-id": "client-B" },
287
- });
288
- } catch {
279
+ await handleHostCuResult({
280
+ body: cuBody(requestId),
281
+ headers: { "x-vellum-client-id": "client-B" },
282
+ }).catch(() => {
289
283
  // expected
290
- }
284
+ });
291
285
 
292
286
  expect(resolvedIds).not.toContain(requestId);
293
287
  expect(pendingStore.has(requestId)).toBe(true);
@@ -331,22 +325,20 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
331
325
  ).toThrow(ForbiddenError);
332
326
  });
333
327
 
334
- test("interaction NOT consumed on 403 actor mismatch", () => {
328
+ test("interaction NOT consumed on 403 actor mismatch", async () => {
335
329
  const requestId = "req-cu-actor-mismatch-stays";
336
330
  actorPrincipalByClient.set("client-A", "user-1");
337
331
  registerPending(requestId, { targetClientId: "client-A" });
338
332
 
339
- try {
340
- handleHostCuResult({
341
- body: cuBody(requestId),
342
- headers: {
343
- "x-vellum-client-id": "client-A",
344
- "x-vellum-actor-principal-id": "user-2",
345
- },
346
- });
347
- } catch {
333
+ await handleHostCuResult({
334
+ body: cuBody(requestId),
335
+ headers: {
336
+ "x-vellum-client-id": "client-A",
337
+ "x-vellum-actor-principal-id": "user-2",
338
+ },
339
+ }).catch(() => {
348
340
  // expected
349
- }
341
+ });
350
342
 
351
343
  expect(resolvedIds).not.toContain(requestId);
352
344
  expect(pendingStore.has(requestId)).toBe(true);