diff --git a/docs/automation/hooks.md b/docs/automation/hooks.md index 36d017b9371..48c4976870b 100644 --- a/docs/automation/hooks.md +++ b/docs/automation/hooks.md @@ -114,7 +114,7 @@ Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (pu **Command events** (`command:new`, `command:reset`): `context.sessionEntry`, `context.previousSessionEntry`, `context.commandSource`, `context.workspaceDir`, `context.cfg`. -**Message events** (`message:received`): `context.from`, `context.content`, `context.channelId`, `context.metadata` (provider-specific data including `senderId`, `senderName`, `guildId`). +**Message events** (`message:received`): `context.from`, `context.content`, `context.channelId`, `context.metadata` (provider-specific data including `senderId`, `senderName`, `guildId`). `context.content` prefers a nonblank command body for command-like messages, then falls back to the raw inbound body and generic body; it does not include agent-only enrichment such as thread history or link summaries. **Message events** (`message:sent`): `context.to`, `context.content`, `context.success`, `context.channelId`. diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index c9aed796ee8..36cd9396124 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -536,7 +536,7 @@ describe("slack prepareSlackMessage inbound contract", () => { subtype: "bot_message", attachments: [ { - text: "Readiness probe failed: Get http://10.42.13.132:8000/status: context deadline exceeded", + text: "Readiness probe failed: Get https://status.example.test/readiness: context deadline exceeded", }, ], }); @@ -545,6 +545,11 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared).toBeTruthy(); expect(prepared!.ctxPayload.RawBody).toContain("Readiness probe failed"); + // Slack message attachments can carry the user-visible body even when the + // top-level message text is empty. + expect(prepared!.ctxPayload.CommandBody).toBe(""); + expect(prepared!.ctxPayload.BodyForCommands).toBe(""); + expect(prepared!.ctxPayload.BodyForAgent).toContain("Readiness probe failed"); }); it("drops bot-authored room messages when allowBots is true but no owner is present (#59284)", async () => { diff --git a/src/hooks/message-hook-mappers.test.ts b/src/hooks/message-hook-mappers.test.ts index ac7cd7618f0..8e39496531b 100644 --- a/src/hooks/message-hook-mappers.test.ts +++ b/src/hooks/message-hook-mappers.test.ts @@ -97,6 +97,30 @@ describe("message hook mappers", () => { expect(canonical.guildId).toBe("guild-1"); }); + it("falls back to raw body when command body is blank", () => { + const canonical = deriveInboundMessageHookContext( + makeInboundCtx({ + BodyForCommands: " \n\t", + RawBody: "Readiness probe failed", + }), + ); + + expect(canonical.content).toBe("Readiness probe failed"); + expect(toPluginMessageReceivedEvent(canonical).content).toBe("Readiness probe failed"); + expect(toInternalMessageReceivedContext(canonical).content).toBe("Readiness probe failed"); + }); + + it("keeps nonblank command body ahead of raw body for hook content", () => { + const canonical = deriveInboundMessageHookContext( + makeInboundCtx({ + BodyForCommands: "/status", + RawBody: "Readiness probe failed", + }), + ); + + expect(canonical.content).toBe("/status"); + }); + it("supports explicit content/messageId overrides", () => { const canonical = deriveInboundMessageHookContext(makeInboundCtx(), { content: "override-content", diff --git a/src/hooks/message-hook-mappers.ts b/src/hooks/message-hook-mappers.ts index 7352765819a..0a6bfc9a1e9 100644 --- a/src/hooks/message-hook-mappers.ts +++ b/src/hooks/message-hook-mappers.ts @@ -80,6 +80,10 @@ export type CanonicalSentMessageHookContext = { groupId?: string; }; +function readNonBlankString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value : undefined; +} + export function deriveInboundMessageHookContext( ctx: FinalizedMsgContext, overrides?: { @@ -89,13 +93,10 @@ export function deriveInboundMessageHookContext( ): CanonicalInboundMessageHookContext { const content = overrides?.content ?? - (typeof ctx.BodyForCommands === "string" - ? ctx.BodyForCommands - : typeof ctx.RawBody === "string" - ? ctx.RawBody - : typeof ctx.Body === "string" - ? ctx.Body - : ""); + readNonBlankString(ctx.BodyForCommands) ?? + readNonBlankString(ctx.RawBody) ?? + readNonBlankString(ctx.Body) ?? + ""; const channelId = normalizeLowercaseStringOrEmpty( ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "", );