Skip to content

Commit 6b52502

Browse files
committed
fix: polish Slack thread starter context (#68594)
1 parent 5cc4426 commit 6b52502

17 files changed

Lines changed: 109 additions & 52 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
6969
- Gateway/restart: keep stale-gateway cleanup from terminating the current process's parent or ancestors, so plugin sidecars like WeChat no longer kill the active gateway and trigger an infinite supervisor restart loop. Fixes #68451. (#68517) Thanks @openperf.
7070
- Gateway/auth: reject gateway auth credentials that match published example placeholders at startup and secret reload, and keep cloud install snippets from publishing copy-paste gateway/keyring secrets. (#68404) Thanks @coygeek.
7171
- CLI/update: preserve macOS restart helper launchctl failures in the update restart log without letting log setup block the restart path. (#68492) Thanks @hclsys.
72+
- Slack/threads: keep file-only root messages as starter context so first thread replies can still hydrate starter media. (#68594) Thanks @martingarramon.
7273

7374
## 2026.4.15
7475

extensions/slack/src/monitor/context.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
} from "openclaw/plugin-sdk/config-runtime";
77
import type { SessionScope } from "openclaw/plugin-sdk/config-runtime";
88
import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/config-runtime";
9+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
910
import { createDedupeCache } from "openclaw/plugin-sdk/infra-runtime";
1011
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
1112
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
@@ -282,7 +283,9 @@ export function createSlackMonitorContext(params: {
282283
status: p.status,
283284
});
284285
} catch (err) {
285-
logVerbose(`slack status update failed for channel ${p.channelId}: ${String(err)}`);
286+
logVerbose(
287+
`slack status update failed for channel ${p.channelId}: ${formatErrorMessage(err)}`,
288+
);
286289
}
287290
};
288291

extensions/slack/src/monitor/dm-auth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { formatAllowlistMatchMeta } from "openclaw/plugin-sdk/allow-from";
22
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
3+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
34
import { resolveSlackAllowListMatch } from "./allow-list.js";
45
import type { SlackMonitorContext } from "./context.js";
56
import { upsertChannelPairingRequest } from "./conversation.runtime.js";
@@ -57,7 +58,7 @@ export async function authorizeSlackDirectMessage(params: {
5758
);
5859
},
5960
onReplyError: (err) => {
60-
params.log(`slack pairing reply failed for ${params.senderId}: ${String(err)}`);
61+
params.log(`slack pairing reply failed for ${params.senderId}: ${formatErrorMessage(err)}`);
6162
},
6263
});
6364
return false;

extensions/slack/src/monitor/events/channels.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
22
import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
33
import { loadConfig, writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
4+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
45
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
56
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
67
import { migrateSlackChannelConfig } from "../../channel-migration.js";
@@ -61,7 +62,9 @@ export function registerSlackChannelEvents(params: {
6162
const channelName = payload.channel?.name;
6263
enqueueChannelSystemEvent({ kind: "created", channelId, channelName });
6364
} catch (err) {
64-
ctx.runtime.error?.(danger(`slack channel created handler failed: ${String(err)}`));
65+
ctx.runtime.error?.(
66+
danger(`slack channel created handler failed: ${formatErrorMessage(err)}`),
67+
);
6568
}
6669
},
6770
);
@@ -80,7 +83,9 @@ export function registerSlackChannelEvents(params: {
8083
const channelName = payload.channel?.name_normalized ?? payload.channel?.name;
8184
enqueueChannelSystemEvent({ kind: "renamed", channelId, channelName });
8285
} catch (err) {
83-
ctx.runtime.error?.(danger(`slack channel rename handler failed: ${String(err)}`));
86+
ctx.runtime.error?.(
87+
danger(`slack channel rename handler failed: ${formatErrorMessage(err)}`),
88+
);
8489
}
8590
},
8691
);
@@ -155,7 +160,9 @@ export function registerSlackChannelEvents(params: {
155160
);
156161
}
157162
} catch (err) {
158-
ctx.runtime.error?.(danger(`slack channel_id_changed handler failed: ${String(err)}`));
163+
ctx.runtime.error?.(
164+
danger(`slack channel_id_changed handler failed: ${formatErrorMessage(err)}`),
165+
);
159166
}
160167
},
161168
);

extensions/slack/src/monitor/events/members.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
2+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
23
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
34
import { danger } from "openclaw/plugin-sdk/runtime-env";
45
import type { SlackMonitorContext } from "../context.js";
@@ -42,7 +43,9 @@ export function registerSlackMemberEvents(params: {
4243
contextKey: `slack:member:${params.verb}:${channelId ?? "unknown"}:${payload.user ?? "unknown"}`,
4344
});
4445
} catch (err) {
45-
ctx.runtime.error?.(danger(`slack ${params.verb} handler failed: ${String(err)}`));
46+
ctx.runtime.error?.(
47+
danger(`slack ${params.verb} handler failed: ${formatErrorMessage(err)}`),
48+
);
4649
}
4750
};
4851

extensions/slack/src/monitor/events/messages.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
2+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
23
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
34
import { danger } from "openclaw/plugin-sdk/runtime-env";
45
import type { SlackAppMentionEvent, SlackMessageEvent } from "../../types.js";
@@ -43,7 +44,7 @@ export function registerSlackMessageEvents(params: {
4344

4445
await handleSlackMessage(message, { source: "message" });
4546
} catch (err) {
46-
ctx.runtime.error?.(danger(`slack handler failed: ${String(err)}`));
47+
ctx.runtime.error?.(danger(`slack handler failed: ${formatErrorMessage(err)}`));
4748
}
4849
};
4950

@@ -77,7 +78,7 @@ export function registerSlackMessageEvents(params: {
7778
wasMentioned: true,
7879
});
7980
} catch (err) {
80-
ctx.runtime.error?.(danger(`slack mention handler failed: ${String(err)}`));
81+
ctx.runtime.error?.(danger(`slack mention handler failed: ${formatErrorMessage(err)}`));
8182
}
8283
});
8384
}

extensions/slack/src/monitor/events/pins.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
2+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
23
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
34
import { danger } from "openclaw/plugin-sdk/runtime-env";
45
import type { SlackMonitorContext } from "../context.js";
@@ -45,7 +46,7 @@ async function handleSlackPinEvent(params: {
4546
},
4647
);
4748
} catch (err) {
48-
ctx.runtime.error?.(danger(`slack ${errorLabel} handler failed: ${String(err)}`));
49+
ctx.runtime.error?.(danger(`slack ${errorLabel} handler failed: ${formatErrorMessage(err)}`));
4950
}
5051
}
5152

extensions/slack/src/monitor/events/reactions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
2+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
23
import { enqueueSystemEvent } from "openclaw/plugin-sdk/infra-runtime";
34
import { danger } from "openclaw/plugin-sdk/runtime-env";
45
import type { SlackMonitorContext } from "../context.js";
@@ -46,7 +47,7 @@ export function registerSlackReactionEvents(params: {
4647
contextKey: `slack:reaction:${action}:${item.channel}:${item.ts}:${event.user}:${emojiLabel}`,
4748
});
4849
} catch (err) {
49-
ctx.runtime.error?.(danger(`slack reaction handler failed: ${String(err)}`));
50+
ctx.runtime.error?.(danger(`slack reaction handler failed: ${formatErrorMessage(err)}`));
5051
}
5152
};
5253

extensions/slack/src/monitor/media.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ describe("resolveSlackThreadStarter", () => {
905905
expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
906906
});
907907

908-
it("returns null when the starter message has no text and no files", async () => {
908+
it("returns null when the starter message has no text or files", async () => {
909909
const replies = vi.fn().mockResolvedValueOnce({ messages: [{ text: " ", user: "U1" }] });
910910
const client = {
911911
conversations: { replies },
@@ -921,6 +921,37 @@ describe("resolveSlackThreadStarter", () => {
921921
expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
922922
});
923923

924+
it("returns a placeholder starter when the root message only has files", async () => {
925+
const replies = vi.fn().mockResolvedValueOnce({
926+
messages: [
927+
{
928+
text: " ",
929+
user: "U1",
930+
ts: "1.000",
931+
files: [{ name: "root.png", mimetype: "image/png" }],
932+
},
933+
],
934+
});
935+
const client = {
936+
conversations: { replies },
937+
} as unknown as Parameters<typeof resolveSlackThreadStarter>[0]["client"];
938+
939+
const result = await resolveSlackThreadStarter({
940+
channelId: "C1",
941+
threadTs: "1.000",
942+
client,
943+
});
944+
945+
expect(result).toEqual({
946+
text: "[attached: root.png]",
947+
userId: "U1",
948+
botId: undefined,
949+
ts: "1.000",
950+
files: [{ name: "root.png", mimetype: "image/png" }],
951+
});
952+
expect(vi.mocked(logVerbose)).not.toHaveBeenCalled();
953+
});
954+
924955
it("returns null and surfaces the error via logVerbose when Slack API throws", async () => {
925956
const replies = vi.fn().mockRejectedValueOnce(new Error("not_in_channel"));
926957
const client = {
@@ -942,7 +973,7 @@ describe("resolveSlackThreadStarter", () => {
942973
expect(vi.mocked(logVerbose)).toHaveBeenCalledWith(expect.stringContaining("ts=9.999"));
943974
});
944975

945-
it("surfaces non-Error thrown values as String(err) via logVerbose", async () => {
976+
it("surfaces non-Error thrown values via logVerbose", async () => {
946977
const replies = vi.fn().mockRejectedValueOnce("rate_limited");
947978
const client = {
948979
conversations: { replies },

extensions/slack/src/monitor/media.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { WebClient as SlackWebClient } from "@slack/web-api";
2+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
23
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
34
import { normalizeHostname } from "openclaw/plugin-sdk/host-runtime";
45
import { fetchWithRuntimeDispatcher } from "openclaw/plugin-sdk/infra-runtime";
@@ -385,18 +386,11 @@ function evictThreadStarterCache(): void {
385386
THREAD_STARTER_CACHE.delete(cacheKey);
386387
}
387388
}
388-
if (THREAD_STARTER_CACHE.size <= THREAD_STARTER_CACHE_MAX) {
389-
return;
390-
}
391-
const excess = THREAD_STARTER_CACHE.size - THREAD_STARTER_CACHE_MAX;
392-
let removed = 0;
393-
for (const cacheKey of THREAD_STARTER_CACHE.keys()) {
394-
THREAD_STARTER_CACHE.delete(cacheKey);
395-
removed += 1;
396-
if (removed >= excess) {
397-
break;
398-
}
399-
}
389+
pruneMapToMaxSize(THREAD_STARTER_CACHE, THREAD_STARTER_CACHE_MAX);
390+
}
391+
392+
function formatSlackFilePlaceholder(files: SlackFile[] | undefined): string {
393+
return `[attached: ${files?.map((file) => file.name ?? "file").join(", ") ?? "file"}]`;
400394
}
401395

402396
export async function resolveSlackThreadStarter(params: {
@@ -430,15 +424,16 @@ export async function resolveSlackThreadStarter(params: {
430424
};
431425
const message = response?.messages?.[0];
432426
const text = (message?.text ?? "").trim();
433-
if (!message || !text) {
427+
const files = message?.files?.length ? message.files : undefined;
428+
if (!message || (!text && !files)) {
434429
return null;
435430
}
436431
const starter: SlackThreadStarter = {
437-
text,
432+
text: text || formatSlackFilePlaceholder(files),
438433
userId: message.user,
439434
botId: message.bot_id,
440435
ts: message.ts,
441-
files: message.files,
436+
files,
442437
};
443438
if (THREAD_STARTER_CACHE.has(cacheKey)) {
444439
THREAD_STARTER_CACHE.delete(cacheKey);
@@ -536,9 +531,7 @@ export async function resolveSlackThreadHistory(params: {
536531

537532
return retained.map((msg) => ({
538533
// For file-only messages, create a placeholder showing attached filenames
539-
text: msg.text?.trim()
540-
? msg.text
541-
: `[attached: ${msg.files?.map((f) => f.name ?? "file").join(", ")}]`,
534+
text: msg.text?.trim() ? msg.text : formatSlackFilePlaceholder(msg.files),
542535
userId: msg.user,
543536
botId: msg.bot_id,
544537
ts: msg.ts,

0 commit comments

Comments
 (0)