Skip to content

Commit 95331e5

Browse files
committed
fix(channels): thread runtime config through sends
1 parent e189741 commit 95331e5

125 files changed

Lines changed: 1451 additions & 794 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

extensions/discord/src/actions/runtime.messaging.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,17 @@ export async function handleDiscordMessagingAction(
109109
}),
110110
);
111111
const accountId = readStringParam(params, "accountId");
112-
const cfgOptions = cfg ? { cfg } : {};
113-
const reactionRuntimeOptions = cfg
112+
if (!cfg) {
113+
throw new Error("Discord messaging actions require a resolved runtime config.");
114+
}
115+
const cfgOptions = { cfg };
116+
const resolvedReactionAccountId = accountId ?? resolveDefaultDiscordAccountId(cfg);
117+
const reactionRuntimeOptions = resolvedReactionAccountId
114118
? createDiscordRuntimeAccountContext({
115119
cfg,
116-
accountId: accountId ?? resolveDefaultDiscordAccountId(cfg),
120+
accountId: resolvedReactionAccountId,
117121
})
118-
: accountId
119-
? { accountId }
120-
: undefined;
122+
: cfgOptions;
121123
const withReactionRuntimeOptions = (extra?: Record<string, unknown>) => ({
122124
...(reactionRuntimeOptions ?? cfgOptions),
123125
...extra,

extensions/discord/src/actions/runtime.test.ts

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ const {
8282
} = discordSendMocks;
8383

8484
const enableAllActions = () => true;
85+
const DISCORD_TEST_CFG = {} as OpenClawConfig;
86+
87+
function handleMessagingAction(
88+
action: string,
89+
params: Record<string, unknown>,
90+
isActionEnabled: (key: keyof DiscordActionConfig) => boolean,
91+
options?: {
92+
mediaLocalRoots?: readonly string[];
93+
mediaReadFile?: (filePath: string) => Promise<Buffer>;
94+
},
95+
cfg: OpenClawConfig = DISCORD_TEST_CFG,
96+
) {
97+
return handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg);
98+
}
8599

86100
const disabledActions = (key: keyof DiscordActionConfig) => key !== "reactions";
87101
const channelInfoEnabled = (key: keyof DiscordActionConfig) => key === "channelInfo";
@@ -112,7 +126,7 @@ describe("handleDiscordMessagingAction", () => {
112126
messageId: "M1",
113127
emoji: "✅",
114128
},
115-
expectedOptions: undefined,
129+
expectedOptions: { cfg: DISCORD_TEST_CFG, accountId: "default" },
116130
},
117131
{
118132
name: "with accountId",
@@ -122,19 +136,21 @@ describe("handleDiscordMessagingAction", () => {
122136
emoji: "✅",
123137
accountId: "ops",
124138
},
125-
expectedOptions: { accountId: "ops" },
139+
expectedOptions: { cfg: DISCORD_TEST_CFG, accountId: "ops" },
126140
},
127141
])("adds reactions $name", async ({ params, expectedOptions }) => {
128-
await handleDiscordMessagingAction("react", params, enableAllActions);
142+
await handleMessagingAction("react", params, enableAllActions);
129143
if (expectedOptions) {
130144
expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅", expectedOptions);
131145
return;
132146
}
133-
expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅", {});
147+
expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅", {
148+
cfg: DISCORD_TEST_CFG,
149+
});
134150
});
135151

136152
it("uses configured defaultAccount when cfg is provided and accountId is omitted", async () => {
137-
await handleDiscordMessagingAction(
153+
await handleMessagingAction(
138154
"react",
139155
{
140156
channelId: "C1",
@@ -164,7 +180,7 @@ describe("handleDiscordMessagingAction", () => {
164180
});
165181

166182
it("removes reactions on empty emoji", async () => {
167-
await handleDiscordMessagingAction(
183+
await handleMessagingAction(
168184
"react",
169185
{
170186
channelId: "C1",
@@ -173,11 +189,14 @@ describe("handleDiscordMessagingAction", () => {
173189
},
174190
enableAllActions,
175191
);
176-
expect(removeOwnReactionsDiscord).toHaveBeenCalledWith("C1", "M1", {});
192+
expect(removeOwnReactionsDiscord).toHaveBeenCalledWith("C1", "M1", {
193+
cfg: DISCORD_TEST_CFG,
194+
accountId: "default",
195+
});
177196
});
178197

179198
it("removes reactions when remove flag set", async () => {
180-
await handleDiscordMessagingAction(
199+
await handleMessagingAction(
181200
"react",
182201
{
183202
channelId: "C1",
@@ -187,12 +206,15 @@ describe("handleDiscordMessagingAction", () => {
187206
},
188207
enableAllActions,
189208
);
190-
expect(removeReactionDiscord).toHaveBeenCalledWith("C1", "M1", "✅", {});
209+
expect(removeReactionDiscord).toHaveBeenCalledWith("C1", "M1", "✅", {
210+
cfg: DISCORD_TEST_CFG,
211+
accountId: "default",
212+
});
191213
});
192214

193215
it("rejects removes without emoji", async () => {
194216
await expect(
195-
handleDiscordMessagingAction(
217+
handleMessagingAction(
196218
"react",
197219
{
198220
channelId: "C1",
@@ -207,7 +229,7 @@ describe("handleDiscordMessagingAction", () => {
207229

208230
it("respects reaction gating", async () => {
209231
await expect(
210-
handleDiscordMessagingAction(
232+
handleMessagingAction(
211233
"react",
212234
{
213235
channelId: "C1",
@@ -220,7 +242,7 @@ describe("handleDiscordMessagingAction", () => {
220242
});
221243

222244
it("parses string booleans for poll options", async () => {
223-
await handleDiscordMessagingAction(
245+
await handleMessagingAction(
224246
"poll",
225247
{
226248
to: "channel:123",
@@ -249,7 +271,7 @@ describe("handleDiscordMessagingAction", () => {
249271
{ id: "1", timestamp: "2026-01-15T10:00:00.000Z" },
250272
] as never);
251273

252-
const result = await handleDiscordMessagingAction(
274+
const result = await handleMessagingAction(
253275
"readMessages",
254276
{ channelId: "C1" },
255277
enableAllActions,
@@ -271,13 +293,7 @@ describe("handleDiscordMessagingAction", () => {
271293
},
272294
},
273295
} as OpenClawConfig;
274-
await handleDiscordMessagingAction(
275-
"readMessages",
276-
{ channelId: "C1" },
277-
enableAllActions,
278-
{},
279-
cfg,
280-
);
296+
await handleMessagingAction("readMessages", { channelId: "C1" }, enableAllActions, {}, cfg);
281297
expect(readMessagesDiscord).toHaveBeenCalledWith("C1", expect.any(Object), { cfg });
282298
});
283299

@@ -287,7 +303,7 @@ describe("handleDiscordMessagingAction", () => {
287303
timestamp: "2026-01-15T11:00:00.000Z",
288304
});
289305

290-
const result = await handleDiscordMessagingAction(
306+
const result = await handleMessagingAction(
291307
"fetchMessage",
292308
{ guildId: "G1", channelId: "C1", messageId: "M1" },
293309
enableAllActions,
@@ -307,7 +323,7 @@ describe("handleDiscordMessagingAction", () => {
307323
},
308324
},
309325
} as OpenClawConfig;
310-
await handleDiscordMessagingAction(
326+
await handleMessagingAction(
311327
"fetchMessage",
312328
{ guildId: "G1", channelId: "C1", messageId: "M1" },
313329
enableAllActions,
@@ -320,11 +336,7 @@ describe("handleDiscordMessagingAction", () => {
320336
it("adds normalized timestamps to listPins payloads", async () => {
321337
listPinsDiscord.mockResolvedValueOnce([{ id: "1", timestamp: "2026-01-15T12:00:00.000Z" }]);
322338

323-
const result = await handleDiscordMessagingAction(
324-
"listPins",
325-
{ channelId: "C1" },
326-
enableAllActions,
327-
);
339+
const result = await handleMessagingAction("listPins", { channelId: "C1" }, enableAllActions);
328340
const payload = result.details as {
329341
pins: Array<{ timestampMs?: number; timestampUtc?: string }>;
330342
};
@@ -340,7 +352,7 @@ describe("handleDiscordMessagingAction", () => {
340352
messages: [[{ id: "1", timestamp: "2026-01-15T13:00:00.000Z" }]],
341353
});
342354

343-
const result = await handleDiscordMessagingAction(
355+
const result = await handleMessagingAction(
344356
"searchMessages",
345357
{ guildId: "G1", content: "hi" },
346358
enableAllActions,
@@ -360,7 +372,7 @@ describe("handleDiscordMessagingAction", () => {
360372
sendVoiceMessageDiscord.mockClear();
361373
sendMessageDiscord.mockClear();
362374

363-
await handleDiscordMessagingAction(
375+
await handleMessagingAction(
364376
"sendMessage",
365377
{
366378
to: "channel:123",
@@ -372,6 +384,7 @@ describe("handleDiscordMessagingAction", () => {
372384
);
373385

374386
expect(sendVoiceMessageDiscord).toHaveBeenCalledWith("channel:123", "/tmp/voice.mp3", {
387+
cfg: DISCORD_TEST_CFG,
375388
replyTo: undefined,
376389
silent: true,
377390
});
@@ -380,7 +393,7 @@ describe("handleDiscordMessagingAction", () => {
380393

381394
it("forwards trusted mediaLocalRoots into sendMessageDiscord", async () => {
382395
sendMessageDiscord.mockClear();
383-
await handleDiscordMessagingAction(
396+
await handleMessagingAction(
384397
"sendMessage",
385398
{
386399
to: "channel:123",
@@ -404,7 +417,7 @@ describe("handleDiscordMessagingAction", () => {
404417
sendMessageDiscord.mockClear();
405418
sendDiscordComponentMessage.mockClear();
406419

407-
await handleDiscordMessagingAction(
420+
await handleMessagingAction(
408421
"sendMessage",
409422
{
410423
to: "channel:123",
@@ -429,7 +442,7 @@ describe("handleDiscordMessagingAction", () => {
429442

430443
it("forwards the optional filename into sendMessageDiscord", async () => {
431444
sendMessageDiscord.mockClear();
432-
await handleDiscordMessagingAction(
445+
await handleMessagingAction(
433446
"sendMessage",
434447
{
435448
to: "channel:123",
@@ -451,7 +464,7 @@ describe("handleDiscordMessagingAction", () => {
451464

452465
it("rejects voice messages that include content", async () => {
453466
await expect(
454-
handleDiscordMessagingAction(
467+
handleMessagingAction(
455468
"sendMessage",
456469
{
457470
to: "channel:123",
@@ -466,7 +479,7 @@ describe("handleDiscordMessagingAction", () => {
466479

467480
it("forwards optional thread content", async () => {
468481
createThreadDiscord.mockClear();
469-
await handleDiscordMessagingAction(
482+
await handleMessagingAction(
470483
"threadCreate",
471484
{
472485
channelId: "C1",
@@ -484,7 +497,7 @@ describe("handleDiscordMessagingAction", () => {
484497
content: "Initial forum post body",
485498
appliedTags: undefined,
486499
},
487-
{},
500+
{ cfg: DISCORD_TEST_CFG },
488501
);
489502
});
490503
});

extensions/discord/src/channel.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -778,8 +778,13 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
778778
idLabel: "discordUserId",
779779
message: PAIRING_APPROVED_MESSAGE,
780780
normalizeAllowEntry: createPairingPrefixStripper(/^(discord|user):/i),
781-
notify: async ({ id, message }) => {
782-
await (await loadDiscordSendModule()).sendMessageDiscord(`user:${id}`, message);
781+
notify: async ({ cfg, id, message, accountId }) => {
782+
await (
783+
await loadDiscordSendModule()
784+
).sendMessageDiscord(`user:${id}`, message, {
785+
cfg,
786+
...(accountId ? { accountId } : {}),
787+
});
783788
},
784789
},
785790
},

extensions/discord/src/client.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RequestClient } from "@buape/carbon";
2-
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
2+
import { requireRuntimeConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
33
import type { RetryConfig, RetryRunner } from "openclaw/plugin-sdk/retry-runtime";
44
import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
55
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
@@ -16,7 +16,7 @@ import type { DiscordRuntimeAccountContext } from "./send.types.js";
1616
import { normalizeDiscordToken } from "./token.js";
1717

1818
export type DiscordClientOpts = {
19-
cfg?: ReturnType<typeof loadConfig>;
19+
cfg?: OpenClawConfig;
2020
token?: string;
2121
accountId?: string;
2222
rest?: RequestClient;
@@ -25,7 +25,7 @@ export type DiscordClientOpts = {
2525
};
2626

2727
export function createDiscordRuntimeAccountContext(params: {
28-
cfg: ReturnType<typeof loadConfig>;
28+
cfg: OpenClawConfig;
2929
accountId: string;
3030
}): DiscordRuntimeAccountContext {
3131
return {
@@ -36,10 +36,16 @@ export function createDiscordRuntimeAccountContext(params: {
3636

3737
export function resolveDiscordClientAccountContext(
3838
opts: Pick<DiscordClientOpts, "cfg" | "accountId">,
39-
cfg?: ReturnType<typeof loadConfig>,
39+
cfg?: OpenClawConfig,
4040
runtime?: Pick<RuntimeEnv, "error">,
4141
) {
42-
const resolvedCfg = opts.cfg ?? cfg ?? loadConfig();
42+
const config = opts.cfg ?? cfg;
43+
if (!config) {
44+
throw new Error(
45+
"Discord client requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.",
46+
);
47+
}
48+
const resolvedCfg = requireRuntimeConfig(config, "Discord client");
4349
const account = resolveAccountWithoutToken({
4450
cfg: resolvedCfg,
4551
accountId: opts.accountId,
@@ -63,7 +69,7 @@ function resolveToken(params: { accountId: string; fallbackToken?: string }) {
6369

6470
export function resolveDiscordProxyFetch(
6571
opts: Pick<DiscordClientOpts, "cfg" | "accountId">,
66-
cfg?: ReturnType<typeof loadConfig>,
72+
cfg?: OpenClawConfig,
6773
runtime?: Pick<RuntimeEnv, "error">,
6874
): typeof fetch | undefined {
6975
return resolveDiscordClientAccountContext(opts, cfg, runtime).proxyFetch;
@@ -72,7 +78,7 @@ export function resolveDiscordProxyFetch(
7278
function resolveRest(
7379
token: string,
7480
account: ResolvedDiscordAccount,
75-
cfg: ReturnType<typeof loadConfig>,
81+
cfg: OpenClawConfig,
7682
rest?: RequestClient,
7783
proxyFetch?: typeof fetch,
7884
) {
@@ -87,7 +93,7 @@ function resolveRest(
8793
}
8894

8995
function resolveAccountWithoutToken(params: {
90-
cfg: ReturnType<typeof loadConfig>;
96+
cfg: OpenClawConfig;
9197
accountId?: string;
9298
}): ResolvedDiscordAccount {
9399
const accountId = normalizeAccountId(params.accountId);
@@ -104,10 +110,7 @@ function resolveAccountWithoutToken(params: {
104110
};
105111
}
106112

107-
export function createDiscordRestClient(
108-
opts: DiscordClientOpts,
109-
cfg?: ReturnType<typeof loadConfig>,
110-
) {
113+
export function createDiscordRestClient(opts: DiscordClientOpts, cfg?: OpenClawConfig) {
111114
const explicitToken = normalizeDiscordToken(opts.token, "channels.discord.token");
112115
const proxyContext = resolveDiscordClientAccountContext(opts, cfg);
113116
const resolvedCfg = proxyContext.cfg;
@@ -126,7 +129,7 @@ export function createDiscordRestClient(
126129

127130
export function createDiscordClient(
128131
opts: DiscordClientOpts,
129-
cfg?: ReturnType<typeof loadConfig>,
132+
cfg?: OpenClawConfig,
130133
): { token: string; rest: RequestClient; request: RetryRunner } {
131134
const { token, rest, account } = createDiscordRestClient(opts, opts.cfg ?? cfg);
132135
const request = createDiscordRetryRunner({

extensions/discord/src/monitor/agent-components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ async function dispatchPluginDiscordInteractiveEvent(params: {
272272
text: buildPluginBindingResolvedText(resolved),
273273
},
274274
{
275+
cfg: params.ctx.cfg,
275276
accountId: params.ctx.accountId,
276277
},
277278
);

0 commit comments

Comments
 (0)