diff --git a/CHANGELOG.md b/CHANGELOG.md index 154f00d2e95..f19e4b54dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai - Doctor/local memory provider checks: stop false-positive local-provider warnings when `provider=local` and no explicit `modelPath` is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite. - Feishu/Run channel fallback: prefer `Provider` over `Surface` when inferring queued run `messageProvider` fallback (when `OriginatingChannel` is missing), preventing Feishu turns from being mislabeled as `webchat` in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai. - Cron/session reaper reliability: move cron session reaper sweeps into `onTimer` `finally` and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob. +- Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9. - Sandbox/Docker setup command parsing: accept `agents.*.sandbox.docker.setupCommand` as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai. - Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin. - macOS/LaunchAgent security defaults: write `Umask=63` (octal `077`) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system `022`. (#32022) Fixes #31905. Thanks @liuxiaopai-ai. diff --git a/src/auto-reply/reply/inbound-meta.test.ts b/src/auto-reply/reply/inbound-meta.test.ts index 8a9941008d7..b39fe5c9805 100644 --- a/src/auto-reply/reply/inbound-meta.test.ts +++ b/src/auto-reply/reply/inbound-meta.test.ts @@ -111,9 +111,10 @@ describe("buildInboundUserContextPrefix", () => { expect(text).toBe(""); }); - it("hides message identifiers for direct chats", () => { + it("hides message identifiers for direct webchat chats", () => { const text = buildInboundUserContextPrefix({ ChatType: "direct", + OriginatingChannel: "webchat", MessageSid: "short-id", MessageSidFull: "provider-full-id", } as TemplateContext); @@ -121,6 +122,33 @@ describe("buildInboundUserContextPrefix", () => { expect(text).toBe(""); }); + it("includes message identifiers for direct external-channel chats", () => { + const text = buildInboundUserContextPrefix({ + ChatType: "direct", + OriginatingChannel: "whatsapp", + MessageSid: "short-id", + MessageSidFull: "provider-full-id", + SenderE164: " +15551234567 ", + } as TemplateContext); + + const conversationInfo = parseConversationInfoPayload(text); + expect(conversationInfo["message_id"]).toBe("short-id"); + expect(conversationInfo["message_id_full"]).toBeUndefined(); + expect(conversationInfo["sender"]).toBe("+15551234567"); + expect(conversationInfo["conversation_label"]).toBeUndefined(); + }); + + it("includes message identifiers for direct chats when channel is inferred from Provider", () => { + const text = buildInboundUserContextPrefix({ + ChatType: "direct", + Provider: "whatsapp", + MessageSid: "provider-only-id", + } as TemplateContext); + + const conversationInfo = parseConversationInfoPayload(text); + expect(conversationInfo["message_id"]).toBe("provider-only-id"); + }); + it("does not treat group chats as direct based on sender id", () => { const text = buildInboundUserContextPrefix({ ChatType: "group", diff --git a/src/auto-reply/reply/inbound-meta.ts b/src/auto-reply/reply/inbound-meta.ts index eea956785ae..519414fa109 100644 --- a/src/auto-reply/reply/inbound-meta.ts +++ b/src/auto-reply/reply/inbound-meta.ts @@ -31,6 +31,17 @@ function formatConversationTimestamp(value: unknown): string | undefined { } } +function resolveInboundChannel(ctx: TemplateContext): string | undefined { + let channelValue = safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface); + if (!channelValue) { + const provider = safeTrim(ctx.Provider); + if (provider !== "webchat" && ctx.Surface !== "webchat") { + channelValue = provider; + } + } + return channelValue; +} + export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string { const chatType = normalizeChatType(ctx.ChatType); const isDirect = !chatType || chatType === "direct"; @@ -44,18 +55,7 @@ export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string { // Resolve channel identity: prefer explicit channel, then surface, then provider. // For webchat/Hub Chat sessions (when Surface is 'webchat' or undefined with no real channel), // omit the channel field entirely rather than falling back to an unrelated provider. - let channelValue = safeTrim(ctx.OriginatingChannel) ?? safeTrim(ctx.Surface); - if (!channelValue) { - // Only fall back to Provider if it represents a real messaging channel. - // For webchat/internal sessions, ctx.Provider may be unrelated (e.g., the user's configured - // default channel), so skip it to avoid incorrect runtime labels like "channel=whatsapp". - const provider = safeTrim(ctx.Provider); - // Check if provider is "webchat" or if we're in an internal/webchat context - if (provider !== "webchat" && ctx.Surface !== "webchat") { - channelValue = provider; - } - // Otherwise leave channelValue undefined (no channel label) - } + const channelValue = resolveInboundChannel(ctx); const payload = { schema: "openclaw.inbound_meta.v1", @@ -85,6 +85,11 @@ export function buildInboundUserContextPrefix(ctx: TemplateContext): string { const blocks: string[] = []; const chatType = normalizeChatType(ctx.ChatType); const isDirect = !chatType || chatType === "direct"; + const directChannelValue = resolveInboundChannel(ctx); + const includeDirectConversationInfo = Boolean( + directChannelValue && directChannelValue !== "webchat", + ); + const shouldIncludeConversationInfo = !isDirect || includeDirectConversationInfo; const messageId = safeTrim(ctx.MessageSid); const messageIdFull = safeTrim(ctx.MessageSidFull); @@ -92,16 +97,16 @@ export function buildInboundUserContextPrefix(ctx: TemplateContext): string { const timestampStr = formatConversationTimestamp(ctx.Timestamp); const conversationInfo = { - message_id: isDirect ? undefined : resolvedMessageId, - reply_to_id: isDirect ? undefined : safeTrim(ctx.ReplyToId), - sender_id: isDirect ? undefined : safeTrim(ctx.SenderId), + message_id: shouldIncludeConversationInfo ? resolvedMessageId : undefined, + reply_to_id: shouldIncludeConversationInfo ? safeTrim(ctx.ReplyToId) : undefined, + sender_id: shouldIncludeConversationInfo ? safeTrim(ctx.SenderId) : undefined, conversation_label: isDirect ? undefined : safeTrim(ctx.ConversationLabel), - sender: isDirect - ? undefined - : (safeTrim(ctx.SenderName) ?? + sender: shouldIncludeConversationInfo + ? (safeTrim(ctx.SenderName) ?? safeTrim(ctx.SenderE164) ?? safeTrim(ctx.SenderId) ?? - safeTrim(ctx.SenderUsername)), + safeTrim(ctx.SenderUsername)) + : undefined, timestamp: timestampStr, group_subject: safeTrim(ctx.GroupSubject), group_channel: safeTrim(ctx.GroupChannel),