@vellumai/assistant 0.12.3-staging.1 → 0.12.3-staging.2

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 (560) hide show
  1. package/ARCHITECTURE.md +43 -29
  2. package/Dockerfile +63 -0
  3. package/docs/architecture/security.md +10 -5
  4. package/docs/architecture/turn-actor.md +2 -0
  5. package/docs/desktop-browser-cli.md +45 -4
  6. package/docs/live-voice-task-outcomes.md +100 -0
  7. package/docs/mode-sessions.md +183 -0
  8. package/docs/voice-input-diagnostics.md +58 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +4 -1
  17. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +38 -7
  18. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +5 -6
  19. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  20. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
  22. package/node_modules/@vellumai/slack-text/src/index.ts +4 -8
  23. package/openapi.yaml +665 -14
  24. package/package.json +1 -1
  25. package/scripts/smoke-desktop-browser-cli.ts +10 -26
  26. package/scripts/smoke-desktop-window-theme.ts +2 -3
  27. package/src/__tests__/agent-loop-output-hooks.test.ts +150 -1
  28. package/src/__tests__/agent-loop-provider-error-recording.test.ts +87 -19
  29. package/src/__tests__/annotate-activity-metadata.test.ts +3 -1
  30. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  31. package/src/__tests__/answered-question-persistence.test.ts +2 -0
  32. package/src/__tests__/anthropic-gateway-shared.test.ts +22 -0
  33. package/src/__tests__/approval-message-composer.test.ts +1 -118
  34. package/src/__tests__/approval-routes-http.test.ts +3 -0
  35. package/src/__tests__/assistant-event-hub-connection-history.test.ts +111 -0
  36. package/src/__tests__/assistant-event-hub-targeted.test.ts +50 -0
  37. package/src/__tests__/assistant-event-targeting.test.ts +186 -0
  38. package/src/__tests__/assistant-stream-state.test.ts +29 -9
  39. package/src/__tests__/browser-fill-credential.test.ts +11 -22
  40. package/src/__tests__/btw-routes.test.ts +4 -1
  41. package/src/__tests__/build-persisted-content.test.ts +1 -1
  42. package/src/__tests__/call-conversation-messages.test.ts +5 -0
  43. package/src/__tests__/call-recovery.test.ts +7 -2
  44. package/src/__tests__/call-routes-http.test.ts +12 -0
  45. package/src/__tests__/call-store.test.ts +7 -2
  46. package/src/__tests__/channel-retry-sweep.test.ts +9 -0
  47. package/src/__tests__/chat-reveal-guard-priming.test.ts +2 -0
  48. package/src/__tests__/checker.test.ts +11 -3
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +35 -1
  50. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +0 -25
  51. package/src/__tests__/computer-use-tools.test.ts +1 -233
  52. package/src/__tests__/config-model-image-provider.test.ts +39 -0
  53. package/src/__tests__/config-schema.test.ts +16 -0
  54. package/src/__tests__/config-watcher.test.ts +24 -0
  55. package/src/__tests__/context-search-agent-runner.test.ts +260 -1
  56. package/src/__tests__/context-search-fanout.test.ts +1 -1
  57. package/src/__tests__/context-search-types.test.ts +7 -7
  58. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +8 -2
  59. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +2 -0
  60. package/src/__tests__/conversation-agent-loop.test.ts +546 -16
  61. package/src/__tests__/conversation-app-control-lifecycle.test.ts +53 -1
  62. package/src/__tests__/conversation-evictor.test.ts +62 -37
  63. package/src/__tests__/conversation-history-boundary-repair-failure.test.ts +174 -0
  64. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +2 -0
  65. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  66. package/src/__tests__/conversation-retry-route.test.ts +22 -0
  67. package/src/__tests__/conversation-routes-disk-view.test.ts +2 -0
  68. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +2 -0
  69. package/src/__tests__/conversation-routes-guardian-reply.test.ts +10 -0
  70. package/src/__tests__/conversation-routes-hidden-queue.test.ts +2 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +29 -0
  73. package/src/__tests__/conversation-surface-snapshots.test.ts +543 -0
  74. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +151 -0
  75. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +63 -1
  76. package/src/__tests__/conversation-tool-setup-attribution.test.ts +23 -0
  77. package/src/__tests__/conversation-undo.test.ts +61 -0
  78. package/src/__tests__/credential-execution-client.test.ts +6 -6
  79. package/src/__tests__/credential-prompt-route.test.ts +35 -3
  80. package/src/__tests__/cu-unified-flow.test.ts +82 -1
  81. package/src/__tests__/db-mode-sessions-migration.test.ts +175 -0
  82. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -0
  83. package/src/__tests__/dm-backfill.test.ts +91 -1
  84. package/src/__tests__/document-create-route.test.ts +88 -0
  85. package/src/__tests__/document-sync-tags.test.ts +27 -0
  86. package/src/__tests__/emit-signal-routing-intent.test.ts +142 -2
  87. package/src/__tests__/events-tail-route.test.ts +25 -0
  88. package/src/__tests__/evict-conversations-for-reload.test.ts +57 -8
  89. package/src/__tests__/guardian-action-sweep.test.ts +16 -0
  90. package/src/__tests__/guardian-dispatch.test.ts +13 -0
  91. package/src/__tests__/headless-browser-interactions.test.ts +64 -20
  92. package/src/__tests__/heartbeat-service.test.ts +2 -59
  93. package/src/__tests__/helpers/mock-conversation.ts +21 -0
  94. package/src/__tests__/helpers/native-web-search-harness.ts +2 -0
  95. package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +5 -0
  96. package/src/__tests__/host-cu-proxy.test.ts +46 -2
  97. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  98. package/src/__tests__/image-credentials.test.ts +49 -0
  99. package/src/__tests__/image-service-dispatcher.test.ts +45 -0
  100. package/src/__tests__/inference-profile-session-handler.test.ts +1 -1
  101. package/src/__tests__/interrupt-on-send.test.ts +57 -43
  102. package/src/__tests__/interrupt-turn-note.test.ts +5 -6
  103. package/src/__tests__/llm-catalog-parity.test.ts +2 -2
  104. package/src/__tests__/media-generate-image.test.ts +49 -1
  105. package/src/__tests__/message-provenance-contact-id.test.ts +69 -0
  106. package/src/__tests__/migration-export-to-gcs.test.ts +142 -0
  107. package/src/__tests__/mode-session-history.test.ts +245 -0
  108. package/src/__tests__/mode-session-recovery.test.ts +222 -0
  109. package/src/__tests__/mode-session-wire.test.ts +141 -0
  110. package/src/__tests__/notification-guardian-path.test.ts +7 -0
  111. package/src/__tests__/notification-telegram-adapter.test.ts +2 -2
  112. package/src/__tests__/notification-vellum-adapter.test.ts +34 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +12 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  115. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  116. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  117. package/src/__tests__/plugin-api-provider.test.ts +5 -0
  118. package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
  119. package/src/__tests__/plugin-import-boundary-guard.test.ts +9 -0
  120. package/src/__tests__/process-message-display-content.test.ts +56 -0
  121. package/src/__tests__/reaction-persistence.test.ts +5 -0
  122. package/src/__tests__/request-secret-standalone-channel-gate.test.ts +25 -9
  123. package/src/__tests__/runtime-events-sse-reconnect.test.ts +1 -1
  124. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +73 -0
  125. package/src/__tests__/secret-ingress-http.test.ts +2 -0
  126. package/src/__tests__/secure-keys.test.ts +43 -1
  127. package/src/__tests__/send-endpoint-busy.test.ts +163 -1
  128. package/src/__tests__/server-history-render.test.ts +91 -1
  129. package/src/__tests__/settings-routes.test.ts +50 -1
  130. package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -2
  132. package/src/__tests__/subagent-manager-notify.test.ts +1 -2
  133. package/src/__tests__/subagent-notify-parent.test.ts +146 -39
  134. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +2 -4
  135. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +2 -0
  136. package/src/__tests__/thread-backfill.test.ts +53 -0
  137. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  138. package/src/__tests__/tool-result-metadata-plumbing.test.ts +3 -1
  139. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  140. package/src/__tests__/twilio-routes.test.ts +1 -0
  141. package/src/__tests__/web-search-backend-failure.test.ts +1 -1
  142. package/src/activation/progress-store.test.ts +12 -7
  143. package/src/agent/loop.ts +99 -41
  144. package/src/api/events/assistant-turn-start.ts +3 -0
  145. package/src/api/events/generation-handoff.ts +2 -0
  146. package/src/api/events/message-complete.ts +2 -0
  147. package/src/api/events/notification-conversation-created.ts +14 -16
  148. package/src/api/events/notification-intent.ts +4 -2
  149. package/src/api/events/tool-result.ts +85 -0
  150. package/src/api/events/tool-use-start.ts +3 -0
  151. package/src/api/events/user-message-echo.ts +3 -0
  152. package/src/api/index.ts +31 -6
  153. package/src/api/mode-session.test.ts +131 -0
  154. package/src/api/mode-session.ts +148 -0
  155. package/src/api/remember-tool.ts +15 -0
  156. package/src/api/responses/conversation-message.ts +6 -0
  157. package/src/approvals/AGENTS.md +4 -2
  158. package/src/browser/__tests__/operations.test.ts +29 -0
  159. package/src/browser/operation-meta.ts +2 -1
  160. package/src/browser/operations.ts +29 -0
  161. package/src/browser/virtual-desktop-target.ts +2 -8
  162. package/src/bundler/bundle-signer.ts +1 -3
  163. package/src/calls/__tests__/barge-in-guard.test.ts +3 -0
  164. package/src/calls/__tests__/call-controller.test.ts +94 -0
  165. package/src/calls/__tests__/call-funnel-telemetry.test.ts +204 -0
  166. package/src/calls/__tests__/voice-control-protocol.test.ts +15 -0
  167. package/src/calls/__tests__/voice-escalation-judge.test.ts +209 -0
  168. package/src/calls/__tests__/voice-leg-coordinator.test.ts +168 -0
  169. package/src/{live-voice/__tests__/live-voice-metrics.test.ts → calls/__tests__/voice-metrics.test.ts} +32 -38
  170. package/src/calls/__tests__/voice-session-bridge.test.ts +576 -122
  171. package/src/calls/barge-in-guard.ts +5 -0
  172. package/src/calls/call-controller.ts +196 -1
  173. package/src/calls/call-domain.ts +4 -0
  174. package/src/calls/call-store.ts +87 -2
  175. package/src/calls/guardian-action-sweep.ts +22 -8
  176. package/src/calls/media-stream-audio-transcode.ts +1 -3
  177. package/src/calls/typesafe-noul.ts +156 -0
  178. package/src/calls/voice-control-protocol.ts +6 -0
  179. package/src/calls/voice-escalation-judge.ts +145 -0
  180. package/src/calls/voice-escalation-target.ts +10 -0
  181. package/src/calls/voice-leg-coordinator.ts +121 -3
  182. package/src/{live-voice/live-voice-metrics.ts → calls/voice-metrics.ts} +94 -87
  183. package/src/calls/voice-session-bridge.ts +256 -112
  184. package/src/calls/voice-triage-escalate.ts +7 -0
  185. package/src/cli/__tests__/roadmap-cli.test.ts +4 -41
  186. package/src/cli/commands/__tests__/notifications.test.ts +35 -10
  187. package/src/cli/commands/browser.help.ts +4 -5
  188. package/src/cli/commands/browser.ts +0 -1
  189. package/src/cli/commands/clients.help.ts +46 -0
  190. package/src/cli/commands/clients.ts +116 -0
  191. package/src/cli/commands/credentials.help.ts +6 -1
  192. package/src/cli/commands/notifications.help.ts +16 -2
  193. package/src/cli/commands/notifications.ts +30 -8
  194. package/src/cli/commands/roadmap.help.ts +18 -21
  195. package/src/cli/commands/roadmap.ts +4 -21
  196. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +5 -2
  197. package/src/cli/lib/bundled-marketplace.json +84 -34
  198. package/src/cli/lib/plugin-marketplace.ts +8 -0
  199. package/src/cli/lib/publish-plugin.ts +1 -2
  200. package/src/config/__tests__/default-provider.test.ts +3 -1
  201. package/src/config/__tests__/memory-retrospective-schema.test.ts +14 -1
  202. package/src/config/bundled-skills/computer-use/SKILL.md +1 -1
  203. package/src/config/bundled-skills/computer-use/TOOLS.json +89 -1
  204. package/src/config/bundled-skills/image-studio/SKILL.md +17 -2
  205. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +7 -11
  206. package/src/config/bundled-skills/sequences/TOOLS.json +1 -5
  207. package/src/config/call-site-defaults.ts +9 -0
  208. package/src/config/feature-flag-registry.json +16 -0
  209. package/src/config/profile-text-generation.test.ts +2 -2
  210. package/src/config/schemas/__tests__/tools.test.ts +31 -0
  211. package/src/config/schemas/call-site-catalog.ts +14 -0
  212. package/src/config/schemas/channels.ts +0 -18
  213. package/src/config/schemas/llm.ts +3 -1
  214. package/src/config/schemas/mcp.ts +2 -1
  215. package/src/config/schemas/memory-retrospective.ts +11 -2
  216. package/src/config/schemas/services.ts +15 -6
  217. package/src/config/schemas/tools.ts +10 -0
  218. package/src/config/session-groups-gate.ts +21 -0
  219. package/src/config/skills.ts +2 -6
  220. package/src/conversations/__tests__/message-consolidation.test.ts +149 -0
  221. package/src/conversations/message-consolidation.ts +102 -5
  222. package/src/credential-execution/ces-connect.test.ts +69 -0
  223. package/src/credential-execution/ces-connect.ts +115 -0
  224. package/src/credential-execution/ces-runtime.ts +55 -93
  225. package/src/credential-execution/executable-discovery.ts +1 -11
  226. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +10 -1
  227. package/src/daemon/__tests__/inference-profile-notification.test.ts +2 -0
  228. package/src/daemon/browser-mode-session.test.ts +402 -0
  229. package/src/daemon/browser-mode-session.ts +186 -0
  230. package/src/daemon/computer-use-mode-session.test.ts +210 -0
  231. package/src/daemon/computer-use-mode-session.ts +105 -0
  232. package/src/daemon/config-watcher.ts +10 -0
  233. package/src/daemon/conversation-agent-loop-handlers.ts +58 -10
  234. package/src/daemon/conversation-agent-loop.ts +219 -25
  235. package/src/daemon/conversation-evictor.ts +5 -24
  236. package/src/daemon/conversation-history.ts +10 -0
  237. package/src/daemon/conversation-interrupt-repair.ts +4 -43
  238. package/src/daemon/conversation-interrupt.ts +7 -11
  239. package/src/daemon/conversation-messaging.ts +58 -5
  240. package/src/daemon/conversation-mode-session.test.ts +1667 -0
  241. package/src/daemon/conversation-mode-session.ts +1008 -0
  242. package/src/daemon/conversation-process.ts +5 -0
  243. package/src/daemon/conversation-runtime-assembly.ts +10 -20
  244. package/src/daemon/conversation-skill-tools.ts +1 -0
  245. package/src/daemon/conversation-store.ts +4 -4
  246. package/src/daemon/conversation-surface-snapshots.ts +135 -0
  247. package/src/daemon/conversation-surfaces.ts +113 -5
  248. package/src/daemon/conversation-tool-setup.ts +8 -3
  249. package/src/daemon/conversation.ts +143 -41
  250. package/src/daemon/handlers/config-model.ts +7 -5
  251. package/src/daemon/handlers/shared.ts +55 -16
  252. package/src/daemon/host-cu-proxy.ts +51 -22
  253. package/src/daemon/lifecycle.ts +30 -13
  254. package/src/daemon/message-protocol.ts +0 -1
  255. package/src/daemon/message-provenance.ts +28 -4
  256. package/src/daemon/mode-session-startup-recovery.ts +23 -0
  257. package/src/daemon/mode-session-tracking.ts +54 -0
  258. package/src/daemon/process-message.ts +13 -4
  259. package/src/daemon/virtual-desktop-context.ts +21 -0
  260. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  261. package/src/desktop/desktop-accessibility-script.ts +88 -0
  262. package/src/desktop/desktop-accessibility.test.ts +160 -0
  263. package/src/desktop/desktop-accessibility.ts +169 -0
  264. package/src/desktop/desktop-automation-lease.test.ts +76 -68
  265. package/src/desktop/desktop-automation-lease.ts +41 -50
  266. package/src/desktop/desktop-browser-endpoint.ts +4 -0
  267. package/src/desktop/desktop-computer-use-routing.test.ts +330 -0
  268. package/src/desktop/desktop-computer-use.test.ts +449 -0
  269. package/src/desktop/desktop-computer-use.ts +502 -0
  270. package/src/desktop/desktop-dependencies.ts +47 -293
  271. package/src/desktop/desktop-display.ts +2 -2
  272. package/src/desktop/desktop-panel-config.test.ts +13 -3
  273. package/src/desktop/desktop-panel-config.ts +109 -15
  274. package/src/desktop/desktop-session-manager.test.ts +69 -21
  275. package/src/desktop/desktop-session-manager.ts +76 -22
  276. package/src/desktop/desktop-stream-bridge.test.ts +11 -122
  277. package/src/desktop/desktop-stream-bridge.ts +6 -8
  278. package/src/desktop/desktop-window-theme.ts +5 -25
  279. package/src/desktop/virtual-desktop-feature.ts +52 -2
  280. package/src/desktop/virtual-desktop-platform.test.ts +5 -9
  281. package/src/documents/document-store.ts +23 -0
  282. package/src/heartbeat/__tests__/heartbeat-service.test.ts +22 -26
  283. package/src/heartbeat/heartbeat-service.ts +5 -33
  284. package/src/ipc/__tests__/browser-ipc.test.ts +215 -6
  285. package/src/ipc/assistant-server.ts +2 -0
  286. package/src/ipc/routes/__tests__/channel-reply-ipc-routes.test.ts +111 -0
  287. package/src/ipc/routes/channel-reply-ipc-routes.ts +73 -0
  288. package/src/live-voice/__tests__/continuation-judge.test.ts +151 -0
  289. package/src/live-voice/__tests__/live-activity-reporter.test.ts +154 -15
  290. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +743 -31
  291. package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +191 -10
  292. package/src/live-voice/__tests__/live-voice-session-groups-gate.test.ts +87 -0
  293. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +5 -1
  294. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +41 -0
  295. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1017 -9
  296. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +224 -1
  297. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +101 -0
  298. package/src/live-voice/__tests__/live-voice-tts.test.ts +44 -0
  299. package/src/live-voice/__tests__/live-voice-vad.test.ts +939 -75
  300. package/src/live-voice/__tests__/microphone-idle-gate.test.ts +87 -0
  301. package/src/live-voice/__tests__/session-controls.test.ts +23 -0
  302. package/src/live-voice/__tests__/task-outcomes.test.ts +117 -0
  303. package/src/live-voice/__tests__/voice-input-diagnostics.test.ts +117 -0
  304. package/src/live-voice/camera-mode-session.test.ts +265 -0
  305. package/src/live-voice/camera-mode-session.ts +277 -0
  306. package/src/live-voice/continuation-judge.ts +123 -0
  307. package/src/live-voice/live-activity-reporter.ts +115 -6
  308. package/src/live-voice/live-voice-manager.ts +36 -1
  309. package/src/live-voice/live-voice-photo.ts +88 -7
  310. package/src/live-voice/live-voice-session-manager.ts +15 -0
  311. package/src/live-voice/live-voice-session.ts +1443 -747
  312. package/src/live-voice/live-voice-tts.ts +22 -4
  313. package/src/live-voice/microphone-idle-gate.ts +56 -0
  314. package/src/live-voice/protocol.ts +171 -4
  315. package/src/live-voice/session-controls.ts +11 -3
  316. package/src/live-voice/task-outcomes.ts +131 -0
  317. package/src/live-voice/voice-input-diagnostics.ts +117 -0
  318. package/src/mcp/__tests__/manager-tool-caps.test.ts +63 -18
  319. package/src/mcp/__tests__/tool-caps.test.ts +28 -1
  320. package/src/mcp/manager.ts +3 -0
  321. package/src/mcp/tool-caps.ts +12 -4
  322. package/src/media/__tests__/image-models.test.ts +58 -0
  323. package/src/media/image-credentials.ts +5 -0
  324. package/src/media/image-models.ts +52 -0
  325. package/src/media/image-service.ts +8 -0
  326. package/src/media/openrouter-image-service.test.ts +133 -0
  327. package/src/media/openrouter-image-service.ts +153 -0
  328. package/src/media/types.ts +1 -1
  329. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +65 -11
  330. package/src/messaging/providers/discord/send.ts +4 -17
  331. package/src/messaging/providers/discord/transport.ts +2 -2
  332. package/src/messaging/providers/outlook/client.ts +16 -22
  333. package/src/messaging/providers/telegram-bot/send.test.ts +236 -1
  334. package/src/messaging/providers/telegram-bot/send.ts +10 -7
  335. package/src/messaging/providers/telegram-bot/transport.ts +3 -10
  336. package/src/messaging/providers/whatsapp/send.test.ts +169 -4
  337. package/src/monitoring/__tests__/control.test.ts +17 -5
  338. package/src/monitoring/control.ts +15 -5
  339. package/src/notifications/AGENTS.md +1 -1
  340. package/src/notifications/README.md +83 -99
  341. package/src/notifications/__tests__/broadcaster.test.ts +1 -1
  342. package/src/notifications/__tests__/channel-allowlist.test.ts +39 -0
  343. package/src/notifications/__tests__/copy-composer.test.ts +1 -1
  344. package/src/notifications/__tests__/decision-engine.test.ts +79 -2
  345. package/src/notifications/__tests__/deterministic-checks.test.ts +10 -10
  346. package/src/notifications/__tests__/home-feed-side-effect.test.ts +93 -4
  347. package/src/notifications/__tests__/notification-utils.test.ts +18 -0
  348. package/src/notifications/adapters/macos.ts +37 -46
  349. package/src/notifications/broadcaster.ts +5 -6
  350. package/src/notifications/channel-allowlist.ts +44 -0
  351. package/src/notifications/copy-composer.ts +0 -23
  352. package/src/notifications/decision-engine.ts +24 -14
  353. package/src/notifications/deterministic-checks.ts +1 -1
  354. package/src/notifications/emit-signal.ts +123 -37
  355. package/src/notifications/home-feed-side-effect.ts +9 -7
  356. package/src/notifications/notification-utils.ts +19 -0
  357. package/src/notifications/signal.ts +4 -19
  358. package/src/notifications/types.ts +15 -0
  359. package/src/oauth/connection-resolver.ts +1 -2
  360. package/src/oauth/oauth-store.ts +1 -3
  361. package/src/oauth/provider-serializer.ts +7 -0
  362. package/src/oauth/seed-providers.ts +30 -0
  363. package/src/onboarding/onboarding-events-store.ts +99 -12
  364. package/src/persistence/__tests__/client-connection-events-store.test.ts +322 -0
  365. package/src/persistence/client-connection-events-store.ts +377 -0
  366. package/src/persistence/conversation-crud.ts +55 -6
  367. package/src/persistence/conversation-mode-sessions.test.ts +424 -0
  368. package/src/persistence/conversation-mode-sessions.ts +460 -0
  369. package/src/persistence/conversation-queries.ts +6 -19
  370. package/src/persistence/llm-usage-store.ts +3 -9
  371. package/src/persistence/message-metadata.ts +25 -0
  372. package/src/persistence/migrations/381-create-conversation-mode-sessions.ts +41 -0
  373. package/src/persistence/migrations/382-create-client-connection-events.ts +40 -0
  374. package/src/persistence/migrations/__tests__/382-create-client-connection-events.test.ts +56 -0
  375. package/src/persistence/schema/conversations.ts +67 -0
  376. package/src/persistence/schema/infrastructure.ts +29 -0
  377. package/src/persistence/steps.ts +17 -0
  378. package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
  379. package/src/plugin-api/effective-context-window.ts +33 -0
  380. package/src/plugin-api/index.ts +24 -5
  381. package/src/plugin-api/plugin-name-env.ts +125 -0
  382. package/src/plugin-api/resolve-credential.ts +79 -20
  383. package/src/plugin-api/vision-support.test.ts +22 -0
  384. package/src/plugin-api/vision-support.ts +17 -5
  385. package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
  386. package/src/plugins/active-plugin-env.ts +51 -0
  387. package/src/plugins/defaults/index.ts +24 -0
  388. package/src/plugins/defaults/memory/__tests__/archive-removal-framing.test.ts +64 -0
  389. package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
  390. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +9 -12
  391. package/src/plugins/defaults/memory/context-search/agent-runner.ts +78 -22
  392. package/src/plugins/defaults/memory/context-search/format.ts +51 -7
  393. package/src/plugins/defaults/memory/context-search/limits.ts +10 -12
  394. package/src/plugins/defaults/memory/context-search/recall-input.ts +38 -0
  395. package/src/plugins/defaults/memory/context-search/search.ts +8 -14
  396. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +1 -0
  397. package/src/plugins/defaults/memory/context-search/types.ts +1 -11
  398. package/src/plugins/defaults/memory/graph/__tests__/handle-remember-v2.test.ts +11 -4
  399. package/src/plugins/defaults/memory/graph/tool-handlers.ts +12 -21
  400. package/src/plugins/defaults/memory/graph/tools.ts +35 -64
  401. package/src/plugins/defaults/memory/src/memory-eval-routes.ts +2 -3
  402. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +5 -0
  403. package/src/plugins/defaults/memory/src/memory-item-routes.ts +2 -1
  404. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +2 -2
  405. package/src/plugins/defaults/memory/tools.test.ts +124 -9
  406. package/src/plugins/defaults/memory/tools.ts +38 -11
  407. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +165 -27
  408. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +626 -27
  409. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +46 -27
  410. package/src/plugins/defaults/memory/v3/orchestrate.ts +78 -38
  411. package/src/plugins/defaults/memory/v3/pool-log-store.ts +65 -79
  412. package/src/plugins/defaults/memory/v3/pool-select.test.ts +14 -5
  413. package/src/plugins/defaults/memory/v3/pool-select.ts +683 -44
  414. package/src/plugins/defaults/memory/worker-control.ts +15 -5
  415. package/src/plugins/defaults/task-progress/__tests__/registration.test.ts +70 -0
  416. package/src/plugins/defaults/task-progress/__tests__/task-progress-context.test.ts +380 -0
  417. package/src/plugins/defaults/task-progress/hooks/post-compact.ts +19 -0
  418. package/src/plugins/defaults/task-progress/hooks/user-prompt-submit.ts +24 -0
  419. package/src/plugins/defaults/task-progress/package.json +14 -0
  420. package/src/plugins/defaults/task-progress/src/apply-task-progress-context.ts +93 -0
  421. package/src/plugins/defaults/task-progress/src/format-task-progress-context.ts +67 -0
  422. package/src/providers/__tests__/context-overflow-error.test.ts +10 -0
  423. package/src/providers/__tests__/search-provider-catalog.test.ts +18 -0
  424. package/src/providers/anthropic/client.ts +10 -1
  425. package/src/providers/anthropic-gateway-shared.ts +6 -0
  426. package/src/providers/fetch-provider-catalog.ts +11 -0
  427. package/src/providers/gemini/client.ts +25 -8
  428. package/src/providers/inference/adapter-factory.ts +1 -1
  429. package/src/providers/jev/client.test.ts +10 -0
  430. package/src/providers/jev/client.ts +20 -4
  431. package/src/providers/model-catalog.ts +4 -3
  432. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +94 -1
  433. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +106 -1
  434. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +24 -0
  435. package/src/providers/openai/__tests__/google-thought-signature.test.ts +1 -2
  436. package/src/providers/openai/chat-completions-provider.ts +100 -29
  437. package/src/providers/openai/google-thought-signature.ts +0 -6
  438. package/src/providers/openai/responses-provider.ts +23 -1
  439. package/src/providers/ratelimit.ts +1 -3
  440. package/src/providers/search-provider-catalog.ts +24 -0
  441. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +24 -0
  442. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +7 -1
  443. package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +50 -1
  444. package/src/providers/speech-to-text/deepgram-flux-frames.ts +11 -0
  445. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +13 -0
  446. package/src/providers/types.ts +5 -0
  447. package/src/routes/control.ts +7 -5
  448. package/src/runtime/AGENTS.md +4 -2
  449. package/src/runtime/actor-trust-resolver.ts +3 -3
  450. package/src/runtime/agent-wake.ts +1 -1
  451. package/src/runtime/approval-message-composer.ts +3 -69
  452. package/src/runtime/assistant-event-hub.ts +73 -75
  453. package/src/runtime/assistant-event-publish-options.ts +7 -0
  454. package/src/runtime/assistant-event-targeting.ts +79 -0
  455. package/src/runtime/assistant-stream-state.ts +13 -100
  456. package/src/runtime/background-job-runner.ts +5 -5
  457. package/src/runtime/channel-retry-sweep.ts +1 -0
  458. package/src/runtime/guardian-action-message-composer.ts +8 -161
  459. package/src/runtime/guardian-reply-router.ts +28 -32
  460. package/src/runtime/http-types.ts +6 -0
  461. package/src/runtime/message-composer-types.ts +3 -61
  462. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +67 -1
  463. package/src/runtime/migrations/vbundle-builder.ts +24 -1
  464. package/src/runtime/migrations/vbundle-import-analyzer.ts +13 -4
  465. package/src/runtime/migrations/vbundle-import-policy.ts +10 -0
  466. package/src/runtime/migrations/vbundle-importer.ts +4 -1
  467. package/src/runtime/migrations/vbundle-streaming-importer.ts +4 -1
  468. package/src/runtime/migrations/vbundle-validator.ts +1 -0
  469. package/src/runtime/pending-interactions-mode-session.test.ts +62 -0
  470. package/src/runtime/pending-interactions.ts +29 -0
  471. package/src/runtime/routes/__tests__/client-history-routes.test.ts +104 -0
  472. package/src/runtime/routes/__tests__/image-generation-routes.test.ts +97 -0
  473. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +1 -1
  474. package/src/runtime/routes/__tests__/roadmap-routes.test.ts +29 -6
  475. package/src/runtime/routes/browser-context.ts +31 -11
  476. package/src/runtime/routes/browser-routes.ts +43 -4
  477. package/src/runtime/routes/btw-routes.ts +9 -4
  478. package/src/runtime/routes/client-routes.ts +108 -0
  479. package/src/runtime/routes/conversation-management-routes.ts +4 -1
  480. package/src/runtime/routes/conversation-routes.ts +113 -27
  481. package/src/runtime/routes/credential-prompt-routes.ts +4 -3
  482. package/src/runtime/routes/credential-routes.ts +1 -2
  483. package/src/runtime/routes/desktop-setup-routes.test.ts +9 -18
  484. package/src/runtime/routes/desktop-setup-routes.ts +4 -6
  485. package/src/runtime/routes/documents-routes.ts +51 -0
  486. package/src/runtime/routes/email-routes.ts +3 -7
  487. package/src/runtime/routes/events-routes.ts +75 -54
  488. package/src/runtime/routes/identity-routes.ts +1 -5
  489. package/src/runtime/routes/image-generation-routes.ts +8 -14
  490. package/src/runtime/routes/inbound-message-handler.ts +35 -38
  491. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.test.ts +168 -0
  492. package/src/runtime/routes/inbound-stages/backfill-sender-provenance.ts +96 -0
  493. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +38 -9
  494. package/src/runtime/routes/inbound-stages/background-dispatch.ts +2 -5
  495. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +2 -0
  496. package/src/runtime/routes/llm-context-normalization.ts +1 -7
  497. package/src/runtime/routes/migration-routes.ts +73 -2
  498. package/src/runtime/routes/notification-routes.ts +10 -0
  499. package/src/runtime/routes/oauth-providers.ts +7 -0
  500. package/src/runtime/routes/plugins-routes.ts +6 -0
  501. package/src/runtime/routes/roadmap-routes.ts +47 -8
  502. package/src/runtime/routes/secret-routes.ts +1 -1
  503. package/src/runtime/routes/settings-routes.ts +10 -4
  504. package/src/runtime/routes/watcher-routes.ts +1 -3
  505. package/src/runtime/trust-class.test.ts +7 -6
  506. package/src/runtime/trust-class.ts +9 -17
  507. package/src/schedule/__tests__/worker-control.test.ts +36 -5
  508. package/src/schedule/worker-control.ts +15 -5
  509. package/src/security/secure-keys.ts +53 -103
  510. package/src/skills/clawhub.ts +3 -8
  511. package/src/skills/managed-store.ts +1 -5
  512. package/src/stt/types.ts +4 -0
  513. package/src/subagent/notify.ts +54 -34
  514. package/src/subagent/parent-notification.ts +6 -0
  515. package/src/telemetry/__tests__/phone-call-funnel.test.ts +72 -0
  516. package/src/telemetry/phone-call-funnel.ts +144 -0
  517. package/src/tools/__tests__/tool-schema-root-combinator-guard.test.ts +81 -0
  518. package/src/tools/ask-question/ask-question-tool.ts +6 -4
  519. package/src/tools/browser/browser-execution.ts +22 -59
  520. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +561 -39
  521. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +435 -39
  522. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +1 -8
  523. package/src/tools/client-os.ts +15 -0
  524. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
  525. package/src/tools/computer-use/target.ts +42 -0
  526. package/src/tools/document/document-tool.ts +13 -18
  527. package/src/tools/execution-target.ts +14 -16
  528. package/src/tools/host-terminal/host-shell.ts +4 -1
  529. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +250 -0
  530. package/src/tools/network/__tests__/web-search.test.ts +203 -0
  531. package/src/tools/network/web-fetch.ts +274 -7
  532. package/src/tools/network/web-search.ts +468 -6
  533. package/src/tools/permission-checker.ts +3 -3
  534. package/src/tools/policy-context.ts +3 -1
  535. package/src/tools/skills/load.ts +2 -1
  536. package/src/tools/skills/sandbox-runner.ts +11 -2
  537. package/src/tools/skills/skill-script-runner.ts +7 -0
  538. package/src/tools/skills/skill-tool-factory.ts +15 -1
  539. package/src/tools/terminal/__tests__/safe-env.test.ts +11 -0
  540. package/src/tools/terminal/safe-env.ts +4 -0
  541. package/src/tools/terminal/shell.ts +2 -0
  542. package/src/tools/tool-approval-handler.ts +5 -1
  543. package/src/tools/tool-types.ts +3 -3
  544. package/src/tools/types.ts +8 -2
  545. package/src/util/__tests__/worker-process-command.test.ts +5 -4
  546. package/src/util/abort-reasons.ts +18 -26
  547. package/src/util/canonicalize-identity.test.ts +21 -0
  548. package/src/util/canonicalize-identity.ts +16 -0
  549. package/src/util/errors.ts +13 -3
  550. package/src/util/provider-error-patterns.ts +3 -0
  551. package/src/util/worker-process.ts +77 -35
  552. package/src/workspace/migrations/157-add-desktop-files-launcher.ts +94 -0
  553. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +4 -10
  554. package/src/workspace/migrations/__tests__/157-add-desktop-files-launcher.test.ts +113 -0
  555. package/src/workspace/migrations/registry.ts +2 -0
  556. package/src/__tests__/computer-use-window-schema.test.ts +0 -24
  557. package/src/api/events/heartbeat-alert.ts +0 -20
  558. package/src/daemon/message-types/web-activity.ts +0 -65
  559. package/src/desktop/desktop-dependencies.test.ts +0 -131
  560. package/src/tools/computer-use/definitions.ts +0 -607
@@ -10,7 +10,15 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
10
10
  import type { ResolvedMcpConfig } from "../../config/schemas/mcp.js";
11
11
 
12
12
  const toolsByServer = new Map<string, Array<{ name: string }>>();
13
- const connectDelays = new Map<string, number>();
13
+ const connectGates = new Map<string, Promise<void>>();
14
+ const listedServers = new Set<string>();
15
+ let mcpGlobalMaxTools: number | undefined;
16
+
17
+ mock.module("../../config/loader.js", () => ({
18
+ getConfig: () => ({
19
+ tools: { exclude: [], mcpGlobalMaxTools },
20
+ }),
21
+ }));
14
22
 
15
23
  mock.module("../client.js", () => ({
16
24
  McpClient: class {
@@ -22,12 +30,13 @@ mock.module("../client.js", () => ({
22
30
  return null;
23
31
  }
24
32
  async connect() {
25
- const delay = connectDelays.get(this.serverId) ?? 0;
26
- if (delay > 0) {
27
- await new Promise((resolve) => setTimeout(resolve, delay));
33
+ const gate = connectGates.get(this.serverId);
34
+ if (gate) {
35
+ await gate;
28
36
  }
29
37
  }
30
38
  async listTools() {
39
+ listedServers.add(this.serverId);
31
40
  return (toolsByServer.get(this.serverId) ?? []).map((tool) => ({
32
41
  name: tool.name,
33
42
  description: `${this.serverId} ${tool.name}`,
@@ -59,7 +68,9 @@ function configWith(ids: string[]): ResolvedMcpConfig {
59
68
  describe("McpServerManager tool selection", () => {
60
69
  beforeEach(() => {
61
70
  toolsByServer.clear();
62
- connectDelays.clear();
71
+ connectGates.clear();
72
+ listedServers.clear();
73
+ mcpGlobalMaxTools = undefined;
63
74
  });
64
75
 
65
76
  test("later servers keep tools when eight servers exceed the global cap", async () => {
@@ -90,22 +101,56 @@ describe("McpServerManager tool selection", () => {
90
101
  test("a slow earlier server does not prevent later servers from connecting", async () => {
91
102
  toolsByServer.set("slow", [{ name: "slow_tool" }]);
92
103
  toolsByServer.set("fast", [{ name: "fast_tool" }]);
93
- connectDelays.set("slow", 40);
104
+ let releaseSlow!: () => void;
105
+ connectGates.set(
106
+ "slow",
107
+ new Promise<void>((resolve) => {
108
+ releaseSlow = resolve;
109
+ }),
110
+ );
94
111
 
95
112
  const manager = new McpServerManager();
96
- const startedAt = Date.now();
97
- const started = await manager.start(configWith(["slow", "fast"]));
98
- const elapsed = Date.now() - startedAt;
113
+ const starting = manager.start(configWith(["slow", "fast"]));
114
+ try {
115
+ await new Promise<void>((resolve) => setImmediate(resolve));
116
+ expect([...listedServers]).toEqual(["fast"]);
99
117
 
100
- expect(started.connectedServerCount).toBe(2);
101
- expect(started.servers.map((server) => server.serverId).sort()).toEqual([
102
- "fast",
103
- "slow",
104
- ]);
105
- expect(
106
- elapsed < 80,
107
- "Servers connect in parallel, so one 40ms delay should not serialize both.",
108
- ).toBe(true);
118
+ releaseSlow();
119
+ const started = await starting;
120
+ expect(started.connectedServerCount).toBe(2);
121
+ expect(started.servers.map((server) => server.serverId).sort()).toEqual([
122
+ "fast",
123
+ "slow",
124
+ ]);
125
+ } finally {
126
+ releaseSlow();
127
+ await starting;
128
+ await manager.stop();
129
+ }
130
+ });
131
+
132
+ test("a workspace global-max override raises how many tools are kept", async () => {
133
+ const ids = Array.from(
134
+ { length: 8 },
135
+ (_, i) => `server-${String(i + 1).padStart(2, "0")}`,
136
+ );
137
+ for (const id of ids) {
138
+ toolsByServer.set(
139
+ id,
140
+ Array.from({ length: 10 }, (_, i) => ({ name: `tool_${i}` })),
141
+ );
142
+ }
143
+
144
+ mcpGlobalMaxTools = 80;
145
+ const manager = new McpServerManager();
146
+ const started = await manager.start(configWith(ids));
147
+
148
+ expect(started.discoveredToolCount).toBe(80);
149
+ expect(started.keptToolCount).toBe(80);
150
+ expect(started.droppedToolCount).toBe(0);
151
+ expect(started.servers.every((server) => server.tools.length === 10)).toBe(
152
+ true,
153
+ );
109
154
  await manager.stop();
110
155
  });
111
156
  });
@@ -10,7 +10,7 @@ import {
10
10
  MCP_GLOBAL_MAX_TOOLS,
11
11
  MCP_MAX_TOOLS_PER_SERVER,
12
12
  } from "../../config/schemas/mcp.js";
13
- import { applyMcpToolCaps } from "../tool-caps.js";
13
+ import { applyMcpToolCaps, resolveMcpGlobalMaxTools } from "../tool-caps.js";
14
14
 
15
15
  function toolsNamed(prefix: string, count: number): string[] {
16
16
  return Array.from({ length: count }, (_, i) => `${prefix}-tool-${i}`);
@@ -104,4 +104,31 @@ describe("applyMcpToolCaps", () => {
104
104
  4,
105
105
  );
106
106
  });
107
+
108
+ test("honors a workspace global-max override", () => {
109
+ const servers = Array.from({ length: 8 }, (_, i) => ({
110
+ serverId: `server-${String(i + 1).padStart(2, "0")}`,
111
+ tools: toolsNamed(`s${i + 1}`, 10),
112
+ }));
113
+
114
+ const result = applyMcpToolCaps(servers, { globalMax: 80, perServerMax: 20 });
115
+
116
+ expect(result.keptToolCount).toBe(80);
117
+ expect(result.droppedToolCount).toBe(0);
118
+ expect(result.servers.every((server) => server.tools.length === 10)).toBe(
119
+ true,
120
+ );
121
+ });
122
+ });
123
+
124
+ describe("resolveMcpGlobalMaxTools", () => {
125
+ test("uses the shipped default when the workspace omits the override", () => {
126
+ expect(resolveMcpGlobalMaxTools()).toBe(MCP_GLOBAL_MAX_TOOLS);
127
+ expect(resolveMcpGlobalMaxTools({})).toBe(MCP_GLOBAL_MAX_TOOLS);
128
+ });
129
+
130
+ test("uses tools.mcpGlobalMaxTools from config.json when set", () => {
131
+ expect(resolveMcpGlobalMaxTools({ mcpGlobalMaxTools: 80 })).toBe(80);
132
+ expect(resolveMcpGlobalMaxTools({ mcpGlobalMaxTools: 1 })).toBe(1);
133
+ });
107
134
  });
@@ -1,3 +1,4 @@
1
+ import { getConfig } from "../config/loader.js";
1
2
  import type {
2
3
  ResolvedMcpConfig,
3
4
  ResolvedMcpServerConfig,
@@ -6,6 +7,7 @@ import { getLogger } from "../util/logger.js";
6
7
  import { McpClient, type McpToolInfo } from "./client.js";
7
8
  import {
8
9
  applyMcpToolCaps,
10
+ resolveMcpGlobalMaxTools,
9
11
  truncatedServerIdsFromCaps,
10
12
  } from "./tool-caps.js";
11
13
 
@@ -78,6 +80,7 @@ export class McpServerManager {
78
80
  serverId: result.serverId,
79
81
  tools: result.tools,
80
82
  })),
83
+ { globalMax: resolveMcpGlobalMaxTools(getConfig().tools) },
81
84
  );
82
85
  const keptByServer = new Map(
83
86
  capped.servers.map((server) => [server.serverId, server.tools]),
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Code-owned MCP tool-count caps and the selection used when they bind.
3
3
  *
4
- * Caps live here, not in workspace config: migration 153 strips
5
- * `maxTools` / `globalMaxTools` from `config.json`. The numbers are a
6
- * context-window budget, not a permission gate. Allowlists and risk
7
- * still decide what a turn may call.
4
+ * The shipped global cap is {@link MCP_GLOBAL_MAX_TOOLS}. A workspace may
5
+ * raise or lower it with `tools.mcpGlobalMaxTools` in config.json. The
6
+ * per-server cap stays code-owned. These numbers are a context-window
7
+ * budget, not a permission gate. Allowlists and risk still decide what
8
+ * a turn may call.
8
9
  *
9
10
  * The global cap is applied by a deterministic round-robin across
10
11
  * servers sorted by id. Insertion order must not empty a later server
@@ -15,6 +16,7 @@ import {
15
16
  MCP_GLOBAL_MAX_TOOLS,
16
17
  MCP_MAX_TOOLS_PER_SERVER,
17
18
  } from "../config/schemas/mcp.js";
19
+ import type { ToolsConfig } from "../config/schemas/tools.js";
18
20
 
19
21
  export interface McpServerToolSet<T> {
20
22
  serverId: string;
@@ -39,6 +41,12 @@ export interface McpToolCapResult<T> {
39
41
  decisions: McpToolCapDecision[];
40
42
  }
41
43
 
44
+ export function resolveMcpGlobalMaxTools(
45
+ tools?: Pick<ToolsConfig, "mcpGlobalMaxTools">,
46
+ ): number {
47
+ return tools?.mcpGlobalMaxTools ?? MCP_GLOBAL_MAX_TOOLS;
48
+ }
49
+
42
50
  export function applyMcpToolCaps<T>(
43
51
  servers: ReadonlyArray<McpServerToolSet<T>>,
44
52
  options?: { globalMax?: number; perServerMax?: number },
@@ -2,9 +2,12 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  DEFAULT_IMAGE_MODEL,
5
+ DEFAULT_OPENROUTER_IMAGE_MODEL,
5
6
  describeImageModels,
6
7
  IMAGE_MODELS,
8
+ qualifyImageModelForOpenRouter,
7
9
  resolveImageModel,
10
+ resolveRequestedImageModel,
8
11
  } from "../image-models.js";
9
12
  import { providerForModel } from "../image-service.js";
10
13
 
@@ -55,3 +58,58 @@ describe("image model registry", () => {
55
58
  }
56
59
  });
57
60
  });
61
+
62
+ describe("resolveRequestedImageModel", () => {
63
+ test("qualifies built-in aliases for OpenRouter", () => {
64
+ expect(resolveRequestedImageModel("fast", "openrouter")).toEqual({
65
+ model: "google/gemini-3.1-flash-image-preview",
66
+ });
67
+ expect(resolveRequestedImageModel("openai", "openrouter")).toEqual({
68
+ model: "openai/gpt-image-2",
69
+ });
70
+ expect(qualifyImageModelForOpenRouter("quality")).toBe(
71
+ "google/gemini-3-pro-image-preview",
72
+ );
73
+ });
74
+
75
+ test("qualifies bare built-in IDs for OpenRouter", () => {
76
+ expect(
77
+ resolveRequestedImageModel("gemini-3.1-flash-image-preview", "openrouter"),
78
+ ).toEqual({ model: DEFAULT_OPENROUTER_IMAGE_MODEL });
79
+ expect(resolveRequestedImageModel("gpt-image-2", "openrouter")).toEqual({
80
+ model: "openai/gpt-image-2",
81
+ });
82
+ });
83
+
84
+ test("accepts arbitrary trimmed OpenRouter slugs", () => {
85
+ expect(
86
+ resolveRequestedImageModel(" google/gemini-3.5-flash ", "openrouter"),
87
+ ).toEqual({ model: "google/gemini-3.5-flash" });
88
+ expect(
89
+ resolveRequestedImageModel("openai/gpt-image-2", "openrouter"),
90
+ ).toEqual({ model: "openai/gpt-image-2" });
91
+ });
92
+
93
+ test("rejects unknown models for built-in providers", () => {
94
+ const gemini = resolveRequestedImageModel(
95
+ "google/gemini-3.5-flash",
96
+ "gemini",
97
+ );
98
+ expect(gemini.model).toBeUndefined();
99
+ expect(gemini.error).toContain("Unknown model");
100
+
101
+ const openai = resolveRequestedImageModel("black-forest-labs/flux", "openai");
102
+ expect(openai.model).toBeUndefined();
103
+ expect(openai.error).toContain("Unknown model");
104
+
105
+ expect(resolveRequestedImageModel("fast", "vellum")).toEqual({
106
+ model: "gemini-3.1-flash-image-preview",
107
+ });
108
+ });
109
+
110
+ test("ignores empty or non-string input", () => {
111
+ expect(resolveRequestedImageModel("", "openrouter")).toEqual({});
112
+ expect(resolveRequestedImageModel(" ", "openrouter")).toEqual({});
113
+ expect(resolveRequestedImageModel(undefined, "gemini")).toEqual({});
114
+ });
115
+ });
@@ -37,6 +37,9 @@ export function resolveImageGenRouting(
37
37
  : svc.model;
38
38
  return { backendProvider: providerForImageModelPrefix(model), managed };
39
39
  }
40
+ if (svc.provider === "openrouter") {
41
+ return { backendProvider: "openrouter", managed: false };
42
+ }
40
43
  return {
41
44
  backendProvider: providerForModel(
42
45
  modelOverride,
@@ -101,5 +104,7 @@ function providerKeyHint(provider: ImageGenProvider): string {
101
104
  return "No Gemini API key configured. Please set your Gemini API key in Settings → Models & Services.";
102
105
  case "openai":
103
106
  return "No OpenAI API key configured. Please set your OpenAI API key in Settings → Models & Services.";
107
+ case "openrouter":
108
+ return "No OpenRouter API key configured. Please set your OpenRouter API key in Settings → Models & Services.";
104
109
  }
105
110
  }
@@ -55,6 +55,58 @@ export function resolveImageModel(model: string): ImageModelEntry | undefined {
55
55
  return IMAGE_MODELS.find((m) => m.alias === model || m.id === model);
56
56
  }
57
57
 
58
+ const OPENROUTER_VENDOR_PREFIX: Record<ImageModelProvider, string> = {
59
+ gemini: "google",
60
+ openai: "openai",
61
+ };
62
+
63
+ export const DEFAULT_OPENROUTER_IMAGE_MODEL =
64
+ "google/gemini-3.1-flash-image-preview";
65
+
66
+ /**
67
+ * Qualify a built-in alias or concrete ID for OpenRouter's Images API.
68
+ * Unknown values are returned trimmed so live catalog slugs pass through.
69
+ */
70
+ export function qualifyImageModelForOpenRouter(model: string): string {
71
+ const trimmed = model.trim();
72
+ const entry = resolveImageModel(trimmed);
73
+ if (!entry) {
74
+ return trimmed;
75
+ }
76
+ return `${OPENROUTER_VENDOR_PREFIX[entry.provider]}/${entry.id}`;
77
+ }
78
+
79
+ /**
80
+ * Resolve a caller-supplied image model against the configured provider.
81
+ *
82
+ * OpenRouter accepts any trimmed slug from its live catalog. Built-in
83
+ * aliases and bare IDs are qualified (`fast` ->
84
+ * `google/gemini-3.1-flash-image-preview`). Gemini, OpenAI, and Vellum
85
+ * still require a registry match.
86
+ */
87
+ export function resolveRequestedImageModel(
88
+ model: unknown,
89
+ configuredProvider: string,
90
+ ): { model?: string; error?: string } {
91
+ if (typeof model !== "string" || !model) {
92
+ return {};
93
+ }
94
+ const trimmed = model.trim();
95
+ if (!trimmed) {
96
+ return {};
97
+ }
98
+ if (configuredProvider === "openrouter") {
99
+ return { model: qualifyImageModelForOpenRouter(trimmed) };
100
+ }
101
+ const entry = resolveImageModel(trimmed);
102
+ if (!entry) {
103
+ return {
104
+ error: `Unknown model "${trimmed}". Available models and aliases:\n${describeImageModels()}`,
105
+ };
106
+ }
107
+ return { model: entry.id };
108
+ }
109
+
58
110
  /**
59
111
  * One line per model: "alias -> id (label)". Used in CLI help text and in
60
112
  * unknown-model error messages so the available set is always current.
@@ -3,6 +3,10 @@ import {
3
3
  mapGeminiError,
4
4
  } from "./gemini-image-service.js";
5
5
  import { generateImageOpenAI, mapOpenAIError } from "./openai-image-service.js";
6
+ import {
7
+ generateImageOpenRouter,
8
+ mapOpenRouterError,
9
+ } from "./openrouter-image-service.js";
6
10
  import type {
7
11
  ImageGenCredentials,
8
12
  ImageGenerationRequest,
@@ -23,6 +27,8 @@ export function generateImage(
23
27
  return generateImageOpenAI(credentials, request);
24
28
  case "gemini":
25
29
  return generateImageGemini(credentials, request);
30
+ case "openrouter":
31
+ return generateImageOpenRouter(credentials, request);
26
32
  default: {
27
33
  const _exhaustive: never = provider;
28
34
  throw new Error(`Unknown image generation provider: ${_exhaustive}`);
@@ -42,6 +48,8 @@ export function mapImageGenError(
42
48
  return mapOpenAIError(error);
43
49
  case "gemini":
44
50
  return mapGeminiError(error);
51
+ case "openrouter":
52
+ return mapOpenRouterError(error);
45
53
  default: {
46
54
  const _exhaustive: never = provider;
47
55
  throw new Error(`Unknown image generation provider: ${_exhaustive}`);
@@ -0,0 +1,133 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import {
4
+ generateImageOpenRouter,
5
+ mapOpenRouterError,
6
+ } from "./openrouter-image-service.js";
7
+
8
+ const originalFetch = globalThis.fetch;
9
+
10
+ afterEach(() => {
11
+ globalThis.fetch = originalFetch;
12
+ });
13
+
14
+ describe("generateImageOpenRouter", () => {
15
+ test("generates images through the dedicated OpenRouter Images API", async () => {
16
+ let request: Request | undefined;
17
+ globalThis.fetch = mock(async (input, init) => {
18
+ request = new Request(input, init);
19
+ return Response.json({
20
+ data: [
21
+ { b64_json: "png-data", media_type: "image/png" },
22
+ { b64_json: "svg-data", media_type: "image/svg+xml" },
23
+ ],
24
+ });
25
+ }) as unknown as typeof fetch;
26
+
27
+ const result = await generateImageOpenRouter(
28
+ { type: "direct", apiKey: "or-key" },
29
+ {
30
+ prompt: "a friendly robot",
31
+ mode: "generate",
32
+ model: "google/gemini-3.1-flash-image-preview",
33
+ variants: 2,
34
+ },
35
+ );
36
+
37
+ expect(request?.url).toBe("https://openrouter.ai/api/v1/images");
38
+ expect(request?.headers.get("authorization")).toBe("Bearer or-key");
39
+ expect(await request?.json()).toEqual({
40
+ model: "google/gemini-3.1-flash-image-preview",
41
+ prompt: "a friendly robot",
42
+ n: 2,
43
+ });
44
+ expect(result).toEqual({
45
+ images: [
46
+ { mimeType: "image/png", dataBase64: "png-data" },
47
+ { mimeType: "image/svg+xml", dataBase64: "svg-data" },
48
+ ],
49
+ text: undefined,
50
+ resolvedModel: "google/gemini-3.1-flash-image-preview",
51
+ });
52
+ });
53
+
54
+ test("sends source images as data URL references for edits", async () => {
55
+ let body: Record<string, unknown> | undefined;
56
+ globalThis.fetch = mock(async (_input, init) => {
57
+ body = JSON.parse(String(init?.body)) as Record<string, unknown>;
58
+ return Response.json({ data: [] });
59
+ }) as unknown as typeof fetch;
60
+
61
+ await generateImageOpenRouter(
62
+ { type: "direct", apiKey: "or-key" },
63
+ {
64
+ prompt: "make it watercolor",
65
+ mode: "edit",
66
+ sourceImages: [{ mimeType: "image/jpeg", dataBase64: "abc123" }],
67
+ },
68
+ );
69
+
70
+ expect(body?.input_references).toEqual([
71
+ {
72
+ type: "image_url",
73
+ image_url: { url: "data:image/jpeg;base64,abc123" },
74
+ },
75
+ ]);
76
+ });
77
+
78
+ test("qualifies a bare built-in model ID at the backend boundary", async () => {
79
+ let body: Record<string, unknown> | undefined;
80
+ globalThis.fetch = mock(async (_input, init) => {
81
+ body = JSON.parse(String(init?.body)) as Record<string, unknown>;
82
+ return Response.json({ data: [] });
83
+ }) as unknown as typeof fetch;
84
+
85
+ const result = await generateImageOpenRouter(
86
+ { type: "direct", apiKey: "or-key" },
87
+ {
88
+ prompt: "a cat",
89
+ mode: "generate",
90
+ model: "gemini-3.1-flash-image-preview",
91
+ },
92
+ );
93
+
94
+ expect(body?.model).toBe("google/gemini-3.1-flash-image-preview");
95
+ expect(result.resolvedModel).toBe("google/gemini-3.1-flash-image-preview");
96
+ });
97
+
98
+ test("defaults an empty model to the OpenRouter Gemini Flash slug", async () => {
99
+ let body: Record<string, unknown> | undefined;
100
+ globalThis.fetch = mock(async (_input, init) => {
101
+ body = JSON.parse(String(init?.body)) as Record<string, unknown>;
102
+ return Response.json({ data: [] });
103
+ }) as unknown as typeof fetch;
104
+
105
+ await generateImageOpenRouter(
106
+ { type: "direct", apiKey: "or-key" },
107
+ { prompt: "a cat", mode: "generate" },
108
+ );
109
+
110
+ expect(body?.model).toBe("google/gemini-3.1-flash-image-preview");
111
+ });
112
+
113
+ test("surfaces the provider's invalid-request message", async () => {
114
+ globalThis.fetch = mock(async () =>
115
+ Response.json(
116
+ { error: { message: "Model does not support image output" } },
117
+ { status: 400 },
118
+ ),
119
+ ) as unknown as typeof fetch;
120
+
121
+ try {
122
+ await generateImageOpenRouter(
123
+ { type: "direct", apiKey: "or-key" },
124
+ { prompt: "a cat", mode: "generate", model: "text-only/model" },
125
+ );
126
+ throw new Error("expected request to fail");
127
+ } catch (error) {
128
+ expect(mapOpenRouterError(error)).toBe(
129
+ "The OpenRouter image request was invalid: Model does not support image output",
130
+ );
131
+ }
132
+ });
133
+ });
@@ -0,0 +1,153 @@
1
+ import {
2
+ DEFAULT_OPENROUTER_IMAGE_MODEL,
3
+ qualifyImageModelForOpenRouter,
4
+ resolveImageModel,
5
+ } from "./image-models.js";
6
+ import {
7
+ type GeneratedImage,
8
+ type ImageGenCredentials,
9
+ type ImageGenerationRequest,
10
+ type ImageGenerationResult,
11
+ isImageProviderBillingError,
12
+ MAX_VARIANTS,
13
+ } from "./types.js";
14
+
15
+ const OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images";
16
+
17
+ /**
18
+ * Qualify a bare built-in ID or alias for OpenRouter. Call sites that skip
19
+ * tool-level validation (app-icon, avatar) still reach this boundary.
20
+ */
21
+ function resolveOpenRouterModel(model: string | undefined): string {
22
+ const trimmed = model?.trim();
23
+ if (!trimmed) {
24
+ return DEFAULT_OPENROUTER_IMAGE_MODEL;
25
+ }
26
+ if (resolveImageModel(trimmed)) {
27
+ return qualifyImageModelForOpenRouter(trimmed);
28
+ }
29
+ return trimmed;
30
+ }
31
+
32
+ const OPENROUTER_BILLING_MESSAGE =
33
+ "Image generation is unavailable because the OpenRouter account or API key is out of credits. " +
34
+ "Add funds with the provider or update the key in Settings. Retrying won't help until credits are added.";
35
+
36
+ class OpenRouterImageError extends Error {
37
+ constructor(
38
+ readonly status: number,
39
+ message: string,
40
+ ) {
41
+ super(message);
42
+ this.name = "OpenRouterImageError";
43
+ }
44
+ }
45
+
46
+ export function mapOpenRouterError(error: unknown): string {
47
+ if (error instanceof OpenRouterImageError) {
48
+ if (
49
+ isImageProviderBillingError({
50
+ status: error.status,
51
+ message: error.message,
52
+ })
53
+ ) {
54
+ return OPENROUTER_BILLING_MESSAGE;
55
+ }
56
+ if (error.status === 400) {
57
+ return `The OpenRouter image request was invalid: ${error.message}`;
58
+ }
59
+ if (error.status === 401 || error.status === 403) {
60
+ return "Authentication failed. Please check your OpenRouter API key.";
61
+ }
62
+ if (error.status === 429) {
63
+ return "OpenRouter rate limit exceeded. Please wait a moment and try again.";
64
+ }
65
+ if (error.status >= 500) {
66
+ return "The OpenRouter service is temporarily unavailable. Please try again later.";
67
+ }
68
+ return `OpenRouter API error (status ${error.status}). Please try again.`;
69
+ }
70
+ if (error instanceof Error) {
71
+ if (isImageProviderBillingError({ message: error.message })) {
72
+ return OPENROUTER_BILLING_MESSAGE;
73
+ }
74
+ return `Image generation failed: ${error.message}`;
75
+ }
76
+ return "An unexpected error occurred during image generation.";
77
+ }
78
+
79
+ function errorMessage(body: unknown): string {
80
+ if (typeof body !== "object" || body === null) {
81
+ return "Request failed";
82
+ }
83
+ const error = (body as { error?: unknown }).error;
84
+ if (typeof error === "string") {
85
+ return error;
86
+ }
87
+ if (typeof error === "object" && error !== null) {
88
+ const message = (error as { message?: unknown }).message;
89
+ if (typeof message === "string" && message) {
90
+ return message;
91
+ }
92
+ }
93
+ return "Request failed";
94
+ }
95
+
96
+ export async function generateImageOpenRouter(
97
+ credentials: ImageGenCredentials,
98
+ request: ImageGenerationRequest,
99
+ ): Promise<ImageGenerationResult> {
100
+ if (credentials.type !== "direct") {
101
+ throw new Error("OpenRouter image generation requires an OpenRouter API key.");
102
+ }
103
+
104
+ const model = resolveOpenRouterModel(request.model);
105
+ const variants = Math.max(1, Math.min(request.variants ?? 1, MAX_VARIANTS));
106
+ const inputReferences = request.sourceImages?.map((image) => ({
107
+ type: "image_url" as const,
108
+ image_url: {
109
+ url: `data:${image.mimeType};base64,${image.dataBase64}`,
110
+ },
111
+ }));
112
+
113
+ const response = await fetch(OPENROUTER_IMAGES_URL, {
114
+ method: "POST",
115
+ headers: {
116
+ Authorization: `Bearer ${credentials.apiKey}`,
117
+ "Content-Type": "application/json",
118
+ },
119
+ body: JSON.stringify({
120
+ model,
121
+ prompt: request.prompt,
122
+ n: variants,
123
+ ...(request.mode === "edit" && inputReferences?.length
124
+ ? { input_references: inputReferences }
125
+ : {}),
126
+ }),
127
+ ...(request.signal ? { signal: request.signal } : {}),
128
+ });
129
+
130
+ const body = (await response.json().catch(() => undefined)) as
131
+ | {
132
+ data?: Array<{ b64_json?: string; media_type?: string }>;
133
+ error?: unknown;
134
+ }
135
+ | undefined;
136
+
137
+ if (!response.ok) {
138
+ throw new OpenRouterImageError(response.status, errorMessage(body));
139
+ }
140
+
141
+ const images: GeneratedImage[] = [];
142
+ for (const entry of body?.data ?? []) {
143
+ if (!entry.b64_json) {
144
+ continue;
145
+ }
146
+ images.push({
147
+ mimeType: entry.media_type || "image/png",
148
+ dataBase64: entry.b64_json,
149
+ });
150
+ }
151
+
152
+ return { images, text: undefined, resolvedModel: model };
153
+ }
@@ -1,4 +1,4 @@
1
- export type ImageGenProvider = "gemini" | "openai";
1
+ export type ImageGenProvider = "gemini" | "openai" | "openrouter";
2
2
 
3
3
  export interface DirectCredentials {
4
4
  type: "direct";