mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:20:43 +00:00
fix(slack): tune socket mode pong timeout
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Tasks/media: infer agent ownership for session-scoped task records so `/tasks` agent-local fallback includes session-backed `video_generate` and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc.
|
||||
- Agents/media: keep long-running `video_generate` and `music_generate` tasks fresh while provider jobs are still pending, so task maintenance does not mark active Discord media renders lost before completion. Thanks @vincentkoc.
|
||||
- CLI/status: treat scope-limited gateway probes as reachable-but-degraded in shared status scans, so `openclaw status --all` no longer reports a live gateway as unreachable after `missing scope: operator.read`. Fixes #49180; supersedes #47981. Thanks @openjay.
|
||||
- Slack/Socket Mode: use a 15s Slack SDK pong timeout by default and add `channels.slack.socketMode.clientPingTimeout`, `serverPingTimeout`, and `pingPongLoggingEnabled` overrides so stale-websocket handling no longer depends on app-event health heuristics. Fixes #14248; refs #58519, #64009, and #63488. Thanks @shivasymbl and @freerk.
|
||||
- Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc.
|
||||
- Slack/auto-reply: keep fully consumed text reset triggers such as `new session` out of `BodyForAgent` after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana.
|
||||
- Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
b1d76b9451b21434325e64d5bb531b9b995ba3bbf8f7b1628c09cce18f24c8e2 config-baseline.json
|
||||
78888d302b2263583430e41b9811277aab91937201d4de90cfbd5761e9b95727 config-baseline.json
|
||||
58e98b59498060d301104b3772332de5600eb674687b06d0d32a202370709ee0 config-baseline.core.json
|
||||
a9f058ee9616e189dab7fc223e1207a49ae52b8490b8028935c9d0a2b16f81b2 config-baseline.channel.json
|
||||
323a9fd49a669951ca5b3442d95aad243bd1330083f9857e83a8dcfae2bbc9d0 config-baseline.channel.json
|
||||
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json
|
||||
|
||||
@@ -117,6 +117,27 @@ openclaw gateway
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Socket Mode transport tuning
|
||||
|
||||
OpenClaw sets the Slack SDK client pong timeout to 15 seconds by default for Socket Mode. Override the transport settings only when you need workspace- or host-specific tuning:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
mode: "socket",
|
||||
socketMode: {
|
||||
clientPingTimeout: 20000,
|
||||
serverPingTimeout: 30000,
|
||||
pingPongLoggingEnabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use this only for Socket Mode workspaces that log Slack websocket pong/server-ping timeouts or run on hosts with known event-loop starvation. `clientPingTimeout` is the pong wait after the SDK sends a client ping; `serverPingTimeout` is the wait for Slack server pings. App messages and events remain application state, not transport liveness signals.
|
||||
|
||||
## Manifest and scope checklist
|
||||
|
||||
The base Slack app manifest is the same for Socket Mode and HTTP Request URLs. Only the `settings` block (and the slash command `url`) differs.
|
||||
|
||||
@@ -63,6 +63,35 @@ describe("slack config schema", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts Socket Mode ping/pong transport tuning", () => {
|
||||
expectSlackConfigValid({
|
||||
mode: "socket",
|
||||
socketMode: {
|
||||
clientPingTimeout: 15_000,
|
||||
serverPingTimeout: 45_000,
|
||||
pingPongLoggingEnabled: true,
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
socketMode: {
|
||||
clientPingTimeout: 20_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid Socket Mode ping/pong transport tuning", () => {
|
||||
expectSlackConfigIssue(
|
||||
{
|
||||
socketMode: {
|
||||
clientPingTimeout: 0,
|
||||
},
|
||||
},
|
||||
"socketMode.clientPingTimeout",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts account-level user token config", () => {
|
||||
expectSlackConfigValid({
|
||||
accounts: {
|
||||
|
||||
@@ -29,6 +29,22 @@ export const slackChannelConfigUiHints = {
|
||||
label: "Slack Allow Bot Messages",
|
||||
help: "Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
},
|
||||
socketMode: {
|
||||
label: "Slack Socket Mode Transport",
|
||||
help: "Slack Socket Mode transport tuning passed to the Slack SDK. Use only when investigating ping/pong timeout or stale websocket behavior.",
|
||||
},
|
||||
"socketMode.clientPingTimeout": {
|
||||
label: "Slack Socket Mode Pong Timeout",
|
||||
help: "Milliseconds the Slack SDK waits for a pong after its client ping before treating the websocket as stale (OpenClaw default: 15000). Increase on hosts with event-loop starvation or slow network scheduling.",
|
||||
},
|
||||
"socketMode.serverPingTimeout": {
|
||||
label: "Slack Socket Mode Server Ping Timeout",
|
||||
help: "Milliseconds the Slack SDK waits for Slack server pings before treating the websocket as stale.",
|
||||
},
|
||||
"socketMode.pingPongLoggingEnabled": {
|
||||
label: "Slack Socket Mode Ping/Pong Logging",
|
||||
help: "Enable Slack SDK ping/pong transport logs while debugging Socket Mode websocket health.",
|
||||
},
|
||||
botToken: {
|
||||
label: "Slack Bot Token",
|
||||
help: "Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change.",
|
||||
|
||||
@@ -5,6 +5,13 @@ import { formatUnknownError, waitForSlackSocketDisconnect } from "./reconnect-po
|
||||
type SlackAppConstructor = typeof import("@slack/bolt").App;
|
||||
type SlackHttpReceiverConstructor = typeof import("@slack/bolt").HTTPReceiver;
|
||||
type SlackSocketModeReceiverConstructor = typeof import("@slack/bolt").SocketModeReceiver;
|
||||
type SlackSocketModeReceiverOptions = ConstructorParameters<SlackSocketModeReceiverConstructor>[0];
|
||||
type SlackSocketModeConfig = Pick<
|
||||
SlackSocketModeReceiverOptions,
|
||||
"clientPingTimeout" | "serverPingTimeout" | "pingPongLoggingEnabled"
|
||||
>;
|
||||
|
||||
export const OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS = 15_000;
|
||||
|
||||
export type SlackBoltResolvedExports = {
|
||||
App: SlackAppConstructor;
|
||||
@@ -167,16 +174,27 @@ export function createSlackBoltApp(params: {
|
||||
signingSecret?: string;
|
||||
slackWebhookPath: string;
|
||||
clientOptions: Record<string, unknown>;
|
||||
socketMode?: SlackSocketModeConfig;
|
||||
}) {
|
||||
const socketModeReceiverOptions: SlackSocketModeReceiverOptions = {
|
||||
appToken: params.appToken ?? "",
|
||||
autoReconnectEnabled: false,
|
||||
clientPingTimeout:
|
||||
params.socketMode?.clientPingTimeout ?? OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS,
|
||||
installerOptions: {
|
||||
clientOptions: params.clientOptions,
|
||||
},
|
||||
};
|
||||
if (params.socketMode?.serverPingTimeout !== undefined) {
|
||||
socketModeReceiverOptions.serverPingTimeout = params.socketMode.serverPingTimeout;
|
||||
}
|
||||
if (params.socketMode?.pingPongLoggingEnabled !== undefined) {
|
||||
socketModeReceiverOptions.pingPongLoggingEnabled = params.socketMode.pingPongLoggingEnabled;
|
||||
}
|
||||
|
||||
const receiver =
|
||||
params.slackMode === "socket"
|
||||
? new params.interop.SocketModeReceiver({
|
||||
appToken: params.appToken ?? "",
|
||||
autoReconnectEnabled: false,
|
||||
installerOptions: {
|
||||
clientOptions: params.clientOptions,
|
||||
},
|
||||
})
|
||||
? new params.interop.SocketModeReceiver(socketModeReceiverOptions)
|
||||
: new params.interop.HTTPReceiver({
|
||||
signingSecret: params.signingSecret ?? "",
|
||||
endpoints: params.slackWebhookPath,
|
||||
|
||||
@@ -158,6 +158,7 @@ describe("createSlackBoltApp", () => {
|
||||
expect((receiver as unknown as FakeSocketModeReceiver).args).toEqual({
|
||||
appToken: "xapp-test",
|
||||
autoReconnectEnabled: false,
|
||||
clientPingTimeout: 15_000,
|
||||
installerOptions: {
|
||||
clientOptions,
|
||||
},
|
||||
@@ -173,6 +174,38 @@ describe("createSlackBoltApp", () => {
|
||||
expect((app as unknown as FakeApp).middleware).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("passes Socket Mode ping/pong options through Slack's public receiver API", () => {
|
||||
const clientOptions = { teamId: "T1" };
|
||||
const { receiver } = createSlackBoltApp({
|
||||
interop: {
|
||||
App: FakeApp as never,
|
||||
HTTPReceiver: FakeHTTPReceiver as never,
|
||||
SocketModeReceiver: FakeSocketModeReceiver as never,
|
||||
},
|
||||
slackMode: "socket",
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
slackWebhookPath: "/slack/events",
|
||||
clientOptions,
|
||||
socketMode: {
|
||||
clientPingTimeout: 20_000,
|
||||
serverPingTimeout: 45_000,
|
||||
pingPongLoggingEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect((receiver as unknown as FakeSocketModeReceiver).args).toEqual({
|
||||
appToken: "xapp-test",
|
||||
autoReconnectEnabled: false,
|
||||
clientPingTimeout: 20_000,
|
||||
serverPingTimeout: 45_000,
|
||||
pingPongLoggingEnabled: true,
|
||||
installerOptions: {
|
||||
clientOptions,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses HTTPReceiver for webhook mode", () => {
|
||||
const clientOptions = { teamId: "T1" };
|
||||
const { app, receiver } = createSlackBoltApp({
|
||||
|
||||
@@ -192,6 +192,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
signingSecret: signingSecret ?? undefined,
|
||||
slackWebhookPath,
|
||||
clientOptions: clientOptions as Record<string, unknown>,
|
||||
...(slackCfg.socketMode ? { socketMode: slackCfg.socketMode } : {}),
|
||||
});
|
||||
|
||||
// Pre-set shuttingDown on the SocketModeClient before app.stop() to prevent
|
||||
|
||||
@@ -11018,6 +11018,25 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
type: "string",
|
||||
enum: ["socket", "http"],
|
||||
},
|
||||
socketMode: {
|
||||
type: "object",
|
||||
properties: {
|
||||
clientPingTimeout: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
serverPingTimeout: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
pingPongLoggingEnabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
signingSecret: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -11932,6 +11951,25 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
type: "string",
|
||||
enum: ["socket", "http"],
|
||||
},
|
||||
socketMode: {
|
||||
type: "object",
|
||||
properties: {
|
||||
clientPingTimeout: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
serverPingTimeout: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
pingPongLoggingEnabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
signingSecret: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -12870,6 +12908,22 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Slack Allow Bot Messages",
|
||||
help: "Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
},
|
||||
socketMode: {
|
||||
label: "Slack Socket Mode Transport",
|
||||
help: "Slack Socket Mode transport tuning passed to the Slack SDK. Use only when investigating ping/pong timeout or stale websocket behavior.",
|
||||
},
|
||||
"socketMode.clientPingTimeout": {
|
||||
label: "Slack Socket Mode Pong Timeout",
|
||||
help: "Milliseconds the Slack SDK waits for a pong after its client ping before treating the websocket as stale (OpenClaw default: 15000). Increase on hosts with event-loop starvation or slow network scheduling.",
|
||||
},
|
||||
"socketMode.serverPingTimeout": {
|
||||
label: "Slack Socket Mode Server Ping Timeout",
|
||||
help: "Milliseconds the Slack SDK waits for Slack server pings before treating the websocket as stale.",
|
||||
},
|
||||
"socketMode.pingPongLoggingEnabled": {
|
||||
label: "Slack Socket Mode Ping/Pong Logging",
|
||||
help: "Enable Slack SDK ping/pong transport logs while debugging Socket Mode websocket health.",
|
||||
},
|
||||
botToken: {
|
||||
label: "Slack Bot Token",
|
||||
help: "Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change.",
|
||||
|
||||
@@ -106,11 +106,22 @@ export type SlackThreadConfig = {
|
||||
requireExplicitMention?: boolean;
|
||||
};
|
||||
|
||||
export type SlackSocketModeConfig = {
|
||||
/** Slack SDK pong timeout in milliseconds. Socket Mode only. Default: 15000. */
|
||||
clientPingTimeout?: number;
|
||||
/** Slack SDK server ping timeout in milliseconds. Socket Mode only. */
|
||||
serverPingTimeout?: number;
|
||||
/** Enable Slack SDK ping/pong transport logging. Socket Mode only. */
|
||||
pingPongLoggingEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type SlackAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** Slack connection mode (socket|http). Default: socket. */
|
||||
mode?: "socket" | "http";
|
||||
/** Slack SDK Socket Mode transport options. Ignored in HTTP mode. */
|
||||
socketMode?: SlackSocketModeConfig;
|
||||
/** Slack signing secret (required for HTTP mode). */
|
||||
signingSecret?: string;
|
||||
/** Slack Events API webhook path (default: /slack/events). */
|
||||
|
||||
@@ -895,10 +895,19 @@ const SlackReplyToModeByChatTypeSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SlackSocketModeSchema = z
|
||||
.object({
|
||||
clientPingTimeout: z.number().int().positive().optional(),
|
||||
serverPingTimeout: z.number().int().positive().optional(),
|
||||
pingPongLoggingEnabled: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SlackAccountSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
mode: z.enum(["socket", "http"]).optional(),
|
||||
socketMode: SlackSocketModeSchema.optional(),
|
||||
signingSecret: SecretInputSchema.optional().register(sensitive),
|
||||
webhookPath: z.string().optional(),
|
||||
capabilities: SlackCapabilitiesSchema.optional(),
|
||||
|
||||
Reference in New Issue
Block a user