@openclaw/discord 2026.2.21 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/discord",
3
- "version": "2026.2.21",
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,
@@ -130,8 +132,12 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
130
132
  },
131
133
  collectWarnings: ({ account, cfg }) => {
132
134
  const warnings: string[] = [];
133
- const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
134
- 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
+ });
135
141
  const guildEntries = account.config.guilds ?? {};
136
142
  const guildsConfigured = Object.keys(guildEntries).length > 0;
137
143
  const channelAllowlistConfigured = guildsConfigured;
@@ -305,11 +311,21 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
305
311
  });
306
312
  return { channel: "discord", ...result };
307
313
  },
308
- 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
+ }) => {
309
324
  const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
310
325
  const result = await send(to, text, {
311
326
  verbose: false,
312
327
  mediaUrl,
328
+ mediaLocalRoots,
313
329
  replyTo: replyToId ?? undefined,
314
330
  accountId: accountId ?? undefined,
315
331
  silent: silent ?? undefined,
@@ -64,6 +64,95 @@ function registerHandlersForTest(
64
64
  return handlers;
65
65
  }
66
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
+
67
156
  describe("discord subagent hook handlers", () => {
68
157
  beforeEach(() => {
69
158
  hookMocks.resolveDiscordAccount.mockClear();
@@ -90,27 +179,9 @@ describe("discord subagent hook handlers", () => {
90
179
 
91
180
  it("binds thread routing on subagent_spawning", async () => {
92
181
  const handlers = registerHandlersForTest();
93
- const handler = handlers.get("subagent_spawning");
94
- if (!handler) {
95
- throw new Error("expected subagent_spawning hook handler");
96
- }
182
+ const handler = getRequiredHandler(handlers, "subagent_spawning");
97
183
 
98
- const result = await handler(
99
- {
100
- childSessionKey: "agent:main:subagent:child",
101
- agentId: "main",
102
- label: "banana",
103
- mode: "session",
104
- requester: {
105
- channel: "discord",
106
- accountId: "work",
107
- to: "channel:123",
108
- threadId: "456",
109
- },
110
- threadRequested: true,
111
- },
112
- {},
113
- );
184
+ const result = await handler(createSpawnEvent(), {});
114
185
 
115
186
  expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1);
116
187
  expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledWith({
@@ -127,82 +198,42 @@ describe("discord subagent hook handlers", () => {
127
198
  });
128
199
 
129
200
  it("returns error when thread-bound subagent spawn is disabled", async () => {
130
- const handlers = registerHandlersForTest({
131
- channels: {
132
- discord: {
133
- threadBindings: {
134
- spawnSubagentSessions: false,
201
+ await expectSubagentSpawningError({
202
+ config: {
203
+ channels: {
204
+ discord: {
205
+ threadBindings: {
206
+ spawnSubagentSessions: false,
207
+ },
135
208
  },
136
209
  },
137
210
  },
211
+ errorContains: "spawnSubagentSessions=true",
138
212
  });
139
- const handler = handlers.get("subagent_spawning");
140
- if (!handler) {
141
- throw new Error("expected subagent_spawning hook handler");
142
- }
143
-
144
- const result = await handler(
145
- {
146
- childSessionKey: "agent:main:subagent:child",
147
- agentId: "main",
148
- requester: {
149
- channel: "discord",
150
- accountId: "work",
151
- to: "channel:123",
152
- },
153
- threadRequested: true,
154
- },
155
- {},
156
- );
157
-
158
- expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled();
159
- expect(result).toMatchObject({ status: "error" });
160
- const errorText = (result as { error?: string }).error ?? "";
161
- expect(errorText).toContain("spawnSubagentSessions=true");
162
213
  });
163
214
 
164
215
  it("returns error when global thread bindings are disabled", async () => {
165
- const handlers = registerHandlersForTest({
166
- session: {
167
- threadBindings: {
168
- enabled: false,
169
- },
170
- },
171
- channels: {
172
- discord: {
216
+ await expectSubagentSpawningError({
217
+ config: {
218
+ session: {
173
219
  threadBindings: {
174
- spawnSubagentSessions: true,
220
+ enabled: false,
175
221
  },
176
222
  },
177
- },
178
- });
179
- const handler = handlers.get("subagent_spawning");
180
- if (!handler) {
181
- throw new Error("expected subagent_spawning hook handler");
182
- }
183
-
184
- const result = await handler(
185
- {
186
- childSessionKey: "agent:main:subagent:child",
187
- agentId: "main",
188
- requester: {
189
- channel: "discord",
190
- accountId: "work",
191
- to: "channel:123",
223
+ channels: {
224
+ discord: {
225
+ threadBindings: {
226
+ spawnSubagentSessions: true,
227
+ },
228
+ },
192
229
  },
193
- threadRequested: true,
194
230
  },
195
- {},
196
- );
197
-
198
- expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled();
199
- expect(result).toMatchObject({ status: "error" });
200
- const errorText = (result as { error?: string }).error ?? "";
201
- expect(errorText).toContain("threadBindings.enabled=true");
231
+ errorContains: "threadBindings.enabled=true",
232
+ });
202
233
  });
203
234
 
204
235
  it("allows account-level threadBindings.enabled to override global disable", async () => {
205
- const handlers = registerHandlersForTest({
236
+ const result = await runSubagentSpawning({
206
237
  session: {
207
238
  threadBindings: {
208
239
  enabled: false,
@@ -221,79 +252,34 @@ describe("discord subagent hook handlers", () => {
221
252
  },
222
253
  },
223
254
  });
224
- const handler = handlers.get("subagent_spawning");
225
- if (!handler) {
226
- throw new Error("expected subagent_spawning hook handler");
227
- }
228
-
229
- const result = await handler(
230
- {
231
- childSessionKey: "agent:main:subagent:child",
232
- agentId: "main",
233
- requester: {
234
- channel: "discord",
235
- accountId: "work",
236
- to: "channel:123",
237
- },
238
- threadRequested: true,
239
- },
240
- {},
241
- );
242
255
 
243
256
  expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1);
244
257
  expect(result).toMatchObject({ status: "ok", threadBindingReady: true });
245
258
  });
246
259
 
247
260
  it("defaults thread-bound subagent spawn to disabled when unset", async () => {
248
- const handlers = registerHandlersForTest({
249
- channels: {
250
- discord: {
251
- threadBindings: {},
261
+ await expectSubagentSpawningError({
262
+ config: {
263
+ channels: {
264
+ discord: {
265
+ threadBindings: {},
266
+ },
252
267
  },
253
268
  },
254
269
  });
255
- const handler = handlers.get("subagent_spawning");
256
- if (!handler) {
257
- throw new Error("expected subagent_spawning hook handler");
258
- }
259
-
260
- const result = await handler(
261
- {
262
- childSessionKey: "agent:main:subagent:child",
263
- agentId: "main",
264
- requester: {
265
- channel: "discord",
266
- accountId: "work",
267
- to: "channel:123",
268
- },
269
- threadRequested: true,
270
- },
271
- {},
272
- );
273
-
274
- expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled();
275
- expect(result).toMatchObject({ status: "error" });
276
270
  });
277
271
 
278
272
  it("no-ops when thread binding is requested on non-discord channel", async () => {
279
- const handlers = registerHandlersForTest();
280
- const handler = handlers.get("subagent_spawning");
281
- if (!handler) {
282
- throw new Error("expected subagent_spawning hook handler");
283
- }
284
-
285
- const result = await handler(
286
- {
287
- childSessionKey: "agent:main:subagent:child",
288
- agentId: "main",
289
- mode: "session",
273
+ const result = await runSubagentSpawning(
274
+ undefined,
275
+ createSpawnEvent({
290
276
  requester: {
291
277
  channel: "signal",
278
+ accountId: "",
292
279
  to: "+123",
280
+ threadId: undefined,
293
281
  },
294
- threadRequested: true,
295
- },
296
- {},
282
+ }),
297
283
  );
298
284
 
299
285
  expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled();
@@ -302,26 +288,7 @@ describe("discord subagent hook handlers", () => {
302
288
 
303
289
  it("returns error when thread bind fails", async () => {
304
290
  hookMocks.autoBindSpawnedDiscordSubagent.mockResolvedValueOnce(null);
305
- const handlers = registerHandlersForTest();
306
- const handler = handlers.get("subagent_spawning");
307
- if (!handler) {
308
- throw new Error("expected subagent_spawning hook handler");
309
- }
310
-
311
- const result = await handler(
312
- {
313
- childSessionKey: "agent:main:subagent:child",
314
- agentId: "main",
315
- mode: "session",
316
- requester: {
317
- channel: "discord",
318
- accountId: "work",
319
- to: "channel:123",
320
- },
321
- threadRequested: true,
322
- },
323
- {},
324
- );
291
+ const result = await runSubagentSpawning();
325
292
 
326
293
  expect(result).toMatchObject({ status: "error" });
327
294
  const errorText = (result as { error?: string }).error ?? "";
@@ -330,10 +297,7 @@ describe("discord subagent hook handlers", () => {
330
297
 
331
298
  it("unbinds thread routing on subagent_ended", () => {
332
299
  const handlers = registerHandlersForTest();
333
- const handler = handlers.get("subagent_ended");
334
- if (!handler) {
335
- throw new Error("expected subagent_ended hook handler");
336
- }
300
+ const handler = getRequiredHandler(handlers, "subagent_ended");
337
301
 
338
302
  handler(
339
303
  {
@@ -361,10 +325,7 @@ describe("discord subagent hook handlers", () => {
361
325
  { accountId: "work", threadId: "777" },
362
326
  ]);
363
327
  const handlers = registerHandlersForTest();
364
- const handler = handlers.get("subagent_delivery_target");
365
- if (!handler) {
366
- throw new Error("expected subagent_delivery_target hook handler");
367
- }
328
+ const handler = getRequiredHandler(handlers, "subagent_delivery_target");
368
329
 
369
330
  const result = handler(
370
331
  {
@@ -404,10 +365,7 @@ describe("discord subagent hook handlers", () => {
404
365
  { accountId: "work", threadId: "888" },
405
366
  ]);
406
367
  const handlers = registerHandlersForTest();
407
- const handler = handlers.get("subagent_delivery_target");
408
- if (!handler) {
409
- throw new Error("expected subagent_delivery_target hook handler");
410
- }
368
+ const handler = getRequiredHandler(handlers, "subagent_delivery_target");
411
369
 
412
370
  const result = handler(
413
371
  {