@vellumai/assistant 0.11.4 → 0.11.5-dev.202608220019.a778cf0

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 (457) hide show
  1. package/AGENTS.md +6 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  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/ingress.ts +103 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  15. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  16. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  19. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  20. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  21. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  22. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +30 -32
  23. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  24. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  25. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  26. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  28. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  29. package/openapi.yaml +487 -11
  30. package/package.json +1 -1
  31. package/scripts/sync-web-search-catalog.ts +6 -0
  32. package/src/__tests__/anthropic-provider.test.ts +10 -3
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  34. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  35. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  36. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  37. package/src/__tests__/channel-readiness-service.test.ts +126 -0
  38. package/src/__tests__/channel-reply-delivery.test.ts +30 -17
  39. package/src/__tests__/checker.test.ts +20 -314
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  41. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  42. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  44. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  45. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  46. package/src/__tests__/conversation-attachments.test.ts +0 -1
  47. package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
  48. package/src/__tests__/conversation-error.test.ts +17 -0
  49. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  50. package/src/__tests__/conversation-pairing.test.ts +186 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  52. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  53. package/src/__tests__/credential-routes.test.ts +22 -3
  54. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  55. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  56. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  57. package/src/__tests__/dm-backfill.test.ts +63 -0
  58. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  59. package/src/__tests__/edit-propagation.test.ts +107 -4
  60. package/src/__tests__/events-client-registration.test.ts +28 -0
  61. package/src/__tests__/file-write-tool.test.ts +4 -2
  62. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  63. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  64. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  65. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  66. package/src/__tests__/host-shell-tool.test.ts +23 -4
  67. package/src/__tests__/image-conversion.test.ts +143 -1
  68. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  69. package/src/__tests__/input-repairs.test.ts +207 -0
  70. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  71. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  72. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  73. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  74. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  75. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  76. package/src/__tests__/mtime-cache.test.ts +2 -0
  77. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  78. package/src/__tests__/openai-provider.test.ts +40 -0
  79. package/src/__tests__/openai-responses-provider.test.ts +18 -0
  80. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  81. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  82. package/src/__tests__/platform.test.ts +16 -1
  83. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  84. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  85. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  86. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  87. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  88. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  89. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  90. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  91. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  92. package/src/__tests__/reaction-persistence.test.ts +200 -7
  93. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  94. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  95. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  96. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  97. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  98. package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
  99. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  100. package/src/__tests__/settings-routes.test.ts +85 -3
  101. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  102. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  103. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  104. package/src/__tests__/tool-executor.test.ts +63 -38
  105. package/src/__tests__/tool-policy.test.ts +97 -1
  106. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  107. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  108. package/src/__tests__/validate-input.test.ts +243 -7
  109. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  110. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  111. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  112. package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
  113. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  114. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  115. package/src/acp/acp-claude-oauth.ts +38 -21
  116. package/src/acp/prepare-agent-env.ts +146 -55
  117. package/src/agent/loop-tool-dedup.test.ts +161 -0
  118. package/src/agent/loop.ts +68 -22
  119. package/src/api/constants/app-tools.ts +34 -0
  120. package/src/api/constants/profile-config-validation.ts +60 -0
  121. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  122. package/src/api/events/tool-result.ts +6 -1
  123. package/src/api/events/watch-retro-completed.ts +52 -0
  124. package/src/api/index.ts +30 -0
  125. package/src/api/responses/resource-pressure-status.ts +25 -0
  126. package/src/approvals/guardian-channel-delivery.ts +70 -6
  127. package/src/approvals/guardian-request-resolvers.ts +32 -66
  128. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  129. package/src/calls/voice-session-bridge.ts +60 -6
  130. package/src/channels/__tests__/message-audience.test.ts +49 -0
  131. package/src/channels/__tests__/types.test.ts +17 -3
  132. package/src/channels/message-audience.ts +40 -0
  133. package/src/channels/types.ts +16 -12
  134. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  135. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  136. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  137. package/src/cli/commands/__tests__/conversations-slack.test.ts +2 -1
  138. package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
  139. package/src/cli/commands/__tests__/inference-providers.test.ts +135 -2
  140. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  141. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  142. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  143. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  144. package/src/cli/commands/conversations.help.ts +34 -0
  145. package/src/cli/commands/conversations.ts +125 -0
  146. package/src/cli/commands/inference-profiles.ts +56 -3
  147. package/src/cli/commands/inference-providers.ts +37 -6
  148. package/src/cli/commands/inference.help.ts +9 -4
  149. package/src/cli/commands/keys.help.ts +13 -1
  150. package/src/cli/commands/keys.ts +26 -15
  151. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  152. package/src/cli/commands/memory/index.help.ts +41 -27
  153. package/src/cli/commands/memory/index.ts +2 -0
  154. package/src/cli/commands/memory/memory-v2.ts +58 -54
  155. package/src/cli/commands/memory/memory-validate.ts +18 -0
  156. package/src/cli/commands/monitoring.ts +1 -0
  157. package/src/cli/commands/oauth/request.test.ts +93 -0
  158. package/src/cli/commands/oauth/request.ts +23 -9
  159. package/src/cli/commands/plugins.help.ts +28 -31
  160. package/src/cli/commands/plugins.ts +10 -0
  161. package/src/cli/commands/skills.help.ts +13 -16
  162. package/src/cli/commands/trust.ts +3 -14
  163. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  164. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  165. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  166. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  167. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  168. package/src/cli/lib/bundled-marketplace.json +26 -0
  169. package/src/cli/lib/daemon-credential-client.ts +25 -3
  170. package/src/cli/lib/install-from-github.ts +35 -24
  171. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  172. package/src/cli/lib/open-browser.test.ts +67 -0
  173. package/src/cli/lib/open-browser.ts +24 -5
  174. package/src/cli/lib/plugin-surfaces.ts +27 -0
  175. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  176. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  177. package/src/config/__tests__/profile-materialization.test.ts +26 -0
  178. package/src/config/default-profile-catalog.ts +5 -6
  179. package/src/config/feature-flag-registry.json +36 -28
  180. package/src/config/profile-materialization.ts +29 -0
  181. package/src/config/sanitize-for-transfer.ts +16 -0
  182. package/src/config/schemas/llm.ts +7 -0
  183. package/src/config/schemas/services.ts +6 -0
  184. package/src/config/webhook-routing.ts +8 -0
  185. package/src/context/compactor.ts +31 -2
  186. package/src/context/outbound-sanitize.ts +6 -0
  187. package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
  188. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  189. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  190. package/src/daemon/conversation-error.ts +19 -2
  191. package/src/daemon/conversation-messaging.ts +22 -2
  192. package/src/daemon/conversation-process.ts +11 -0
  193. package/src/daemon/conversation-store.ts +10 -0
  194. package/src/daemon/conversation-surfaces.ts +21 -8
  195. package/src/daemon/conversation.ts +12 -10
  196. package/src/daemon/daemon-control.ts +1 -0
  197. package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
  198. package/src/daemon/handlers/config-ingress.ts +115 -5
  199. package/src/daemon/lifecycle.ts +30 -2
  200. package/src/daemon/message-types/conversations.ts +5 -7
  201. package/src/daemon/message-types/web-activity.ts +3 -2
  202. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  203. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  204. package/src/daemon/resource-pressure-guard.ts +387 -0
  205. package/src/daemon/shutdown-handlers.ts +2 -0
  206. package/src/daemon/startup-error.ts +16 -0
  207. package/src/daemon/trust-context.ts +14 -10
  208. package/src/daemon/unsendable-image-notice.ts +76 -0
  209. package/src/hooks/registry.ts +65 -51
  210. package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
  211. package/src/inbound/tunnel-probe.ts +261 -0
  212. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  213. package/src/ipc/gateway-client.test.ts +1 -1
  214. package/src/ipc/gateway-client.ts +14 -21
  215. package/src/ipc/socket-cleanup.ts +2 -11
  216. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  217. package/src/messaging/provider-message-metadata.ts +128 -0
  218. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +138 -73
  219. package/src/messaging/providers/channel-transport.ts +100 -16
  220. package/src/messaging/providers/discord/send.ts +16 -0
  221. package/src/messaging/providers/discord/transport.ts +23 -1
  222. package/src/messaging/providers/index.ts +84 -16
  223. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  224. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  225. package/src/messaging/providers/slack/send.test.ts +88 -34
  226. package/src/messaging/providers/slack/send.ts +99 -50
  227. package/src/messaging/providers/slack/transport.ts +35 -37
  228. package/src/messaging/providers/telegram-bot/send.test.ts +88 -18
  229. package/src/messaging/providers/telegram-bot/send.ts +36 -0
  230. package/src/messaging/providers/telegram-bot/transport.ts +22 -8
  231. package/src/messaging/read-provider-metadata.test.ts +157 -0
  232. package/src/messaging/read-provider-metadata.ts +51 -0
  233. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  234. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  235. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  236. package/src/notifications/adapters/macos.ts +55 -0
  237. package/src/notifications/adapters/slack.ts +10 -5
  238. package/src/notifications/assistant-reply-producer.ts +37 -4
  239. package/src/notifications/conversation-pairing.ts +111 -21
  240. package/src/notifications/edit-notification.ts +38 -8
  241. package/src/notifications/emit-signal.ts +12 -14
  242. package/src/notifications/home-feed-side-effect.ts +218 -15
  243. package/src/notifications/types.ts +5 -0
  244. package/src/permissions/AGENTS.md +16 -0
  245. package/src/permissions/checker.test.ts +75 -122
  246. package/src/permissions/checker.ts +44 -560
  247. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  248. package/src/permissions/confirmation-guardian-request.ts +5 -1
  249. package/src/persistence/conversation-crud.ts +69 -20
  250. package/src/persistence/conversation-queries.ts +14 -1
  251. package/src/persistence/conversation-types.ts +22 -0
  252. package/src/persistence/db-async-query.ts +2 -1
  253. package/src/persistence/db-maintenance.ts +15 -0
  254. package/src/persistence/delivery-crud.ts +122 -15
  255. package/src/persistence/embeddings/qdrant-manager.ts +1 -0
  256. package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
  257. package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
  258. package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
  259. package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
  260. package/src/persistence/schema/index.ts +1 -0
  261. package/src/persistence/schema/watch.ts +29 -0
  262. package/src/persistence/steps.ts +4 -0
  263. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  264. package/src/plugin-api/conversation-turn.ts +27 -0
  265. package/src/plugin-api/credential-scope.test.ts +15 -0
  266. package/src/plugin-api/credential-scope.ts +14 -0
  267. package/src/plugin-api/index.ts +19 -1
  268. package/src/plugin-api/resolve-credential.ts +15 -10
  269. package/src/plugin-api/store-credential.ts +148 -0
  270. package/src/plugin-api/system-card.ts +36 -0
  271. package/src/plugin-api/vision-support.test.ts +82 -0
  272. package/src/plugin-api/vision-support.ts +14 -2
  273. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  274. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  275. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  276. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  277. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  278. package/src/plugins/defaults/main.ts +15 -0
  279. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  280. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  281. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  282. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  283. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  284. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  286. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  287. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  288. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  289. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  290. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  291. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  292. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  293. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  294. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  295. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  296. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  297. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  298. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  299. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  300. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  301. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  302. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  303. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  304. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  305. package/src/plugins/mtime-cache.ts +1 -1
  306. package/src/plugins/pipeline.ts +14 -7
  307. package/src/plugins/plugin-execution-context.ts +35 -11
  308. package/src/plugins/plugin-tree-walk.ts +63 -4
  309. package/src/providers/__tests__/retry-network-error.test.ts +84 -0
  310. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  311. package/src/providers/anthropic/client.ts +332 -241
  312. package/src/providers/connection-resolution.ts +25 -2
  313. package/src/providers/content-blocks.ts +22 -0
  314. package/src/providers/fetch-provider-catalog.ts +19 -0
  315. package/src/providers/gemini/client.ts +5 -2
  316. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  317. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  318. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  319. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  320. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  321. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  322. package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
  323. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  324. package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
  325. package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
  326. package/src/providers/inference/adapter-factory.ts +33 -2
  327. package/src/providers/inference/auth.ts +13 -2
  328. package/src/providers/inference/credential-usage.ts +37 -0
  329. package/src/providers/inference/endpoint-probe.ts +115 -0
  330. package/src/providers/inference/missing-credential-guard.ts +110 -0
  331. package/src/providers/inference/profile-probe.ts +256 -0
  332. package/src/providers/inference/resolve-auth.ts +7 -7
  333. package/src/providers/media-resolve.ts +176 -15
  334. package/src/providers/model-catalog.ts +10 -0
  335. package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
  336. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  337. package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
  338. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  339. package/src/providers/openai/api-error-normalization.ts +16 -2
  340. package/src/providers/openai/chat-completions-provider.ts +326 -44
  341. package/src/providers/openai/responses-provider.ts +5 -2
  342. package/src/providers/provider-send-message.ts +11 -0
  343. package/src/providers/retry.ts +6 -0
  344. package/src/providers/search-provider-catalog.ts +20 -0
  345. package/src/routes/route-host-protocol.ts +7 -0
  346. package/src/routes/worker.ts +31 -10
  347. package/src/runtime/AGENTS.md +36 -0
  348. package/src/runtime/__tests__/host-observe.test.ts +302 -0
  349. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  350. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  351. package/src/runtime/assistant-event-hub.ts +38 -0
  352. package/src/runtime/channel-readiness-service.ts +39 -13
  353. package/src/runtime/channel-readiness-types.ts +49 -2
  354. package/src/runtime/channel-reply-delivery.ts +55 -32
  355. package/src/runtime/channel-retry-sweep.ts +6 -4
  356. package/src/runtime/effective-capabilities.test.ts +19 -0
  357. package/src/runtime/effective-capabilities.ts +25 -0
  358. package/src/runtime/guardian-reply-router.ts +6 -2
  359. package/src/runtime/host-observe.ts +246 -0
  360. package/src/runtime/http-errors.ts +1 -0
  361. package/src/runtime/http-server.ts +185 -6
  362. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  363. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  364. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
  365. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  366. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
  367. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +72 -1
  368. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
  369. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  370. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  371. package/src/runtime/routes/__tests__/plugins-routes.test.ts +103 -71
  372. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  373. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  374. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  375. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  376. package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
  377. package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
  378. package/src/runtime/routes/approval-routes.ts +37 -4
  379. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  380. package/src/runtime/routes/canned-message-complete.ts +68 -29
  381. package/src/runtime/routes/channel-readiness-routes.ts +14 -2
  382. package/src/runtime/routes/client-routes.ts +113 -0
  383. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  384. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  385. package/src/runtime/routes/conversation-query-routes.ts +10 -0
  386. package/src/runtime/routes/conversation-routes.ts +36 -22
  387. package/src/runtime/routes/credential-in-use.ts +85 -0
  388. package/src/runtime/routes/credential-routes.ts +55 -85
  389. package/src/runtime/routes/guardian-approval-interception.ts +44 -53
  390. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  391. package/src/runtime/routes/host-cu-routes.ts +18 -0
  392. package/src/runtime/routes/identity-routes.ts +5 -121
  393. package/src/runtime/routes/inbound-message-handler.ts +74 -30
  394. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  395. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +182 -297
  396. package/src/runtime/routes/inbound-stages/background-dispatch.ts +173 -386
  397. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  398. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  399. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  400. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  401. package/src/runtime/routes/index.ts +4 -0
  402. package/src/runtime/routes/inference-profiles-routes.ts +232 -31
  403. package/src/runtime/routes/inference-provider-connection-routes.ts +33 -11
  404. package/src/runtime/routes/ingress-status-routes.ts +180 -0
  405. package/src/runtime/routes/monitoring-routes.ts +9 -1
  406. package/src/runtime/routes/oauth-commands-routes.ts +1 -1
  407. package/src/runtime/routes/plugins-routes.ts +28 -32
  408. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  409. package/src/runtime/routes/secret-routes.ts +39 -8
  410. package/src/runtime/routes/settings-routes.ts +15 -12
  411. package/src/runtime/routes/surface-action-routes.ts +20 -19
  412. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  413. package/src/runtime/routes/user-route-resolution.ts +18 -1
  414. package/src/runtime/routes/watch-routes.ts +807 -0
  415. package/src/runtime/slack-reply-session.test.ts +23 -13
  416. package/src/runtime/slack-reply-session.ts +38 -55
  417. package/src/runtime/web-presence.ts +90 -0
  418. package/src/security/__tests__/untrusted-content.test.ts +42 -0
  419. package/src/security/untrusted-content.ts +28 -9
  420. package/src/skills/validate-input.ts +177 -40
  421. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  422. package/src/subagent/consult-context.ts +7 -23
  423. package/src/tools/credentials/broker.ts +24 -79
  424. package/src/tools/credentials/ref-parse.ts +35 -0
  425. package/src/tools/credentials/resolve.ts +4 -10
  426. package/src/tools/credentials/store.ts +168 -0
  427. package/src/tools/credentials/tool-policy.ts +67 -0
  428. package/src/tools/executor.ts +53 -15
  429. package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
  430. package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
  431. package/src/tools/network/__tests__/web-search.test.ts +138 -3
  432. package/src/tools/network/firecrawl-compat.ts +90 -0
  433. package/src/tools/network/url-safety.ts +9 -16
  434. package/src/tools/network/web-fetch.ts +142 -62
  435. package/src/tools/network/web-search.ts +162 -58
  436. package/src/tools/permission-checker.ts +43 -52
  437. package/src/tools/schema-transforms.ts +40 -5
  438. package/src/tools/shared/input-repairs.ts +160 -0
  439. package/src/tools/skills/skill-tool-factory.ts +18 -4
  440. package/src/tools/subagent/spawn.ts +0 -1
  441. package/src/tools/tool-approval-handler.ts +16 -10
  442. package/src/tools/tool-types.ts +26 -13
  443. package/src/tools/types.ts +6 -6
  444. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  445. package/src/util/cgroup-memory.ts +3 -0
  446. package/src/util/container-cpu-sampler.ts +250 -0
  447. package/src/util/image-conversion.ts +178 -14
  448. package/src/util/platform.ts +93 -10
  449. package/src/util/worker-process.ts +1 -0
  450. package/src/watch/__tests__/watch-retro.test.ts +665 -0
  451. package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
  452. package/src/watch/__tests__/watch-timeline.test.ts +670 -0
  453. package/src/watch/watch-retro.ts +480 -0
  454. package/src/watch/watch-session-manager.ts +575 -0
  455. package/src/watch/watch-timeline.ts +848 -0
  456. package/src/permissions/ipc-risk-types.ts +0 -143
  457. package/src/permissions/risk-types.ts +0 -76
@@ -9,17 +9,19 @@ const slack = {
9
9
  Promise.resolve({ ts: "slack-ts" }),
10
10
  ),
11
11
  sendSlackReaction: mock((..._args: unknown[]) => Promise.resolve()),
12
- sendSlackAssistantThreadStatus: mock((..._args: unknown[]) =>
13
- Promise.resolve(),
14
- ),
12
+ sendSlackAgentSessionStatus: mock((..._args: unknown[]) => Promise.resolve()),
15
13
  sendSlackAttachments: mock((..._args: unknown[]) =>
16
14
  Promise.resolve({ allFailed: false, failureCount: 0 }),
17
15
  ),
18
16
  sendSlackStreamOp: mock((..._args: unknown[]) =>
19
17
  Promise.resolve({ ok: true, ts: "stream-ts" }),
20
18
  ),
19
+ updateSlackMessage: mock((..._args: unknown[]) =>
20
+ Promise.resolve({ ts: "slack-ts" }),
21
+ ),
21
22
  };
22
23
  const telegram = {
24
+ editTelegramMessage: mock((..._args: unknown[]) => Promise.resolve()),
23
25
  sendTelegramReply: mock((..._args: unknown[]) => Promise.resolve()),
24
26
  sendTelegramRichReply: mock((..._args: unknown[]) => Promise.resolve()),
25
27
  sendTelegramTypingIndicator: mock((..._args: unknown[]) => Promise.resolve()),
@@ -57,8 +59,15 @@ mock.module("../../../util/logger.js", () => ({
57
59
  getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
58
60
  }));
59
61
 
60
- const { deliverDirect, isDirectDelivery, getTransportForCallback } =
61
- await import("../index.js");
62
+ const {
63
+ deliverDirect,
64
+ editChannelMessage,
65
+ sendChannelStreamOp,
66
+ setChannelActivity,
67
+ supportsChannelActivity,
68
+ isDirectDelivery,
69
+ getTransportForCallback,
70
+ } = await import("../index.js");
62
71
 
63
72
  const BASE = "https://gateway.internal";
64
73
 
@@ -134,56 +143,49 @@ describe("Slack sub-operation selection", () => {
134
143
  expect(opts.threadTs).toBe("1700.9");
135
144
  });
136
145
 
137
- test("reaction routes to sendSlackReaction, not the text path", async () => {
138
- await deliverDirect(
139
- `${BASE}/deliver/slack`,
140
- payload({
141
- reaction: {
142
- action: "add",
143
- name: "white_check_mark",
144
- messageTs: "1700.5",
145
- },
146
- }),
147
- );
148
- expect(slack.sendSlackReaction).toHaveBeenCalledTimes(1);
146
+ test("editChannelMessage updates in place instead of posting", async () => {
147
+ await editChannelMessage(`${BASE}/deliver/slack`, {
148
+ chatId: "C1",
149
+ messageId: "1700.5",
150
+ text: "revised",
151
+ });
152
+
153
+ expect(slack.updateSlackMessage).toHaveBeenCalledTimes(1);
154
+ // The distinction the split exists for: an edit never reaches the post
155
+ // path, so a failed edit cannot become a second visible message.
149
156
  expect(slack.sendSlackReply).not.toHaveBeenCalled();
150
157
  });
151
158
 
152
- test("assistantThreadStatus routes to sendSlackAssistantThreadStatus", async () => {
153
- await deliverDirect(
154
- `${BASE}/deliver/slack`,
155
- payload({
156
- assistantThreadStatus: {
157
- channel: "C1",
158
- threadTs: "1700.5",
159
- status: "is thinking",
160
- },
161
- }),
162
- );
163
- expect(slack.sendSlackAssistantThreadStatus).toHaveBeenCalledTimes(1);
159
+ test("setChannelActivity reaches Slack without touching the text path", async () => {
160
+ await setChannelActivity(`${BASE}/deliver/slack`, {
161
+ chatId: "C1",
162
+ phase: "thinking",
163
+ });
164
+
165
+ expect(slack.sendSlackAgentSessionStatus).toHaveBeenCalledTimes(1);
164
166
  expect(slack.sendSlackReply).not.toHaveBeenCalled();
165
167
  });
166
168
 
167
- test("a typing payload to Slack falls through to deliver (no typing capability)", async () => {
168
- await deliverDirect(
169
- `${BASE}/deliver/slack`,
170
- payload({ chatAction: "typing", text: "hi" }),
171
- );
172
- expect(slack.sendSlackReply).toHaveBeenCalledTimes(1);
169
+ test("a channel with no activity indicator resolves quietly", async () => {
170
+ const result = await setChannelActivity(`${BASE}/deliver/whatsapp`, {
171
+ chatId: "123",
172
+ phase: "thinking",
173
+ });
174
+
175
+ expect(result).toEqual({ ok: true });
176
+ expect(slack.sendSlackReply).not.toHaveBeenCalled();
173
177
  });
174
178
 
175
- test("slackStream routes to sendSlackStreamOp ahead of the text path", async () => {
176
- const result = await deliverDirect(
179
+ test("sendChannelStreamOp reaches Slack without touching the text path", async () => {
180
+ const result = await sendChannelStreamOp(
177
181
  `${BASE}/deliver/slack?threadTs=1700.5`,
178
- payload({
179
- text: "ignored while streaming",
180
- slackStream: { action: "start", threadTs: "1700.5" },
181
- }),
182
+ "C1",
183
+ { action: "start", threadTs: "1700.5", markdownText: "hi" },
182
184
  );
183
185
  expect(slack.sendSlackStreamOp).toHaveBeenCalledTimes(1);
184
186
  expect(slack.sendSlackStreamOp.mock.calls[0]).toEqual([
185
187
  "C1",
186
- { action: "start", threadTs: "1700.5" },
188
+ { action: "start", threadTs: "1700.5", markdownText: "hi" },
187
189
  ]);
188
190
  expect(slack.sendSlackReply).not.toHaveBeenCalled();
189
191
  expect(result).toEqual({ ok: true, ts: "stream-ts" });
@@ -191,22 +193,87 @@ describe("Slack sub-operation selection", () => {
191
193
  });
192
194
 
193
195
  describe("capability gating across channels", () => {
194
- test("a reaction payload to Telegram falls through to deliver (no sendReaction)", async () => {
195
- await deliverDirect(
196
- `${BASE}/deliver/telegram`,
197
- payload({
198
- text: "hi",
199
- reaction: { action: "add", name: "x", messageTs: "1" },
196
+ test("Slack renders a muted edit as its own context block", async () => {
197
+ await editChannelMessage(`${BASE}/deliver/slack`, {
198
+ chatId: "C1",
199
+ messageId: "1700.5",
200
+ text: "This approval request has been resolved.",
201
+ emphasis: "muted",
202
+ });
203
+
204
+ // The producer asked for a settled message and named no markup. Slack
205
+ // decides that means a context block.
206
+ const [, , , options] = slack.updateSlackMessage.mock.calls[0]!;
207
+ expect((options as { blocks?: unknown[] }).blocks).toEqual([
208
+ {
209
+ type: "context",
210
+ elements: [
211
+ {
212
+ type: "mrkdwn",
213
+ text: "This approval request has been resolved.",
214
+ },
215
+ ],
216
+ },
217
+ ]);
218
+ });
219
+
220
+ test("an edit with no emphasis carries no channel markup", async () => {
221
+ await editChannelMessage(`${BASE}/deliver/slack`, {
222
+ chatId: "C1",
223
+ messageId: "1700.5",
224
+ text: "revised",
225
+ });
226
+
227
+ const [, , , options] = slack.updateSlackMessage.mock.calls[0]!;
228
+ expect((options as { blocks?: unknown[] }).blocks).toBeUndefined();
229
+ });
230
+
231
+ test("editChannelMessage now reaches Telegram, not only Slack", async () => {
232
+ // The capability gate is what let Telegram gain this: no caller changed,
233
+ // the transport grew the method.
234
+ await editChannelMessage(`${BASE}/deliver/telegram`, {
235
+ chatId: "123",
236
+ messageId: "456",
237
+ text: "revised",
238
+ });
239
+
240
+ expect(telegram.editTelegramMessage).toHaveBeenCalledTimes(1);
241
+ expect(telegram.sendTelegramReply).not.toHaveBeenCalled();
242
+ });
243
+
244
+ test("a channel that cannot revise a sent message resolves quietly", async () => {
245
+ // Discord has no `edit` yet. A channel without the method is not a failed
246
+ // delivery: nothing is attempted, nothing throws, and no fresh message is
247
+ // posted in place of the revision.
248
+ expect(
249
+ await editChannelMessage(`${BASE}/deliver/discord`, {
250
+ chatId: "C1",
251
+ messageId: "1",
252
+ text: "revised",
200
253
  }),
201
- );
202
- expect(telegram.sendTelegramReply).toHaveBeenCalledTimes(1);
254
+ ).toEqual({ ok: true });
255
+ expect(discord.sendDiscordReply).not.toHaveBeenCalled();
203
256
  });
204
257
 
205
- test("typing to Telegram routes to its typing indicator", async () => {
206
- await deliverDirect(
207
- `${BASE}/deliver/telegram`,
208
- payload({ chatAction: "typing" }),
209
- );
258
+ test("the activity capability is read from the transport, not the channel name", () => {
259
+ // The gate in background-dispatch asks this rather than testing
260
+ // `sourceChannel === "slack"`, so a channel that implements the method
261
+ // starts showing an indicator without a caller being changed. All three
262
+ // answer the same question now, which is the point of the single method:
263
+ // Slack holds its indicator and the other two re-assert theirs, and a
264
+ // caller cannot tell the difference.
265
+ expect(supportsChannelActivity(`${BASE}/deliver/slack`)).toBe(true);
266
+ expect(supportsChannelActivity(`${BASE}/deliver/telegram`)).toBe(true);
267
+ expect(supportsChannelActivity(`${BASE}/deliver/discord`)).toBe(true);
268
+ expect(supportsChannelActivity(`${BASE}/deliver/whatsapp`)).toBe(false);
269
+ });
270
+
271
+ test("setChannelActivity reaches Telegram's typing indicator", async () => {
272
+ await setChannelActivity(`${BASE}/deliver/telegram`, {
273
+ chatId: "123",
274
+ phase: "thinking",
275
+ });
276
+
210
277
  expect(telegram.sendTelegramTypingIndicator).toHaveBeenCalledTimes(1);
211
278
  });
212
279
 
@@ -244,26 +311,24 @@ describe("capability gating across channels", () => {
244
311
  });
245
312
  });
246
313
 
247
- test("a typing payload to Discord falls through to deliver (no sendTyping)", async () => {
248
- // Only the Telegram heartbeat produces `chatAction: "typing"`, and it is
249
- // gated on sourceChannel === "telegram", so Discord can never receive one.
250
- // The transport implements no sendTyping, so this falls through.
251
- await deliverDirect(
252
- `${BASE}/deliver/discord`,
253
- payload({ chatAction: "typing", text: "hi" }),
254
- );
255
- expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
314
+ test("setChannelActivity reaches Discord's typing indicator", async () => {
315
+ await setChannelActivity(`${BASE}/deliver/discord`, {
316
+ chatId: "999",
317
+ phase: "thinking",
318
+ });
319
+
320
+ expect(discord.sendDiscordTypingIndicator).toHaveBeenCalledTimes(1);
256
321
  });
257
322
 
258
- test("a Slack-only stream payload to Discord falls through to deliver", async () => {
259
- await deliverDirect(
260
- `${BASE}/deliver/discord`,
261
- payload({
262
- text: "hi",
263
- slackStream: { action: "start", threadTs: "1700.5" },
264
- }),
265
- );
266
- expect(discord.sendDiscordReply).toHaveBeenCalledTimes(1);
323
+ test("a channel that cannot stream resolves quietly, and posts nothing", async () => {
324
+ const result = await sendChannelStreamOp(`${BASE}/deliver/discord`, "999", {
325
+ action: "start",
326
+ threadTs: "1700.5",
327
+ markdownText: "hi",
328
+ });
329
+
330
+ expect(result).toEqual({ ok: true });
331
+ expect(discord.sendDiscordReply).not.toHaveBeenCalled();
267
332
  });
268
333
  });
269
334
 
@@ -1,8 +1,10 @@
1
1
  import type {
2
2
  ChannelDeliveryResult,
3
3
  ChannelReplyPayload,
4
+ SlackStreamOp,
4
5
  } from "@vellumai/gateway-client";
5
6
 
7
+ import type { AssistantActivityPhase } from "../../api/index.js";
6
8
  import type { ChannelId } from "../../channels/types.js";
7
9
 
8
10
  /**
@@ -15,15 +17,69 @@ export interface CallbackContext {
15
17
  readonly params: Readonly<Record<string, string>>;
16
18
  }
17
19
 
20
+ /**
21
+ * How busy the assistant is in one conversation, for the channel to show.
22
+ *
23
+ * `phase` is the daemon's own activity phase rather than a channel's word for
24
+ * it, so every channel is told the same lifecycle and decides how to render
25
+ * it. A channel whose indicator expires on its own re-sends while the busy
26
+ * phases last; a channel whose indicator holds sets it once and clears it on
27
+ * `idle`. Neither shape leaks into what the caller says.
28
+ */
29
+ export interface ActivityTarget {
30
+ readonly chatId: string;
31
+ readonly phase: AssistantActivityPhase;
32
+ /**
33
+ * Who started the turn. Channels that attribute a session to a person read
34
+ * it when the session is created, and ignore it afterwards.
35
+ */
36
+ readonly initiatorUserId?: string;
37
+ }
38
+
39
+ /**
40
+ * Whether a phase means a turn is actually running.
41
+ *
42
+ * A channel with a plain busy indicator shows it for these and nothing else.
43
+ * `awaiting_confirmation` is deliberately not one: an approval is waiting on a
44
+ * person, and a typing bubble there promises output that is not coming.
45
+ */
46
+ export function isBusyActivityPhase(phase: AssistantActivityPhase): boolean {
47
+ return (
48
+ phase === "thinking" || phase === "streaming" || phase === "tool_running"
49
+ );
50
+ }
51
+
52
+ /**
53
+ * An existing message to replace in place, and what to replace it with.
54
+ *
55
+ * `messageId` is the target in the channel's own id space, the same way
56
+ * `chatId` is. `renderRichly` and `emphasis` are the rendering inputs the edit
57
+ * carries, so a replacement renders the way the original did rather than
58
+ * degrading to plain text.
59
+ */
60
+ export interface EditTarget {
61
+ readonly chatId: string;
62
+ readonly messageId: string;
63
+ readonly text: string;
64
+ readonly renderRichly?: boolean;
65
+ /**
66
+ * How prominent the replacement should read. Surface-agnostic, the way
67
+ * `ApprovalActionOption.emphasis` is: each channel translates it to its own
68
+ * token, and one with no equivalent renders plain text. `muted` marks a
69
+ * message as settled rather than current.
70
+ */
71
+ readonly emphasis?: "muted";
72
+ }
73
+
18
74
  /**
19
75
  * Direct outbound delivery for one channel, wrapping the channel's provider-API
20
76
  * send functions behind a uniform surface. Transports are registered statically
21
77
  * (delivery runs in non-daemon contexts) and dispatched by channel, resolved
22
78
  * from the gateway callback URL via `callback-routing.ts`.
23
79
  *
24
- * The dispatcher routes a payload to the optional sub-operation methods when the
25
- * matching payload field is set and the method exists; otherwise it calls
26
- * `deliver`. A transport only implements the sub-operations it supports.
80
+ * Each operation takes its own parameters and is reached through its own entry
81
+ * point; a transport implements only the operations its channel supports, so
82
+ * an absent method is an absent capability.
27
83
  */
28
84
  export interface ChannelTransport {
29
85
  /** Canonical source channel id, e.g. `"slack"`. */
@@ -35,27 +91,55 @@ export interface ChannelTransport {
35
91
  payload: ChannelReplyPayload,
36
92
  ): Promise<ChannelDeliveryResult>;
37
93
 
38
- /** Send a typing indicator. Routed when `payload.chatAction === "typing"`. */
39
- sendTyping?(
94
+ /**
95
+ * Replace a message the assistant already sent.
96
+ *
97
+ * Distinct from `deliver`, which always posts something new. A channel that
98
+ * cannot revise a sent message omits this, and a caller that needs the
99
+ * revision to be visible has to post instead of silently doing nothing.
100
+ *
101
+ * An implementation must not fall back to posting when the edit fails. The
102
+ * original would remain beside the replacement, which reads as the assistant
103
+ * answering twice.
104
+ */
105
+ edit?(
40
106
  ctx: CallbackContext,
41
- payload: ChannelReplyPayload,
107
+ target: EditTarget,
42
108
  ): Promise<ChannelDeliveryResult>;
43
109
 
44
- /** Add an emoji reaction. Routed when `payload.reaction` is set. */
45
- sendReaction?(
110
+ /**
111
+ * Show how busy the assistant is, in whatever form the channel has.
112
+ *
113
+ * One capability rather than one per indicator shape: a self-expiring typing
114
+ * bubble and a status that holds until cleared answer the same question, and
115
+ * splitting them by how the channel keeps them alive forces every caller to
116
+ * know which channel it is talking to. A channel without any such affordance
117
+ * omits the method, so implementing it is the whole of declaring it.
118
+ */
119
+ setActivity?(
46
120
  ctx: CallbackContext,
47
- payload: ChannelReplyPayload,
121
+ target: ActivityTarget,
48
122
  ): Promise<ChannelDeliveryResult>;
49
123
 
50
- /** Update an assistant-thread status surface. Routed when `payload.assistantThreadStatus` is set. */
51
- setThreadStatus?(
52
- ctx: CallbackContext,
53
- payload: ChannelReplyPayload,
54
- ): Promise<ChannelDeliveryResult>;
124
+ /**
125
+ * How often a busy indicator must be re-asserted to stay visible, for a
126
+ * channel whose indicator expires on its own.
127
+ *
128
+ * Omitted by a channel whose indicator holds until it is changed, which is
129
+ * how a caller knows to set it once rather than run a timer. Declaring the
130
+ * cadence here keeps the caller from having to know which channel it is.
131
+ */
132
+ readonly activityRefreshMs?: number;
55
133
 
56
- /** Perform one streaming operation. Routed when `payload.slackStream` is set. */
134
+ /**
135
+ * Advance a streamed reply: open it, add to it, or close it.
136
+ *
137
+ * A channel that can only post a finished message omits this, and the
138
+ * caller falls back to delivering the reply whole.
139
+ */
57
140
  streamReply?(
58
141
  ctx: CallbackContext,
59
- payload: ChannelReplyPayload,
142
+ chatId: string,
143
+ op: SlackStreamOp,
60
144
  ): Promise<ChannelDeliveryResult>;
61
145
  }
@@ -210,3 +210,19 @@ export async function sendDiscordAttachments(
210
210
  totalCount: attachments.length,
211
211
  };
212
212
  }
213
+
214
+ /**
215
+ * Show the Discord typing indicator in a channel.
216
+ *
217
+ * Discord expires it after about ten seconds, so a caller that wants it to
218
+ * persist re-sends on a timer rather than clearing it; there is no stop route.
219
+ */
220
+ export async function sendDiscordTypingIndicator(
221
+ target: DiscordSendTarget,
222
+ ): Promise<void> {
223
+ await callDiscordApi(
224
+ "POST",
225
+ `/channels/${encodeURIComponent(target.channelId)}/typing`,
226
+ {},
227
+ );
228
+ }
@@ -5,9 +5,14 @@ import type {
5
5
  CallbackContext,
6
6
  ChannelTransport,
7
7
  } from "../channel-transport.js";
8
+ import { isBusyActivityPhase } from "../channel-transport.js";
8
9
  import { openDiscordDmChannel } from "./api.js";
9
10
  import type { DiscordSendTarget } from "./send.js";
10
- import { sendDiscordAttachments, sendDiscordReply } from "./send.js";
11
+ import {
12
+ sendDiscordAttachments,
13
+ sendDiscordReply,
14
+ sendDiscordTypingIndicator,
15
+ } from "./send.js";
11
16
 
12
17
  const log = getLogger("discord-transport");
13
18
 
@@ -43,6 +48,23 @@ async function sendTarget(
43
48
  export const discordTransport: ChannelTransport = {
44
49
  channel: "discord",
45
50
 
51
+ // Discord clears a typing indicator after ten seconds.
52
+ activityRefreshMs: 8_000,
53
+
54
+ async setActivity(ctx, target) {
55
+ // Discord's typing indicator expires by itself after ten seconds, so a
56
+ // phase that is not running needs no clearing call.
57
+ if (!isBusyActivityPhase(target.phase)) {
58
+ return { ok: true };
59
+ }
60
+ await sendDiscordTypingIndicator(await sendTarget(ctx, target.chatId));
61
+ log.debug(
62
+ { chatId: target.chatId, phase: target.phase },
63
+ "Discord typing indicator delivered (direct)",
64
+ );
65
+ return { ok: true };
66
+ },
67
+
46
68
  async deliver(ctx, payload) {
47
69
  const { chatId, text, attachments, approval } = payload;
48
70
  const target = await sendTarget(ctx, chatId);
@@ -11,12 +11,18 @@
11
11
  import type {
12
12
  ChannelDeliveryResult,
13
13
  ChannelReplyPayload,
14
+ SlackStreamOp,
14
15
  } from "@vellumai/gateway-client";
15
16
 
16
17
  import { a2aTransport } from "./a2a/transport.js";
17
18
  import type { DirectDeliveryChannel } from "./callback-routing.js";
18
19
  import { channelForCallback } from "./callback-routing.js";
19
- import type { CallbackContext, ChannelTransport } from "./channel-transport.js";
20
+ import type {
21
+ ActivityTarget,
22
+ CallbackContext,
23
+ ChannelTransport,
24
+ EditTarget,
25
+ } from "./channel-transport.js";
20
26
  import { discordTransport } from "./discord/transport.js";
21
27
  import { slackTransport } from "./slack/transport.js";
22
28
  import { telegramTransport } from "./telegram-bot/transport.js";
@@ -45,6 +51,81 @@ export function getTransportForCallback(
45
51
  return channel ? TRANSPORTS[channel] : undefined;
46
52
  }
47
53
 
54
+ /**
55
+ * Whether the channel this callback addresses can show how busy the assistant
56
+ * is.
57
+ *
58
+ * Asks the transport rather than the channel id, so a channel that gains the
59
+ * method starts being asked without a caller being told about it.
60
+ */
61
+ export function supportsChannelActivity(callbackUrl: string): boolean {
62
+ return getTransportForCallback(callbackUrl)?.setActivity !== undefined;
63
+ }
64
+
65
+ /**
66
+ * How often this channel's busy indicator has to be re-asserted, or
67
+ * `undefined` when it holds until changed and one call is enough.
68
+ */
69
+ export function channelActivityRefreshMs(
70
+ callbackUrl: string,
71
+ ): number | undefined {
72
+ return getTransportForCallback(callbackUrl)?.activityRefreshMs;
73
+ }
74
+
75
+ /**
76
+ * Show how busy the assistant is on the channel this callback addresses.
77
+ *
78
+ * Resolves to nothing when the channel has no such affordance, which is the
79
+ * ordinary case rather than a failure: the indicator is decoration, and a
80
+ * channel that cannot show one is not degraded by its absence.
81
+ */
82
+ export async function setChannelActivity(
83
+ callbackUrl: string,
84
+ target: ActivityTarget,
85
+ ): Promise<ChannelDeliveryResult> {
86
+ const transport = getTransportForCallback(callbackUrl);
87
+ if (!transport?.setActivity) {
88
+ return { ok: true };
89
+ }
90
+ return transport.setActivity(callbackContext(callbackUrl), target);
91
+ }
92
+
93
+ /**
94
+ * Replace a message the assistant already sent.
95
+ *
96
+ * Returns `ok` without acting when the channel cannot revise a sent message,
97
+ * matching the other capability entry points: an absent method is an absent
98
+ * capability, not an error.
99
+ */
100
+ export async function editChannelMessage(
101
+ callbackUrl: string,
102
+ target: EditTarget,
103
+ ): Promise<ChannelDeliveryResult> {
104
+ const transport = getTransportForCallback(callbackUrl);
105
+ if (!transport?.edit) {
106
+ return { ok: true };
107
+ }
108
+ return transport.edit(callbackContext(callbackUrl), target);
109
+ }
110
+
111
+ /**
112
+ * Advance a streamed reply on the channel this callback addresses.
113
+ *
114
+ * Resolves to nothing when the channel cannot stream, so a caller that wants a
115
+ * streamed reply learns it has to send the whole thing instead.
116
+ */
117
+ export async function sendChannelStreamOp(
118
+ callbackUrl: string,
119
+ chatId: string,
120
+ op: SlackStreamOp,
121
+ ): Promise<ChannelDeliveryResult> {
122
+ const transport = getTransportForCallback(callbackUrl);
123
+ if (!transport?.streamReply) {
124
+ return { ok: true };
125
+ }
126
+ return transport.streamReply(callbackContext(callbackUrl), chatId, op);
127
+ }
128
+
48
129
  function callbackContext(callbackUrl: string): CallbackContext {
49
130
  const params: Record<string, string> = {};
50
131
  try {
@@ -74,9 +155,8 @@ export function isDirectDelivery(callbackUrl: string): boolean {
74
155
  * Deliver a channel reply directly to the provider API, bypassing the gateway
75
156
  * HTTP proxy. Callers MUST check `isDirectDelivery()` first.
76
157
  *
77
- * Sub-operations (reaction, thread status, typing) route to the transport's
78
- * optional method when both the payload field and the method are present;
79
- * otherwise the reply is delivered as text / approval / attachments.
158
+ * Delivers the reply itself: text, approval card, attachments. Every other
159
+ * operation a transport supports is reached through its own entry point.
80
160
  */
81
161
  export async function deliverDirect(
82
162
  callbackUrl: string,
@@ -90,17 +170,5 @@ export async function deliverDirect(
90
170
  }
91
171
 
92
172
  const ctx = callbackContext(callbackUrl);
93
- if (payload.slackStream && transport.streamReply) {
94
- return transport.streamReply(ctx, payload);
95
- }
96
- if (payload.reaction && transport.sendReaction) {
97
- return transport.sendReaction(ctx, payload);
98
- }
99
- if (payload.assistantThreadStatus && transport.setThreadStatus) {
100
- return transport.setThreadStatus(ctx, payload);
101
- }
102
- if (payload.chatAction === "typing" && transport.sendTyping) {
103
- return transport.sendTyping(ctx, payload);
104
- }
105
173
  return transport.deliver(ctx, payload);
106
174
  }
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import type { ProviderMessageMetadata } from "../../provider-message-metadata.js";
4
+
3
5
  /**
4
6
  * Typed Slack message metadata stored flat in the `messages.metadata` column
5
7
  * alongside whatever other top-level keys the broader metadata envelope
@@ -288,6 +290,49 @@ export function readSlackMetadataFromMessageMetadata(
288
290
  return opts?.allowFlatLegacy ? readSlackMetadata(metadata) : null;
289
291
  }
290
292
 
293
+ /**
294
+ * Present a Slack row's stored metadata as the channel-neutral shape.
295
+ *
296
+ * `channelTs` is the row's own provider id and `targetChannelTs` the id of the
297
+ * message a reaction was attached to. Both are Slack `ts` strings, which is a
298
+ * Slack detail of the id format, not of the fact.
299
+ *
300
+ * The rest of Slack's envelope (file markers, timezone labels, channel name)
301
+ * stays there and is read from there by the Slack transcript renderer, which
302
+ * is a provider renderer by design.
303
+ */
304
+ export function slackMetadataAsProviderMetadata(
305
+ meta: SlackMessageMetadata,
306
+ ): ProviderMessageMetadata {
307
+ return {
308
+ source: "slack",
309
+ conversationExternalId: meta.channelId,
310
+ // A reaction row stores the reacted message's ts in `channelTs`, which is
311
+ // its target rather than an id of its own.
312
+ ...(meta.eventKind === "reaction" ? {} : { messageId: meta.channelTs }),
313
+ ...(meta.actorExternalUserId
314
+ ? { actorExternalId: meta.actorExternalUserId }
315
+ : {}),
316
+ ...(meta.threadTs ? { threadId: meta.threadTs } : {}),
317
+ ...(meta.displayName ? { displayName: meta.displayName } : {}),
318
+ eventKind: meta.eventKind,
319
+ ...(meta.reaction
320
+ ? {
321
+ reaction: {
322
+ targetMessageId: meta.reaction.targetChannelTs,
323
+ emoji: meta.reaction.emoji,
324
+ op: meta.reaction.op,
325
+ ...(meta.reaction.actorDisplayName
326
+ ? { actorDisplayName: meta.reaction.actorDisplayName }
327
+ : {}),
328
+ },
329
+ }
330
+ : {}),
331
+ ...(meta.editedAt !== undefined ? { editedAt: meta.editedAt } : {}),
332
+ ...(meta.deletedAt !== undefined ? { deletedAt: meta.deletedAt } : {}),
333
+ };
334
+ }
335
+
291
336
  /**
292
337
  * Serialize `SlackMessageMetadata` to a JSON string suitable for a fresh
293
338
  * write to the `messages.metadata` column. Use `mergeSlackMetadata` when an