@openclaw/discord 2026.2.19 → 2026.2.22

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.
package/index.ts CHANGED
@@ -2,6 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
3
  import { discordPlugin } from "./src/channel.js";
4
4
  import { setDiscordRuntime } from "./src/runtime.js";
5
+ import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js";
5
6
 
6
7
  const plugin = {
7
8
  id: "discord",
@@ -11,6 +12,7 @@ const plugin = {
11
12
  register(api: OpenClawPluginApi) {
12
13
  setDiscordRuntime(api.runtime);
13
14
  api.registerChannel({ plugin: discordPlugin });
15
+ registerDiscordSubagentHooks(api);
14
16
  },
15
17
  };
16
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/discord",
3
- "version": "2026.2.19",
3
+ "version": "2026.2.22",
4
4
  "description": "OpenClaw Discord channel plugin",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -0,0 +1,36 @@
1
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { discordPlugin } from "./channel.js";
4
+ import { setDiscordRuntime } from "./runtime.js";
5
+
6
+ describe("discordPlugin outbound", () => {
7
+ it("forwards mediaLocalRoots to sendMessageDiscord", async () => {
8
+ const sendMessageDiscord = vi.fn(async () => ({ messageId: "m1" }));
9
+ setDiscordRuntime({
10
+ channel: {
11
+ discord: {
12
+ sendMessageDiscord,
13
+ },
14
+ },
15
+ } as unknown as PluginRuntime);
16
+
17
+ const result = await discordPlugin.outbound!.sendMedia!({
18
+ cfg: {} as OpenClawConfig,
19
+ to: "channel:123",
20
+ text: "hi",
21
+ mediaUrl: "/tmp/image.png",
22
+ mediaLocalRoots: ["/tmp/agent-root"],
23
+ accountId: "work",
24
+ });
25
+
26
+ expect(sendMessageDiscord).toHaveBeenCalledWith(
27
+ "channel:123",
28
+ "hi",
29
+ expect.objectContaining({
30
+ mediaUrl: "/tmp/image.png",
31
+ mediaLocalRoots: ["/tmp/agent-root"],
32
+ }),
33
+ );
34
+ expect(result).toMatchObject({ channel: "discord", messageId: "m1" });
35
+ });
36
+ });
package/src/channel.ts CHANGED
@@ -22,6 +22,8 @@ import {
22
22
  resolveDefaultDiscordAccountId,
23
23
  resolveDiscordGroupRequireMention,
24
24
  resolveDiscordGroupToolPolicy,
25
+ resolveOpenProviderRuntimeGroupPolicy,
26
+ resolveDefaultGroupPolicy,
25
27
  setAccountEnabledInConfigSection,
26
28
  type ChannelMessageActionAdapter,
27
29
  type ChannelPlugin,
@@ -110,6 +112,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
110
112
  .map((entry) => String(entry).trim())
111
113
  .filter(Boolean)
112
114
  .map((entry) => entry.toLowerCase()),
115
+ resolveDefaultTo: ({ cfg, accountId }) =>
116
+ resolveDiscordAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
113
117
  },
114
118
  security: {
115
119
  resolveDmPolicy: ({ cfg, accountId, account }) => {
@@ -128,8 +132,12 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
128
132
  },
129
133
  collectWarnings: ({ account, cfg }) => {
130
134
  const warnings: string[] = [];
131
- const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
132
- const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
135
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
136
+ const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
137
+ providerConfigPresent: cfg.channels?.discord !== undefined,
138
+ groupPolicy: account.config.groupPolicy,
139
+ defaultGroupPolicy,
140
+ });
133
141
  const guildEntries = account.config.guilds ?? {};
134
142
  const guildsConfigured = Object.keys(guildEntries).length > 0;
135
143
  const channelAllowlistConfigured = guildsConfigured;
@@ -303,11 +311,21 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
303
311
  });
304
312
  return { channel: "discord", ...result };
305
313
  },
306
- sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, silent }) => {
314
+ sendMedia: async ({
315
+ to,
316
+ text,
317
+ mediaUrl,
318
+ mediaLocalRoots,
319
+ accountId,
320
+ deps,
321
+ replyToId,
322
+ silent,
323
+ }) => {
307
324
  const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
308
325
  const result = await send(to, text, {
309
326
  verbose: false,
310
327
  mediaUrl,
328
+ mediaLocalRoots,
311
329
  replyTo: replyToId ?? undefined,
312
330
  accountId: accountId ?? undefined,
313
331
  silent: silent ?? undefined,
@@ -0,0 +1,388 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { registerDiscordSubagentHooks } from "./subagent-hooks.js";
4
+
5
+ type ThreadBindingRecord = {
6
+ accountId: string;
7
+ threadId: string;
8
+ };
9
+
10
+ type MockResolvedDiscordAccount = {
11
+ accountId: string;
12
+ config: {
13
+ threadBindings?: {
14
+ enabled?: boolean;
15
+ spawnSubagentSessions?: boolean;
16
+ };
17
+ };
18
+ };
19
+
20
+ const hookMocks = vi.hoisted(() => ({
21
+ resolveDiscordAccount: vi.fn(
22
+ (params?: { accountId?: string }): MockResolvedDiscordAccount => ({
23
+ accountId: params?.accountId?.trim() || "default",
24
+ config: {
25
+ threadBindings: {
26
+ spawnSubagentSessions: true,
27
+ },
28
+ },
29
+ }),
30
+ ),
31
+ autoBindSpawnedDiscordSubagent: vi.fn(
32
+ async (): Promise<{ threadId: string } | null> => ({ threadId: "thread-1" }),
33
+ ),
34
+ listThreadBindingsBySessionKey: vi.fn((_params?: unknown): ThreadBindingRecord[] => []),
35
+ unbindThreadBindingsBySessionKey: vi.fn(() => []),
36
+ }));
37
+
38
+ vi.mock("openclaw/plugin-sdk", () => ({
39
+ resolveDiscordAccount: hookMocks.resolveDiscordAccount,
40
+ autoBindSpawnedDiscordSubagent: hookMocks.autoBindSpawnedDiscordSubagent,
41
+ listThreadBindingsBySessionKey: hookMocks.listThreadBindingsBySessionKey,
42
+ unbindThreadBindingsBySessionKey: hookMocks.unbindThreadBindingsBySessionKey,
43
+ }));
44
+
45
+ function registerHandlersForTest(
46
+ config: Record<string, unknown> = {
47
+ channels: {
48
+ discord: {
49
+ threadBindings: {
50
+ spawnSubagentSessions: true,
51
+ },
52
+ },
53
+ },
54
+ },
55
+ ) {
56
+ const handlers = new Map<string, (event: unknown, ctx: unknown) => unknown>();
57
+ const api = {
58
+ config,
59
+ on: (hookName: string, handler: (event: unknown, ctx: unknown) => unknown) => {
60
+ handlers.set(hookName, handler);
61
+ },
62
+ } as unknown as OpenClawPluginApi;
63
+ registerDiscordSubagentHooks(api);
64
+ return handlers;
65
+ }
66
+
67
+ function getRequiredHandler(
68
+ handlers: Map<string, (event: unknown, ctx: unknown) => unknown>,
69
+ hookName: string,
70
+ ): (event: unknown, ctx: unknown) => unknown {
71
+ const handler = handlers.get(hookName);
72
+ if (!handler) {
73
+ throw new Error(`expected ${hookName} hook handler`);
74
+ }
75
+ return handler;
76
+ }
77
+
78
+ function createSpawnEvent(overrides?: {
79
+ childSessionKey?: string;
80
+ agentId?: string;
81
+ label?: string;
82
+ mode?: string;
83
+ requester?: {
84
+ channel?: string;
85
+ accountId?: string;
86
+ to?: string;
87
+ threadId?: string;
88
+ };
89
+ threadRequested?: boolean;
90
+ }): {
91
+ childSessionKey: string;
92
+ agentId: string;
93
+ label: string;
94
+ mode: string;
95
+ requester: {
96
+ channel: string;
97
+ accountId: string;
98
+ to: string;
99
+ threadId?: string;
100
+ };
101
+ threadRequested: boolean;
102
+ } {
103
+ const base = {
104
+ childSessionKey: "agent:main:subagent:child",
105
+ agentId: "main",
106
+ label: "banana",
107
+ mode: "session",
108
+ requester: {
109
+ channel: "discord",
110
+ accountId: "work",
111
+ to: "channel:123",
112
+ threadId: "456",
113
+ },
114
+ threadRequested: true,
115
+ };
116
+ return {
117
+ ...base,
118
+ ...overrides,
119
+ requester: {
120
+ ...base.requester,
121
+ ...(overrides?.requester ?? {}),
122
+ },
123
+ };
124
+ }
125
+
126
+ function createSpawnEventWithoutThread() {
127
+ return createSpawnEvent({
128
+ label: "",
129
+ requester: { threadId: undefined },
130
+ });
131
+ }
132
+
133
+ async function runSubagentSpawning(
134
+ config?: Record<string, unknown>,
135
+ event = createSpawnEventWithoutThread(),
136
+ ) {
137
+ const handlers = registerHandlersForTest(config);
138
+ const handler = getRequiredHandler(handlers, "subagent_spawning");
139
+ return await handler(event, {});
140
+ }
141
+
142
+ async function expectSubagentSpawningError(params?: {
143
+ config?: Record<string, unknown>;
144
+ errorContains?: string;
145
+ event?: ReturnType<typeof createSpawnEvent>;
146
+ }) {
147
+ const result = await runSubagentSpawning(params?.config, params?.event);
148
+ expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled();
149
+ expect(result).toMatchObject({ status: "error" });
150
+ if (params?.errorContains) {
151
+ const errorText = (result as { error?: string }).error ?? "";
152
+ expect(errorText).toContain(params.errorContains);
153
+ }
154
+ }
155
+
156
+ describe("discord subagent hook handlers", () => {
157
+ beforeEach(() => {
158
+ hookMocks.resolveDiscordAccount.mockClear();
159
+ hookMocks.resolveDiscordAccount.mockImplementation((params?: { accountId?: string }) => ({
160
+ accountId: params?.accountId?.trim() || "default",
161
+ config: {
162
+ threadBindings: {
163
+ spawnSubagentSessions: true,
164
+ },
165
+ },
166
+ }));
167
+ hookMocks.autoBindSpawnedDiscordSubagent.mockClear();
168
+ hookMocks.listThreadBindingsBySessionKey.mockClear();
169
+ hookMocks.unbindThreadBindingsBySessionKey.mockClear();
170
+ });
171
+
172
+ it("registers subagent hooks", () => {
173
+ const handlers = registerHandlersForTest();
174
+ expect(handlers.has("subagent_spawning")).toBe(true);
175
+ expect(handlers.has("subagent_delivery_target")).toBe(true);
176
+ expect(handlers.has("subagent_spawned")).toBe(false);
177
+ expect(handlers.has("subagent_ended")).toBe(true);
178
+ });
179
+
180
+ it("binds thread routing on subagent_spawning", async () => {
181
+ const handlers = registerHandlersForTest();
182
+ const handler = getRequiredHandler(handlers, "subagent_spawning");
183
+
184
+ const result = await handler(createSpawnEvent(), {});
185
+
186
+ expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1);
187
+ expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledWith({
188
+ accountId: "work",
189
+ channel: "discord",
190
+ to: "channel:123",
191
+ threadId: "456",
192
+ childSessionKey: "agent:main:subagent:child",
193
+ agentId: "main",
194
+ label: "banana",
195
+ boundBy: "system",
196
+ });
197
+ expect(result).toMatchObject({ status: "ok", threadBindingReady: true });
198
+ });
199
+
200
+ it("returns error when thread-bound subagent spawn is disabled", async () => {
201
+ await expectSubagentSpawningError({
202
+ config: {
203
+ channels: {
204
+ discord: {
205
+ threadBindings: {
206
+ spawnSubagentSessions: false,
207
+ },
208
+ },
209
+ },
210
+ },
211
+ errorContains: "spawnSubagentSessions=true",
212
+ });
213
+ });
214
+
215
+ it("returns error when global thread bindings are disabled", async () => {
216
+ await expectSubagentSpawningError({
217
+ config: {
218
+ session: {
219
+ threadBindings: {
220
+ enabled: false,
221
+ },
222
+ },
223
+ channels: {
224
+ discord: {
225
+ threadBindings: {
226
+ spawnSubagentSessions: true,
227
+ },
228
+ },
229
+ },
230
+ },
231
+ errorContains: "threadBindings.enabled=true",
232
+ });
233
+ });
234
+
235
+ it("allows account-level threadBindings.enabled to override global disable", async () => {
236
+ const result = await runSubagentSpawning({
237
+ session: {
238
+ threadBindings: {
239
+ enabled: false,
240
+ },
241
+ },
242
+ channels: {
243
+ discord: {
244
+ accounts: {
245
+ work: {
246
+ threadBindings: {
247
+ enabled: true,
248
+ spawnSubagentSessions: true,
249
+ },
250
+ },
251
+ },
252
+ },
253
+ },
254
+ });
255
+
256
+ expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1);
257
+ expect(result).toMatchObject({ status: "ok", threadBindingReady: true });
258
+ });
259
+
260
+ it("defaults thread-bound subagent spawn to disabled when unset", async () => {
261
+ await expectSubagentSpawningError({
262
+ config: {
263
+ channels: {
264
+ discord: {
265
+ threadBindings: {},
266
+ },
267
+ },
268
+ },
269
+ });
270
+ });
271
+
272
+ it("no-ops when thread binding is requested on non-discord channel", async () => {
273
+ const result = await runSubagentSpawning(
274
+ undefined,
275
+ createSpawnEvent({
276
+ requester: {
277
+ channel: "signal",
278
+ accountId: "",
279
+ to: "+123",
280
+ threadId: undefined,
281
+ },
282
+ }),
283
+ );
284
+
285
+ expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled();
286
+ expect(result).toBeUndefined();
287
+ });
288
+
289
+ it("returns error when thread bind fails", async () => {
290
+ hookMocks.autoBindSpawnedDiscordSubagent.mockResolvedValueOnce(null);
291
+ const result = await runSubagentSpawning();
292
+
293
+ expect(result).toMatchObject({ status: "error" });
294
+ const errorText = (result as { error?: string }).error ?? "";
295
+ expect(errorText).toMatch(/unable to create or bind/i);
296
+ });
297
+
298
+ it("unbinds thread routing on subagent_ended", () => {
299
+ const handlers = registerHandlersForTest();
300
+ const handler = getRequiredHandler(handlers, "subagent_ended");
301
+
302
+ handler(
303
+ {
304
+ targetSessionKey: "agent:main:subagent:child",
305
+ targetKind: "subagent",
306
+ reason: "subagent-complete",
307
+ sendFarewell: true,
308
+ accountId: "work",
309
+ },
310
+ {},
311
+ );
312
+
313
+ expect(hookMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
314
+ expect(hookMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
315
+ targetSessionKey: "agent:main:subagent:child",
316
+ accountId: "work",
317
+ targetKind: "subagent",
318
+ reason: "subagent-complete",
319
+ sendFarewell: true,
320
+ });
321
+ });
322
+
323
+ it("resolves delivery target from matching bound thread", () => {
324
+ hookMocks.listThreadBindingsBySessionKey.mockReturnValueOnce([
325
+ { accountId: "work", threadId: "777" },
326
+ ]);
327
+ const handlers = registerHandlersForTest();
328
+ const handler = getRequiredHandler(handlers, "subagent_delivery_target");
329
+
330
+ const result = handler(
331
+ {
332
+ childSessionKey: "agent:main:subagent:child",
333
+ requesterSessionKey: "agent:main:main",
334
+ requesterOrigin: {
335
+ channel: "discord",
336
+ accountId: "work",
337
+ to: "channel:123",
338
+ threadId: "777",
339
+ },
340
+ childRunId: "run-1",
341
+ spawnMode: "session",
342
+ expectsCompletionMessage: true,
343
+ },
344
+ {},
345
+ );
346
+
347
+ expect(hookMocks.listThreadBindingsBySessionKey).toHaveBeenCalledWith({
348
+ targetSessionKey: "agent:main:subagent:child",
349
+ accountId: "work",
350
+ targetKind: "subagent",
351
+ });
352
+ expect(result).toEqual({
353
+ origin: {
354
+ channel: "discord",
355
+ accountId: "work",
356
+ to: "channel:777",
357
+ threadId: "777",
358
+ },
359
+ });
360
+ });
361
+
362
+ it("keeps original routing when delivery target is ambiguous", () => {
363
+ hookMocks.listThreadBindingsBySessionKey.mockReturnValueOnce([
364
+ { accountId: "work", threadId: "777" },
365
+ { accountId: "work", threadId: "888" },
366
+ ]);
367
+ const handlers = registerHandlersForTest();
368
+ const handler = getRequiredHandler(handlers, "subagent_delivery_target");
369
+
370
+ const result = handler(
371
+ {
372
+ childSessionKey: "agent:main:subagent:child",
373
+ requesterSessionKey: "agent:main:main",
374
+ requesterOrigin: {
375
+ channel: "discord",
376
+ accountId: "work",
377
+ to: "channel:123",
378
+ },
379
+ childRunId: "run-1",
380
+ spawnMode: "session",
381
+ expectsCompletionMessage: true,
382
+ },
383
+ {},
384
+ );
385
+
386
+ expect(result).toBeUndefined();
387
+ });
388
+ });
@@ -0,0 +1,152 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import {
3
+ autoBindSpawnedDiscordSubagent,
4
+ listThreadBindingsBySessionKey,
5
+ resolveDiscordAccount,
6
+ unbindThreadBindingsBySessionKey,
7
+ } from "openclaw/plugin-sdk";
8
+
9
+ function summarizeError(err: unknown): string {
10
+ if (err instanceof Error) {
11
+ return err.message;
12
+ }
13
+ if (typeof err === "string") {
14
+ return err;
15
+ }
16
+ return "error";
17
+ }
18
+
19
+ export function registerDiscordSubagentHooks(api: OpenClawPluginApi) {
20
+ const resolveThreadBindingFlags = (accountId?: string) => {
21
+ const account = resolveDiscordAccount({
22
+ cfg: api.config,
23
+ accountId,
24
+ });
25
+ const baseThreadBindings = api.config.channels?.discord?.threadBindings;
26
+ const accountThreadBindings =
27
+ api.config.channels?.discord?.accounts?.[account.accountId]?.threadBindings;
28
+ return {
29
+ enabled:
30
+ accountThreadBindings?.enabled ??
31
+ baseThreadBindings?.enabled ??
32
+ api.config.session?.threadBindings?.enabled ??
33
+ true,
34
+ spawnSubagentSessions:
35
+ accountThreadBindings?.spawnSubagentSessions ??
36
+ baseThreadBindings?.spawnSubagentSessions ??
37
+ false,
38
+ };
39
+ };
40
+
41
+ api.on("subagent_spawning", async (event) => {
42
+ if (!event.threadRequested) {
43
+ return;
44
+ }
45
+ const channel = event.requester?.channel?.trim().toLowerCase();
46
+ if (channel !== "discord") {
47
+ // Ignore non-Discord channels so channel-specific plugins can handle
48
+ // their own thread/session provisioning without Discord blocking them.
49
+ return;
50
+ }
51
+ const threadBindingFlags = resolveThreadBindingFlags(event.requester?.accountId);
52
+ if (!threadBindingFlags.enabled) {
53
+ return {
54
+ status: "error" as const,
55
+ error:
56
+ "Discord thread bindings are disabled (set channels.discord.threadBindings.enabled=true to override for this account, or session.threadBindings.enabled=true globally).",
57
+ };
58
+ }
59
+ if (!threadBindingFlags.spawnSubagentSessions) {
60
+ return {
61
+ status: "error" as const,
62
+ error:
63
+ "Discord thread-bound subagent spawns are disabled for this account (set channels.discord.threadBindings.spawnSubagentSessions=true to enable).",
64
+ };
65
+ }
66
+ try {
67
+ const binding = await autoBindSpawnedDiscordSubagent({
68
+ accountId: event.requester?.accountId,
69
+ channel: event.requester?.channel,
70
+ to: event.requester?.to,
71
+ threadId: event.requester?.threadId,
72
+ childSessionKey: event.childSessionKey,
73
+ agentId: event.agentId,
74
+ label: event.label,
75
+ boundBy: "system",
76
+ });
77
+ if (!binding) {
78
+ return {
79
+ status: "error" as const,
80
+ error:
81
+ "Unable to create or bind a Discord thread for this subagent session. Session mode is unavailable for this target.",
82
+ };
83
+ }
84
+ return { status: "ok" as const, threadBindingReady: true };
85
+ } catch (err) {
86
+ return {
87
+ status: "error" as const,
88
+ error: `Discord thread bind failed: ${summarizeError(err)}`,
89
+ };
90
+ }
91
+ });
92
+
93
+ api.on("subagent_ended", (event) => {
94
+ unbindThreadBindingsBySessionKey({
95
+ targetSessionKey: event.targetSessionKey,
96
+ accountId: event.accountId,
97
+ targetKind: event.targetKind,
98
+ reason: event.reason,
99
+ sendFarewell: event.sendFarewell,
100
+ });
101
+ });
102
+
103
+ api.on("subagent_delivery_target", (event) => {
104
+ if (!event.expectsCompletionMessage) {
105
+ return;
106
+ }
107
+ const requesterChannel = event.requesterOrigin?.channel?.trim().toLowerCase();
108
+ if (requesterChannel !== "discord") {
109
+ return;
110
+ }
111
+ const requesterAccountId = event.requesterOrigin?.accountId?.trim();
112
+ const requesterThreadId =
113
+ event.requesterOrigin?.threadId != null && event.requesterOrigin.threadId !== ""
114
+ ? String(event.requesterOrigin.threadId).trim()
115
+ : "";
116
+ const bindings = listThreadBindingsBySessionKey({
117
+ targetSessionKey: event.childSessionKey,
118
+ ...(requesterAccountId ? { accountId: requesterAccountId } : {}),
119
+ targetKind: "subagent",
120
+ });
121
+ if (bindings.length === 0) {
122
+ return;
123
+ }
124
+
125
+ let binding: (typeof bindings)[number] | undefined;
126
+ if (requesterThreadId) {
127
+ binding = bindings.find((entry) => {
128
+ if (entry.threadId !== requesterThreadId) {
129
+ return false;
130
+ }
131
+ if (requesterAccountId && entry.accountId !== requesterAccountId) {
132
+ return false;
133
+ }
134
+ return true;
135
+ });
136
+ }
137
+ if (!binding && bindings.length === 1) {
138
+ binding = bindings[0];
139
+ }
140
+ if (!binding) {
141
+ return;
142
+ }
143
+ return {
144
+ origin: {
145
+ channel: "discord",
146
+ accountId: binding.accountId,
147
+ to: `channel:${binding.threadId}`,
148
+ threadId: binding.threadId,
149
+ },
150
+ };
151
+ });
152
+ }