From be445dd1c12840b77055965dd7da1f659236f8f4 Mon Sep 17 00:00:00 2001 From: "openclaw-clownfish[bot]" <280122609+openclaw-clownfish[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:04:20 -0700 Subject: [PATCH] fix(imessage): normalize leading echoed text corruption Fixes #59973 --- CHANGELOG.md | 1 + extensions/imessage/src/monitor/echo-cache.ts | 7 ++++- .../monitor-provider.echo-cache.test.ts | 30 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c92fab139f..cb439f719c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai - Active Memory: register the prompt-build hook with the configured recall timeout plus setup grace instead of the 150s maximum budget, so default memory recall cannot delay turn startup for multiple minutes. Thanks @vincentkoc. - CLI/channels logs: reuse the rolling log-file resolver so `openclaw channels logs` falls back to the active dated log across date boundaries without reading unrelated custom log files. Fixes #42875; carries forward #42904 and #43043. Thanks @ethanclaw and @wdskuki. - CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007. +- iMessage: normalize known leading attributedBody corruption markers on sent-message echo text keys so delayed reflected echoes with U+FFFD/U+FFFE/U+FFFF/FEFF prefixes are dropped without collapsing interior text. Fixes #59973; carries forward #59980 and #62191. Thanks @neeravmakwana and @maguilar631697. - Security/audit: recognize dangerous node command IDs as valid `gateway.nodes.denyCommands` entries, so audit only warns on real typos or unsupported patterns. (#56923) Thanks @chziyue. - Telegram/exec approvals: stop treating general Telegram chat allowlists and `defaultTo` routes as native exec approvers; Telegram now uses explicit `execApprovals.approvers` or owner identity from `commands.ownerAllowFrom`, matching the first-pairing owner bootstrap path. Thanks @pashpashpash. - Chat commands: route sensitive group `/diagnostics` and `/export-trajectory` approvals and results to a private owner route, preferring same-surface DMs before falling back to the first configured owner route, so Discord group invocations can land in Telegram when that is the primary owner interface. Thanks @pashpashpash. diff --git a/extensions/imessage/src/monitor/echo-cache.ts b/extensions/imessage/src/monitor/echo-cache.ts index 5104596ff1c..c5f27b1c632 100644 --- a/extensions/imessage/src/monitor/echo-cache.ts +++ b/extensions/imessage/src/monitor/echo-cache.ts @@ -22,12 +22,17 @@ export type SentMessageCache = { // duplicate delivery (noisy but not lossy) — never message loss. const SENT_MESSAGE_TEXT_TTL_MS = 4_000; const SENT_MESSAGE_ID_TTL_MS = 60_000; +const LEADING_ATTRIBUTED_BODY_CORRUPTION_MARKERS = /^[\uFEFF\uFFFD\uFFFE\uFFFF]+/u; function normalizeEchoTextKey(text: string | undefined): string | null { if (!text) { return null; } - const normalized = text.replace(/\r\n?/g, "\n").trim(); + const normalized = text + .replace(/\r\n?/g, "\n") + .trim() + .replace(LEADING_ATTRIBUTED_BODY_CORRUPTION_MARKERS, "") + .trim(); return normalized ? normalized : null; } diff --git a/extensions/imessage/src/monitor/monitor-provider.echo-cache.test.ts b/extensions/imessage/src/monitor/monitor-provider.echo-cache.test.ts index 4adeed4aafa..b8a0798292e 100644 --- a/extensions/imessage/src/monitor/monitor-provider.echo-cache.test.ts +++ b/extensions/imessage/src/monitor/monitor-provider.echo-cache.test.ts @@ -17,6 +17,36 @@ describe("iMessage sent-message echo cache", () => { expect(cache.has("acct:imessage:+1666", { text: "Reasoning:\n_step_" })).toBe(false); }); + it("matches delayed reflected echoes with leading attributedBody corruption markers", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-02-25T00:00:00Z")); + const cache = createSentMessageCache(); + + cache.remember("acct:imessage:+1555", { text: "Delayed echo reply" }); + + expect( + cache.has("acct:imessage:+1555", { + text: "\uFFFD\uFFFE\uFFFF\uFEFFDelayed echo reply", + }), + ).toBe(true); + }); + + it("keeps attributedBody corruption cleanup leading-only", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-02-25T00:00:00Z")); + const cache = createSentMessageCache(); + + cache.remember("acct:imessage:+1555", { text: "Delayed echo reply" }); + + expect( + cache.has("acct:imessage:+1555", { + text: "Delayed \uFFFD echo reply", + }), + ).toBe(false); + expect(cache.has("acct:imessage:+1555", { text: "Delayed\techo reply" })).toBe(false); + expect(cache.has("acct:imessage:+1555", { text: "Delayed\necho reply" })).toBe(false); + }); + it("matches by outbound message id and ignores placeholder ids", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));