diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ec796774e..d1e7b81b781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai - CLI/doctor: trust a ready gateway memory probe when CLI-side active memory backend resolution is unavailable, preventing false "No active memory plugin is registered" warnings for healthy runtime setups. Fixes #76792. Thanks @som-686. - Memory/status: keep plain `openclaw memory status` and `openclaw memory status --json` on the cheap read-only path by reserving vector and embedding provider probes for `--deep` or `--index`. Fixes #76769. Thanks @daruire. - Telegram: suppress stale same-session replies when a newer accepted message arrives before an older in-flight Telegram dispatch finalizes. Fixes #76642. Thanks @chinar-amrutkar. +- Slack: collapse routine Socket Mode pong-timeout reconnects into one OpenClaw reconnect line and suppress the duplicate Slack SDK pong warning. - Gateway/diagnostics: abort-drain embedded runs after an extended no-progress stall so a single dead session no longer leaves queued Discord/channel turns blocked behind repeated `recovery=none` liveness warnings. - Plugins/ClawHub: accept the live artifact resolver `kind`/`sha256` field names alongside the typed `artifactKind`/`artifactSha256` form so `clawhub:` installs of npm-pack and legacy ZIP packages no longer miss downloadable artifacts. Thanks @romneyda. - Control UI/Sessions: avoid full `sessions.list` reloads for chat-turn `sessions.changed` payloads, so large session stores no longer add multi-second delays while chat responses are being delivered. (#76676) Thanks @VACInc. diff --git a/extensions/slack/src/monitor/provider-support.ts b/extensions/slack/src/monitor/provider-support.ts index ac0d1bc096f..dbecde195f0 100644 --- a/extensions/slack/src/monitor/provider-support.ts +++ b/extensions/slack/src/monitor/provider-support.ts @@ -10,8 +10,13 @@ type SlackSocketModeConfig = Pick< SlackSocketModeReceiverOptions, "clientPingTimeout" | "serverPingTimeout" | "pingPongLoggingEnabled" >; +type SlackSdkLogger = NonNullable; +type SlackSdkLogLevel = ReturnType; const OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS = 15_000; +const SLACK_SOCKET_PONG_TIMEOUT_WARNING_PREFIX = "A pong wasn't received from the server"; +const SLACK_SOCKET_LOG_LEVEL_IGNORED_WARNING_RE = + /^The logLevel given to .+ was ignored as you also gave logger$/; export type SlackBoltResolvedExports = { App: SlackAppConstructor; @@ -133,6 +138,42 @@ export function publishSlackDisconnectedStatus( }); } +function isSlackSocketPongTimeoutWarning(args: readonly unknown[]) { + return ( + typeof args[0] === "string" && args[0].startsWith(SLACK_SOCKET_PONG_TIMEOUT_WARNING_PREFIX) + ); +} + +function isSlackSocketSelfInflictedLoggerWarning(args: readonly unknown[]) { + return typeof args[0] === "string" && SLACK_SOCKET_LOG_LEVEL_IGNORED_WARNING_RE.test(args[0]); +} + +export function createSlackSocketModeLogger( + sink: Pick = console, +): SlackSdkLogger { + let level = "info" as SlackSdkLogLevel; + let name = "socket-mode"; + const prefix = () => `socket-mode:${name}`; + return { + debug: () => {}, + info: () => {}, + warn: (...args: unknown[]) => { + if (isSlackSocketPongTimeoutWarning(args) || isSlackSocketSelfInflictedLoggerWarning(args)) { + return; + } + sink.warn(prefix(), ...args); + }, + error: (...args: unknown[]) => sink.error(prefix(), ...args), + setLevel: (nextLevel) => { + level = nextLevel; + }, + getLevel: () => level, + setName: (nextName) => { + name = nextName; + }, + }; +} + function asRecord(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) @@ -181,6 +222,7 @@ export function createSlackBoltApp(params: { autoReconnectEnabled: false, clientPingTimeout: params.socketMode?.clientPingTimeout ?? OPENCLAW_SLACK_CLIENT_PING_TIMEOUT_MS, + logger: createSlackSocketModeLogger(), installerOptions: { clientOptions: params.clientOptions, }, diff --git a/extensions/slack/src/monitor/provider.interop.test.ts b/extensions/slack/src/monitor/provider.interop.test.ts index 76815a96618..5dd2b5a006a 100644 --- a/extensions/slack/src/monitor/provider.interop.test.ts +++ b/extensions/slack/src/monitor/provider.interop.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { createSlackBoltApp, + createSlackSocketModeLogger, resolveSlackBoltInterop, shouldSkipOpenClawSlackSelfEvent, } from "./provider-support.js"; @@ -159,6 +160,10 @@ describe("createSlackBoltApp", () => { appToken: "xapp-test", autoReconnectEnabled: false, clientPingTimeout: 15_000, + logger: expect.objectContaining({ + error: expect.any(Function), + warn: expect.any(Function), + }), installerOptions: { clientOptions, }, @@ -200,6 +205,10 @@ describe("createSlackBoltApp", () => { clientPingTimeout: 20_000, serverPingTimeout: 45_000, pingPongLoggingEnabled: true, + logger: expect.objectContaining({ + error: expect.any(Function), + warn: expect.any(Function), + }), installerOptions: { clientOptions, }, @@ -264,6 +273,23 @@ describe("createSlackBoltApp", () => { expect(eagerAuthTestCalls).toBe(0); }); + it("suppresses Slack's redundant pong timeout warning while forwarding other SDK warnings", () => { + const warnCalls: unknown[][] = []; + const logger = createSlackSocketModeLogger({ + debug: () => {}, + info: () => {}, + warn: (...args: unknown[]) => warnCalls.push(args), + error: () => {}, + }); + + logger.setName("SlackWebSocket:1"); + logger.warn("A pong wasn't received from the server before the timeout of 15000ms!"); + logger.warn("The logLevel given to Socket Mode was ignored as you also gave logger"); + logger.warn("another socket warning"); + + expect(warnCalls).toEqual([["socket-mode:SlackWebSocket:1", "another socket warning"]]); + }); + it("keeps Bolt self filtering except assistant message_changed events", () => { expect( shouldSkipOpenClawSlackSelfEvent({ diff --git a/extensions/slack/src/monitor/provider.reconnect.test.ts b/extensions/slack/src/monitor/provider.reconnect.test.ts index e6d7f78d42a..3cdaaefc65c 100644 --- a/extensions/slack/src/monitor/provider.reconnect.test.ts +++ b/extensions/slack/src/monitor/provider.reconnect.test.ts @@ -5,6 +5,7 @@ import { publishSlackDisconnectedStatus, startSlackSocketAndWaitForDisconnect, } from "./provider-support.js"; +import { formatSlackSocketReconnectMessage } from "./provider.js"; import { waitForSlackSocketDisconnect } from "./reconnect-policy.js"; class FakeEmitter { @@ -85,6 +86,17 @@ describe("slack socket reconnect helpers", () => { }); }); + it("formats recoverable disconnects as a single reconnect status line", () => { + expect( + formatSlackSocketReconnectMessage({ + event: "disconnect", + attempt: 1, + maxAttempts: 12, + delayMs: 2_340, + }), + ).toBe("slack socket disconnected (disconnect); reconnecting in 2s (attempt 1/12)"); + }); + it("resolves disconnect waiter on socket disconnect event", async () => { const client = new FakeEmitter(); const app = { receiver: { client } }; diff --git a/extensions/slack/src/monitor/provider.ts b/extensions/slack/src/monitor/provider.ts index 57ae70cfa44..46fb13459a5 100644 --- a/extensions/slack/src/monitor/provider.ts +++ b/extensions/slack/src/monitor/provider.ts @@ -85,6 +85,18 @@ async function getSlackBoltInterop(): Promise { const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024; const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 30_000; +export function formatSlackSocketReconnectMessage(params: { + event: string; + attempt: number; + maxAttempts: number; + delayMs: number; + error?: unknown; +}) { + const maxAttempts = params.maxAttempts > 0 ? String(params.maxAttempts) : "∞"; + const suffix = params.error ? ` (${formatUnknownError(params.error)})` : ""; + return `slack socket disconnected (${params.event}); reconnecting in ${Math.round(params.delayMs / 1000)}s (attempt ${params.attempt}/${maxAttempts})${suffix}`; +} + function parseApiAppIdFromAppToken(raw?: string) { const token = raw?.trim(); if (!token) { @@ -443,6 +455,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { try { if (slackMode === "socket") { let reconnectAttempts = 0; + let hasLoggedSocketConnected = false; while (!opts.abortSignal?.aborted) { try { const disconnect = await startSlackSocketAndWaitForDisconnect({ @@ -451,7 +464,10 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { onStarted: () => { reconnectAttempts = 0; publishSlackConnectedStatus(opts.setStatus); - runtime.log?.("slack socket mode connected"); + if (!hasLoggedSocketConnected) { + hasLoggedSocketConnected = true; + runtime.log?.("slack socket mode connected"); + } }, }); if (!disconnect) { @@ -483,10 +499,16 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { } const delayMs = computeBackoff(SLACK_SOCKET_RECONNECT_POLICY, reconnectAttempts); - runtime.error?.( - `slack socket disconnected (${disconnect.event}). retry ${reconnectAttempts}/${SLACK_SOCKET_RECONNECT_POLICY.maxAttempts || "∞"} in ${Math.round(delayMs / 1000)}s${ - disconnect.error ? ` (${formatUnknownError(disconnect.error)})` : "" - }`, + runtime.log?.( + warn( + formatSlackSocketReconnectMessage({ + event: disconnect.event, + attempt: reconnectAttempts, + maxAttempts: SLACK_SOCKET_RECONNECT_POLICY.maxAttempts, + delayMs, + error: disconnect.error, + }), + ), ); await gracefulStop(); try {