@vellumai/assistant 0.10.3 → 0.10.4-staging.1

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 (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -1,277 +1,101 @@
1
1
  /**
2
2
  * Direct channel delivery — bypasses the gateway HTTP proxy.
3
3
  *
4
- * Each channel that supports direct delivery registers its callback-URL
5
- * matcher and send logic here. The gateway-client consults
4
+ * Each channel exposes a `ChannelTransport`; the callback-URL → channel mapping
5
+ * lives in `callback-routing.ts`. The gateway-client consults
6
6
  * `isDirectDelivery()` before falling back to the HTTP proxy path.
7
7
  *
8
- * Currently supported: WhatsApp, Telegram, Slack, A2A.
8
+ * Supported: Slack, Telegram, WhatsApp, A2A.
9
9
  */
10
10
 
11
11
  import type {
12
12
  ChannelDeliveryResult,
13
13
  ChannelReplyPayload,
14
14
  } from "@vellumai/gateway-client";
15
- import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
16
15
 
17
- import { getLogger } from "../../util/logger.js";
18
- import { deliverA2AReply } from "./a2a/deliver.js";
19
- import {
20
- sendSlackAssistantThreadStatus,
21
- sendSlackAttachments,
22
- sendSlackReaction,
23
- sendSlackReply,
24
- sendSlackTypingIndicator,
25
- } from "./slack/send.js";
26
- import {
27
- sendTelegramAttachments,
28
- sendTelegramReply,
29
- sendTelegramTypingIndicator,
30
- } from "./telegram-bot/send.js";
31
- import { sendWhatsAppAttachments, sendWhatsAppReply } from "./whatsapp/send.js";
16
+ import { a2aTransport } from "./a2a/transport.js";
17
+ import type { DirectDeliveryChannel } from "./callback-routing.js";
18
+ import { channelForCallback } from "./callback-routing.js";
19
+ import type { CallbackContext, ChannelTransport } from "./channel-transport.js";
20
+ import { slackTransport } from "./slack/transport.js";
21
+ import { telegramTransport } from "./telegram-bot/transport.js";
22
+ import { whatsappTransport } from "./whatsapp/transport.js";
23
+
24
+ // Keyed by `DirectDeliveryChannel` so the type checker enforces that the
25
+ // registered transports cover exactly the channels `callback-routing` resolves:
26
+ // add a channel to that set and this object fails to compile until its transport
27
+ // is registered here (and vice versa). No second list to drift against.
28
+ const TRANSPORTS: Record<DirectDeliveryChannel, ChannelTransport> = {
29
+ slack: slackTransport,
30
+ telegram: telegramTransport,
31
+ whatsapp: whatsappTransport,
32
+ a2a: a2aTransport,
33
+ };
32
34
 
33
- const log = getLogger("direct-delivery");
34
-
35
- // ---------------------------------------------------------------------------
36
- // Callback-URL matchers
37
- // ---------------------------------------------------------------------------
38
-
39
- function matchesPathname(callbackUrl: string, pathname: string): boolean {
40
- try {
41
- return new URL(callbackUrl).pathname === pathname;
42
- } catch {
43
- return callbackUrl.endsWith(pathname);
44
- }
45
- }
46
-
47
- function isWhatsAppCallback(callbackUrl: string): boolean {
48
- return matchesPathname(callbackUrl, "/deliver/whatsapp");
49
- }
50
-
51
- function isTelegramCallback(callbackUrl: string): boolean {
52
- return matchesPathname(callbackUrl, "/deliver/telegram");
53
- }
54
-
55
- function isSlackCallback(callbackUrl: string): boolean {
56
- try {
57
- return new URL(callbackUrl).pathname === "/deliver/slack";
58
- } catch {
59
- return callbackUrl.endsWith("/deliver/slack");
60
- }
61
- }
62
-
63
- function isA2ACallback(callbackUrl: string): boolean {
64
- return matchesPathname(callbackUrl, "/deliver/a2a");
35
+ /**
36
+ * Resolve the transport that owns a gateway callback URL, or `undefined` when
37
+ * no channel delivers it directly.
38
+ */
39
+ export function getTransportForCallback(
40
+ callbackUrl: string,
41
+ ): ChannelTransport | undefined {
42
+ const channel = channelForCallback(callbackUrl);
43
+ return channel ? TRANSPORTS[channel] : undefined;
65
44
  }
66
45
 
67
- function parseSlackCallbackParams(callbackUrl: string): {
68
- channel?: string;
69
- threadTs?: string;
70
- messageTs?: string;
71
- } {
46
+ function callbackContext(callbackUrl: string): CallbackContext {
47
+ const params: Record<string, string> = {};
72
48
  try {
73
- const url = new URL(callbackUrl);
74
- return {
75
- channel: url.searchParams.get("channel") ?? undefined,
76
- threadTs: url.searchParams.get("threadTs") ?? undefined,
77
- messageTs: url.searchParams.get("messageTs") ?? undefined,
78
- };
79
- } catch {
80
- return {};
81
- }
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Per-channel direct delivery
86
- // ---------------------------------------------------------------------------
87
-
88
- async function deliverWhatsApp(
89
- payload: ChannelReplyPayload,
90
- ): Promise<ChannelDeliveryResult> {
91
- const { chatId, text, attachments, approval } = payload;
92
-
93
- if (text) {
94
- await sendWhatsAppReply(chatId, text, approval);
95
- } else if (approval) {
96
- await sendWhatsAppReply(
97
- chatId,
98
- approval.plainTextFallback || "Approval required",
99
- approval,
100
- );
101
- }
102
-
103
- if (attachments && attachments.length > 0) {
104
- const result = await sendWhatsAppAttachments(chatId, attachments);
105
- if (result.allFailed && !text) {
106
- throw new ChannelDeliveryError(
107
- 502,
108
- `All ${result.failureCount} attachments failed to deliver`,
109
- );
110
- }
111
- }
112
-
113
- log.info({ chatId, hasText: !!text }, "WhatsApp reply delivered (direct)");
114
- return { ok: true };
115
- }
116
-
117
- async function deliverTelegram(
118
- payload: ChannelReplyPayload,
119
- ): Promise<ChannelDeliveryResult> {
120
- const { chatId, text, attachments, approval, chatAction } = payload;
121
-
122
- if (chatAction === "typing") {
123
- await sendTelegramTypingIndicator(chatId);
124
- log.debug({ chatId }, "Telegram typing indicator delivered (direct)");
125
- return { ok: true };
126
- }
127
-
128
- if (text) {
129
- await sendTelegramReply(chatId, text, approval);
130
- } else if (approval) {
131
- await sendTelegramReply(
132
- chatId,
133
- approval.plainTextFallback || "Approval required",
134
- approval,
135
- );
136
- }
137
-
138
- if (attachments && attachments.length > 0) {
139
- const result = await sendTelegramAttachments(chatId, attachments);
140
- if (result.allFailed && !text) {
141
- throw new ChannelDeliveryError(
142
- 502,
143
- `All ${result.failureCount} attachments failed to deliver`,
144
- );
145
- }
146
- }
147
-
148
- log.info({ chatId, hasText: !!text }, "Telegram reply delivered (direct)");
149
- return { ok: true };
150
- }
151
-
152
- async function deliverSlack(
153
- callbackUrl: string,
154
- payload: ChannelReplyPayload,
155
- ): Promise<ChannelDeliveryResult> {
156
- const { chatId, text, attachments, chatAction, blocks } = payload;
157
- const params = parseSlackCallbackParams(callbackUrl);
158
- const threadTs = params.threadTs;
159
-
160
- // Emoji reaction
161
- if (payload.reaction) {
162
- await sendSlackReaction(
163
- chatId,
164
- payload.reaction.name,
165
- payload.reaction.messageTs,
166
- payload.reaction.action,
167
- );
168
- return { ok: true };
169
- }
170
-
171
- // Assistants API thread status
172
- if (payload.assistantThreadStatus) {
173
- const {
174
- channel,
175
- threadTs: statusThreadTs,
176
- status,
177
- loadingMessages,
178
- } = payload.assistantThreadStatus;
179
- await sendSlackAssistantThreadStatus(
180
- channel,
181
- statusThreadTs,
182
- status,
183
- loadingMessages,
184
- );
185
- return { ok: true };
186
- }
187
-
188
- // Typing indicator
189
- if (chatAction === "typing") {
190
- const placeholderTs = await sendSlackTypingIndicator(chatId, threadTs);
191
- log.debug({ chatId }, "Slack typing indicator delivered (direct)");
192
- return { ok: true, ts: placeholderTs };
193
- }
194
-
195
- // Text + blocks delivery
196
- let sentTs: string | undefined;
197
- if (text) {
198
- const result = await sendSlackReply(chatId, text, {
199
- threadTs,
200
- blocks,
201
- approval: payload.approval,
202
- useBlocks: payload.useBlocks,
203
- ephemeral: payload.ephemeral,
204
- user: payload.user,
205
- messageTs: payload.messageTs,
206
- });
207
- sentTs = result.ts;
208
- } else if (payload.approval) {
209
- const result = await sendSlackReply(
210
- chatId,
211
- payload.approval.plainTextFallback || "Approval required",
212
- {
213
- threadTs,
214
- approval: payload.approval,
215
- },
216
- );
217
- sentTs = result.ts;
218
- }
219
-
220
- // Attachments
221
- if (attachments && attachments.length > 0) {
222
- const result = await sendSlackAttachments(chatId, attachments, threadTs);
223
- if (result.allFailed && !text) {
224
- throw new ChannelDeliveryError(
225
- 502,
226
- `All ${result.failureCount} attachments failed to deliver`,
227
- );
49
+ // Resolve against a dummy base so base-less callbacks (e.g.
50
+ // `/deliver/slack?threadTs=…`) still expose their params. `channelForCallback`
51
+ // already routes those as direct delivery, so dispatch must not drop
52
+ // threadTs/taskId for them.
53
+ const url = new URL(callbackUrl, "http://callback.invalid");
54
+ for (const [key, value] of url.searchParams) {
55
+ params[key] = value;
228
56
  }
57
+ } catch {
58
+ // Unparseable callback URL — deliver with no params.
229
59
  }
230
-
231
- log.info({ chatId, hasText: !!text }, "Slack reply delivered (direct)");
232
- return { ok: true, ts: sentTs };
60
+ return { callbackUrl, params };
233
61
  }
234
62
 
235
- // ---------------------------------------------------------------------------
236
- // Public API
237
- // ---------------------------------------------------------------------------
238
-
239
63
  /**
240
- * Returns true when the given callback URL targets a channel whose
241
- * outbound delivery is handled directly by the assistant (no gateway hop).
64
+ * True when the callback URL targets a channel whose outbound delivery the
65
+ * assistant handles directly (no gateway hop).
242
66
  */
243
67
  export function isDirectDelivery(callbackUrl: string): boolean {
244
- return (
245
- isWhatsAppCallback(callbackUrl) ||
246
- isTelegramCallback(callbackUrl) ||
247
- isSlackCallback(callbackUrl) ||
248
- isA2ACallback(callbackUrl)
249
- );
68
+ return getTransportForCallback(callbackUrl) !== undefined;
250
69
  }
251
70
 
252
71
  /**
253
- * Deliver a channel reply directly to the provider API, bypassing the
254
- * gateway HTTP proxy. Callers MUST check `isDirectDelivery()` first.
72
+ * Deliver a channel reply directly to the provider API, bypassing the gateway
73
+ * HTTP proxy. Callers MUST check `isDirectDelivery()` first.
74
+ *
75
+ * Sub-operations (reaction, thread status, typing) route to the transport's
76
+ * optional method when both the payload field and the method are present;
77
+ * otherwise the reply is delivered as text / approval / attachments.
255
78
  */
256
79
  export async function deliverDirect(
257
80
  callbackUrl: string,
258
81
  payload: ChannelReplyPayload,
259
82
  ): Promise<ChannelDeliveryResult> {
260
- if (isWhatsAppCallback(callbackUrl)) {
261
- return deliverWhatsApp(payload);
83
+ const transport = getTransportForCallback(callbackUrl);
84
+ if (!transport) {
85
+ throw new Error(
86
+ `deliverDirect called for unsupported callback: ${callbackUrl}`,
87
+ );
262
88
  }
263
- if (isTelegramCallback(callbackUrl)) {
264
- return deliverTelegram(payload);
89
+
90
+ const ctx = callbackContext(callbackUrl);
91
+ if (payload.reaction && transport.sendReaction) {
92
+ return transport.sendReaction(ctx, payload);
265
93
  }
266
- if (isSlackCallback(callbackUrl)) {
267
- return deliverSlack(callbackUrl, payload);
94
+ if (payload.assistantThreadStatus && transport.setThreadStatus) {
95
+ return transport.setThreadStatus(ctx, payload);
268
96
  }
269
- if (isA2ACallback(callbackUrl)) {
270
- return deliverA2AReply(callbackUrl, payload);
97
+ if (payload.chatAction === "typing" && transport.sendTyping) {
98
+ return transport.sendTyping(ctx, payload);
271
99
  }
272
-
273
- // Defensive — isDirectDelivery should have returned false.
274
- throw new Error(
275
- `deliverDirect called for unsupported callback: ${callbackUrl}`,
276
- );
100
+ return transport.deliver(ctx, payload);
277
101
  }
@@ -0,0 +1,62 @@
1
+ import { getConfig } from "../../../config/loader.js";
2
+ import type { ExternalConversationBinding } from "../../../memory/external-conversation-store.js";
3
+ import type { ChannelBindingMetadata } from "../../channel-binding-schema.js";
4
+ import {
5
+ buildSlackMessageDeepLinks,
6
+ buildSlackWebChannelUrl,
7
+ } from "./deep-link.js";
8
+
9
+ /**
10
+ * Slack's contribution to a serialized conversation channel binding: a
11
+ * human-readable channel name (falling back to the channel id) plus deep
12
+ * links that jump back to the source thread and channel in the Slack app or
13
+ * web client.
14
+ *
15
+ * The return type is derived from the channel-binding Zod schema
16
+ * (`ChannelBindingMetadata`) — the single source of truth that also drives
17
+ * `openapi.yaml` and the web client's generated types — so this builder cannot
18
+ * drift from the wire contract. Slack is the only channel that can currently
19
+ * produce message-level deep links, because the link inputs (workspace team
20
+ * id/url + a stable per-message timestamp) only exist for Slack.
21
+ */
22
+ export function buildSlackBindingMetadata(
23
+ binding: ExternalConversationBinding,
24
+ ): ChannelBindingMetadata {
25
+ const externalChatName =
26
+ binding.externalChatName?.trim() || binding.externalChatId;
27
+ const slackConfig = getConfig().slack;
28
+
29
+ const threadLink =
30
+ slackConfig && binding.externalThreadId
31
+ ? buildSlackMessageDeepLinks({
32
+ teamId: slackConfig.teamId,
33
+ teamUrl: slackConfig.teamUrl,
34
+ channelId: binding.externalChatId,
35
+ messageTs: binding.externalThreadId,
36
+ })
37
+ : undefined;
38
+ const slackThread = binding.externalThreadId
39
+ ? {
40
+ channelId: binding.externalChatId,
41
+ threadTs: binding.externalThreadId,
42
+ ...(threadLink ? { link: threadLink } : {}),
43
+ }
44
+ : undefined;
45
+
46
+ const channelWebUrl = slackConfig
47
+ ? buildSlackWebChannelUrl({
48
+ teamUrl: slackConfig.teamUrl,
49
+ channelId: binding.externalChatId,
50
+ })
51
+ : undefined;
52
+
53
+ return {
54
+ externalChatName,
55
+ ...(slackThread ? { slackThread } : {}),
56
+ slackChannel: {
57
+ channelId: binding.externalChatId,
58
+ name: externalChatName,
59
+ ...(channelWebUrl ? { link: { webUrl: channelWebUrl } } : {}),
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,92 @@
1
+ import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
2
+
3
+ import { getLogger } from "../../../util/logger.js";
4
+ import type { ChannelTransport } from "../channel-transport.js";
5
+ import {
6
+ sendSlackAssistantThreadStatus,
7
+ sendSlackAttachments,
8
+ sendSlackReaction,
9
+ sendSlackReply,
10
+ sendSlackTypingIndicator,
11
+ } from "./send.js";
12
+
13
+ const log = getLogger("slack-transport");
14
+
15
+ export const slackTransport: ChannelTransport = {
16
+ channel: "slack",
17
+
18
+ async deliver(ctx, payload) {
19
+ const { chatId, text, attachments, blocks } = payload;
20
+ const threadTs = ctx.params.threadTs;
21
+
22
+ let sentTs: string | undefined;
23
+ if (text) {
24
+ const result = await sendSlackReply(chatId, text, {
25
+ threadTs,
26
+ blocks,
27
+ approval: payload.approval,
28
+ useBlocks: payload.useBlocks,
29
+ ephemeral: payload.ephemeral,
30
+ user: payload.user,
31
+ messageTs: payload.messageTs,
32
+ });
33
+ sentTs = result.ts;
34
+ } else if (payload.approval) {
35
+ const result = await sendSlackReply(
36
+ chatId,
37
+ payload.approval.plainTextFallback || "Approval required",
38
+ { threadTs, approval: payload.approval },
39
+ );
40
+ sentTs = result.ts;
41
+ }
42
+
43
+ if (attachments && attachments.length > 0) {
44
+ const result = await sendSlackAttachments(chatId, attachments, threadTs);
45
+ if (result.allFailed && !text) {
46
+ throw new ChannelDeliveryError(
47
+ 502,
48
+ `All ${result.failureCount} attachments failed to deliver`,
49
+ );
50
+ }
51
+ }
52
+
53
+ log.info({ chatId, hasText: !!text }, "Slack reply delivered (direct)");
54
+ return { ok: true, ts: sentTs };
55
+ },
56
+
57
+ async sendTyping(ctx, payload) {
58
+ const placeholderTs = await sendSlackTypingIndicator(
59
+ payload.chatId,
60
+ ctx.params.threadTs,
61
+ );
62
+ log.debug(
63
+ { chatId: payload.chatId },
64
+ "Slack typing indicator delivered (direct)",
65
+ );
66
+ return { ok: true, ts: placeholderTs };
67
+ },
68
+
69
+ async sendReaction(_ctx, payload) {
70
+ const reaction = payload.reaction;
71
+ if (!reaction) return { ok: true };
72
+ await sendSlackReaction(
73
+ payload.chatId,
74
+ reaction.name,
75
+ reaction.messageTs,
76
+ reaction.action,
77
+ );
78
+ return { ok: true };
79
+ },
80
+
81
+ async setThreadStatus(_ctx, payload) {
82
+ const status = payload.assistantThreadStatus;
83
+ if (!status) return { ok: true };
84
+ await sendSlackAssistantThreadStatus(
85
+ status.channel,
86
+ status.threadTs,
87
+ status.status,
88
+ status.loadingMessages,
89
+ );
90
+ return { ok: true };
91
+ },
92
+ };
@@ -0,0 +1,51 @@
1
+ import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
2
+
3
+ import { getLogger } from "../../../util/logger.js";
4
+ import type { ChannelTransport } from "../channel-transport.js";
5
+ import {
6
+ sendTelegramAttachments,
7
+ sendTelegramReply,
8
+ sendTelegramTypingIndicator,
9
+ } from "./send.js";
10
+
11
+ const log = getLogger("telegram-transport");
12
+
13
+ export const telegramTransport: ChannelTransport = {
14
+ channel: "telegram",
15
+
16
+ async deliver(_ctx, payload) {
17
+ const { chatId, text, attachments, approval } = payload;
18
+
19
+ if (text) {
20
+ await sendTelegramReply(chatId, text, approval);
21
+ } else if (approval) {
22
+ await sendTelegramReply(
23
+ chatId,
24
+ approval.plainTextFallback || "Approval required",
25
+ approval,
26
+ );
27
+ }
28
+
29
+ if (attachments && attachments.length > 0) {
30
+ const result = await sendTelegramAttachments(chatId, attachments);
31
+ if (result.allFailed && !text) {
32
+ throw new ChannelDeliveryError(
33
+ 502,
34
+ `All ${result.failureCount} attachments failed to deliver`,
35
+ );
36
+ }
37
+ }
38
+
39
+ log.info({ chatId, hasText: !!text }, "Telegram reply delivered (direct)");
40
+ return { ok: true };
41
+ },
42
+
43
+ async sendTyping(_ctx, payload) {
44
+ await sendTelegramTypingIndicator(payload.chatId);
45
+ log.debug(
46
+ { chatId: payload.chatId },
47
+ "Telegram typing indicator delivered (direct)",
48
+ );
49
+ return { ok: true };
50
+ },
51
+ };
@@ -0,0 +1,38 @@
1
+ import { ChannelDeliveryError } from "@vellumai/gateway-client/http-delivery";
2
+
3
+ import { getLogger } from "../../../util/logger.js";
4
+ import type { ChannelTransport } from "../channel-transport.js";
5
+ import { sendWhatsAppAttachments, sendWhatsAppReply } from "./send.js";
6
+
7
+ const log = getLogger("whatsapp-transport");
8
+
9
+ export const whatsappTransport: ChannelTransport = {
10
+ channel: "whatsapp",
11
+
12
+ async deliver(_ctx, payload) {
13
+ const { chatId, text, attachments, approval } = payload;
14
+
15
+ if (text) {
16
+ await sendWhatsAppReply(chatId, text, approval);
17
+ } else if (approval) {
18
+ await sendWhatsAppReply(
19
+ chatId,
20
+ approval.plainTextFallback || "Approval required",
21
+ approval,
22
+ );
23
+ }
24
+
25
+ if (attachments && attachments.length > 0) {
26
+ const result = await sendWhatsAppAttachments(chatId, attachments);
27
+ if (result.allFailed && !text) {
28
+ throw new ChannelDeliveryError(
29
+ 502,
30
+ `All ${result.failureCount} attachments failed to deliver`,
31
+ );
32
+ }
33
+ }
34
+
35
+ log.info({ chatId, hasText: !!text }, "WhatsApp reply delivered (direct)");
36
+ return { ok: true };
37
+ },
38
+ };
@@ -37,14 +37,6 @@ mock.module("../../contacts/guardian-delivery-reader.js", () => ({
37
37
  getGuardianDelivery: async () => null,
38
38
  }));
39
39
 
40
- // Use the real destination-resolver (DB-free via the local-read stub below)
41
- // so this mock does not leak into destination-resolver.test.ts under a shared
42
- // bun-test invocation. With no guardian, the resolver still yields a vellum
43
- // destination, which is all these tests exercise.
44
- mock.module("../../contacts/contact-store.js", () => ({
45
- findGuardianForChannel: () => null,
46
- }));
47
-
48
40
  mock.module("../conversation-pairing.js", () => ({
49
41
  pairDeliveryWithConversation: async () => ({
50
42
  conversationId: undefined,