diff --git a/CHANGELOG.md b/CHANGELOG.md
index 626b64a4354..25d0a996963 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256
index e1b7d10e5ea..be1b0245a74 100644
--- a/docs/.generated/config-baseline.sha256
+++ b/docs/.generated/config-baseline.sha256
@@ -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
diff --git a/docs/channels/discord.md b/docs/channels/discord.md
index 9e2c58ba098..a182112aba9 100644
--- a/docs/channels/discord.md
+++ b/docs/channels/discord.md
@@ -1255,6 +1255,22 @@ openclaw logs --follow
+
+ 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..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..gatewayRuntimeReadyTimeoutMs`
+ - runtime env fallback when config is unset: `OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS`
+ - runtime default: `30000` (30 seconds), max: `120000`
+
+
+
`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`
diff --git a/extensions/discord/src/channel.test.ts b/extensions/discord/src/channel.test.ts
index 882a92b90d9..60563c66e26 100644
--- a/extensions/discord/src/channel.test.ts
+++ b/extensions/discord/src/channel.test.ts
@@ -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"));
diff --git a/extensions/discord/src/config-ui-hints.ts b/extensions/discord/src/config-ui-hints.ts
index 8c5fc6d129e..4255a710ada 100644
--- a/extensions/discord/src/config-ui-hints.ts
+++ b/extensions/discord/src/config-ui-hints.ts
@@ -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.",
diff --git a/extensions/discord/src/monitor/provider.lifecycle.test.ts b/extensions/discord/src/monitor/provider.lifecycle.test.ts
index 93d438c9f07..095334b02e6 100644
--- a/extensions/discord/src/monitor/provider.lifecycle.test.ts
+++ b/extensions/discord/src/monitor/provider.lifecycle.test.ts
@@ -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({
diff --git a/extensions/discord/src/monitor/provider.lifecycle.ts b/extensions/discord/src/monitor/provider.lifecycle.ts
index e782a5deec4..d0720a5eddf 100644
--- a/extensions/discord/src/monitor/provider.lifecycle.ts
+++ b/extensions/discord/src/monitor/provider.lifecycle.ts
@@ -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;
abortSignal?: AbortSignal;
@@ -158,6 +192,7 @@ function createGatewayStatusObserver(params: {
runtime: RuntimeEnv;
pushStatus: (patch: Parameters[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[0]) => void;
runtime: RuntimeEnv;
beforeRestart?: () => Promise | void;
+ readyTimeoutMs: number;
}): Promise {
const waitUntilReady = async (): Promise => {
- 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[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") {
diff --git a/extensions/discord/src/monitor/provider.test.ts b/extensions/discord/src/monitor/provider.test.ts
index 9627c617490..7733746faaa 100644
--- a/extensions/discord/src/monitor/provider.test.ts
+++ b/extensions/discord/src/monitor/provider.test.ts
@@ -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(),
diff --git a/extensions/discord/src/monitor/provider.ts b/extensions/discord/src/monitor/provider.ts
index 2b4bf255521..9f679ecb7e7 100644
--- a/extensions/discord/src/monitor/provider.ts
+++ b/extensions/discord/src/monitor/provider.ts
@@ -626,6 +626,8 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
voiceManagerRef,
threadBindings,
gatewaySupervisor,
+ gatewayReadyTimeoutMs: account.config.gatewayReadyTimeoutMs,
+ gatewayRuntimeReadyTimeoutMs: account.config.gatewayRuntimeReadyTimeoutMs,
});
} finally {
cleanupDiscordProviderStartup({
diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts
index 0200fffc75f..b93c850864f 100644
--- a/src/config/bundled-channel-config-metadata.generated.ts
+++ b/src/config/bundled-channel-config-metadata.generated.ts
@@ -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.",
diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts
index 9142ba43af0..d70cf8fb5b0 100644
--- a/src/config/types.discord.ts
+++ b/src/config/types.discord.ts
@@ -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";
/**
diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts
index 8f99b19b235..09fb4041fde 100644
--- a/src/config/zod-schema.providers-core.ts
+++ b/src/config/zod-schema.providers-core.ts
@@ -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"),