diff --git a/CHANGELOG.md b/CHANGELOG.md index d09bad834d1..e9f59a617a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Google video generation: fall back to the REST `predictLongRunning` Veo endpoint for text-only SDK 404s while keeping reference image/video generation on the SDK path. Fixes #62309 and #63008. (#62343) Thanks @leoleedev. - MiniMax music generation: switch the bundled default model from the unsupported `music-2.5+` id to the current `music-2.6` API model. Fixes #64870 and addresses the music default from #62315. Thanks @noahclanman and @edwardzheng1. - Cron: hydrate flat legacy job rows with top-level `cron`, `tz`, `session`, and `message` fields into canonical schedule, target, and payload objects before startup recomputes run times. Fixes #43351. +- Agents/replies: let pending group chat history trigger bare mentioned turns without treating metadata-only inbound context as user input. Fixes #71489. (#71520) Thanks @SymbolStar. - Google media generation: strip a configured trailing `/v1beta` from Google music/video provider base URLs before calling the Google GenAI SDK, preventing doubled `/v1beta/v1beta` paths. Fixes #63240. (#63258) Thanks @Hybirdss. - Discord: restore direct-message voice-note preflight transcription and classify URL-only Ogg/Opus voice attachments as audio while skipping partial attachments without usable URLs. Fixes #61314 and #64803. - Google Chat: preserve reply text when a typing indicator message is deleted or can no longer be updated, so media captions and first text chunks are resent instead of silently disappearing. (#71498) Thanks @colin-lgtm. diff --git a/src/auto-reply/reply/get-reply-run.media-only.test.ts b/src/auto-reply/reply/get-reply-run.media-only.test.ts index 1a96d567000..51ef9e95a85 100644 --- a/src/auto-reply/reply/get-reply-run.media-only.test.ts +++ b/src/auto-reply/reply/get-reply-run.media-only.test.ts @@ -458,6 +458,90 @@ describe("runPreparedReply media-only handling", () => { expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); }); + it("allows pending inbound history to trigger a bare mention turn", async () => { + vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce( + [ + "Chat history since last reply (untrusted, for context):", + "```json", + JSON.stringify( + [{ sender: "Alice", timestamp_ms: 1_700_000_000_000, body: "what changed?" }], + null, + 2, + ), + "```", + ].join("\n"), + ); + + const result = await runPreparedReply( + baseParams({ + ctx: { + Body: "", + RawBody: "", + CommandBody: "", + ChatType: "group", + WasMentioned: true, + }, + sessionCtx: { + Body: "", + BodyStripped: "", + Provider: "feishu", + OriginatingChannel: "feishu", + OriginatingTo: "chat-1", + ChatType: "group", + WasMentioned: true, + InboundHistory: [ + { sender: "Alice", timestamp: 1_700_000_000_000, body: "what changed?" }, + ], + }, + }), + ); + + expect(result).toEqual({ text: "ok" }); + expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); + const call = vi.mocked(runReplyAgent).mock.calls[0]?.[0]; + expect(call?.followupRun.prompt).toContain("Chat history since last reply"); + expect(call?.followupRun.prompt).toContain("what changed?"); + expect(call?.followupRun.prompt).not.toContain("[User sent media without caption]"); + }); + + it("does not treat blank pending inbound history as user input", async () => { + vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce( + [ + "Chat history since last reply (untrusted, for context):", + "```json", + JSON.stringify([{ sender: "Alice", timestamp_ms: 1_700_000_000_000, body: "" }], null, 2), + "```", + ].join("\n"), + ); + + const result = await runPreparedReply( + baseParams({ + ctx: { + Body: "", + RawBody: "", + CommandBody: "", + ChatType: "group", + WasMentioned: true, + }, + sessionCtx: { + Body: "", + BodyStripped: "", + Provider: "feishu", + OriginatingChannel: "feishu", + OriginatingTo: "chat-1", + ChatType: "group", + WasMentioned: true, + InboundHistory: [{ sender: "Alice", timestamp: 1_700_000_000_000, body: "\u0000 " }], + }, + }), + ); + + expect(result).toEqual({ + text: "I didn't receive any text in your message. Please resend or add a caption.", + }); + expect(vi.mocked(runReplyAgent)).not.toHaveBeenCalled(); + }); + it("allows webchat pure-image turns when image content is carried outside MediaPath", async () => { vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce( [ diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 7583126238d..19873755e65 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -165,6 +165,13 @@ function stripPromptThinkingDirectives(body: string): string { .join("\n"); } +function hasInboundHistoryBody(ctx: TemplateContext): boolean { + return ( + Array.isArray(ctx.InboundHistory) && + ctx.InboundHistory.some((entry) => entry.body.replaceAll("\u0000", "").trim().length > 0) + ); +} + type RunPreparedReplyParams = { ctx: MsgContext; sessionCtx: TemplateContext; @@ -458,7 +465,7 @@ export async function runPreparedReply( const hasUserBody = baseBodyFinal.trim().length > 0 || softResetTail.length > 0 || - (inboundUserContext != null && inboundUserContext.trim().length > 0); + hasInboundHistoryBody(sessionCtx); const hasMediaAttachment = hasInboundMedia(sessionCtx) || (opts?.images?.length ?? 0) > 0; if (!hasUserBody && !hasMediaAttachment) { // Skip onReplyStart when typing is suppressed (e.g. sendPolicy deny) —