@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
@@ -1,6 +1,7 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { ChannelReplyPayload } from "@vellumai/gateway-client";
4
+ import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
4
5
 
5
6
  // Replace each channel's provider-API send layer with spies so the dispatcher's
6
7
  // routing and sub-operation selection can be asserted without network calls.
@@ -559,25 +560,78 @@ describe("acknowledged provider posts", () => {
559
560
  });
560
561
 
561
562
  test("a delivery with no text acknowledges no post", async () => {
563
+ const attachments = [
564
+ {
565
+ id: "att-1",
566
+ filename: "a.txt",
567
+ mimeType: "text/plain",
568
+ sizeBytes: 1,
569
+ kind: "file",
570
+ },
571
+ ];
562
572
  const result = await deliverDirect(
563
573
  `${BASE}/deliver/telegram`,
564
- payload({
565
- attachments: [
566
- {
567
- id: "att-1",
568
- filename: "a.txt",
569
- mimeType: "text/plain",
570
- sizeBytes: 1,
571
- kind: "file",
572
- },
573
- ],
574
- } as Partial<ChannelReplyPayload>),
574
+ payload({ attachments }),
575
575
  );
576
576
  expect(result).toEqual({ ok: true, messageIds: [] });
577
577
  expect(telegram.sendTelegramReply).not.toHaveBeenCalled();
578
+ expect(telegram.sendTelegramAttachments).toHaveBeenCalledTimes(1);
579
+ expect(telegram.sendTelegramAttachments.mock.calls[0]?.slice(0, 2)).toEqual(
580
+ ["C1", attachments],
581
+ );
578
582
  });
579
583
  });
580
584
 
585
+ describe("attachment-only delivery failure", () => {
586
+ const attachments = [
587
+ {
588
+ id: "att-1",
589
+ filename: "a.txt",
590
+ mimeType: "text/plain",
591
+ sizeBytes: 1,
592
+ kind: "file",
593
+ },
594
+ {
595
+ id: "att-2",
596
+ filename: "b.txt",
597
+ mimeType: "text/plain",
598
+ sizeBytes: 1,
599
+ kind: "file",
600
+ },
601
+ ];
602
+ const allFailed = { allFailed: true, failureCount: 2, totalCount: 2 };
603
+
604
+ for (const { channel, sendAttachments } of [
605
+ { channel: "telegram", sendAttachments: telegram.sendTelegramAttachments },
606
+ { channel: "whatsapp", sendAttachments: whatsapp.sendWhatsAppAttachments },
607
+ ]) {
608
+ test(`${channel} fails with 502 when every attachment fails and there is no text`, async () => {
609
+ sendAttachments.mockImplementationOnce(() => Promise.resolve(allFailed));
610
+
611
+ const delivery = deliverDirect(
612
+ `${BASE}/deliver/${channel}`,
613
+ payload({ attachments }),
614
+ );
615
+
616
+ await expect(delivery).rejects.toBeInstanceOf(ChannelDeliveryError);
617
+ await expect(delivery).rejects.toMatchObject({ statusCode: 502 });
618
+ expect(sendAttachments).toHaveBeenCalledTimes(1);
619
+ });
620
+
621
+ test(`${channel} succeeds when every attachment fails but the text went out`, async () => {
622
+ sendAttachments.mockImplementationOnce(() => Promise.resolve(allFailed));
623
+
624
+ const result = await deliverDirect(
625
+ `${BASE}/deliver/${channel}`,
626
+ payload({ text: "hi", attachments }),
627
+ );
628
+
629
+ expect(result.ok).toBe(true);
630
+ expect(sendAttachments).toHaveBeenCalledTimes(1);
631
+ });
632
+ }
633
+ });
634
+
581
635
  describe("unsupported callback", () => {
582
636
  test("throws when no transport owns the callback", async () => {
583
637
  await expect(
@@ -9,11 +9,7 @@ import type {
9
9
  ApprovalUIMetadata,
10
10
  ChannelDeliveryResult,
11
11
  } from "@vellumai/gateway-client";
12
- import {
13
- classifyReactionEmojiSpelling,
14
- parseDiscordEmojiMention,
15
- type ReactionEmojiIdentity,
16
- } from "@vellumai/service-contracts/reactions";
12
+ import { parseDiscordEmojiMention } from "@vellumai/service-contracts/reactions";
17
13
 
18
14
  import { getAttachmentContent } from "../../../persistence/attachments-store.js";
19
15
  import type { RuntimeAttachmentMetadata } from "../../../runtime/http-types.js";
@@ -45,8 +41,9 @@ const DISCORD_ALLOWED_MENTIONS = { parse: ["users"] } as const;
45
41
 
46
42
  /**
47
43
  * Upper bound on an outbound attachment. Discord's real limit is the guild's
48
- * boost tier (10 MiB with no boosts, more above that), which the API does not
49
- * expose here, so the client-side guard is only the ceiling no tier exceeds:
44
+ * boost tier (20 MiB per file by default, more above that; see
45
+ * https://discord.com/developers/docs/reference#uploading-files), which the
46
+ * API does not expose here, so the client-side guard is only the ceiling no tier exceeds:
50
47
  * past it the upload is provably futile and not worth the bandwidth. Anything
51
48
  * under it is attempted and, if the guild's own tier rejects it, reported
52
49
  * through the same failure notice as any other attachment error.
@@ -433,16 +430,6 @@ function discordReactionPathEmoji(emoji: string): string {
433
430
  return encodeURIComponent(custom ? `${custom.name}:${custom.id}` : emoji);
434
431
  }
435
432
 
436
- /**
437
- * What a spelling the assistant reacts with means on Discord: a guild emoji
438
- * in its mention form, else the character itself.
439
- */
440
- export function describeDiscordReactionEmoji(
441
- emoji: string,
442
- ): ReactionEmojiIdentity {
443
- return classifyReactionEmojiSpelling(emoji);
444
- }
445
-
446
433
  /**
447
434
  * Add or remove the bot's own emoji reaction on a message.
448
435
  *
@@ -1,4 +1,5 @@
1
1
  import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
2
+ import { classifyReactionEmojiSpelling } from "@vellumai/service-contracts/reactions";
2
3
 
3
4
  import { getLogger } from "../../../util/logger.js";
4
5
  import { directDeliveryContext } from "../callback-routing.js";
@@ -10,7 +11,6 @@ import { isBusyActivityPhase } from "../channel-transport.js";
10
11
  import { openDiscordDmChannel } from "./api.js";
11
12
  import type { DiscordSendTarget } from "./send.js";
12
13
  import {
13
- describeDiscordReactionEmoji,
14
14
  editDiscordMessage,
15
15
  sendDiscordAttachments,
16
16
  sendDiscordReaction,
@@ -95,7 +95,7 @@ export const discordTransport: ChannelTransport = {
95
95
  return { ok: true };
96
96
  },
97
97
 
98
- describeReactionEmoji: describeDiscordReactionEmoji,
98
+ describeReactionEmoji: classifyReactionEmojiSpelling,
99
99
 
100
100
  async react(target) {
101
101
  return sendDiscordReaction(
@@ -69,28 +69,22 @@ async function request<T>(
69
69
  const canRetry = isIdempotent(method);
70
70
 
71
71
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
72
- let resp: OAuthConnectionResponse;
73
- try {
74
- const extraHeaders =
75
- options?.headers &&
76
- typeof options.headers === "object" &&
77
- !Array.isArray(options.headers)
78
- ? (options.headers as Record<string, string>)
79
- : {};
80
- resp = await connection.request({
81
- method,
82
- path,
83
- query,
84
- headers: {
85
- "Content-Type": "application/json",
86
- ...extraHeaders,
87
- },
88
- body: options?.body ? JSON.parse(options.body as string) : undefined,
89
- });
90
- } catch (err) {
91
- // Network-level errors from connection.request() are not retryable
92
- throw err;
93
- }
72
+ const extraHeaders =
73
+ options?.headers &&
74
+ typeof options.headers === "object" &&
75
+ !Array.isArray(options.headers)
76
+ ? (options.headers as Record<string, string>)
77
+ : {};
78
+ const resp: OAuthConnectionResponse = await connection.request({
79
+ method,
80
+ path,
81
+ query,
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ ...extraHeaders,
85
+ },
86
+ body: options?.body ? JSON.parse(options.body as string) : undefined,
87
+ });
94
88
 
95
89
  if (resp.status < 200 || resp.status >= 300) {
96
90
  if (canRetry && isRetryable(resp.status) && attempt < MAX_RETRIES) {
@@ -5,20 +5,30 @@ import type {
5
5
  ChannelReplyPayload,
6
6
  } from "@vellumai/gateway-client";
7
7
 
8
+ import type { RuntimeAttachmentMetadata } from "../../../runtime/http-types.js";
8
9
  import type { CallbackContext } from "../channel-transport.js";
9
10
 
10
11
  // Derive the mock signature from the real export so the test cannot drift from
11
12
  // the production call signature.
12
13
  type CallTelegramBotApi = typeof import("./api.js").callTelegramBotApi;
14
+ type CallTelegramBotApiMultipart =
15
+ typeof import("./api.js").callTelegramBotApiMultipart;
16
+ type GetAttachmentContent =
17
+ typeof import("../../../persistence/attachments-store.js").getAttachmentContent;
13
18
 
14
19
  const callTelegramBotApiMock = mock<CallTelegramBotApi>(
15
20
  async () => ({}) as never,
16
21
  );
22
+ const callTelegramBotApiMultipartMock = mock<CallTelegramBotApiMultipart>(
23
+ async () => ({}) as never,
24
+ );
25
+ const getAttachmentContentMock = mock<GetAttachmentContent>(() => null);
17
26
 
18
27
  mock.module("./api.js", () => ({
19
28
  callTelegramBotApi: (method: string, body: Record<string, unknown>) =>
20
29
  callTelegramBotApiMock(method, body),
21
- callTelegramBotApiMultipart: async () => ({}),
30
+ callTelegramBotApiMultipart: (method: string, form: FormData) =>
31
+ callTelegramBotApiMultipartMock(method, form),
22
32
  TelegramNonRetryableError: class TelegramNonRetryableError extends Error {
23
33
  readonly description: string | undefined;
24
34
  constructor(message: string, description?: string) {
@@ -29,9 +39,15 @@ mock.module("./api.js", () => ({
29
39
  },
30
40
  }));
31
41
 
42
+ mock.module("../../../persistence/attachments-store.js", () => ({
43
+ getAttachmentContent: (attachmentId: string) =>
44
+ getAttachmentContentMock(attachmentId),
45
+ }));
46
+
32
47
  const { TelegramNonRetryableError } = await import("./api.js");
33
48
  const {
34
49
  editTelegramMessage,
50
+ sendTelegramAttachments,
35
51
  sendTelegramReaction,
36
52
  sendTelegramReply,
37
53
  sendTelegramRichReply,
@@ -61,6 +77,10 @@ const callsTo = (method: string) =>
61
77
  beforeEach(() => {
62
78
  callTelegramBotApiMock.mockReset();
63
79
  callTelegramBotApiMock.mockImplementation(async () => ({}) as never);
80
+ callTelegramBotApiMultipartMock.mockReset();
81
+ callTelegramBotApiMultipartMock.mockImplementation(async () => ({}) as never);
82
+ getAttachmentContentMock.mockReset();
83
+ getAttachmentContentMock.mockImplementation(() => null);
64
84
  });
65
85
 
66
86
  describe("sendTelegramRichReply", () => {
@@ -670,3 +690,218 @@ describe("telegramTransport.streamReply", () => {
670
690
  expect(result).toEqual({ ok: false });
671
691
  });
672
692
  });
693
+
694
+ describe("sendTelegramAttachments", () => {
695
+ // The Bot API's multipart upload limits: a photo, and any other file. The
696
+ // sender skips anything over the second.
697
+ const MAX_PHOTO_BYTES = 10 * 1024 * 1024;
698
+ const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
699
+
700
+ function attachment(
701
+ id: string,
702
+ filename: string,
703
+ mimeType: string,
704
+ sizeBytes = 3,
705
+ ): RuntimeAttachmentMetadata {
706
+ return { id, filename, mimeType, sizeBytes, kind: "file" };
707
+ }
708
+
709
+ /** Serve these bytes from the attachment store, keyed by attachment id. */
710
+ function storeHolds(contents: Record<string, Buffer>) {
711
+ getAttachmentContentMock.mockImplementation((id) => contents[id] ?? null);
712
+ }
713
+
714
+ const multipartCalls = () =>
715
+ callTelegramBotApiMultipartMock.mock.calls.map(([method, form]) => ({
716
+ method,
717
+ form,
718
+ }));
719
+
720
+ const noticeTexts = () =>
721
+ callsTo("sendMessage").map(([, body]) => body.text as string);
722
+
723
+ test("sends an image from the store with sendPhoto", async () => {
724
+ storeHolds({ "att-1": Buffer.from("png") });
725
+
726
+ const result = await sendTelegramAttachments("123", [
727
+ attachment("att-1", "chart.png", "image/png"),
728
+ ]);
729
+
730
+ const [call] = multipartCalls();
731
+ expect(multipartCalls()).toHaveLength(1);
732
+ expect(call?.method).toBe("sendPhoto");
733
+ expect(call?.form.get("chat_id")).toBe("123");
734
+ expect(call?.form.get("document")).toBeNull();
735
+ const photo = call?.form.get("photo") as File;
736
+ expect(photo.name).toBe("chart.png");
737
+ expect(photo.type).toBe("image/png");
738
+ expect(await photo.text()).toBe("png");
739
+ expect(noticeTexts()).toEqual([]);
740
+ expect(result).toEqual({
741
+ allFailed: false,
742
+ failureCount: 0,
743
+ totalCount: 1,
744
+ });
745
+ });
746
+
747
+ test("sends a non-image from the store with sendDocument", async () => {
748
+ storeHolds({ "att-1": Buffer.from("pdf") });
749
+
750
+ await sendTelegramAttachments("123", [
751
+ attachment("att-1", "report.pdf", "application/pdf"),
752
+ ]);
753
+
754
+ const [call] = multipartCalls();
755
+ expect(multipartCalls()).toHaveLength(1);
756
+ expect(call?.method).toBe("sendDocument");
757
+ expect(call?.form.get("photo")).toBeNull();
758
+ const document = call?.form.get("document") as File;
759
+ expect(document.name).toBe("report.pdf");
760
+ expect(document.type).toBe("application/pdf");
761
+ expect(await document.text()).toBe("pdf");
762
+ });
763
+
764
+ test("sends an image over the photo limit with sendDocument", async () => {
765
+ storeHolds({
766
+ "att-1": Buffer.alloc(MAX_PHOTO_BYTES),
767
+ "att-2": Buffer.alloc(MAX_PHOTO_BYTES + 1),
768
+ });
769
+
770
+ const result = await sendTelegramAttachments("123", [
771
+ attachment("att-1", "fits.png", "image/png", MAX_PHOTO_BYTES),
772
+ attachment("att-2", "scan.png", "image/png", MAX_PHOTO_BYTES + 1),
773
+ ]);
774
+
775
+ expect(multipartCalls().map((c) => c.method)).toEqual([
776
+ "sendPhoto",
777
+ "sendDocument",
778
+ ]);
779
+ const document = multipartCalls()[1]?.form.get("document") as File;
780
+ expect(document.name).toBe("scan.png");
781
+ expect(document.type).toBe("image/png");
782
+ expect(document.size).toBe(MAX_PHOTO_BYTES + 1);
783
+ expect(noticeTexts()).toEqual([]);
784
+ expect(result).toEqual({
785
+ allFailed: false,
786
+ failureCount: 0,
787
+ totalCount: 2,
788
+ });
789
+ });
790
+
791
+ test("targets the topic on both the upload and the failure notice", async () => {
792
+ storeHolds({ "att-1": Buffer.from("png") });
793
+
794
+ await sendTelegramAttachments(
795
+ "123",
796
+ [
797
+ attachment("att-1", "chart.png", "image/png"),
798
+ attachment("att-2", "gone.txt", "text/plain"),
799
+ ],
800
+ { messageThreadId: "42" },
801
+ );
802
+
803
+ expect(multipartCalls()[0]?.form.get("message_thread_id")).toBe("42");
804
+ const [notice] = callsTo("sendMessage");
805
+ expect(notice?.[1].message_thread_id).toBe(42);
806
+ });
807
+
808
+ test("leaves the topic field off a main-chat upload", async () => {
809
+ storeHolds({ "att-1": Buffer.from("png") });
810
+
811
+ await sendTelegramAttachments("123", [
812
+ attachment("att-1", "chart.png", "image/png"),
813
+ ]);
814
+
815
+ expect(multipartCalls()[0]?.form.has("message_thread_id")).toBe(false);
816
+ });
817
+
818
+ test("skips an attachment whose declared size is over the cap without reading it", async () => {
819
+ storeHolds({ "att-1": Buffer.from("png") });
820
+
821
+ const result = await sendTelegramAttachments("123", [
822
+ attachment("att-1", "huge.mov", "video/mp4", MAX_ATTACHMENT_BYTES + 1),
823
+ ]);
824
+
825
+ expect(getAttachmentContentMock).not.toHaveBeenCalled();
826
+ expect(multipartCalls()).toHaveLength(0);
827
+ expect(noticeTexts()).toEqual([
828
+ "\u26a0\ufe0f 1 attachment(s) could not be delivered: huge.mov",
829
+ ]);
830
+ expect(result).toEqual({ allFailed: true, failureCount: 1, totalCount: 1 });
831
+ });
832
+
833
+ test("skips an attachment whose stored bytes are over the cap", async () => {
834
+ // The declared size understates what the store actually holds.
835
+ storeHolds({ "att-1": Buffer.alloc(MAX_ATTACHMENT_BYTES + 1) });
836
+
837
+ const result = await sendTelegramAttachments("123", [
838
+ attachment("att-1", "huge.bin", "application/octet-stream"),
839
+ ]);
840
+
841
+ expect(multipartCalls()).toHaveLength(0);
842
+ expect(noticeTexts()).toEqual([
843
+ "\u26a0\ufe0f 1 attachment(s) could not be delivered: huge.bin",
844
+ ]);
845
+ expect(result).toEqual({ allFailed: true, failureCount: 1, totalCount: 1 });
846
+ });
847
+
848
+ test("reports an attachment the store has no content for", async () => {
849
+ const result = await sendTelegramAttachments("123", [
850
+ attachment("att-1", "missing.txt", "text/plain"),
851
+ ]);
852
+
853
+ expect(getAttachmentContentMock).toHaveBeenCalledWith("att-1");
854
+ expect(multipartCalls()).toHaveLength(0);
855
+ expect(noticeTexts()).toEqual([
856
+ "\u26a0\ufe0f 1 attachment(s) could not be delivered: missing.txt",
857
+ ]);
858
+ expect(result).toEqual({ allFailed: true, failureCount: 1, totalCount: 1 });
859
+ });
860
+
861
+ test("keeps sending after one upload fails and names only the failure", async () => {
862
+ storeHolds({
863
+ "att-1": Buffer.from("a"),
864
+ "att-2": Buffer.from("b"),
865
+ "att-3": Buffer.from("c"),
866
+ });
867
+ callTelegramBotApiMultipartMock.mockImplementation(
868
+ async (_method, form) => {
869
+ if ((form.get("document") as File | null)?.name === "b.txt") {
870
+ throw new Error("Bad Request: file is empty");
871
+ }
872
+ return {} as never;
873
+ },
874
+ );
875
+
876
+ const result = await sendTelegramAttachments("123", [
877
+ attachment("att-1", "a.txt", "text/plain"),
878
+ attachment("att-2", "b.txt", "text/plain"),
879
+ attachment("att-3", "c.txt", "text/plain"),
880
+ ]);
881
+
882
+ expect(
883
+ multipartCalls().map((c) => (c.form.get("document") as File).name),
884
+ ).toEqual(["a.txt", "b.txt", "c.txt"]);
885
+ expect(noticeTexts()).toEqual([
886
+ "\u26a0\ufe0f 1 attachment(s) could not be delivered: b.txt",
887
+ ]);
888
+ expect(result).toEqual({
889
+ allFailed: false,
890
+ failureCount: 1,
891
+ totalCount: 3,
892
+ });
893
+ });
894
+
895
+ test("still returns the result when the failure notice cannot be sent", async () => {
896
+ callTelegramBotApiMock.mockImplementation(async () => {
897
+ throw new Error("Forbidden: bot was blocked by the user");
898
+ });
899
+
900
+ const result = await sendTelegramAttachments("123", [
901
+ attachment("att-1", "missing.txt", "text/plain"),
902
+ ]);
903
+
904
+ expect(callsTo("sendMessage")).toHaveLength(1);
905
+ expect(result).toEqual({ allFailed: true, failureCount: 1, totalCount: 1 });
906
+ });
907
+ });
@@ -31,7 +31,9 @@ const TELEGRAM_MAX_MESSAGE_LEN = 4000;
31
31
  /** Telegram Bot API enforces a 1-64 byte limit on InlineKeyboardButton callback_data. */
32
32
  const TELEGRAM_MAX_CALLBACK_DATA_BYTES = 64;
33
33
 
34
- // Telegram Bot API sendDocument upload limit is 50 MB
34
+ // Bot API limits for a multipart upload: 10 MB for a photo, 50 MB for any
35
+ // other file (https://core.telegram.org/bots/api#sending-files).
36
+ const TELEGRAM_MAX_PHOTO_BYTES = 10 * 1024 * 1024;
35
37
  const TELEGRAM_MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
36
38
 
37
39
  const TELEGRAM_IMAGE_MIME_PREFIXES = [
@@ -296,8 +298,9 @@ export type TelegramAttachmentResult = {
296
298
  };
297
299
 
298
300
  /**
299
- * Send attachments to a Telegram chat, using sendPhoto for images and
300
- * sendDocument for everything else.
301
+ * Send attachments to a Telegram chat, using sendPhoto for an image within
302
+ * the photo limit and sendDocument for everything else, so an image over it
303
+ * still arrives as a file.
301
304
  */
302
305
  export async function sendTelegramAttachments(
303
306
  chatId: string,
@@ -351,10 +354,10 @@ export async function sendTelegramAttachments(
351
354
  form.set("message_thread_id", String(threadFields.message_thread_id));
352
355
  }
353
356
 
354
- const isImage = TELEGRAM_IMAGE_MIME_PREFIXES.some((p) =>
355
- mimeType.startsWith(p),
356
- );
357
- if (isImage) {
357
+ const sendsAsPhoto =
358
+ TELEGRAM_IMAGE_MIME_PREFIXES.some((p) => mimeType.startsWith(p)) &&
359
+ content.length <= TELEGRAM_MAX_PHOTO_BYTES;
360
+ if (sendsAsPhoto) {
358
361
  form.set("photo", blob, filename);
359
362
  await callTelegramBotApiMultipart("sendPhoto", form);
360
363
  } else {
@@ -55,15 +55,6 @@ function draftText(text: string, plan: StreamPlan | undefined): string {
55
55
  return [body, planText].filter(Boolean).join("\n\n");
56
56
  }
57
57
 
58
- /**
59
- * Telegram's draft id is minted by the caller, unlike a stream id a platform
60
- * hands back, and only has to be non-zero and stable for the life of one
61
- * draft. The clock supplies that without any state to keep between calls.
62
- */
63
- function mintDraftId(): number {
64
- return Date.now();
65
- }
66
-
67
58
  export const telegramTransport: ChannelTransport = {
68
59
  channel: "telegram",
69
60
 
@@ -166,7 +157,9 @@ export const telegramTransport: ChannelTransport = {
166
157
  if (op.action === "stop") {
167
158
  return { ok: true, ts: op.streamId };
168
159
  }
169
- const draftId = op.action === "start" ? mintDraftId() : Number(op.streamId);
160
+ // Telegram mints draft ids on the caller. A non-zero clock tick is enough
161
+ // for the life of one draft.
162
+ const draftId = op.action === "start" ? Date.now() : Number(op.streamId);
170
163
  if (!Number.isFinite(draftId) || draftId === 0) {
171
164
  return { ok: false };
172
165
  }