From 7b39543e8dbe3e7435999144b2f962a6c52f9bba Mon Sep 17 00:00:00 2001 From: Aldo Date: Sat, 14 Feb 2026 06:29:42 -0600 Subject: [PATCH] fix(reply): honour explicit [[reply_to_*]] tags when replyToMode is off (#16174) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 778fc2559ade85a37209866c2edbe33bfc5bbb86 Co-authored-by: aldoeliacim <17973757+aldoeliacim@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete --- CHANGELOG.md | 1 + docs/channels/discord.md | 2 ++ docs/channels/slack.md | 2 ++ docs/channels/telegram.md | 2 ++ .../reply/reply-payloads.auto-threading.test.ts | 13 +++++++++++++ src/auto-reply/reply/reply-threading.ts | 6 ++++-- 6 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e8e558a8..0d5ea9626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai - BlueBubbles: gracefully degrade when Private API is disabled by filtering private-only actions, skipping private-only reactions/reply effects, and avoiding private reply markers so non-private flows remain usable. (#16002) Thanks @L-U-C-K-Y. - Outbound: add a write-ahead delivery queue with crash-recovery retries to prevent lost outbound messages after gateway restarts. (#15636) Thanks @nabbilkhan, @thewilloftheshadow. - Auto-reply/Threading: auto-inject implicit reply threading so `replyToMode` works without requiring model-emitted `[[reply_to_current]]`, while preserving `replyToMode: "off"` behavior for implicit Slack replies and keeping block-streaming chunk coalescing stable under `replyToMode: "first"`. (#14976) Thanks @Diaspar4u. +- Auto-reply/Threading: honor explicit `[[reply_to_*]]` tags even when `replyToMode` is `off`. (#16174) Thanks @aldoeliacim. - Outbound/Threading: pass `replyTo` and `threadId` from `message send` tool actions through the core outbound send path to channel adapters, preserving thread/reply routing. (#14948) Thanks @mcaxtr. - Auto-reply/Media: allow image-only inbound messages (no caption) to reach the agent instead of short-circuiting as empty text, and preserve thread context in queued/followup prompt bodies for media-only runs. (#11916) Thanks @arosstale. - Discord: route autoThread replies to existing threads instead of the root channel. (#8302) Thanks @gavinbmoore, @thewilloftheshadow. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 06f8ddf76..498cec33b 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -273,6 +273,8 @@ See [Slash commands](/tools/slash-commands) for command catalog and behavior. - `first` - `all` + Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored. + Message IDs are surfaced in context/history so agents can target specific messages. diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 42844aa6d..93fd32907 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -233,6 +233,8 @@ Manual reply tags are supported: - `[[reply_to_current]]` - `[[reply_to:]]` +Note: `replyToMode="off"` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored. + ## Media, chunking, and delivery diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 54864f22b..25e46a9dc 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -416,6 +416,8 @@ curl "https://api.telegram.org/bot/getUpdates" - `first` - `all` + Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored. + diff --git a/src/auto-reply/reply/reply-payloads.auto-threading.test.ts b/src/auto-reply/reply/reply-payloads.auto-threading.test.ts index 8a3c379b3..80578f4b7 100644 --- a/src/auto-reply/reply/reply-payloads.auto-threading.test.ts +++ b/src/auto-reply/reply/reply-payloads.auto-threading.test.ts @@ -72,4 +72,17 @@ describe("applyReplyThreading auto-threading", () => { expect(result[0].replyToId).toBe("42"); expect(result[0].replyToTag).toBe(true); }); + + it("keeps explicit tags for Telegram when off mode is enabled", () => { + const result = applyReplyThreading({ + payloads: [{ text: "[[reply_to_current]]A" }], + replyToMode: "off", + replyToChannel: "telegram", + currentMessageId: "42", + }); + + expect(result).toHaveLength(1); + expect(result[0].replyToId).toBe("42"); + expect(result[0].replyToTag).toBe(true); + }); }); diff --git a/src/auto-reply/reply/reply-threading.ts b/src/auto-reply/reply/reply-threading.ts index e745f1656..cfc6f3a73 100644 --- a/src/auto-reply/reply/reply-threading.ts +++ b/src/auto-reply/reply/reply-threading.ts @@ -54,9 +54,11 @@ export function createReplyToModeFilterForChannel( channel?: OriginatingChannelType, ) { const provider = normalizeChannelId(channel); + // Always honour explicit [[reply_to_*]] tags even when replyToMode is "off". + // Per-channel opt-out is possible but the safe default is to allow them. const allowTagsWhenOff = provider - ? Boolean(getChannelDock(provider)?.threading?.allowTagsWhenOff) - : false; + ? (getChannelDock(provider)?.threading?.allowTagsWhenOff ?? true) + : true; return createReplyToModeFilter(mode, { allowTagsWhenOff, });