From 7d18799bbeb2dd71f3869bbc373e15a678ea7eee Mon Sep 17 00:00:00 2001 From: Harold Hunt Date: Fri, 27 Mar 2026 17:23:24 -0400 Subject: [PATCH] Hooks: pass inbound attachment arrays to plugins (#55452) Merged via squash. Prepared head SHA: 062f8d0513cd4dd36c762de8750316fb408624ba Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com> Reviewed-by: @huntharo --- CHANGELOG.md | 1 + src/hooks/message-hook-mappers.test.ts | 27 ++++++++++++++++++++++++++ src/hooks/message-hook-mappers.ts | 20 +++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d164dcdba..cb5265136d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai - Agents/Codex fallback: classify Codex `server_error` payloads as failoverable, sanitize `Codex error:` payloads before they reach chat, preserve context-overflow guidance for prefixed `invalid_request_error` payloads, and omit provider `request_id` values from user-facing UI copy. (#42892) Thanks @xaeon2026. - Memory/search: share memory embedding provider registrations across split plugin runtimes so memory search no longer fails with unknown provider errors after memory-core registers built-in adapters. (#55945) Thanks @glitch418x. - Discord/Carbon beta: update `@buape/carbon` to the latest beta and pass the new `RateLimitError` request argument so Discord stays compatible with the upstream beta constructor change. (#55980) Thanks @ngutman. +- Plugins/inbound claims: pass full inbound attachment arrays through `inbound_claim` hook metadata while keeping the legacy singular media attachment fields for compatibility. (#55452) Thanks @huntharo. ## 2026.3.24 diff --git a/src/hooks/message-hook-mappers.test.ts b/src/hooks/message-hook-mappers.test.ts index 53660054a15..7bdf8f1048c 100644 --- a/src/hooks/message-hook-mappers.test.ts +++ b/src/hooks/message-hook-mappers.test.ts @@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { buildCanonicalSentMessageHookContext, deriveInboundMessageHookContext, + toPluginInboundClaimEvent, toPluginInboundClaimContext, toInternalMessagePreprocessedContext, toInternalMessageReceivedContext, @@ -67,6 +68,32 @@ describe("message hook mappers", () => { expect(canonical.messageId).toBe("override-msg"); }); + it("preserves multi-attachment arrays for inbound claim metadata", () => { + const canonical = deriveInboundMessageHookContext( + makeInboundCtx({ + MediaPath: undefined, + MediaType: undefined, + MediaPaths: ["/tmp/tree.jpg", "/tmp/ramp.jpg"], + MediaTypes: ["image/jpeg", "image/jpeg"], + }), + ); + + expect(canonical.mediaPath).toBe("/tmp/tree.jpg"); + expect(canonical.mediaType).toBe("image/jpeg"); + expect(canonical.mediaPaths).toEqual(["/tmp/tree.jpg", "/tmp/ramp.jpg"]); + expect(canonical.mediaTypes).toEqual(["image/jpeg", "image/jpeg"]); + expect(toPluginInboundClaimEvent(canonical)).toEqual( + expect.objectContaining({ + metadata: expect.objectContaining({ + mediaPath: "/tmp/tree.jpg", + mediaType: "image/jpeg", + mediaPaths: ["/tmp/tree.jpg", "/tmp/ramp.jpg"], + mediaTypes: ["image/jpeg", "image/jpeg"], + }), + }), + ); + }); + it("maps canonical inbound context to plugin/internal received payloads", () => { const canonical = deriveInboundMessageHookContext(makeInboundCtx()); diff --git a/src/hooks/message-hook-mappers.ts b/src/hooks/message-hook-mappers.ts index 968a4d50719..d8341adfa55 100644 --- a/src/hooks/message-hook-mappers.ts +++ b/src/hooks/message-hook-mappers.ts @@ -35,6 +35,8 @@ export type CanonicalInboundMessageHookContext = { threadId?: string | number; mediaPath?: string; mediaType?: string; + mediaPaths?: string[]; + mediaTypes?: string[]; originatingChannel?: string; originatingTo?: string; guildId?: string; @@ -75,6 +77,16 @@ export function deriveInboundMessageHookContext( const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase(); const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined; const isGroup = Boolean(ctx.GroupSubject || ctx.GroupChannel); + const mediaPaths = Array.isArray(ctx.MediaPaths) + ? ctx.MediaPaths.filter( + (value): value is string => typeof value === "string" && value.length > 0, + ) + : undefined; + const mediaTypes = Array.isArray(ctx.MediaTypes) + ? ctx.MediaTypes.filter( + (value): value is string => typeof value === "string" && value.length > 0, + ) + : undefined; return { from: ctx.From ?? "", to: ctx.To, @@ -102,8 +114,10 @@ export function deriveInboundMessageHookContext( provider: ctx.Provider, surface: ctx.Surface, threadId: ctx.MessageThreadId, - mediaPath: ctx.MediaPath, - mediaType: ctx.MediaType, + mediaPath: ctx.MediaPath ?? mediaPaths?.[0], + mediaType: ctx.MediaType ?? mediaTypes?.[0], + mediaPaths, + mediaTypes, originatingChannel: ctx.OriginatingChannel, originatingTo: ctx.OriginatingTo, guildId: ctx.GroupSpace, @@ -272,6 +286,8 @@ export function toPluginInboundClaimEvent( senderE164: canonical.senderE164, mediaPath: canonical.mediaPath, mediaType: canonical.mediaType, + mediaPaths: canonical.mediaPaths, + mediaTypes: canonical.mediaTypes, guildId: canonical.guildId, channelName: canonical.channelName, groupId: canonical.groupId,