From 4dc0c66399e107cb089e090e745679da216ff105 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 5 Mar 2026 07:50:55 -0500 Subject: [PATCH] fix(subagents): strip leaked [[reply_to]] tags from completion announces (#34503) * fix(subagents): strip reply tags from completion delivery text * test(subagents): cover reply-tag stripping in cron completion sends * changelog: note iMessage reply-tag stripping in completion announces * Update CHANGELOG.md * Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + .../subagent-announce.format.e2e.test.ts | 34 +++++++++++++++++++ src/agents/subagent-announce.ts | 6 +++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 787e05abb78..25ae198965e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- iMessage/cron completion announces: strip leaked inline reply tags (for example `[[reply_to:6100]]`) from user-visible completion text so announcement deliveries do not expose threading metadata. (#24600) Thanks @vincentkoc. - Agents/context pruning: guard assistant thinking/text char estimation against malformed blocks (missing `thinking`/`text` strings or null entries) so pruning no longer crashes with malformed provider content. (openclaw#35146) thanks @Sid-Qin. - Agents/schema cleaning: detect Venice + Grok model IDs as xAI-proxied targets so unsupported JSON Schema keywords are stripped before requests, preventing Venice/Grok `Invalid arguments` failures. (openclaw#35355) thanks @Sid-Qin. - Skills/native command deduplication: centralize skill command dedupe by canonical `skillName` in `listSkillCommandsForAgents` so duplicate suffixed variants (for example `_2`) are no longer surfaced across interfaces outside Discord. (#27521) thanks @shivama205. diff --git a/src/agents/subagent-announce.format.e2e.test.ts b/src/agents/subagent-announce.format.e2e.test.ts index 1f1698c4722..28ddc538251 100644 --- a/src/agents/subagent-announce.format.e2e.test.ts +++ b/src/agents/subagent-announce.format.e2e.test.ts @@ -430,6 +430,40 @@ describe("subagent announce formatting", () => { expect(msg).not.toContain("Convert the result above into your normal assistant voice"); }); + it("strips reply tags from cron completion direct-send messages", async () => { + sessionStore = { + "agent:main:subagent:test": { + sessionId: "child-session-cron-direct", + }, + "agent:main:main": { + sessionId: "requester-session-cron-direct", + }, + }; + + const didAnnounce = await runSubagentAnnounceFlow({ + childSessionKey: "agent:main:subagent:test", + childRunId: "run-cron-reply-tag-strip", + requesterSessionKey: "agent:main:main", + requesterDisplayKey: "main", + requesterOrigin: { channel: "imessage", to: "imessage:+15550001111" }, + ...defaultOutcomeAnnounce, + announceType: "cron job", + expectsCompletionMessage: true, + roundOneReply: + "[[reply_to:6100]] this is a hype post + a gentle callout for the NYC meet. In short:", + }); + + expect(didAnnounce).toBe(true); + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(agentSpy).not.toHaveBeenCalled(); + const call = sendSpy.mock.calls[0]?.[0] as { params?: Record }; + const rawMessage = call?.params?.message; + const msg = typeof rawMessage === "string" ? rawMessage : ""; + expect(call?.params?.channel).toBe("imessage"); + expect(msg).toBe("this is a hype post + a gentle callout for the NYC meet. In short:"); + expect(msg).not.toContain("[[reply_to:"); + }); + it("keeps direct completion send when only the announcing run itself is pending", async () => { sessionStore = { "agent:main:subagent:test": { diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index 8b0c432db3b..97d2065b084 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -21,6 +21,7 @@ import { mergeDeliveryContext, normalizeDeliveryContext, } from "../utils/delivery-context.js"; +import { parseInlineDirectives } from "../utils/directive-tags.js"; import { isDeliverableMessageChannel, isInternalMessageChannel } from "../utils/message-channel.js"; import { buildAnnounceIdFromChildRun, @@ -82,7 +83,10 @@ function buildCompletionDeliveryMessage(params: { outcome?: SubagentRunOutcome; announceType?: SubagentAnnounceType; }): string { - const findingsText = params.findings.trim(); + const findingsText = parseInlineDirectives(params.findings, { + stripAudioTag: false, + stripReplyTags: true, + }).text; if (isAnnounceSkip(findingsText)) { return ""; }