Skip to content

Commit b3f60a6

Browse files
hou-rongTakhoffman
andauthored
fix(slack): thread agent identity through channel reply path (openclaw#27134) thanks @hou-rong
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: hou-rong <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent 4ba0a4d commit b3f60a6

File tree

4 files changed

+73
-1
lines changed

4 files changed

+73
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ Docs: https://docs.openclaw.ai
232232

233233
### Fixes
234234

235+
- Slack/Identity: thread agent outbound identity (`chat:write.customize` overrides) through the channel reply delivery path so per-agent username, icon URL, and icon emoji are applied to all Slack replies including media messages. (#27134) Thanks @hou-rong.
235236
- Agents/Subagents delivery: refactor subagent completion announce dispatch into an explicit queue/direct/fallback state machine, recover outbound channel-plugin resolution in cold/stale plugin-registry states across announce/message/gateway send paths, finalize cleanup bookkeeping when announce flow rejects, and treat Telegram sends without `message_id` as delivery failures (instead of false-success `"unknown"` IDs). (#26867, #25961, #26803, #25069, #26741) Thanks @SmithLabsLLC and @docaohieu2808.
236237
- Telegram/Webhook: pre-initialize webhook bots, switch webhook processing to callback-mode JSON handling, and preserve full near-limit payload reads under delayed handlers to prevent webhook request hangs and dropped updates. (#26156)
237238
- Slack/Session threads: prevent oversized parent-session inheritance from silently bricking new thread sessions, surface embedded context-overflow empty-result failures to users, and add configurable `session.parentForkMaxTokens` (default `100000`, `0` disables). (#26912) Thanks @markshields-tl.

src/slack/monitor/message-handler/dispatch.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createReplyPrefixOptions } from "../../../channels/reply-prefix.js";
99
import { createTypingCallbacks } from "../../../channels/typing.js";
1010
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
1111
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
12+
import { resolveAgentOutboundIdentity } from "../../../infra/outbound/identity.js";
1213
import { removeSlackReaction } from "../../actions.js";
1314
import { createSlackDraftStream } from "../../draft-stream.js";
1415
import {
@@ -70,6 +71,16 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
7071
const cfg = ctx.cfg;
7172
const runtime = ctx.runtime;
7273

74+
// Resolve agent identity for Slack chat:write.customize overrides.
75+
const outboundIdentity = resolveAgentOutboundIdentity(cfg, route.agentId);
76+
const slackIdentity = outboundIdentity
77+
? {
78+
username: outboundIdentity.name,
79+
iconUrl: outboundIdentity.avatarUrl,
80+
iconEmoji: outboundIdentity.emoji,
81+
}
82+
: undefined;
83+
7384
if (prepared.isDirectMessage) {
7485
const sessionCfg = cfg.session;
7586
const storePath = resolveStorePath(sessionCfg?.store, {
@@ -190,6 +201,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
190201
textLimit: ctx.textLimit,
191202
replyThreadTs,
192203
replyToMode: ctx.replyToMode,
204+
...(slackIdentity ? { identity: slackIdentity } : {}),
193205
});
194206
replyPlan.markSent();
195207
};

src/slack/monitor/replies.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const sendMock = vi.fn();
4+
vi.mock("../send.js", () => ({
5+
sendMessageSlack: (...args: unknown[]) => sendMock(...args),
6+
}));
7+
8+
import { deliverReplies } from "./replies.js";
9+
10+
function baseParams(overrides?: Record<string, unknown>) {
11+
return {
12+
replies: [{ text: "hello" }],
13+
target: "C123",
14+
token: "xoxb-test",
15+
runtime: { log: () => {}, error: () => {}, exit: () => {} },
16+
textLimit: 4000,
17+
replyToMode: "off" as const,
18+
...overrides,
19+
};
20+
}
21+
22+
describe("deliverReplies identity passthrough", () => {
23+
beforeEach(() => {
24+
sendMock.mockReset();
25+
});
26+
it("passes identity to sendMessageSlack for text replies", async () => {
27+
sendMock.mockResolvedValue(undefined);
28+
const identity = { username: "Bot", iconEmoji: ":robot:" };
29+
await deliverReplies(baseParams({ identity }));
30+
31+
expect(sendMock).toHaveBeenCalledOnce();
32+
expect(sendMock.mock.calls[0][2]).toMatchObject({ identity });
33+
});
34+
35+
it("passes identity to sendMessageSlack for media replies", async () => {
36+
sendMock.mockResolvedValue(undefined);
37+
const identity = { username: "Bot", iconUrl: "https://example.com/icon.png" };
38+
await deliverReplies(
39+
baseParams({
40+
identity,
41+
replies: [{ text: "caption", mediaUrls: ["https://example.com/img.png"] }],
42+
}),
43+
);
44+
45+
expect(sendMock).toHaveBeenCalledOnce();
46+
expect(sendMock.mock.calls[0][2]).toMatchObject({ identity });
47+
});
48+
49+
it("omits identity key when not provided", async () => {
50+
sendMock.mockResolvedValue(undefined);
51+
await deliverReplies(baseParams());
52+
53+
expect(sendMock).toHaveBeenCalledOnce();
54+
expect(sendMock.mock.calls[0][2]).not.toHaveProperty("identity");
55+
});
56+
});

src/slack/monitor/replies.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
66
import type { MarkdownTableMode } from "../../config/types.base.js";
77
import type { RuntimeEnv } from "../../runtime.js";
88
import { markdownToSlackMrkdwnChunks } from "../format.js";
9-
import { sendMessageSlack } from "../send.js";
9+
import { sendMessageSlack, type SlackSendIdentity } from "../send.js";
1010

1111
export async function deliverReplies(params: {
1212
replies: ReplyPayload[];
@@ -17,6 +17,7 @@ export async function deliverReplies(params: {
1717
textLimit: number;
1818
replyThreadTs?: string;
1919
replyToMode: "off" | "first" | "all";
20+
identity?: SlackSendIdentity;
2021
}) {
2122
for (const payload of params.replies) {
2223
// Keep reply tags opt-in: when replyToMode is off, explicit reply tags
@@ -38,6 +39,7 @@ export async function deliverReplies(params: {
3839
token: params.token,
3940
threadTs,
4041
accountId: params.accountId,
42+
...(params.identity ? { identity: params.identity } : {}),
4143
});
4244
} else {
4345
let first = true;
@@ -49,6 +51,7 @@ export async function deliverReplies(params: {
4951
mediaUrl,
5052
threadTs,
5153
accountId: params.accountId,
54+
...(params.identity ? { identity: params.identity } : {}),
5255
});
5356
}
5457
}

0 commit comments

Comments
 (0)