From 7ef7b94bc0df0d0ba9e11caf9a7e689673cb293d Mon Sep 17 00:00:00 2001 From: Basit Mustafa Date: Sun, 18 Jan 2026 20:20:55 -0700 Subject: [PATCH 1/5] fix(slack): handle bolt import for CJS/ESM compatibility --- src/slack/monitor/provider.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/slack/monitor/provider.ts b/src/slack/monitor/provider.ts index ae024a024..f793ac6f8 100644 --- a/src/slack/monitor/provider.ts +++ b/src/slack/monitor/provider.ts @@ -30,8 +30,12 @@ const slackBoltModule = SlackBolt as typeof import("@slack/bolt") & { default?: typeof import("@slack/bolt"); }; // Bun allows named imports from CJS; Node ESM doesn't. Use default+fallback for compatibility. -const slackBolt = slackBoltModule.default ?? slackBoltModule; -const { App, HTTPReceiver } = slackBolt; +const slackBolt = slackBoltModule.default || slackBoltModule; +const App = slackBolt.App || (slackBolt.default && slackBolt.default.App) || slackBoltModule.App; +const HTTPReceiver = + slackBolt.HTTPReceiver || + (slackBolt.default && slackBolt.default.HTTPReceiver) || + slackBoltModule.HTTPReceiver; function parseApiAppIdFromAppToken(raw?: string) { const token = raw?.trim(); if (!token) return undefined; From 4ed1b7c7ed5d872aa223908b53e6a1e3baa27ee5 Mon Sep 17 00:00:00 2001 From: Basit Mustafa Date: Sun, 18 Jan 2026 20:45:11 -0700 Subject: [PATCH 2/5] fix(slack): resolve bolt constructors --- src/slack/monitor/provider.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/slack/monitor/provider.ts b/src/slack/monitor/provider.ts index f793ac6f8..2ff3d9f18 100644 --- a/src/slack/monitor/provider.ts +++ b/src/slack/monitor/provider.ts @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import SlackBolt from "@slack/bolt"; +import SlackBoltDefault, * as SlackBoltModule from "@slack/bolt"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../auto-reply/reply/history.js"; @@ -26,16 +26,23 @@ import { normalizeAllowList } from "./allow-list.js"; import type { MonitorSlackOpts } from "./types.js"; -const slackBoltModule = SlackBolt as typeof import("@slack/bolt") & { - default?: typeof import("@slack/bolt"); -}; -// Bun allows named imports from CJS; Node ESM doesn't. Use default+fallback for compatibility. -const slackBolt = slackBoltModule.default || slackBoltModule; -const App = slackBolt.App || (slackBolt.default && slackBolt.default.App) || slackBoltModule.App; -const HTTPReceiver = - slackBolt.HTTPReceiver || - (slackBolt.default && slackBolt.default.HTTPReceiver) || - slackBoltModule.HTTPReceiver; +type SlackBoltNamespace = typeof import("@slack/bolt"); + +const slackBoltDefault = SlackBoltDefault as unknown; +const slackBoltNamespace = + (typeof slackBoltDefault === "object" && slackBoltDefault + ? ("default" in slackBoltDefault + ? (slackBoltDefault as { default?: unknown }).default + : slackBoltDefault) + : undefined) as SlackBoltNamespace | undefined; +// Bun allows named imports from CJS; Node ESM doesn't. Resolve default/module shapes for compatibility. +const App = + ((typeof slackBoltDefault === "function" + ? slackBoltDefault + : slackBoltNamespace?.App) ?? + SlackBoltModule.App) as SlackBoltNamespace["App"]; +const HTTPReceiver = (slackBoltNamespace?.HTTPReceiver ?? + SlackBoltModule.HTTPReceiver) as SlackBoltNamespace["HTTPReceiver"]; function parseApiAppIdFromAppToken(raw?: string) { const token = raw?.trim(); if (!token) return undefined; From a6db1edee33359b85723165ce763ed20cb1889a9 Mon Sep 17 00:00:00 2001 From: Basit Mustafa Date: Sun, 18 Jan 2026 20:58:50 -0700 Subject: [PATCH 3/5] test(slack): mock HTTPReceiver --- ...ool-result.forces-thread-replies-replytoid-is-set.test.ts | 5 ++++- ...r.tool-result.sends-tool-summaries-responseprefix.test.ts | 5 ++++- ...sult.threads-top-level-replies-replytomode-is-all.test.ts | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts index 8912a4821..5ecb206f0 100644 --- a/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts +++ b/src/slack/monitor.tool-result.forces-thread-replies-replytoid-is-set.test.ts @@ -95,7 +95,10 @@ vi.mock("@slack/bolt", () => { start = vi.fn().mockResolvedValue(undefined); stop = vi.fn().mockResolvedValue(undefined); } - return { App, default: { App } }; + class HTTPReceiver { + requestListener = vi.fn(); + } + return { App, HTTPReceiver, default: { App, HTTPReceiver } }; }); const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index 70527bfd5..67f72e2e1 100644 --- a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -97,7 +97,10 @@ vi.mock("@slack/bolt", () => { start = vi.fn().mockResolvedValue(undefined); stop = vi.fn().mockResolvedValue(undefined); } - return { App, default: { App } }; + class HTTPReceiver { + requestListener = vi.fn(); + } + return { App, HTTPReceiver, default: { App, HTTPReceiver } }; }); const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts b/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts index 35bcff5fe..609d576fd 100644 --- a/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts +++ b/src/slack/monitor.tool-result.threads-top-level-replies-replytomode-is-all.test.ts @@ -95,7 +95,10 @@ vi.mock("@slack/bolt", () => { start = vi.fn().mockResolvedValue(undefined); stop = vi.fn().mockResolvedValue(undefined); } - return { App, default: { App } }; + class HTTPReceiver { + requestListener = vi.fn(); + } + return { App, HTTPReceiver, default: { App, HTTPReceiver } }; }); const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); From 115b4379bf966e2ee7d66dd396916a42e91e2a06 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 20 Jan 2026 10:31:44 +0000 Subject: [PATCH 4/5] fix: handle Slack Bolt import + gateway node ids (#1208) (thanks @24601) --- CHANGELOG.md | 1 + src/gateway/protocol/schema/frames.ts | 18 +++++++++------- src/gateway/server.cron.test.ts | 2 ++ src/slack/monitor/provider.ts | 30 +++++++++++++++++---------- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b5ec756b..853d39fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.clawd.bot - CLI: avoid duplicating --profile/--dev flags when formatting commands. - Auth: dedupe codex-cli profiles when tokens match custom openai-codex entries. (#1264) — thanks @odrobnik. - Agents: avoid misclassifying context-window-too-small errors as context overflow. (#1266) — thanks @humanwritten. +- Slack: resolve Bolt default-export shapes for monitor startup. (#1208) — thanks @24601. ## 2026.1.19-3 diff --git a/src/gateway/protocol/schema/frames.ts b/src/gateway/protocol/schema/frames.ts index dfaac1c26..f9cae661c 100644 --- a/src/gateway/protocol/schema/frames.ts +++ b/src/gateway/protocol/schema/frames.ts @@ -39,14 +39,16 @@ export const ConnectParamsSchema = Type.Object( permissions: Type.Optional(Type.Record(NonEmptyString, Type.Boolean())), role: Type.Optional(NonEmptyString), scopes: Type.Optional(Type.Array(NonEmptyString)), - device: Type.Object( - { - id: NonEmptyString, - publicKey: NonEmptyString, - signature: NonEmptyString, - signedAt: Type.Integer({ minimum: 0 }), - }, - { additionalProperties: false }, + device: Type.Optional( + Type.Object( + { + id: NonEmptyString, + publicKey: NonEmptyString, + signature: NonEmptyString, + signedAt: Type.Integer({ minimum: 0 }), + }, + { additionalProperties: false }, + ), ), auth: Type.Optional( Type.Object( diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index ca701e67f..48269cca7 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -185,6 +185,7 @@ describe("gateway server cron", () => { test("accepts jobId for cron.update", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-cron-")); testState.cronStorePath = path.join(dir, "cron", "jobs.json"); + testState.cronEnabled = false; await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true }); await fs.writeFile(testState.cronStorePath, JSON.stringify({ version: 1, jobs: [] })); @@ -218,6 +219,7 @@ describe("gateway server cron", () => { await server.close(); await rmTempDir(dir); testState.cronStorePath = undefined; + testState.cronEnabled = undefined; }); test("disables cron jobs via enabled:false patches", async () => { diff --git a/src/slack/monitor/provider.ts b/src/slack/monitor/provider.ts index 2ff3d9f18..3cdb0f2de 100644 --- a/src/slack/monitor/provider.ts +++ b/src/slack/monitor/provider.ts @@ -27,20 +27,21 @@ import { normalizeAllowList } from "./allow-list.js"; import type { MonitorSlackOpts } from "./types.js"; type SlackBoltNamespace = typeof import("@slack/bolt"); +type SlackBoltDefault = SlackBoltNamespace | SlackBoltNamespace["App"]; -const slackBoltDefault = SlackBoltDefault as unknown; +const slackBoltDefaultImport = SlackBoltDefault as SlackBoltDefault | undefined; +const slackBoltModuleDefault = (SlackBoltModule as { default?: SlackBoltDefault }).default; +const slackBoltDefault = slackBoltDefaultImport ?? slackBoltModuleDefault; const slackBoltNamespace = - (typeof slackBoltDefault === "object" && slackBoltDefault - ? ("default" in slackBoltDefault - ? (slackBoltDefault as { default?: unknown }).default - : slackBoltDefault) - : undefined) as SlackBoltNamespace | undefined; + typeof slackBoltDefault === "object" && slackBoltDefault + ? (slackBoltDefault as SlackBoltNamespace) + : typeof slackBoltModuleDefault === "object" && slackBoltModuleDefault + ? (slackBoltModuleDefault as SlackBoltNamespace) + : undefined; // Bun allows named imports from CJS; Node ESM doesn't. Resolve default/module shapes for compatibility. -const App = - ((typeof slackBoltDefault === "function" - ? slackBoltDefault - : slackBoltNamespace?.App) ?? - SlackBoltModule.App) as SlackBoltNamespace["App"]; +const App = ((typeof slackBoltDefault === "function" ? slackBoltDefault : undefined) ?? + slackBoltNamespace?.App ?? + SlackBoltModule.App) as SlackBoltNamespace["App"]; const HTTPReceiver = (slackBoltNamespace?.HTTPReceiver ?? SlackBoltModule.HTTPReceiver) as SlackBoltNamespace["HTTPReceiver"]; function parseApiAppIdFromAppToken(raw?: string) { @@ -132,6 +133,13 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const mediaMaxBytes = (opts.mediaMaxMb ?? slackCfg.mediaMaxMb ?? 20) * 1024 * 1024; const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; + if (!App) { + throw new Error("Slack Bolt App export missing; check @slack/bolt installation."); + } + if (slackMode === "http" && !HTTPReceiver) { + throw new Error("Slack Bolt HTTPReceiver export missing; check @slack/bolt installation."); + } + const receiver = slackMode === "http" ? new HTTPReceiver({ From cf04b0e3bf83b84d11790945153c5bba3c5a099e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 20 Jan 2026 10:45:59 +0000 Subject: [PATCH 5/5] fix: align gateway presence + config defaults tests (#1208) (thanks @24601) --- src/cli/devices-cli.ts | 2 +- src/config/config.identity-defaults.test.ts | 7 +++++-- src/gateway/server.auth.test.ts | 2 +- src/gateway/server/ws-connection/message-handler.ts | 2 +- src/infra/device-pairing.ts | 3 ++- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cli/devices-cli.ts b/src/cli/devices-cli.ts index e7290fe47..d30f2967d 100644 --- a/src/cli/devices-cli.ts +++ b/src/cli/devices-cli.ts @@ -2,7 +2,7 @@ import type { Command } from "commander"; import { callGateway } from "../gateway/call.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; -import { defaultRuntime } from "./runtime.js"; +import { defaultRuntime } from "../runtime.js"; import { withProgress } from "./progress.js"; type DevicesRpcOpts = { diff --git a/src/config/config.identity-defaults.test.ts b/src/config/config.identity-defaults.test.ts index 2792ca229..829264a29 100644 --- a/src/config/config.identity-defaults.test.ts +++ b/src/config/config.identity-defaults.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; import { withTempHome } from "./test-helpers.js"; describe("config identity defaults", () => { @@ -284,7 +285,7 @@ describe("config identity defaults", () => { }); }); - it("does not synthesize agent/session when absent", async () => { + it("does not synthesize agent list/session when absent", async () => { await withTempHome(async (home) => { const configDir = path.join(home, ".clawdbot"); await fs.mkdir(configDir, { recursive: true }); @@ -306,7 +307,9 @@ describe("config identity defaults", () => { expect(cfg.messages?.responsePrefix).toBeUndefined(); expect(cfg.messages?.groupChat?.mentionPatterns).toBeUndefined(); - expect(cfg.agents).toBeUndefined(); + expect(cfg.agents?.list).toBeUndefined(); + expect(cfg.agents?.defaults?.maxConcurrent).toBe(DEFAULT_AGENT_MAX_CONCURRENT); + expect(cfg.agents?.defaults?.subagents?.maxConcurrent).toBe(DEFAULT_SUBAGENT_MAX_CONCURRENT); expect(cfg.session).toBeUndefined(); }); }); diff --git a/src/gateway/server.auth.test.ts b/src/gateway/server.auth.test.ts index 3092a88e3..f48043240 100644 --- a/src/gateway/server.auth.test.ts +++ b/src/gateway/server.auth.test.ts @@ -73,7 +73,7 @@ describe("gateway server auth/connect", () => { }); test("rejects invalid token", async () => { - const { server, ws, port, prevToken } = await startServerWithClient("secret"); + const { server, ws, prevToken } = await startServerWithClient("secret"); const res = await connectReq(ws, { token: "wrong" }); expect(res.ok).toBe(false); expect(res.error?.message ?? "").toContain("unauthorized"); diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 485577bb3..975e344ce 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -551,7 +551,7 @@ export function attachGatewayWsMessageHandler(params: { deviceFamily: connectParams.client.deviceFamily, modelIdentifier: connectParams.client.modelIdentifier, mode: connectParams.client.mode, - instanceId: instanceId ?? connectParams.device?.id, + instanceId: connectParams.device?.id ?? instanceId, reason: "connect", }); incrementPresenceVersion(); diff --git a/src/infra/device-pairing.ts b/src/infra/device-pairing.ts index c5594df34..9f161fea1 100644 --- a/src/infra/device-pairing.ts +++ b/src/infra/device-pairing.ts @@ -399,7 +399,8 @@ export async function verifyDeviceToken(params: { return { ok: false, reason: "scope-mismatch" }; } entry.lastUsedAtMs = Date.now(); - device.tokens = { ...(device.tokens ?? {}), [role]: entry }; + device.tokens ??= {}; + device.tokens[role] = entry; state.pairedByDeviceId[device.deviceId] = device; await persistState(state, params.baseDir); return { ok: true };