mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(discord): configure gateway ready timeouts
This commit is contained in:
@@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/DMs: keep no-guild inbound messages on direct-message routing when Discord channel lookup is temporarily unavailable, preventing degraded DMs from forking into channel sessions. Fixes #59817. Thanks @DooPeePey.
|
||||
- Discord: retry outbound API calls on HTTP 5xx, request-timeout, and transient transport failures instead of only Discord rate limits, reducing dropped cron and agent replies during short Discord or network outages. Fixes #52396. Thanks @sunshineo.
|
||||
- Discord: include Components v2 Text Display content from referenced replies and forwarded snapshots, so component-only messages still appear in reply context. Fixes #56228. Thanks @HollandDrive.
|
||||
- Discord: add configurable gateway READY timeouts for startup and runtime reconnects, so staggered multi-account setups can avoid false restart loops. Fixes #72273. Thanks @sergionsantos.
|
||||
- Gateway/config: log config health-state write failures instead of silently hiding config observe-recovery write errors. Thanks @sallyom.
|
||||
- Diagnostics: reset stuck-session timers on reply, tool, status, block, and ACP progress events, and back off repeated `session.stuck` diagnostics while a session remains unchanged. Supersedes #72010. Thanks @rubencu.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
8bbb620e445cba64aa8a451cfc1a7142ac24e8c80088d74a2fc813ee9e221680 config-baseline.json
|
||||
d145a87759d16d5f58873db337a25cb134ab25e776cd454812dca99bb9cb12a7 config-baseline.core.json
|
||||
c401cd3450f1737bc92418cfea301d20b54b7fbef9e6049834acc01af338e538 config-baseline.channel.json
|
||||
7731a0b93cb335b56fac4c807447ba659fea51ea7a6cd844dc0ef5616669ee75 config-baseline.plugin.json
|
||||
1d9157a39ad18841d666af90c58e0539d6427cbd2ad0c1ce29047a5a2131ba7e config-baseline.json
|
||||
80e6e8dce647aef2d1310de55a81d27de52cca47fc24bd7ad81b80f43a72b84c config-baseline.core.json
|
||||
1cec599c3d27c258b9df3446baa547cb164e502afa9b30c052bba8737183f551 config-baseline.channel.json
|
||||
8346667910d2b3a3884efce8f96591adebc4f7ea99ce18337b80e4d70bf8e4d2 config-baseline.plugin.json
|
||||
|
||||
@@ -1255,6 +1255,22 @@ openclaw logs --follow
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Gateway READY timeout restarts">
|
||||
OpenClaw waits for Discord's gateway `READY` event during startup and after runtime reconnects. Multi-account setups with startup staggering can need a longer startup READY window than the default.
|
||||
|
||||
READY timeout knobs:
|
||||
|
||||
- startup single-account: `channels.discord.gatewayReadyTimeoutMs`
|
||||
- startup multi-account: `channels.discord.accounts.<accountId>.gatewayReadyTimeoutMs`
|
||||
- startup env fallback when config is unset: `OPENCLAW_DISCORD_READY_TIMEOUT_MS`
|
||||
- startup default: `15000` (15 seconds), max: `120000`
|
||||
- runtime single-account: `channels.discord.gatewayRuntimeReadyTimeoutMs`
|
||||
- runtime multi-account: `channels.discord.accounts.<accountId>.gatewayRuntimeReadyTimeoutMs`
|
||||
- runtime env fallback when config is unset: `OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS`
|
||||
- runtime default: `30000` (30 seconds), max: `120000`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Permissions audit mismatches">
|
||||
`channels status --probe` permission checks only work for numeric channel IDs.
|
||||
|
||||
@@ -1301,7 +1317,7 @@ Primary reference: [Configuration reference - Discord](/gateway/config-channels#
|
||||
- policy: `groupPolicy`, `dm.*`, `guilds.*`, `guilds.*.channels.*`
|
||||
- command: `commands.native`, `commands.useAccessGroups`, `configWrites`, `slashCommand.*`
|
||||
- event queue: `eventQueue.listenerTimeout` (listener budget), `eventQueue.maxQueueSize`, `eventQueue.maxConcurrency`
|
||||
- gateway metadata: `gatewayInfoTimeoutMs`
|
||||
- gateway: `gatewayInfoTimeoutMs`, `gatewayReadyTimeoutMs`, `gatewayRuntimeReadyTimeoutMs`
|
||||
- reply/history: `replyToMode`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
|
||||
- delivery: `textChunkLimit`, `chunkMode`, `maxLinesPerMessage`
|
||||
- streaming: `streaming` (legacy alias: `streamMode`), `streaming.preview.toolProgress`, `draftChunk`, `blockStreaming`, `blockStreamingCoalesce`
|
||||
|
||||
@@ -168,6 +168,33 @@ describe("discordPlugin outbound", () => {
|
||||
expect(resolveReplyToMode({ cfg, accountId: "default" })).toBe("all");
|
||||
});
|
||||
|
||||
it("inherits Discord gateway READY timeout settings per account", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "discord-token",
|
||||
gatewayReadyTimeoutMs: 90_000,
|
||||
gatewayRuntimeReadyTimeoutMs: 120_000,
|
||||
accounts: {
|
||||
work: {
|
||||
token: "discord-token-work",
|
||||
gatewayReadyTimeoutMs: 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveAccount(cfg).config).toMatchObject({
|
||||
gatewayReadyTimeoutMs: 90_000,
|
||||
gatewayRuntimeReadyTimeoutMs: 120_000,
|
||||
});
|
||||
expect(resolveAccount(cfg, "work").config).toMatchObject({
|
||||
gatewayReadyTimeoutMs: 60_000,
|
||||
gatewayRuntimeReadyTimeoutMs: 120_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards full media send context to sendMessageDiscord", async () => {
|
||||
const sendMessageDiscord = vi.fn(async () => ({ messageId: "m1" }));
|
||||
const mediaReadFile = vi.fn(async () => Buffer.from("media"));
|
||||
|
||||
@@ -141,6 +141,14 @@ export const discordChannelConfigUiHints = {
|
||||
label: "Discord Gateway Metadata Timeout (ms)",
|
||||
help: "Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default is 30000; OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS can override when config is unset.",
|
||||
},
|
||||
gatewayReadyTimeoutMs: {
|
||||
label: "Discord Gateway READY Timeout (ms)",
|
||||
help: "Startup wait for the Discord gateway READY event before restarting the socket. Default is 15000; OPENCLAW_DISCORD_READY_TIMEOUT_MS can override when config is unset.",
|
||||
},
|
||||
gatewayRuntimeReadyTimeoutMs: {
|
||||
label: "Discord Gateway Runtime READY Timeout (ms)",
|
||||
help: "Runtime reconnect wait for the Discord gateway READY event before force-stopping the lifecycle. Default is 30000; OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS can override when config is unset.",
|
||||
},
|
||||
"voice.enabled": {
|
||||
label: "Discord Voice Enabled",
|
||||
help: "Enable Discord voice channel conversations. Text-only Discord configs leave voice off by default; set true to enable /vc commands and the Guild Voice States intent.",
|
||||
|
||||
@@ -59,9 +59,15 @@ vi.mock("./gateway-registry.js", () => ({
|
||||
|
||||
describe("runDiscordGatewayLifecycle", () => {
|
||||
let runDiscordGatewayLifecycle: typeof import("./provider.lifecycle.js").runDiscordGatewayLifecycle;
|
||||
let resolveDiscordGatewayReadyTimeoutMs: typeof import("./provider.lifecycle.js").resolveDiscordGatewayReadyTimeoutMs;
|
||||
let resolveDiscordGatewayRuntimeReadyTimeoutMs: typeof import("./provider.lifecycle.js").resolveDiscordGatewayRuntimeReadyTimeoutMs;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js"));
|
||||
({
|
||||
runDiscordGatewayLifecycle,
|
||||
resolveDiscordGatewayReadyTimeoutMs,
|
||||
resolveDiscordGatewayRuntimeReadyTimeoutMs,
|
||||
} = await import("./provider.lifecycle.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -143,24 +149,25 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
error: runtimeError,
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const lifecycleParams: LifecycleParams = {
|
||||
accountId: "default",
|
||||
gateway: gateway ? (gateway as unknown as MutableDiscordGateway) : undefined,
|
||||
runtime,
|
||||
isDisallowedIntentsError: params?.isDisallowedIntentsError ?? (() => false),
|
||||
voiceManager: null,
|
||||
voiceManagerRef: { current: null },
|
||||
threadBindings: { stop: threadStop },
|
||||
gatewaySupervisor,
|
||||
statusSink,
|
||||
abortSignal: undefined,
|
||||
};
|
||||
return {
|
||||
threadStop,
|
||||
runtimeLog,
|
||||
runtimeError,
|
||||
gatewaySupervisor,
|
||||
statusSink,
|
||||
lifecycleParams: {
|
||||
accountId: "default",
|
||||
gateway: gateway ? (gateway as unknown as MutableDiscordGateway) : undefined,
|
||||
runtime,
|
||||
isDisallowedIntentsError: params?.isDisallowedIntentsError ?? (() => false),
|
||||
voiceManager: null,
|
||||
voiceManagerRef: { current: null },
|
||||
threadBindings: { stop: threadStop },
|
||||
gatewaySupervisor,
|
||||
statusSink,
|
||||
abortSignal: undefined as AbortSignal | undefined,
|
||||
} satisfies LifecycleParams,
|
||||
lifecycleParams,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -176,6 +183,26 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
expect(params.gatewaySupervisor.detachLifecycle).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
it("resolves gateway READY timeouts from config, env, then defaults", () => {
|
||||
expect(resolveDiscordGatewayReadyTimeoutMs({ configuredTimeoutMs: 45_000 })).toBe(45_000);
|
||||
expect(
|
||||
resolveDiscordGatewayReadyTimeoutMs({
|
||||
env: { OPENCLAW_DISCORD_READY_TIMEOUT_MS: "90000" },
|
||||
}),
|
||||
).toBe(90_000);
|
||||
expect(resolveDiscordGatewayReadyTimeoutMs({ env: {} })).toBe(15_000);
|
||||
|
||||
expect(resolveDiscordGatewayRuntimeReadyTimeoutMs({ configuredTimeoutMs: 60_000 })).toBe(
|
||||
60_000,
|
||||
);
|
||||
expect(
|
||||
resolveDiscordGatewayRuntimeReadyTimeoutMs({
|
||||
env: { OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS: "120000" },
|
||||
}),
|
||||
).toBe(120_000);
|
||||
expect(resolveDiscordGatewayRuntimeReadyTimeoutMs({ env: {} })).toBe(30_000);
|
||||
});
|
||||
|
||||
it("cleans up thread bindings when gateway wait fails before READY", async () => {
|
||||
waitForDiscordGatewayStopMock.mockRejectedValueOnce(new Error("startup failed"));
|
||||
const { lifecycleParams, threadStop, gatewaySupervisor } = createLifecycleHarness();
|
||||
@@ -228,14 +255,15 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
gateway: null,
|
||||
},
|
||||
);
|
||||
lifecycleParams.gatewayReadyTimeoutMs = 5_000;
|
||||
|
||||
const lifecyclePromise = runDiscordGatewayLifecycle(lifecycleParams);
|
||||
lifecyclePromise.catch(() => {});
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.advanceTimersByTimeAsync(15_500);
|
||||
await vi.advanceTimersByTimeAsync(5_500);
|
||||
|
||||
await expect(lifecyclePromise).rejects.toThrow(
|
||||
"discord gateway did not reach READY within 15000ms",
|
||||
"discord gateway did not reach READY within 5000ms",
|
||||
);
|
||||
expect(statusSink).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -606,15 +634,16 @@ describe("runDiscordGatewayLifecycle", () => {
|
||||
);
|
||||
|
||||
const { lifecycleParams, runtimeError, statusSink } = createLifecycleHarness({ gateway });
|
||||
lifecycleParams.gatewayRuntimeReadyTimeoutMs = 5_000;
|
||||
const lifecyclePromise = runDiscordGatewayLifecycle(lifecycleParams);
|
||||
lifecyclePromise.catch(() => {});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_500);
|
||||
await vi.advanceTimersByTimeAsync(5_500);
|
||||
await expect(lifecyclePromise).rejects.toThrow(
|
||||
"discord gateway opened but did not reach READY within 30000ms",
|
||||
"discord gateway opened but did not reach READY within 5000ms",
|
||||
);
|
||||
expect(runtimeError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("did not reach READY within 30000ms"),
|
||||
expect.stringContaining("did not reach READY within 5000ms"),
|
||||
);
|
||||
expect(statusSink).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -19,8 +19,11 @@ import {
|
||||
} from "./gateway-supervisor.js";
|
||||
import type { DiscordMonitorStatusSink } from "./status.js";
|
||||
|
||||
const DISCORD_GATEWAY_READY_TIMEOUT_MS = 15_000;
|
||||
const DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_DISCORD_GATEWAY_READY_TIMEOUT_MS = 15_000;
|
||||
const DEFAULT_DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS = 30_000;
|
||||
const MAX_DISCORD_GATEWAY_READY_TIMEOUT_MS = 120_000;
|
||||
const DISCORD_GATEWAY_READY_TIMEOUT_ENV = "OPENCLAW_DISCORD_READY_TIMEOUT_MS";
|
||||
const DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_ENV = "OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS";
|
||||
const DISCORD_GATEWAY_READY_POLL_MS = 250;
|
||||
const DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS = 5_000;
|
||||
const DISCORD_GATEWAY_STARTUP_TERMINATE_CLOSE_TIMEOUT_MS = 1_000;
|
||||
@@ -28,6 +31,37 @@ const DISCORD_GATEWAY_TRANSPORT_ACTIVITY_STATUS_MIN_INTERVAL_MS = 30_000;
|
||||
|
||||
type GatewayReadyWaitResult = "ready" | "stopped" | "timeout";
|
||||
|
||||
function normalizeGatewayReadyTimeoutMs(value: unknown): number | undefined {
|
||||
const numeric =
|
||||
typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.min(Math.floor(numeric), MAX_DISCORD_GATEWAY_READY_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
export function resolveDiscordGatewayReadyTimeoutMs(params?: {
|
||||
configuredTimeoutMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): number {
|
||||
return (
|
||||
normalizeGatewayReadyTimeoutMs(params?.configuredTimeoutMs) ??
|
||||
normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_READY_TIMEOUT_ENV]) ??
|
||||
DEFAULT_DISCORD_GATEWAY_READY_TIMEOUT_MS
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDiscordGatewayRuntimeReadyTimeoutMs(params?: {
|
||||
configuredTimeoutMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): number {
|
||||
return (
|
||||
normalizeGatewayReadyTimeoutMs(params?.configuredTimeoutMs) ??
|
||||
normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_ENV]) ??
|
||||
DEFAULT_DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS
|
||||
);
|
||||
}
|
||||
|
||||
async function restartGatewayAfterReadyTimeout(params: {
|
||||
gateway?: Pick<MutableDiscordGateway, "connect" | "disconnect" | "ws">;
|
||||
abortSignal?: AbortSignal;
|
||||
@@ -158,6 +192,7 @@ function createGatewayStatusObserver(params: {
|
||||
runtime: RuntimeEnv;
|
||||
pushStatus: (patch: Parameters<DiscordMonitorStatusSink>[0]) => void;
|
||||
isLifecycleStopping: () => boolean;
|
||||
runtimeReadyTimeoutMs: number;
|
||||
}) {
|
||||
let forceStopHandler: ((err: unknown) => void) | undefined;
|
||||
let queuedForceStopError: unknown;
|
||||
@@ -214,7 +249,7 @@ function createGatewayStatusObserver(params: {
|
||||
}
|
||||
const at = Date.now();
|
||||
const error = new Error(
|
||||
`discord gateway opened but did not reach READY within ${DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS}ms`,
|
||||
`discord gateway opened but did not reach READY within ${params.runtimeReadyTimeoutMs}ms`,
|
||||
);
|
||||
params.pushStatus({
|
||||
connected: false,
|
||||
@@ -227,7 +262,7 @@ function createGatewayStatusObserver(params: {
|
||||
});
|
||||
params.runtime.error?.(danger(error.message));
|
||||
triggerForceStop(error);
|
||||
}, DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS);
|
||||
}, params.runtimeReadyTimeoutMs);
|
||||
readyTimeoutId.unref?.();
|
||||
}
|
||||
};
|
||||
@@ -292,9 +327,10 @@ async function waitForGatewayReady(params: {
|
||||
pushStatus?: (patch: Parameters<DiscordMonitorStatusSink>[0]) => void;
|
||||
runtime: RuntimeEnv;
|
||||
beforeRestart?: () => Promise<void> | void;
|
||||
readyTimeoutMs: number;
|
||||
}): Promise<void> {
|
||||
const waitUntilReady = async (): Promise<GatewayReadyWaitResult> => {
|
||||
const deadlineAt = Date.now() + DISCORD_GATEWAY_READY_TIMEOUT_MS;
|
||||
const deadlineAt = Date.now() + params.readyTimeoutMs;
|
||||
while (!params.abortSignal?.aborted) {
|
||||
if ((await params.beforePoll?.()) === "stop") {
|
||||
return "stopped";
|
||||
@@ -324,16 +360,12 @@ async function waitForGatewayReady(params: {
|
||||
return;
|
||||
}
|
||||
if (!params.gateway) {
|
||||
throw new Error(
|
||||
`discord gateway did not reach READY within ${DISCORD_GATEWAY_READY_TIMEOUT_MS}ms`,
|
||||
);
|
||||
throw new Error(`discord gateway did not reach READY within ${params.readyTimeoutMs}ms`);
|
||||
}
|
||||
|
||||
const restartAt = Date.now();
|
||||
params.runtime.error?.(
|
||||
danger(
|
||||
`discord: gateway was not ready after ${DISCORD_GATEWAY_READY_TIMEOUT_MS}ms; restarting gateway`,
|
||||
),
|
||||
danger(`discord: gateway was not ready after ${params.readyTimeoutMs}ms; restarting gateway`),
|
||||
);
|
||||
params.pushStatus?.({
|
||||
connected: false,
|
||||
@@ -356,7 +388,7 @@ async function waitForGatewayReady(params: {
|
||||
|
||||
if ((await waitUntilReady()) === "timeout") {
|
||||
throw new Error(
|
||||
`discord gateway did not reach READY within ${DISCORD_GATEWAY_READY_TIMEOUT_MS}ms after restart`,
|
||||
`discord gateway did not reach READY within ${params.readyTimeoutMs}ms after restart`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -372,6 +404,8 @@ export async function runDiscordGatewayLifecycle(params: {
|
||||
threadBindings: { stop: () => void };
|
||||
gatewaySupervisor: DiscordGatewaySupervisor;
|
||||
statusSink?: DiscordMonitorStatusSink;
|
||||
gatewayReadyTimeoutMs?: number;
|
||||
gatewayRuntimeReadyTimeoutMs?: number;
|
||||
}) {
|
||||
const gateway = params.gateway;
|
||||
if (gateway) {
|
||||
@@ -387,12 +421,21 @@ export async function runDiscordGatewayLifecycle(params: {
|
||||
const pushStatus = (patch: Parameters<DiscordMonitorStatusSink>[0]) => {
|
||||
params.statusSink?.(patch);
|
||||
};
|
||||
const gatewayReadyTimeoutMs = resolveDiscordGatewayReadyTimeoutMs({
|
||||
configuredTimeoutMs: params.gatewayReadyTimeoutMs,
|
||||
env: process.env,
|
||||
});
|
||||
const gatewayRuntimeReadyTimeoutMs = resolveDiscordGatewayRuntimeReadyTimeoutMs({
|
||||
configuredTimeoutMs: params.gatewayRuntimeReadyTimeoutMs,
|
||||
env: process.env,
|
||||
});
|
||||
const statusObserver = createGatewayStatusObserver({
|
||||
gateway,
|
||||
abortSignal: params.abortSignal,
|
||||
runtime: params.runtime,
|
||||
pushStatus,
|
||||
isLifecycleStopping: () => lifecycleStopping,
|
||||
runtimeReadyTimeoutMs: gatewayRuntimeReadyTimeoutMs,
|
||||
});
|
||||
gatewayEmitter?.on("debug", statusObserver.onGatewayDebug);
|
||||
let lastTransportActivityStatusAt: number | undefined;
|
||||
@@ -460,6 +503,7 @@ export async function runDiscordGatewayLifecycle(params: {
|
||||
pushStatus,
|
||||
runtime: params.runtime,
|
||||
beforeRestart: statusObserver.clearReadyWatch,
|
||||
readyTimeoutMs: gatewayReadyTimeoutMs,
|
||||
});
|
||||
|
||||
if (drainPendingGatewayErrors() === "stop") {
|
||||
|
||||
@@ -382,6 +382,33 @@ describe("monitorDiscordProvider", () => {
|
||||
expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes configured gateway READY timeouts to the lifecycle monitor", async () => {
|
||||
resolveDiscordAccountMock.mockReturnValueOnce({
|
||||
accountId: "default",
|
||||
token: "cfg-token",
|
||||
config: {
|
||||
commands: { native: true, nativeSkills: false },
|
||||
voice: { enabled: false },
|
||||
agentComponents: { enabled: false },
|
||||
execApprovals: { enabled: false },
|
||||
gatewayReadyTimeoutMs: 90_000,
|
||||
gatewayRuntimeReadyTimeoutMs: 120_000,
|
||||
},
|
||||
});
|
||||
|
||||
await monitorDiscordProvider({
|
||||
config: baseConfig(),
|
||||
runtime: baseRuntime(),
|
||||
});
|
||||
|
||||
expect(monitorLifecycleMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gatewayReadyTimeoutMs: 90_000,
|
||||
gatewayRuntimeReadyTimeoutMs: 120_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not load the Discord voice runtime when voice is disabled", async () => {
|
||||
await monitorDiscordProvider({
|
||||
config: baseConfig(),
|
||||
|
||||
@@ -626,6 +626,8 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
voiceManagerRef,
|
||||
threadBindings,
|
||||
gatewaySupervisor,
|
||||
gatewayReadyTimeoutMs: account.config.gatewayReadyTimeoutMs,
|
||||
gatewayRuntimeReadyTimeoutMs: account.config.gatewayRuntimeReadyTimeoutMs,
|
||||
});
|
||||
} finally {
|
||||
cleanupDiscordProviderStartup({
|
||||
|
||||
@@ -806,6 +806,16 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 120000,
|
||||
},
|
||||
gatewayReadyTimeoutMs: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 120000,
|
||||
},
|
||||
gatewayRuntimeReadyTimeoutMs: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 120000,
|
||||
},
|
||||
allowBots: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -2182,6 +2192,16 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 120000,
|
||||
},
|
||||
gatewayReadyTimeoutMs: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 120000,
|
||||
},
|
||||
gatewayRuntimeReadyTimeoutMs: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 120000,
|
||||
},
|
||||
allowBots: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -3573,6 +3593,14 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Discord Gateway Metadata Timeout (ms)",
|
||||
help: "Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default is 30000; OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS can override when config is unset.",
|
||||
},
|
||||
gatewayReadyTimeoutMs: {
|
||||
label: "Discord Gateway READY Timeout (ms)",
|
||||
help: "Startup wait for the Discord gateway READY event before restarting the socket. Default is 15000; OPENCLAW_DISCORD_READY_TIMEOUT_MS can override when config is unset.",
|
||||
},
|
||||
gatewayRuntimeReadyTimeoutMs: {
|
||||
label: "Discord Gateway Runtime READY Timeout (ms)",
|
||||
help: "Runtime reconnect wait for the Discord gateway READY event before force-stopping the lifecycle. Default is 30000; OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS can override when config is unset.",
|
||||
},
|
||||
"voice.enabled": {
|
||||
label: "Discord Voice Enabled",
|
||||
help: "Enable Discord voice channel conversations. Text-only Discord configs leave voice off by default; set true to enable /vc commands and the Guild Voice States intent.",
|
||||
|
||||
@@ -251,6 +251,10 @@ export type DiscordAccountConfig = {
|
||||
proxy?: string;
|
||||
/** Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default: 30000. */
|
||||
gatewayInfoTimeoutMs?: number;
|
||||
/** Startup wait for the gateway READY event before restarting the socket. Default: 15000. */
|
||||
gatewayReadyTimeoutMs?: number;
|
||||
/** Runtime reconnect wait for the gateway READY event before force-stopping the lifecycle. Default: 30000. */
|
||||
gatewayRuntimeReadyTimeoutMs?: number;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). Set "mentions" to gate on mentions. */
|
||||
allowBots?: boolean | "mentions";
|
||||
/**
|
||||
|
||||
@@ -532,6 +532,8 @@ export const DiscordAccountSchema = z
|
||||
applicationId: DiscordIdSchema.optional(),
|
||||
proxy: z.string().optional(),
|
||||
gatewayInfoTimeoutMs: z.number().int().positive().max(120_000).optional(),
|
||||
gatewayReadyTimeoutMs: z.number().int().positive().max(120_000).optional(),
|
||||
gatewayRuntimeReadyTimeoutMs: z.number().int().positive().max(120_000).optional(),
|
||||
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),
|
||||
dangerouslyAllowNameMatching: z.boolean().optional(),
|
||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||
|
||||
Reference in New Issue
Block a user