diff --git a/docs/plugins/hooks.md b/docs/plugins/hooks.md index 8a80e554966c..e3d0b8d21eef 100644 --- a/docs/plugins/hooks.md +++ b/docs/plugins/hooks.md @@ -768,6 +768,9 @@ has visibility-filtered quoted message data: `replyToId`, `replyToIdFull`, `replyToBody`, `replyToSender`, and `replyToIsQuote`. Prefer these first-class fields before reading legacy metadata. +`before_dispatch` receives the canonical inbound `messageId` in both its event +and context. + Prefer typed `threadId` and `replyToId` fields before using channel-specific metadata. diff --git a/src/auto-reply/reply/dispatch-from-config.choose-route.ts b/src/auto-reply/reply/dispatch-from-config.choose-route.ts index 9cf3f05c59c2..512222e2fdc5 100644 --- a/src/auto-reply/reply/dispatch-from-config.choose-route.ts +++ b/src/auto-reply/reply/dispatch-from-config.choose-route.ts @@ -466,6 +466,7 @@ export async function chooseDispatchRoute(state: PrepareDispatchOperationReadySt () => hookRunner.runBeforeDispatch( { + messageId: state.hookContext.messageId, content: state.hookContext.content, body: state.hookContext.bodyForAgent ?? state.hookContext.body, channel: state.hookContext.channelId, @@ -480,6 +481,7 @@ export async function chooseDispatchRoute(state: PrepareDispatchOperationReadySt timestamp: state.hookContext.timestamp, }, { + messageId: state.hookContext.messageId, channelId: state.hookContext.channelId, accountId: state.hookContext.accountId, conversationId: state.inboundClaimContext.conversationId, diff --git a/src/auto-reply/reply/dispatch-from-config.hooks-and-send-policy.test-utils.ts b/src/auto-reply/reply/dispatch-from-config.hooks-and-send-policy.test-utils.ts index 854f5e1af04b..3e7adaa5ae1b 100644 --- a/src/auto-reply/reply/dispatch-from-config.hooks-and-send-policy.test-utils.ts +++ b/src/auto-reply/reply/dispatch-from-config.hooks-and-send-policy.test-utils.ts @@ -148,6 +148,8 @@ describe("before_dispatch hook", () => { hookMocks.runner.runBeforeDispatch.mockResolvedValue({ handled: true }); const dispatcher = createDispatcher(); const ctx = createHookCtx({ + MessageSid: "discord-message-456", + MessageSidFull: " ", ReplyToId: "discord-reply-123", ReplyToIdFull: "discord:channel-1:discord-reply-123", ReplyToBody: "the quoted parent message", @@ -163,6 +165,7 @@ describe("before_dispatch hook", () => { ) as | [ { + messageId?: unknown; replyToId?: unknown; replyToIdFull?: unknown; replyToBody?: unknown; @@ -170,6 +173,7 @@ describe("before_dispatch hook", () => { replyToIsQuote?: unknown; }, { + messageId?: unknown; replyToId?: unknown; replyToIdFull?: unknown; replyToBody?: unknown; @@ -179,6 +183,7 @@ describe("before_dispatch hook", () => { ] | undefined; expect(beforeDispatchCall?.[0]).toMatchObject({ + messageId: "discord-message-456", replyToId: "discord-reply-123", replyToIdFull: "discord:channel-1:discord-reply-123", replyToBody: "the quoted parent message", @@ -186,6 +191,7 @@ describe("before_dispatch hook", () => { replyToIsQuote: true, }); expect(beforeDispatchCall?.[1]).toMatchObject({ + messageId: "discord-message-456", replyToId: "discord-reply-123", replyToIdFull: "discord:channel-1:discord-reply-123", replyToBody: "the quoted parent message", diff --git a/src/hooks/message-hook-mappers.test.ts b/src/hooks/message-hook-mappers.test.ts index e3d33ed93738..c288ec75fdb3 100644 --- a/src/hooks/message-hook-mappers.test.ts +++ b/src/hooks/message-hook-mappers.test.ts @@ -132,6 +132,19 @@ describe("message hook mappers", () => { expect(canonical.guildId).toBe("guild-1"); }); + it("normalizes canonical inbound message id precedence", () => { + expect( + deriveInboundMessageHookContext( + makeInboundCtx({ MessageSidFull: "full-message-id", MessageSid: "short-message-id" }), + ).messageId, + ).toBe("full-message-id"); + expect( + deriveInboundMessageHookContext( + makeInboundCtx({ MessageSidFull: " ", MessageSid: "short-message-id" }), + ).messageId, + ).toBe("short-message-id"); + }); + it("uses the session key as the Control UI conversation id", () => { const canonical = deriveInboundMessageHookContext( makeInboundCtx({ diff --git a/src/hooks/message-hook-mappers.ts b/src/hooks/message-hook-mappers.ts index 65dde8ce06cd..569957d55c67 100644 --- a/src/hooks/message-hook-mappers.ts +++ b/src/hooks/message-hook-mappers.ts @@ -188,11 +188,11 @@ export function deriveInboundMessageHookContext( sessionKey: ctx.SessionKey, agentId: ctx.AgentId, messageId: - overrides?.messageId ?? - ctx.MessageSidFull ?? - ctx.MessageSid ?? - ctx.MessageSidFirst ?? - ctx.MessageSidLast, + normalizeOptionalString(overrides?.messageId) ?? + normalizeOptionalString(ctx.MessageSidFull) ?? + normalizeOptionalString(ctx.MessageSid) ?? + normalizeOptionalString(ctx.MessageSidFirst) ?? + normalizeOptionalString(ctx.MessageSidLast), senderId: ctx.SenderId, senderName: ctx.SenderName, senderUsername: ctx.SenderUsername, diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts index 8d7518d99318..5d8fff144902 100644 --- a/src/plugins/hook-types.ts +++ b/src/plugins/hook-types.ts @@ -511,6 +511,7 @@ export type PluginHookInboundClaimResult = { }; export type PluginHookBeforeDispatchEvent = { + messageId?: string; content: string; body?: string; channel?: string; @@ -526,6 +527,7 @@ export type PluginHookBeforeDispatchEvent = { }; export type PluginHookBeforeDispatchContext = { + messageId?: string; channelId?: string; accountId?: string; conversationId?: string;