@vellumai/assistant 0.11.4 → 0.11.5-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 (367) hide show
  1. package/AGENTS.md +2 -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__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  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/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -18,6 +18,9 @@ const slack = {
18
18
  sendSlackStreamOp: mock((..._args: unknown[]) =>
19
19
  Promise.resolve({ ok: true, ts: "stream-ts" }),
20
20
  ),
21
+ updateSlackMessage: mock((..._args: unknown[]) =>
22
+ Promise.resolve({ ts: "slack-ts" }),
23
+ ),
21
24
  };
22
25
  const telegram = {
23
26
  sendTelegramReply: mock((..._args: unknown[]) => Promise.resolve()),
@@ -57,8 +60,17 @@ mock.module("../../../util/logger.js", () => ({
57
60
  getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
58
61
  }));
59
62
 
60
- const { deliverDirect, isDirectDelivery, getTransportForCallback } =
61
- await import("../index.js");
63
+ const {
64
+ deliverDirect,
65
+ editChannelMessage,
66
+ sendChannelReaction,
67
+ sendChannelStreamOp,
68
+ sendChannelTyping,
69
+ setChannelThreadStatus,
70
+ supportsChannelTyping,
71
+ isDirectDelivery,
72
+ getTransportForCallback,
73
+ } = await import("../index.js");
62
74
 
63
75
  const BASE = "https://gateway.internal";
64
76
 
@@ -134,56 +146,70 @@ describe("Slack sub-operation selection", () => {
134
146
  expect(opts.threadTs).toBe("1700.9");
135
147
  });
136
148
 
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
- );
149
+ test("sendChannelReaction reaches Slack without touching the text path", async () => {
150
+ await sendChannelReaction(`${BASE}/deliver/slack`, {
151
+ chatId: "C1",
152
+ messageId: "1700.5",
153
+ emoji: "white_check_mark",
154
+ action: "add",
155
+ });
156
+
148
157
  expect(slack.sendSlackReaction).toHaveBeenCalledTimes(1);
149
158
  expect(slack.sendSlackReply).not.toHaveBeenCalled();
150
159
  });
151
160
 
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
- );
161
+ test("editChannelMessage updates in place instead of posting", async () => {
162
+ await editChannelMessage(`${BASE}/deliver/slack`, {
163
+ chatId: "C1",
164
+ messageId: "1700.5",
165
+ text: "revised",
166
+ });
167
+
168
+ expect(slack.updateSlackMessage).toHaveBeenCalledTimes(1);
169
+ // The distinction the split exists for: an edit never reaches the post
170
+ // path, so a failed edit cannot become a second visible message.
171
+ expect(slack.sendSlackReply).not.toHaveBeenCalled();
172
+ });
173
+
174
+ test("setChannelThreadStatus reaches Slack without touching the text path", async () => {
175
+ await setChannelThreadStatus(`${BASE}/deliver/slack`, {
176
+ chatId: "C1",
177
+ threadTs: "1700.5",
178
+ status: "is thinking",
179
+ });
180
+
163
181
  expect(slack.sendSlackAssistantThreadStatus).toHaveBeenCalledTimes(1);
164
182
  expect(slack.sendSlackReply).not.toHaveBeenCalled();
165
183
  });
166
184
 
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);
185
+ test("a channel with no status surface resolves quietly", async () => {
186
+ const result = await setChannelThreadStatus(`${BASE}/deliver/telegram`, {
187
+ chatId: "123",
188
+ threadTs: "1700.5",
189
+ status: "is thinking",
190
+ });
191
+
192
+ expect(result).toEqual({ ok: true });
193
+ expect(telegram.sendTelegramReply).not.toHaveBeenCalled();
173
194
  });
174
195
 
175
- test("slackStream routes to sendSlackStreamOp ahead of the text path", async () => {
176
- const result = await deliverDirect(
196
+ test("sendChannelTyping resolves quietly for a channel with no typing capability", async () => {
197
+ const result = await sendChannelTyping(`${BASE}/deliver/slack`, "C1");
198
+
199
+ expect(result).toEqual({ ok: true });
200
+ expect(slack.sendSlackReply).not.toHaveBeenCalled();
201
+ });
202
+
203
+ test("sendChannelStreamOp reaches Slack without touching the text path", async () => {
204
+ const result = await sendChannelStreamOp(
177
205
  `${BASE}/deliver/slack?threadTs=1700.5`,
178
- payload({
179
- text: "ignored while streaming",
180
- slackStream: { action: "start", threadTs: "1700.5" },
181
- }),
206
+ "C1",
207
+ { action: "start", threadTs: "1700.5", markdownText: "hi" },
182
208
  );
183
209
  expect(slack.sendSlackStreamOp).toHaveBeenCalledTimes(1);
184
210
  expect(slack.sendSlackStreamOp.mock.calls[0]).toEqual([
185
211
  "C1",
186
- { action: "start", threadTs: "1700.5" },
212
+ { action: "start", threadTs: "1700.5", markdownText: "hi" },
187
213
  ]);
188
214
  expect(slack.sendSlackReply).not.toHaveBeenCalled();
189
215
  expect(result).toEqual({ ok: true, ts: "stream-ts" });
@@ -191,22 +217,49 @@ describe("Slack sub-operation selection", () => {
191
217
  });
192
218
 
193
219
  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" },
220
+ test("a channel with no reaction capability resolves quietly", async () => {
221
+ // Slack is the only channel that implements it, because the only producer
222
+ // is Slack's own acknowledgement fallback. A channel without the method
223
+ // is not a failed delivery, so nothing is attempted and nothing throws.
224
+ const target = {
225
+ chatId: "C1",
226
+ messageId: "1",
227
+ emoji: "eyes",
228
+ action: "add",
229
+ } as const;
230
+ expect(
231
+ await sendChannelReaction(`${BASE}/deliver/telegram`, target),
232
+ ).toEqual({ ok: true });
233
+ expect(telegram.sendTelegramReply).not.toHaveBeenCalled();
234
+ });
235
+
236
+ test("a channel that cannot revise a sent message resolves quietly", async () => {
237
+ // Only Slack implements `edit` today. A channel without the method is not
238
+ // a failed delivery: nothing is attempted, nothing throws, and no fresh
239
+ // message is posted in place of the revision.
240
+ expect(
241
+ await editChannelMessage(`${BASE}/deliver/telegram`, {
242
+ chatId: "C1",
243
+ messageId: "1",
244
+ text: "revised",
200
245
  }),
201
- );
202
- expect(telegram.sendTelegramReply).toHaveBeenCalledTimes(1);
246
+ ).toEqual({ ok: true });
247
+ expect(telegram.sendTelegramReply).not.toHaveBeenCalled();
203
248
  });
204
249
 
205
- test("typing to Telegram routes to its typing indicator", async () => {
206
- await deliverDirect(
207
- `${BASE}/deliver/telegram`,
208
- payload({ chatAction: "typing" }),
209
- );
250
+ test("the typing capability is read from the transport, not the channel name", () => {
251
+ // The heartbeat gate in background-dispatch asks this rather than testing
252
+ // `sourceChannel === "telegram"`, so a channel that implements the method
253
+ // starts showing an indicator without a caller being changed.
254
+ expect(supportsChannelTyping(`${BASE}/deliver/telegram`)).toBe(true);
255
+ expect(supportsChannelTyping(`${BASE}/deliver/discord`)).toBe(true);
256
+ expect(supportsChannelTyping(`${BASE}/deliver/slack`)).toBe(false);
257
+ expect(supportsChannelTyping(`${BASE}/deliver/whatsapp`)).toBe(false);
258
+ });
259
+
260
+ test("sendChannelTyping reaches Telegram's typing indicator", async () => {
261
+ await sendChannelTyping(`${BASE}/deliver/telegram`, "123");
262
+
210
263
  expect(telegram.sendTelegramTypingIndicator).toHaveBeenCalledTimes(1);
211
264
  });
212
265
 
@@ -244,26 +297,21 @@ describe("capability gating across channels", () => {
244
297
  });
245
298
  });
246
299
 
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);
300
+ test("sendChannelTyping reaches Discord's typing indicator", async () => {
301
+ await sendChannelTyping(`${BASE}/deliver/discord`, "999");
302
+
303
+ expect(discord.sendDiscordTypingIndicator).toHaveBeenCalledTimes(1);
256
304
  });
257
305
 
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);
306
+ test("a channel that cannot stream resolves quietly, and posts nothing", async () => {
307
+ const result = await sendChannelStreamOp(`${BASE}/deliver/discord`, "999", {
308
+ action: "start",
309
+ threadTs: "1700.5",
310
+ markdownText: "hi",
311
+ });
312
+
313
+ expect(result).toEqual({ ok: true });
314
+ expect(discord.sendDiscordReply).not.toHaveBeenCalled();
267
315
  });
268
316
  });
269
317
 
@@ -1,6 +1,8 @@
1
+ import type { KnownBlock } from "@slack/types";
1
2
  import type {
2
3
  ChannelDeliveryResult,
3
4
  ChannelReplyPayload,
5
+ SlackStreamOp,
4
6
  } from "@vellumai/gateway-client";
5
7
 
6
8
  import type { ChannelId } from "../../channels/types.js";
@@ -15,15 +17,52 @@ export interface CallbackContext {
15
17
  readonly params: Readonly<Record<string, string>>;
16
18
  }
17
19
 
20
+ /** The message an emoji reaction lands on, and what to do there. */
21
+ export interface ReactionTarget {
22
+ readonly chatId: string;
23
+ /** The target message, in the channel's own id space. */
24
+ readonly messageId: string;
25
+ readonly emoji: string;
26
+ readonly action: "add" | "remove";
27
+ }
28
+
29
+ /**
30
+ * A status surface update. `chatId` is the room, spelled the way every other
31
+ * method on this interface spells it.
32
+ */
33
+ export interface ThreadStatus {
34
+ readonly chatId: string;
35
+ readonly threadTs: string;
36
+ readonly status: string;
37
+ /** Rotating hints shown under the status while it holds. */
38
+ readonly loadingMessages?: readonly string[];
39
+ }
40
+
41
+ /**
42
+ * An existing message to replace in place, and what to replace it with.
43
+ *
44
+ * `messageId` is the target in the channel's own id space, the same way
45
+ * `chatId` is. `blocks` and `useBlocks` are the rendering inputs the edit
46
+ * carries, so a replacement renders the way the original did rather than
47
+ * degrading to plain text.
48
+ */
49
+ export interface EditTarget {
50
+ readonly chatId: string;
51
+ readonly messageId: string;
52
+ readonly text: string;
53
+ readonly blocks?: readonly KnownBlock[];
54
+ readonly useBlocks?: boolean;
55
+ }
56
+
18
57
  /**
19
58
  * Direct outbound delivery for one channel, wrapping the channel's provider-API
20
59
  * send functions behind a uniform surface. Transports are registered statically
21
60
  * (delivery runs in non-daemon contexts) and dispatched by channel, resolved
22
61
  * from the gateway callback URL via `callback-routing.ts`.
23
62
  *
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.
63
+ * Each operation takes its own parameters and is reached through its own entry
64
+ * point; a transport implements only the operations its channel supports, so
65
+ * an absent method is an absent capability.
27
66
  */
28
67
  export interface ChannelTransport {
29
68
  /** Canonical source channel id, e.g. `"slack"`. */
@@ -35,27 +74,65 @@ export interface ChannelTransport {
35
74
  payload: ChannelReplyPayload,
36
75
  ): Promise<ChannelDeliveryResult>;
37
76
 
38
- /** Send a typing indicator. Routed when `payload.chatAction === "typing"`. */
39
- sendTyping?(
77
+ /**
78
+ * Replace a message the assistant already sent.
79
+ *
80
+ * Distinct from `deliver`, which always posts something new. A channel that
81
+ * cannot revise a sent message omits this, and a caller that needs the
82
+ * revision to be visible has to post instead of silently doing nothing.
83
+ *
84
+ * An implementation must not fall back to posting when the edit fails. The
85
+ * original would remain beside the replacement, which reads as the assistant
86
+ * answering twice.
87
+ */
88
+ edit?(
40
89
  ctx: CallbackContext,
41
- payload: ChannelReplyPayload,
90
+ target: EditTarget,
42
91
  ): Promise<ChannelDeliveryResult>;
43
92
 
44
- /** Add an emoji reaction. Routed when `payload.reaction` is set. */
45
- sendReaction?(
93
+ /**
94
+ * Show that the assistant is working, in whatever form the channel has.
95
+ *
96
+ * Takes the place and nothing else, because that is all it needs. A channel
97
+ * without the affordance omits the method, so implementing it is the whole
98
+ * of declaring the capability.
99
+ */
100
+ typing?(ctx: CallbackContext, chatId: string): Promise<ChannelDeliveryResult>;
101
+
102
+ /**
103
+ * Add or remove one of the assistant's own emoji reactions on a message.
104
+ *
105
+ * `messageId` is the target's id in the channel's own space, the same way
106
+ * `chatId` is: Slack spells it as a timestamp, Discord and Telegram as ids.
107
+ * Nothing outside the channel reads it, so nothing outside needs a shared
108
+ * spelling for it.
109
+ */
110
+ react?(
46
111
  ctx: CallbackContext,
47
- payload: ChannelReplyPayload,
112
+ target: ReactionTarget,
48
113
  ): Promise<ChannelDeliveryResult>;
49
114
 
50
- /** Update an assistant-thread status surface. Routed when `payload.assistantThreadStatus` is set. */
115
+ /**
116
+ * Set or clear the channel's own "working on it" surface.
117
+ *
118
+ * Distinct from `typing`: an indicator a channel refreshes on a timer versus
119
+ * a status a channel holds until it is changed. Slack has both and uses this
120
+ * one; a channel with only the first implements only `typing`.
121
+ */
51
122
  setThreadStatus?(
52
123
  ctx: CallbackContext,
53
- payload: ChannelReplyPayload,
124
+ status: ThreadStatus,
54
125
  ): Promise<ChannelDeliveryResult>;
55
126
 
56
- /** Perform one streaming operation. Routed when `payload.slackStream` is set. */
127
+ /**
128
+ * Advance a streamed reply: open it, add to it, or close it.
129
+ *
130
+ * A channel that can only post a finished message omits this, and the
131
+ * caller falls back to delivering the reply whole.
132
+ */
57
133
  streamReply?(
58
134
  ctx: CallbackContext,
59
- payload: ChannelReplyPayload,
135
+ chatId: string,
136
+ op: SlackStreamOp,
60
137
  ): Promise<ChannelDeliveryResult>;
61
138
  }
@@ -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
+ }
@@ -7,7 +7,11 @@ import type {
7
7
  } from "../channel-transport.js";
8
8
  import { openDiscordDmChannel } from "./api.js";
9
9
  import type { DiscordSendTarget } from "./send.js";
10
- import { sendDiscordAttachments, sendDiscordReply } from "./send.js";
10
+ import {
11
+ sendDiscordAttachments,
12
+ sendDiscordReply,
13
+ sendDiscordTypingIndicator,
14
+ } from "./send.js";
11
15
 
12
16
  const log = getLogger("discord-transport");
13
17
 
@@ -43,6 +47,12 @@ async function sendTarget(
43
47
  export const discordTransport: ChannelTransport = {
44
48
  channel: "discord",
45
49
 
50
+ async typing(ctx, chatId) {
51
+ await sendDiscordTypingIndicator(await sendTarget(ctx, chatId));
52
+ log.debug({ chatId }, "Discord typing indicator delivered (direct)");
53
+ return { ok: true };
54
+ },
55
+
46
56
  async deliver(ctx, payload) {
47
57
  const { chatId, text, attachments, approval } = payload;
48
58
  const target = await sendTarget(ctx, chatId);
@@ -11,12 +11,19 @@
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
+ CallbackContext,
22
+ ChannelTransport,
23
+ EditTarget,
24
+ ReactionTarget,
25
+ ThreadStatus,
26
+ } from "./channel-transport.js";
20
27
  import { discordTransport } from "./discord/transport.js";
21
28
  import { slackTransport } from "./slack/transport.js";
22
29
  import { telegramTransport } from "./telegram-bot/transport.js";
@@ -45,6 +52,105 @@ export function getTransportForCallback(
45
52
  return channel ? TRANSPORTS[channel] : undefined;
46
53
  }
47
54
 
55
+ /**
56
+ * Whether the channel this callback addresses can show a working indicator.
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 supportsChannelTyping(callbackUrl: string): boolean {
62
+ return getTransportForCallback(callbackUrl)?.typing !== undefined;
63
+ }
64
+
65
+ /**
66
+ * Show that the assistant is working on the channel this callback addresses.
67
+ *
68
+ * Resolves to nothing when the channel has no such affordance, which is the
69
+ * ordinary case rather than a failure: the indicator is decoration, and a
70
+ * channel that cannot show one is not degraded by its absence.
71
+ */
72
+ export async function sendChannelTyping(
73
+ callbackUrl: string,
74
+ chatId: string,
75
+ ): Promise<ChannelDeliveryResult> {
76
+ const transport = getTransportForCallback(callbackUrl);
77
+ if (!transport?.typing) {
78
+ return { ok: true };
79
+ }
80
+ return transport.typing(callbackContext(callbackUrl), chatId);
81
+ }
82
+
83
+ /**
84
+ * Add or remove one of the assistant's own reactions on a message.
85
+ *
86
+ * Resolves to nothing when the channel has none, the same as typing: a
87
+ * reaction is an acknowledgement, and a channel that cannot show one is not a
88
+ * failed delivery.
89
+ */
90
+ /**
91
+ * Replace a message the assistant already sent.
92
+ *
93
+ * Returns `ok` without acting when the channel cannot revise a sent message,
94
+ * matching the other capability entry points: an absent method is an absent
95
+ * capability, not an error.
96
+ */
97
+ export async function editChannelMessage(
98
+ callbackUrl: string,
99
+ target: EditTarget,
100
+ ): Promise<ChannelDeliveryResult> {
101
+ const transport = getTransportForCallback(callbackUrl);
102
+ if (!transport?.edit) {
103
+ return { ok: true };
104
+ }
105
+ return transport.edit(callbackContext(callbackUrl), target);
106
+ }
107
+
108
+ export async function sendChannelReaction(
109
+ callbackUrl: string,
110
+ target: ReactionTarget,
111
+ ): Promise<ChannelDeliveryResult> {
112
+ const transport = getTransportForCallback(callbackUrl);
113
+ if (!transport?.react) {
114
+ return { ok: true };
115
+ }
116
+ return transport.react(callbackContext(callbackUrl), target);
117
+ }
118
+
119
+ /**
120
+ * Set or clear the channel's status surface.
121
+ *
122
+ * Resolves to nothing when the channel holds none, so a caller does not have
123
+ * to know which channels do.
124
+ */
125
+ export async function setChannelThreadStatus(
126
+ callbackUrl: string,
127
+ status: ThreadStatus,
128
+ ): Promise<ChannelDeliveryResult> {
129
+ const transport = getTransportForCallback(callbackUrl);
130
+ if (!transport?.setThreadStatus) {
131
+ return { ok: true };
132
+ }
133
+ return transport.setThreadStatus(callbackContext(callbackUrl), status);
134
+ }
135
+
136
+ /**
137
+ * Advance a streamed reply on the channel this callback addresses.
138
+ *
139
+ * Resolves to nothing when the channel cannot stream, so a caller that wants a
140
+ * streamed reply learns it has to send the whole thing instead.
141
+ */
142
+ export async function sendChannelStreamOp(
143
+ callbackUrl: string,
144
+ chatId: string,
145
+ op: SlackStreamOp,
146
+ ): Promise<ChannelDeliveryResult> {
147
+ const transport = getTransportForCallback(callbackUrl);
148
+ if (!transport?.streamReply) {
149
+ return { ok: true };
150
+ }
151
+ return transport.streamReply(callbackContext(callbackUrl), chatId, op);
152
+ }
153
+
48
154
  function callbackContext(callbackUrl: string): CallbackContext {
49
155
  const params: Record<string, string> = {};
50
156
  try {
@@ -74,9 +180,8 @@ export function isDirectDelivery(callbackUrl: string): boolean {
74
180
  * Deliver a channel reply directly to the provider API, bypassing the gateway
75
181
  * HTTP proxy. Callers MUST check `isDirectDelivery()` first.
76
182
  *
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.
183
+ * Delivers the reply itself: text, approval card, attachments. Every other
184
+ * operation a transport supports is reached through its own entry point.
80
185
  */
81
186
  export async function deliverDirect(
82
187
  callbackUrl: string,
@@ -90,17 +195,5 @@ export async function deliverDirect(
90
195
  }
91
196
 
92
197
  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
198
  return transport.deliver(ctx, payload);
106
199
  }
@@ -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
@@ -6,8 +6,7 @@
6
6
  * `{role, content}[]` chronologically ordered with compact Slack tags so the
7
7
  * model can reason across sibling threads in one channel.
8
8
  *
9
- * The function is pure: no I/O, no implicit clock reads. Time is taken from
10
- * `opts.now` only when needed for relative formatting. Sort and tag rendering
9
+ * The function is pure: no I/O, no implicit clock reads. Sort and tag rendering
11
10
  * are deterministic.
12
11
  *
13
12
  * Consumers wire this into inbound history rendering and the compaction
@@ -63,8 +62,6 @@ export interface RenderableSlackMessage {
63
62
  }
64
63
 
65
64
  export interface RenderOptions {
66
- /** Reserved for future relative-time rendering; currently unused. */
67
- now?: Date;
68
65
  /** Cap rendered reactions per parent message; default 5. */
69
66
  maxReactionsPerMessage?: number;
70
67
  }