fix(imessage): prevent self-chat dedupe false positives (#47830)#55359
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed7d8dc643
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (!skipIdShortCircuit) { | ||
| return false; |
There was a problem hiding this comment.
Keep text fallback when inbound ID has no cache match
This early return disables text-based echo matching whenever an inbound message carries any messageId that is not found in the ID cache. That breaks a real fallback path: sendMessageIMessage can return unusable IDs like "ok"/"unknown" (which normalizeEchoMessageIdKey intentionally ignores), so remember() may store only text; later inbound messages still include numeric IDs, hit this branch, and return false before the text check. The result is reflected outbound messages no longer being deduped in those cases, causing duplicate dispatches (and potential loop behavior) that the previous implementation would suppress via text matching.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — this is a real path. When sendMessageIMessage returns a fallback ID like "ok" or "unknown" (which normalizeEchoMessageIdKey correctly drops), remember() only stores text. The early return then prevents text matching on the inbound echo.
Failure mode is duplicate delivery (noisy, not lossy) since the echo leaks through rather than being suppressed. I'll push a fix into this PR: the ID-mismatch early return should only fire when the cache actually stored a real messageId for that scope. When only text was cached, text fallback must still run.
Thanks for flagging this 👍
Greptile SummaryThis PR fixes a latent silent message-loss bug in the iMessage extension where user self-chat messages ( Key observations:
Confidence Score: 5/5Safe to merge — logic is sound, all prior review concerns are addressed, and coverage is thorough (19 new tests + 48-hour soak). No P0 or P1 issues found. The single finding is a minor P2 memory-efficiency nit in No files require special attention;
|
| Filename | Overview |
|---|---|
| extensions/imessage/src/monitor/echo-cache.ts | Adds skipIdShortCircuit flag to has() and a scope-level ID-presence guard so text-fallback still fires when outbound IDs were junk. TTL tightened from 5 s to 4 s. One minor style nit on hasAnyIdForScope (array spread vs for-of); logic is correct. |
| extensions/imessage/src/monitor/inbound-processing.ts | Adds isSelfChat detection (sender == chat_identifier for non-group DMs) and routes is_from_me=true self-chat messages through the echo cache (with skipIdShortCircuit=true) before deciding to dispatch or drop. Normal DM and group paths are unchanged. |
| extensions/imessage/src/monitor/deliver.ts | Removes the pre-send remember() call (full un-chunked text) and replaces it with per-chunk post-send remember() calls, narrowing the false-positive window and fixing the self-chat scope-collision bug. |
| extensions/imessage/src/monitor/deliver.test.ts | Three updated tests verify: only two post-send remember() calls (no pre-send call), and each chunk cached with its own distinct message ID (imsg-1/imsg-2), addressing the prior review concern. |
| extensions/imessage/src/monitor/self-chat-dedupe.test.ts | 19 new tests covering ID-type canary, TTL boundary at 4 s, text-only fallback, skipIdShortCircuit behaviour, real user self-chat dispatch, agent echo drop, is_from_me=false reflection via selfChatCache, normal-DM regression, and null-id edge cases. |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/imessage/src/monitor/echo-cache.ts
Line: 78-80
Comment:
**Unnecessary array allocation in `hasAnyIdForScope`**
`[...this.messageIdCache.keys()].some(...)` spreads all map keys into a temporary array before searching. Under the ID TTL of 60 s, a busy multi-conversation deployment can accumulate many entries, making this allocation non-trivial on every `has()` call that carries a valid inbound message ID.
A `for...of` loop iterates lazily and short-circuits without materialising the full key set:
```suggestion
let hasAnyIdForScope = false;
for (const key of this.messageIdCache.keys()) {
if (key.startsWith(`${scope}:`)) {
hasAnyIdForScope = true;
break;
}
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "refactor(imessage): hoist inboundMessage..." | Re-trigger Greptile
| chatId, | ||
| sender, | ||
| }); | ||
| const messageText = params.messageText; |
There was a problem hiding this comment.
Local
messageText shadows the outer declaration
const messageText = params.messageText is declared here inside the if (isSelfChat) block, and then const messageText = params.messageText.trim() is declared again at line 279 (outside the is_from_me block). TypeScript allows this because the scopes are different, and normalizeEchoTextKey() trims internally so the behaviour is identical either way — but a future reader scanning the function might assume the trimmed version is used throughout.
Consider either removing the inner alias and reading params.messageText directly in the has() call (the cache normalises it anyway), or renaming the outer one to trimmedMessageText to make the scope split explicit.
| const messageText = params.messageText; | |
| const rawMessageText = params.messageText; |
And update the has() call below to use rawMessageText.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/imessage/src/monitor/inbound-processing.ts
Line: 185
Comment:
**Local `messageText` shadows the outer declaration**
`const messageText = params.messageText` is declared here inside the `if (isSelfChat)` block, and then `const messageText = params.messageText.trim()` is declared again at line 279 (outside the `is_from_me` block). TypeScript allows this because the scopes are different, and `normalizeEchoTextKey()` trims internally so the behaviour is identical either way — but a future reader scanning the function might assume the trimmed version is used throughout.
Consider either removing the inner alias and reading `params.messageText` directly in the `has()` call (the cache normalises it anyway), or renaming the outer one to `trimmedMessageText` to make the scope split explicit.
```suggestion
const rawMessageText = params.messageText;
```
And update the `has()` call below to use `rawMessageText`.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fair point on the shadowing — removing the inner alias and reading params.messageText directly is cleaner since normalizeEchoTextKey() handles the trim anyway. Will fix.
|
Pushed a fix commit (
Re: the Test count: 118/118 iMessage extension tests passing, 223/223 channel contract tests passing. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59a64846b5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const hasAnyIdForScope = [...this.messageIdCache.keys()].some((key) => | ||
| key.startsWith(`${scope}:`), | ||
| ); | ||
| if (!skipIdShortCircuit && hasAnyIdForScope) { | ||
| return false; |
There was a problem hiding this comment.
Preserve text fallback for mixed-ID echo cache scopes
When a scope contains at least one real outbound ID, has() now returns false on any inbound ID mismatch before checking text, which breaks dedupe for later sends that were cached text-only (messageId normalized out as "ok"/"unknown"). In a realistic sequence (first send stores a GUID, second send stores only text, then inbound echo for the second message arrives with numeric SQLite ID), this branch skips text matching and lets the echo dispatch, reintroducing duplicate/loop-prone behavior that the previous implementation suppressed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch on the theoretical edge case, but iMessage sends always return a normalized 'ok' messageId — the scope never contains real outbound IDs, so the ID mismatch short-circuit doesn't fire.
Text fallback matching is preserved for that path. This PR is scoped to iMessage self-chat; if other channels have mixed ID behavior, that'd be a separate consideration.
|
Addressed the (Previously left as-is since both lived in separate scopes, but on reflection it's cleaner hoisted. Pure refactor, no behavior change.) |
86ce882 to
ab24fe3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab24fe3ec6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| (params.messageText || inboundMessageId) && | ||
| params.echoCache.has( | ||
| echoScope, | ||
| { text: params.messageText || undefined, messageId: inboundMessageId }, | ||
| true, // skipIdShortCircuit |
There was a problem hiding this comment.
Use bodyText fallback for self-chat echo matching
In the new is_from_me self-chat branch, echoCache.has is keyed with params.messageText only, but attachment notifications can legitimately have empty messageText while bodyText is populated with a media placeholder by the monitor path. In that case the self-chat check cannot match by text, and because IDs in this path are expected to differ (skipIdShortCircuit=true), the lookup misses and skipSelfChatHasCheck lets the message dispatch as if it were user input. This means a self-chat agent media echo (for example, media without caption or later media items) can be processed as a fresh inbound message instead of being dropped.
Useful? React with 👍 / 👎.
|
Rebased cleanly onto latest CI results:
The All iMessage-specific checks pass (118/118 unit, 223/223 contract). 2+ days live burn-in, zero dropped messages. Ready for maintainer review. |
|
@greptileai review again |
|
Patched the remaining self-chat/media echo gap. Change: unified echo matching on effective payload text ( Verification: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 837ab2dc34
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!skipIdShortCircuit && hasAnyIdForScope) { | ||
| return false; |
There was a problem hiding this comment.
Allow text fallback in mixed ID/text echo-cache scopes
This early return skips text matching whenever the scope has any cached real message ID, which breaks dedupe for later outbound messages that were cached text-only (messageId normalized out as "ok"/"unknown"). In a mixed scope (one GUID-backed send followed by a text-only send), an inbound echo for the text-only send with a non-matching numeric ID will return false here and bypass the text fallback, so the echo is dispatched as a new inbound message. Fresh evidence: this commit explicitly preserves GUID ID handling while the send path still falls back to "ok"/"unknown", so mixed scopes are a valid runtime state.
Useful? React with 👍 / 👎.
| params.echoCache.has( | ||
| echoScope, | ||
| { text: bodyText || undefined, messageId: inboundMessageId }, | ||
| true, // skipIdShortCircuit |
There was a problem hiding this comment.
Avoid self-chat media dedupe on generic placeholder text
For self-chat is_from_me=true, this call forces text fallback even when IDs differ, and the lookup key is only bodyText; with the new media caching path storing generic placeholders (for example <media:image>), a real user image sent to self within the TTL after an agent image will match this placeholder and be dropped as agent echo in self-chat. This requires self-chat plus media-only messages close together, but in that window it causes user message loss.
Useful? React with 👍 / 👎.
Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830
Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s)
837ab2d to
5764686
Compare
obviyus
left a comment
There was a problem hiding this comment.
Reviewed latest changes. Manually tested the self-chat path with imsg during landing; changes worked well.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57646864c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const inboundMessageId = | ||
| normalizeReplyField(params.message.guid) ?? | ||
| (params.message.id != null ? String(params.message.id) : undefined); |
There was a problem hiding this comment.
Use numeric message id before guid in echo lookup
This new guid-first selection can miss real echoes when the send RPC only returns a numeric id (a supported path in send.ts, exercised by channel.outbound.test.ts) but inbound notifications carry both id and guid. In that case remember() stores the numeric ID, has() is queried with the GUID, and the new ID-mismatch short-circuit returns false before text fallback for ID-backed entries, so the agent’s own outbound echo is processed as a fresh inbound message.
Useful? React with 👍 / 👎.
This comment was marked as spam.
This comment was marked as spam.
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
…@rmarr) * fix(imessage): prevent self-chat dedupe false positives (openclaw#47830) Move echo cache remember() to post-send only, add early return when inbound message ID doesn't match cached IDs (prevents text-based false positives in self-chat), and reduce text TTL from 5s to 3s. Three targeted changes to fix silent user message loss in self-chat: 1. deliver.ts: Remove pre-send remember() call — cache only reflects successfully-delivered content, not pre-send full text. 2. echo-cache.ts: Skip text fallback when inbound has a valid message ID that doesn't match any cached outbound ID. In self-chat, sender == target so scopes collide; a user message with a fresh ID but matching text was incorrectly dropped as an echo. 3. echo-cache.ts: Reduce text TTL from 5000ms to 3000ms — agent echoes arrive within 1-2s, 5s was too wide. Adds self-chat-dedupe.test.ts (7 tests) + updates deliver.test.ts. BlueBubbles uses a different cache pattern — no changes needed there. Closes openclaw#47830 * review(imessage): strip debug logs, bump echo TTL to 4s (openclaw#47830) Bruce Phase 4 review changes: - Remove all [IMSG-DEBUG] console.error calls from inbound-processing.ts and monitor-provider.ts (23 lines, left over from Phase 2 debug deploy) - Bump SENT_MESSAGE_TEXT_TTL_MS from 3s to 4s in echo-cache.ts to give ~2s margin above the observed 2.2s echo arrival time under load - Update TTL tests to reflect 4s TTL (expired at 5s, live at 3s) * fix(imessage): add dedupe comments and canary/compat/TTL tests * fix(imessage): address review feedback on echo cache, shadowing, and test IDs * refactor(imessage): hoist inboundMessageId to eliminate duplicate computation (openclaw#47830) * fix(imessage): unify self-chat echo matching * fix: use inbound guid for self-chat echo matching (openclaw#55359) (thanks @rmarr) --------- Co-authored-by: Rohan Marr <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
Summary
is_from_me=truemessages in self-chat to be discarded.SelfChatCachelogic untouched (still handlesis_from_me=falsereflection dedup). Normal DM behavior unaffected. Group chat unaffected (different routing). Compatible with PR fix(imessage): dedupe reflected self-chat duplicates #38440'sSelfChatCache(orthogonal concerns). BlueBubbles uses different message keying and doesn't need this fix.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause / Regression History (if applicable)
is_from_me=true. The echo cache used text-based matching as a fallback when message IDs didn't match, but in self-chat the scope collision meant genuine user messages matched against agent reply text in the cache. Additionally,SentMessageCache.remember()was called optimistically before send, widening the window for false-positive matches.Regression Test Plan (if applicable)
self-chat-dedupe.test.ts(new, 19 tests),deliver.test.ts(3 updated tests)is_from_me=truemessages must dispatch (not be treated as echoes); genuine echoes must still be caught; TTL boundaries; text-only fallback when outbound IDs are junk ("ok"/"unknown"); ID short-circuit preservation with real GUIDs.User-visible / Behavior Changes
Diagram (if applicable)
Security Impact (required)
NoNoNoNoNoRepro + Verification
Environment
Steps
Expected
Actual
Evidence
Failing test/log before + passing after
Trace/log snippets
Screenshot/recording
Perf numbers (if relevant)
19 new tests in
self-chat-dedupe.test.ts(canary, TTL boundary, backward compat, chunked reply false-positive prevention, null-ID edge cases, text-only fallback, ID short-circuit preservation)3 updated tests in
deliver.test.tsfor post-send cache behavior70 tests passing across all modified files
118/118 iMessage extension tests passing
223/223 channel contract tests passing
48+ hour soak test on live self-chat with no message drops or duplications observed
Human Verification (required)
is_from_me=truemessages in self-chat (confirmed not exploitable — agents don't send media-only replies,selfChatCachecatches reflections); fallback ID ("ok"/"unknown") text-only cache behavior; TTL boundary at 4sCodex CLI Review Notes
Automated
codex review --base origin/mainidentified a theoretical gap: media-onlyis_from_me=truemessages in self-chat could bypass echo detection becausemessageTextis empty for attachment-only messages. Adversarial review confirmed this is not exploitable in practice — worst case is a single spurious dispatch, not amplification. Documented for potential future hardening in a separate PR.Post-review fix: Codex also flagged a P1 in the
has()method — whensendMessageIMessagereturns a fallback ID ("ok"/"unknown"), only text gets cached, but the ID-mismatch early return would incorrectly prevent text fallback for inbound echoes. Fixed: the early return now only fires when the scope actually has cached real message IDs. Two new tests cover both directions.Review Conversations
Compatibility / Migration
YesNoNoRisks and Mitigations
Risk: Tightened echo TTL (5s → 4s) could theoretically miss a slow echo on degraded networks.
selfChatCacheprovides a second layer of dedup foris_from_me=falsereflections regardless of TTL. 48-hour soak test showed no missed echoes.Risk:
skipIdShortCircuitparameter adds a code path that could be misused if called incorrectly in non-self-chat contexts.inbound-processing.ts. Non-self-chat paths are unchanged and don't pass this parameter.AI Disclosure
This PR was AI-assisted:
codex review --base origin/main)Files Changed
extensions/imessage/src/monitor/deliver.tsextensions/imessage/src/monitor/deliver.test.tsextensions/imessage/src/monitor/echo-cache.tsskipIdShortCircuitparameterextensions/imessage/src/monitor/inbound-processing.tsextensions/imessage/src/monitor/self-chat-dedupe.test.ts5 files changed, 615 insertions, 18 deletions