fix(cron): pass agent identity through delivery path (#16218) (#16242)

* fix(cron): pass agent identity through delivery path

Cron delivery messages now include agent identity (name, avatar) in
outbound messages. Identity fields are passed best-effort for Slack
(graceful fallback if chat:write.customize scope is missing).

Fixes #16218

* fix: fix Slack cron delivery identity (#16242) (thanks @robbyczgw-cla)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Robby
2026-02-14 16:08:51 +01:00
committed by GitHub
parent 497b060e49
commit 09e1cbc35d
8 changed files with 222 additions and 23 deletions

View File

@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../../slack/send.js", () => ({
sendMessageSlack: vi.fn().mockResolvedValue({ ts: "1234.5678", channel: "C123" }),
sendMessageSlack: vi.fn().mockResolvedValue({ messageId: "1234.5678", channelId: "C123" }),
}));
vi.mock("../../../plugins/hook-runner-global.js", () => ({
@@ -37,6 +37,45 @@ describe("slack outbound hook wiring", () => {
});
});
it("forwards identity opts when present", async () => {
vi.mocked(getGlobalHookRunner).mockReturnValue(null);
await slackOutbound.sendText({
to: "C123",
text: "hello",
accountId: "default",
replyToId: "1111.2222",
username: "My Agent",
icon_url: "https://example.com/avatar.png",
icon_emoji: ":should_not_send:",
});
expect(sendMessageSlack).toHaveBeenCalledWith("C123", "hello", {
threadTs: "1111.2222",
accountId: "default",
username: "My Agent",
icon_url: "https://example.com/avatar.png",
});
});
it("forwards icon_emoji only when icon_url is absent", async () => {
vi.mocked(getGlobalHookRunner).mockReturnValue(null);
await slackOutbound.sendText({
to: "C123",
text: "hello",
accountId: "default",
replyToId: "1111.2222",
icon_emoji: ":lobster:",
});
expect(sendMessageSlack).toHaveBeenCalledWith("C123", "hello", {
threadTs: "1111.2222",
accountId: "default",
icon_emoji: ":lobster:",
});
});
it("calls message_sending hook before sending", async () => {
const mockRunner = {
hasHooks: vi.fn().mockReturnValue(true),

View File

@@ -6,7 +6,17 @@ export const slackOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
chunker: null,
textChunkLimit: 4000,
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
sendText: async ({
to,
text,
accountId,
deps,
replyToId,
threadId,
username,
icon_url,
icon_emoji,
}) => {
const send = deps?.sendSlack ?? sendMessageSlack;
// Use threadId fallback so routed tool notifications stay in the Slack thread.
const threadTs = replyToId ?? (threadId != null ? String(threadId) : undefined);
@@ -35,10 +45,24 @@ export const slackOutbound: ChannelOutboundAdapter = {
const result = await send(to, finalText, {
threadTs,
accountId: accountId ?? undefined,
...(username ? { username } : {}),
...(icon_url ? { icon_url } : {}),
...(icon_emoji && !icon_url ? { icon_emoji } : {}),
});
return { channel: "slack", ...result };
},
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, threadId }) => {
sendMedia: async ({
to,
text,
mediaUrl,
accountId,
deps,
replyToId,
threadId,
username,
icon_url,
icon_emoji,
}) => {
const send = deps?.sendSlack ?? sendMessageSlack;
// Use threadId fallback so routed tool notifications stay in the Slack thread.
const threadTs = replyToId ?? (threadId != null ? String(threadId) : undefined);
@@ -68,6 +92,9 @@ export const slackOutbound: ChannelOutboundAdapter = {
mediaUrl,
threadTs,
accountId: accountId ?? undefined,
...(username ? { username } : {}),
...(icon_url ? { icon_url } : {}),
...(icon_emoji && !icon_url ? { icon_emoji } : {}),
});
return { channel: "slack", ...result };
},

View File

@@ -79,6 +79,9 @@ export type ChannelOutboundContext = {
replyToId?: string | null;
threadId?: string | number | null;
accountId?: string | null;
username?: string;
icon_url?: string;
icon_emoji?: string;
deps?: OutboundSendDeps;
silent?: boolean;
};