discord: expose EventQueue listenerTimeout as configurable option (fixes #24458)

This commit is contained in:
Glucksberg
2026-02-27 05:28:43 +00:00
committed by Peter Steinberger
parent 8629b996a1
commit a25a73e707
4 changed files with 79 additions and 1 deletions

View File

@@ -302,6 +302,20 @@ export type DiscordAccountConfig = {
activityType?: 0 | 1 | 2 | 3 | 4 | 5;
/** Streaming URL (Twitch/YouTube). Required when activityType=1. */
activityUrl?: string;
/**
* Carbon EventQueue configuration. Controls how Discord gateway events are processed.
* The most important option is `listenerTimeout` which defaults to 30s in Carbon --
* too short for LLM calls with extended thinking. Set a higher value (e.g. 120000)
* to prevent the event queue from killing long-running message handlers.
*/
eventQueue?: {
/** Max time (ms) a single listener can run before being killed. Default: 120000. */
listenerTimeout?: number;
/** Max events queued before backpressure is applied. Default: 10000. */
maxQueueSize?: number;
/** Max concurrent event processing operations. Default: 50. */
maxConcurrency?: number;
};
};
export type DiscordConfig = {

View File

@@ -517,6 +517,14 @@ export const DiscordAccountSchema = z
.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)])
.optional(),
activityUrl: z.string().url().optional(),
eventQueue: z
.object({
listenerTimeout: z.number().int().positive().optional(),
maxQueueSize: z.number().int().positive().optional(),
maxConcurrency: z.number().int().positive().optional(),
})
.strict()
.optional(),
})
.strict()
.superRefine((value, ctx) => {

View File

@@ -6,6 +6,7 @@ import type { RuntimeEnv } from "../../runtime.js";
const {
clientFetchUserMock,
clientGetPluginMock,
clientConstructorOptionsMock,
createDiscordNativeCommandMock,
createNoopThreadBindingManagerMock,
createThreadBindingManagerMock,
@@ -21,6 +22,7 @@ const {
} = vi.hoisted(() => {
const createdBindingManagers: Array<{ stop: ReturnType<typeof vi.fn> }> = [];
return {
clientConstructorOptionsMock: vi.fn(),
clientFetchUserMock: vi.fn(async (_target: string) => ({ id: "bot-1" })),
clientGetPluginMock: vi.fn<(_name: string) => unknown>(() => undefined),
createDiscordNativeCommandMock: vi.fn(() => ({ name: "mock-command" })),
@@ -69,9 +71,12 @@ vi.mock("@buape/carbon", () => {
class Client {
listeners: unknown[];
rest: { put: ReturnType<typeof vi.fn> };
constructor(_options: unknown, handlers: { listeners?: unknown[] }) {
options: unknown;
constructor(options: unknown, handlers: { listeners?: unknown[] }) {
this.options = options;
this.listeners = handlers.listeners ?? [];
this.rest = { put: vi.fn(async () => undefined) };
clientConstructorOptionsMock(options);
}
async handleDeployRequest() {
return undefined;
@@ -254,6 +259,7 @@ describe("monitorDiscordProvider", () => {
}) as OpenClawConfig;
beforeEach(() => {
clientConstructorOptionsMock.mockClear();
clientFetchUserMock.mockClear().mockResolvedValue({ id: "bot-1" });
clientGetPluginMock.mockClear().mockReturnValue(undefined);
createDiscordNativeCommandMock.mockClear().mockReturnValue({ name: "mock-command" });
@@ -334,4 +340,47 @@ describe("monitorDiscordProvider", () => {
expect(lifecycleArgs.pendingGatewayErrors).toHaveLength(1);
expect(String(lifecycleArgs.pendingGatewayErrors?.[0])).toContain("4014");
});
it("passes default eventQueue.listenerTimeout of 120s to Carbon Client", async () => {
const { monitorDiscordProvider } = await import("./provider.js");
await monitorDiscordProvider({
config: baseConfig(),
runtime: baseRuntime(),
});
expect(clientConstructorOptionsMock).toHaveBeenCalledTimes(1);
const opts = clientConstructorOptionsMock.mock.calls[0]?.[0] as {
eventQueue?: { listenerTimeout?: number };
};
expect(opts.eventQueue).toBeDefined();
expect(opts.eventQueue?.listenerTimeout).toBe(120_000);
});
it("forwards custom eventQueue config from discord config to Carbon Client", async () => {
const { monitorDiscordProvider } = await import("./provider.js");
resolveDiscordAccountMock.mockImplementation(() => ({
accountId: "default",
token: "cfg-token",
config: {
commands: { native: true, nativeSkills: false },
voice: { enabled: false },
agentComponents: { enabled: false },
execApprovals: { enabled: false },
eventQueue: { listenerTimeout: 300_000 },
},
}));
await monitorDiscordProvider({
config: baseConfig(),
runtime: baseRuntime(),
});
expect(clientConstructorOptionsMock).toHaveBeenCalledTimes(1);
const opts = clientConstructorOptionsMock.mock.calls[0]?.[0] as {
eventQueue?: { listenerTimeout?: number };
};
expect(opts.eventQueue?.listenerTimeout).toBe(300_000);
});
});

View File

@@ -517,6 +517,12 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
if (voiceEnabled) {
clientPlugins.push(new VoicePlugin());
}
// Pass eventQueue config to Carbon so the listener timeout can be tuned.
// Default listenerTimeout is 120s (Carbon defaults to 30s which is too short for LLM calls).
const eventQueueOpts = {
listenerTimeout: 120_000,
...discordCfg.eventQueue,
};
const client = new Client(
{
baseUrl: "http://localhost",
@@ -525,6 +531,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
publicKey: "a",
token,
autoDeploy: false,
eventQueue: eventQueueOpts,
},
{
commands,