Skip to content

Commit e3e2a19

Browse files
committed
fix(imessage): drop ambiguous reflected self-chat echoes
1 parent c3d3cf2 commit e3e2a19

3 files changed

Lines changed: 11 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
7676
- Gateway/thread routing: preserve Slack, Telegram, and Mattermost thread-child delivery targets so bound subagent completion messages land in the originating thread instead of top-level channels. (#54840) Thanks @yzzymt.
7777
- ACP/stream relay: pass parent delivery context to ACP stream relay system events so `streamTo="parent"` updates route to the correct thread or topic instead of falling back to the main DM. (#57056) Thanks @pingren.
7878
- Agents/sessions: preserve announce `threadId` when `sessions.list` fallback rehydrates agent-to-agent announce targets so final announce messages stay in the originating thread/topic. (#63506) Thanks @SnowSky1.
79+
- iMessage/self-chat: remember ambiguous `sender === chat_identifier` outbound rows with missing `destination_caller_id` in self-chat dedupe state so the later reflected inbound copy still drops instead of re-entering inbound handling when the echo cache misses. Thanks @neeravmakwana.
7980

8081
## 2026.4.9
8182

extensions/imessage/src/monitor/inbound-processing.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,20 @@ export function resolveIMessageInboundDecision(params: {
217217
chatIdentifierNormalized != null &&
218218
senderNormalized === chatIdentifierNormalized &&
219219
matchesSelfChatDestination;
220+
const isAmbiguousSelfThread =
221+
!isGroup &&
222+
chatIdentifierNormalized != null &&
223+
senderNormalized === chatIdentifierNormalized &&
224+
destinationCallerIdNormalized == null;
220225
let skipSelfChatHasCheck = false;
221226
const inboundMessageIds = resolveInboundEchoMessageIds(params.message);
222227
const inboundMessageId = inboundMessageIds[0];
223228
const hasInboundGuid = Boolean(normalizeReplyField(params.message.guid));
224229

225230
if (params.message.is_from_me) {
231+
if (isAmbiguousSelfThread) {
232+
params.selfChatCache?.remember(selfChatLookup);
233+
}
226234
if (isSelfChat) {
227235
params.selfChatCache?.remember(selfChatLookup);
228236
const echoScope = buildIMessageEchoScope({

extensions/imessage/src/monitor/self-chat-dedupe.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ describe("self-chat is_from_me=true handling (Bruce Phase 2 fix)", () => {
630630
expect(decision).toEqual({ kind: "drop", reason: "from me" });
631631
});
632632

633-
it("does not drop reflected inbound when destination_caller_id is absent (#63980)", () => {
633+
it("drops reflected inbound when destination_caller_id is absent (#63980)", () => {
634634
vi.useFakeTimers();
635635
vi.setSystemTime(new Date("2026-03-24T12:00:00Z"));
636636

@@ -674,7 +674,7 @@ describe("self-chat is_from_me=true handling (Bruce Phase 2 fix)", () => {
674674
}),
675675
);
676676

677-
expect(reflection.kind).toBe("dispatch");
677+
expect(reflection).toEqual({ kind: "drop", reason: "self-chat echo" });
678678
});
679679

680680
it("normal DM is_from_me=true is still dropped (regression test)", () => {

0 commit comments

Comments
 (0)