Skip to content

Commit 3aad6c8

Browse files
fix(slack): guard Socket Mode listeners access during startup (openclaw#28702) thanks @Glucksberg
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: Glucksberg <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent b3f60a6 commit 3aad6c8

File tree

3 files changed

+85
-2
lines changed

3 files changed

+85
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Docs: https://docs.openclaw.ai
9393
- File tools/tilde paths: expand `~/...` against the user home directory before workspace-root checks in host file read/write/edit paths, while preserving root-boundary enforcement so outside-root targets remain blocked. (#29779) Thanks @Glucksberg.
9494
- Slack/HTTP mode startup: treat Slack HTTP accounts as configured when `botToken` + `signingSecret` are present (without requiring `appToken`) in channel config/runtime status so webhook mode is not silently skipped. (#30567)
9595
- Slack/Usage footer formatting: wrap session keys in inline code in full response-usage footers so Slack does not parse colon-delimited session segments as emoji shortcodes. (#30258) Thanks @pushkarsingh32.
96+
- Slack/Socket Mode slash startup: treat `app.options()` registration as best-effort and fall back to static arg menus when listener registration fails, preventing Slack monitor startup crash loops on receiver init edge cases. (#21715)
9697
- Onboarding/Custom providers: raise default custom-provider model context window to the runtime hard minimum (16k) and auto-heal existing custom model entries below that threshold during reconfiguration, preventing immediate `Model context window too small (4096 tokens)` failures. (#21653) Thanks @r4jiv007.
9798
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
9899
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.

src/slack/monitor/slash.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,78 @@ describe("Slack native command argument menus", () => {
435435
expect(testHarness.optionsReceiverContexts[0]).toBe(testHarness.app);
436436
});
437437

438+
it("falls back to static menus when app.options() throws during registration", async () => {
439+
const commands = new Map<string, (args: unknown) => Promise<void>>();
440+
const actions = new Map<string, (args: unknown) => Promise<void>>();
441+
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
442+
const app = {
443+
client: { chat: { postEphemeral } },
444+
command: (name: string, handler: (args: unknown) => Promise<void>) => {
445+
commands.set(name, handler);
446+
},
447+
action: (id: string, handler: (args: unknown) => Promise<void>) => {
448+
actions.set(id, handler);
449+
},
450+
// Simulate Bolt throwing during options registration (e.g. receiver not initialized)
451+
options: () => {
452+
throw new Error("Cannot read properties of undefined (reading 'listeners')");
453+
},
454+
};
455+
const ctx = {
456+
cfg: { commands: { native: true, nativeSkills: false } },
457+
runtime: {},
458+
botToken: "bot-token",
459+
botUserId: "bot",
460+
teamId: "T1",
461+
allowFrom: ["*"],
462+
dmEnabled: true,
463+
dmPolicy: "open",
464+
groupDmEnabled: false,
465+
groupDmChannels: [],
466+
defaultRequireMention: true,
467+
groupPolicy: "open",
468+
useAccessGroups: false,
469+
channelsConfig: undefined,
470+
slashCommand: {
471+
enabled: true,
472+
name: "openclaw",
473+
ephemeral: true,
474+
sessionPrefix: "slack:slash",
475+
},
476+
textLimit: 4000,
477+
app,
478+
isChannelAllowed: () => true,
479+
resolveChannelName: async () => ({ name: "dm", type: "im" }),
480+
resolveUserName: async () => ({ name: "Ada" }),
481+
} as unknown;
482+
const account = {
483+
accountId: "acct",
484+
config: { commands: { native: true, nativeSkills: false } },
485+
} as unknown;
486+
487+
// Registration should not throw despite app.options() throwing
488+
await registerCommands(ctx, account);
489+
expect(commands.size).toBeGreaterThan(0);
490+
expect(actions.has("openclaw_cmdarg")).toBe(true);
491+
492+
// The /reportexternal command (140 choices) should fall back to static_select
493+
// instead of external_select since options registration failed
494+
const handler = commands.get("/reportexternal");
495+
expect(handler).toBeDefined();
496+
const respond = vi.fn().mockResolvedValue(undefined);
497+
const ack = vi.fn().mockResolvedValue(undefined);
498+
await handler!({
499+
command: createSlashCommand(),
500+
ack,
501+
respond,
502+
});
503+
expect(respond).toHaveBeenCalledTimes(1);
504+
const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> };
505+
const actionsBlock = findFirstActionsBlock(payload);
506+
// Should be static_select (fallback) not external_select
507+
expect(actionsBlock?.elements?.[0]?.type).toBe("static_select");
508+
});
509+
438510
it("shows a button menu when required args are omitted", async () => {
439511
const { respond } = await runCommandHandler(usageHandler);
440512
const actions = expectArgMenuLayout(respond);

src/slack/monitor/slash.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ export async function registerSlackMonitorSlashCommands(params: {
274274

275275
const supportsInteractiveArgMenus =
276276
typeof (ctx.app as { action?: unknown }).action === "function";
277-
const supportsExternalArgMenus = typeof (ctx.app as { options?: unknown }).options === "function";
277+
let supportsExternalArgMenus = typeof (ctx.app as { options?: unknown }).options === "function";
278278

279279
const slashCommand = resolveSlackSlashCommandConfig(
280280
ctx.slashCommand ?? account.config.slashCommand,
@@ -758,7 +758,17 @@ export async function registerSlackMonitorSlashCommands(params: {
758758
await ack({ options });
759759
});
760760
};
761-
registerArgOptions();
761+
// Treat external arg-menu registration as best-effort: if Bolt's app.options()
762+
// throws (e.g. from receiver init issues), disable external selects and fall back
763+
// to static_select/button menus instead of crashing the entire provider startup.
764+
try {
765+
registerArgOptions();
766+
} catch (err) {
767+
supportsExternalArgMenus = false;
768+
logVerbose(
769+
`slack: external arg-menu registration failed, falling back to static menus: ${String(err)}`,
770+
);
771+
}
762772

763773
const registerArgAction = (actionId: string) => {
764774
(

0 commit comments

Comments
 (0)