Skip to content

Commit 4a0e376

Browse files
vincentkocopenclaw-clownfish[bot]drvoss
authored
fix(imessage): normalize leading NUL echo-cache prefixes (#93511)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com> Co-authored-by: jason <[email protected]>
1 parent 2196ea2 commit 4a0e376

3 files changed

Lines changed: 42 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424
### Fixes
2525

2626
- Channels and delivery: preserve account-scoped DM channel send policy, rich Telegram final replies, rich Telegram tables and lists, Telegram thread-create CLI remapping, Slack outbound `message_sent` hooks, contributed message-tool schema optionality, same-channel generated media completions, and channel chunking around surrogate pairs and Infinity limits. (#92788, #92679, #89421, #89943, #91137, #91246, #92735) Thanks @yetval, @obviyus, @spacegeologist, @rishitamrakar, @lundog, @TurboTheTurtle, and @yhterrance.
27+
- iMessage: normalize leading NUL sent-message echo prefixes while preserving interior NUL bytes and the leading attributedBody marker handling from #73942. Carries forward #63581. Thanks @drvoss.
2728
- Discord: give generated auto-thread titles a 60-second timeout and 4,096-token reasoning-model output budget, clamped to the selected model output cap. (#64734) Thanks @hanamizuki.
2829
- Agent, cron, and Gateway runtime: mark active main sessions before restart shutdown aborts, pause yielded subagent runs whose terminal also signals abort, preserve yielded media completions, de-duplicate main-session heartbeat events, expose session identity in runtime prompts, reject unknown OpenAI agent selectors, keep generated media completions and slash-command block replies in WebChat, preserve fresh post-compaction usage while clearing stale usage snapshots, and require admin privileges for HTTP session/model override surfaces. (#91357, #92631, #92146, #91287, #92468, #92510, #91246, #50795, #50845, #82874, #92651, #92646) Thanks @ooiuuii, @openperf, @IWhatsskill, @ZengWen-DT, @zhangguiping-xydt, @Hollychou924, @leno23, and @TurboTheTurtle.
2930
- Providers and model replay: preserve storeless OpenAI Responses replay compatibility, avoid eager tool streaming for Claude 4.5 in Copilot, honor profile auth for SecretRef model entries, bound model browsing, strip provider prefixes where runtimes need bare IDs, and surface nested embedding fetch failures. (#90706, #75393, #90686, #92247, #92627, #91218, #92628) Thanks @snowzlm, @Kailigithub, @rohitjavvadi, @samson910022, @liuhao1024, @bymle, and @mushuiyu886.

extensions/imessage/src/monitor/echo-cache.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,28 @@ export type SentMessageCache = {
3434
// duplicate delivery (noisy but not lossy) — never message loss.
3535
const SENT_MESSAGE_TEXT_TTL_MS = 4_000;
3636
const SENT_MESSAGE_ID_TTL_MS = 60_000;
37-
const LEADING_ATTRIBUTED_BODY_CORRUPTION_MARKERS = /^[\uFEFF\uFFFD\uFFFE\uFFFF]+/u;
37+
38+
function isLeadingEchoTextCorruptionMarker(code: number): boolean {
39+
return (
40+
code === 0x0000 || code === 0xfeff || code === 0xfffd || code === 0xfffe || code === 0xffff
41+
);
42+
}
43+
44+
function stripLeadingEchoTextCorruptionMarkers(text: string): string {
45+
let offset = 0;
46+
while (offset < text.length && isLeadingEchoTextCorruptionMarker(text.charCodeAt(offset))) {
47+
offset += 1;
48+
}
49+
return offset === 0 ? text : text.slice(offset);
50+
}
3851

3952
function normalizeEchoTextKey(text: string | undefined): string | null {
4053
if (!text) {
4154
return null;
4255
}
43-
const normalized = text
44-
.replace(/\r\n?/g, "\n")
45-
.trim()
46-
.replace(LEADING_ATTRIBUTED_BODY_CORRUPTION_MARKERS, "")
47-
.trim();
56+
const normalized = stripLeadingEchoTextCorruptionMarkers(
57+
text.replace(/\r\n?/g, "\n").trim(),
58+
).trim();
4859
return normalized ? normalized : null;
4960
}
5061

extensions/imessage/src/monitor/monitor-provider.echo-cache.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ describe("iMessage sent-message echo cache", () => {
5151
).toBe(true);
5252
});
5353

54+
it("matches delayed reflected echoes with leading NUL corruption markers", () => {
55+
vi.useFakeTimers();
56+
vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));
57+
const cache = createSentMessageCache();
58+
59+
cache.remember("acct:imessage:+1555", { text: "Delayed echo reply" });
60+
61+
expect(
62+
cache.has("acct:imessage:+1555", {
63+
text: "\u0000\u0000Delayed echo reply",
64+
}),
65+
).toBe(true);
66+
});
67+
5468
it("keeps attributedBody corruption cleanup leading-only", () => {
5569
vi.useFakeTimers();
5670
vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));
@@ -67,6 +81,16 @@ describe("iMessage sent-message echo cache", () => {
6781
expect(cache.has("acct:imessage:+1555", { text: "Delayed\necho reply" })).toBe(false);
6882
});
6983

84+
it("keeps NUL corruption cleanup leading-only", () => {
85+
vi.useFakeTimers();
86+
vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));
87+
const cache = createSentMessageCache();
88+
89+
cache.remember("acct:imessage:+1555", { text: "Delayed echo reply" });
90+
91+
expect(cache.has("acct:imessage:+1555", { text: "Delayed\u0000echo reply" })).toBe(false);
92+
});
93+
7094
it("matches by outbound message id and ignores placeholder ids", () => {
7195
vi.useFakeTimers();
7296
vi.setSystemTime(new Date("2026-02-25T00:00:00Z"));

0 commit comments

Comments
 (0)