@openclaw/discord 2026.2.19 → 2026.2.21
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 +2 -0
- package/package.json +1 -1
- package/src/channel.ts +2 -0
- package/src/subagent-hooks.test.ts +430 -0
- package/src/subagent-hooks.ts +152 -0
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
package/src/channel.ts
CHANGED
|
@@ -110,6 +110,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
|
|
110
110
|
.map((entry) => String(entry).trim())
|
|
111
111
|
.filter(Boolean)
|
|
112
112
|
.map((entry) => entry.toLowerCase()),
|
|
113
|
+
resolveDefaultTo: ({ cfg, accountId }) =>
|
|
114
|
+
resolveDiscordAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
|
|
113
115
|
},
|
|
114
116
|
security: {
|
|
115
117
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
@@ -0,0 +1,430 @@
|
|
|
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
|
+
describe("discord subagent hook handlers", () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
hookMocks.resolveDiscordAccount.mockClear();
|
|
70
|
+
hookMocks.resolveDiscordAccount.mockImplementation((params?: { accountId?: string }) => ({
|
|
71
|
+
accountId: params?.accountId?.trim() || "default",
|
|
72
|
+
config: {
|
|
73
|
+
threadBindings: {
|
|
74
|
+
spawnSubagentSessions: true,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
hookMocks.autoBindSpawnedDiscordSubagent.mockClear();
|
|
79
|
+
hookMocks.listThreadBindingsBySessionKey.mockClear();
|
|
80
|
+
hookMocks.unbindThreadBindingsBySessionKey.mockClear();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("registers subagent hooks", () => {
|
|
84
|
+
const handlers = registerHandlersForTest();
|
|
85
|
+
expect(handlers.has("subagent_spawning")).toBe(true);
|
|
86
|
+
expect(handlers.has("subagent_delivery_target")).toBe(true);
|
|
87
|
+
expect(handlers.has("subagent_spawned")).toBe(false);
|
|
88
|
+
expect(handlers.has("subagent_ended")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("binds thread routing on subagent_spawning", async () => {
|
|
92
|
+
const handlers = registerHandlersForTest();
|
|
93
|
+
const handler = handlers.get("subagent_spawning");
|
|
94
|
+
if (!handler) {
|
|
95
|
+
throw new Error("expected subagent_spawning hook handler");
|
|
96
|
+
}
|
|
97
|
+
|
|
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
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1);
|
|
116
|
+
expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledWith({
|
|
117
|
+
accountId: "work",
|
|
118
|
+
channel: "discord",
|
|
119
|
+
to: "channel:123",
|
|
120
|
+
threadId: "456",
|
|
121
|
+
childSessionKey: "agent:main:subagent:child",
|
|
122
|
+
agentId: "main",
|
|
123
|
+
label: "banana",
|
|
124
|
+
boundBy: "system",
|
|
125
|
+
});
|
|
126
|
+
expect(result).toMatchObject({ status: "ok", threadBindingReady: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns error when thread-bound subagent spawn is disabled", async () => {
|
|
130
|
+
const handlers = registerHandlersForTest({
|
|
131
|
+
channels: {
|
|
132
|
+
discord: {
|
|
133
|
+
threadBindings: {
|
|
134
|
+
spawnSubagentSessions: false,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
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
|
+
});
|
|
163
|
+
|
|
164
|
+
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: {
|
|
173
|
+
threadBindings: {
|
|
174
|
+
spawnSubagentSessions: true,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
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",
|
|
192
|
+
},
|
|
193
|
+
threadRequested: true,
|
|
194
|
+
},
|
|
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");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("allows account-level threadBindings.enabled to override global disable", async () => {
|
|
205
|
+
const handlers = registerHandlersForTest({
|
|
206
|
+
session: {
|
|
207
|
+
threadBindings: {
|
|
208
|
+
enabled: false,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
channels: {
|
|
212
|
+
discord: {
|
|
213
|
+
accounts: {
|
|
214
|
+
work: {
|
|
215
|
+
threadBindings: {
|
|
216
|
+
enabled: true,
|
|
217
|
+
spawnSubagentSessions: true,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
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
|
+
|
|
243
|
+
expect(hookMocks.autoBindSpawnedDiscordSubagent).toHaveBeenCalledTimes(1);
|
|
244
|
+
expect(result).toMatchObject({ status: "ok", threadBindingReady: true });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("defaults thread-bound subagent spawn to disabled when unset", async () => {
|
|
248
|
+
const handlers = registerHandlersForTest({
|
|
249
|
+
channels: {
|
|
250
|
+
discord: {
|
|
251
|
+
threadBindings: {},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
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
|
+
});
|
|
277
|
+
|
|
278
|
+
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",
|
|
290
|
+
requester: {
|
|
291
|
+
channel: "signal",
|
|
292
|
+
to: "+123",
|
|
293
|
+
},
|
|
294
|
+
threadRequested: true,
|
|
295
|
+
},
|
|
296
|
+
{},
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
expect(hookMocks.autoBindSpawnedDiscordSubagent).not.toHaveBeenCalled();
|
|
300
|
+
expect(result).toBeUndefined();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("returns error when thread bind fails", async () => {
|
|
304
|
+
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
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(result).toMatchObject({ status: "error" });
|
|
327
|
+
const errorText = (result as { error?: string }).error ?? "";
|
|
328
|
+
expect(errorText).toMatch(/unable to create or bind/i);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("unbinds thread routing on subagent_ended", () => {
|
|
332
|
+
const handlers = registerHandlersForTest();
|
|
333
|
+
const handler = handlers.get("subagent_ended");
|
|
334
|
+
if (!handler) {
|
|
335
|
+
throw new Error("expected subagent_ended hook handler");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
handler(
|
|
339
|
+
{
|
|
340
|
+
targetSessionKey: "agent:main:subagent:child",
|
|
341
|
+
targetKind: "subagent",
|
|
342
|
+
reason: "subagent-complete",
|
|
343
|
+
sendFarewell: true,
|
|
344
|
+
accountId: "work",
|
|
345
|
+
},
|
|
346
|
+
{},
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(hookMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledTimes(1);
|
|
350
|
+
expect(hookMocks.unbindThreadBindingsBySessionKey).toHaveBeenCalledWith({
|
|
351
|
+
targetSessionKey: "agent:main:subagent:child",
|
|
352
|
+
accountId: "work",
|
|
353
|
+
targetKind: "subagent",
|
|
354
|
+
reason: "subagent-complete",
|
|
355
|
+
sendFarewell: true,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("resolves delivery target from matching bound thread", () => {
|
|
360
|
+
hookMocks.listThreadBindingsBySessionKey.mockReturnValueOnce([
|
|
361
|
+
{ accountId: "work", threadId: "777" },
|
|
362
|
+
]);
|
|
363
|
+
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
|
+
}
|
|
368
|
+
|
|
369
|
+
const result = handler(
|
|
370
|
+
{
|
|
371
|
+
childSessionKey: "agent:main:subagent:child",
|
|
372
|
+
requesterSessionKey: "agent:main:main",
|
|
373
|
+
requesterOrigin: {
|
|
374
|
+
channel: "discord",
|
|
375
|
+
accountId: "work",
|
|
376
|
+
to: "channel:123",
|
|
377
|
+
threadId: "777",
|
|
378
|
+
},
|
|
379
|
+
childRunId: "run-1",
|
|
380
|
+
spawnMode: "session",
|
|
381
|
+
expectsCompletionMessage: true,
|
|
382
|
+
},
|
|
383
|
+
{},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
expect(hookMocks.listThreadBindingsBySessionKey).toHaveBeenCalledWith({
|
|
387
|
+
targetSessionKey: "agent:main:subagent:child",
|
|
388
|
+
accountId: "work",
|
|
389
|
+
targetKind: "subagent",
|
|
390
|
+
});
|
|
391
|
+
expect(result).toEqual({
|
|
392
|
+
origin: {
|
|
393
|
+
channel: "discord",
|
|
394
|
+
accountId: "work",
|
|
395
|
+
to: "channel:777",
|
|
396
|
+
threadId: "777",
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("keeps original routing when delivery target is ambiguous", () => {
|
|
402
|
+
hookMocks.listThreadBindingsBySessionKey.mockReturnValueOnce([
|
|
403
|
+
{ accountId: "work", threadId: "777" },
|
|
404
|
+
{ accountId: "work", threadId: "888" },
|
|
405
|
+
]);
|
|
406
|
+
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
|
+
}
|
|
411
|
+
|
|
412
|
+
const result = handler(
|
|
413
|
+
{
|
|
414
|
+
childSessionKey: "agent:main:subagent:child",
|
|
415
|
+
requesterSessionKey: "agent:main:main",
|
|
416
|
+
requesterOrigin: {
|
|
417
|
+
channel: "discord",
|
|
418
|
+
accountId: "work",
|
|
419
|
+
to: "channel:123",
|
|
420
|
+
},
|
|
421
|
+
childRunId: "run-1",
|
|
422
|
+
spawnMode: "session",
|
|
423
|
+
expectsCompletionMessage: true,
|
|
424
|
+
},
|
|
425
|
+
{},
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
expect(result).toBeUndefined();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
@@ -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
|
+
}
|