Skip to content

fix: delete messages outside the loaded transcript - #379

Merged
steipete merged 1 commit into
openclaw:mainfrom
logananderson:fix/delete-unloaded-messages
Nov 5, 2026
Merged

steipete merged 1 commit into
openclaw:mainfrom
logananderson:fix/delete-unloaded-messages

Conversation

@logananderson

@logananderson logananderson commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

What

delete-message returns queued but deletes nothing for most messages. -[IMChat deleteChatItems:] only acts on the chat's own loaded transcript items, which cover only the most recent messages, and silently ignores anything else. For an older message, findMessageItem builds a new chat item from IMChatHistoryController loadMessageWithGUID:, the delete ignores it, and the helper still reports success. Photos with a caption, tapbacks, and unsent messages don't delete either.

This change keeps the existing deleteChatItems: call and Recently Deleted behavior. It changes what gets passed in, and when to refuse:

  • It collects the chat's own transcript items for the message, one per part (a photo and its caption are two).
  • If there are none, it loads the transcript around the message with loadMessagesBeforeAndAfterGUID:numberOfMessagesToLoadBeforeGUID:numberOfMessagesToLoadAfterGUID:loadImmediately:threadIdentifier: (inside @try) and waits up to 2 s for it.
  • It passes every part to deleteChatItems:.

Behavior changes:

  • An older message, or a photo with a caption: moved to Recently Deleted (was queued, nothing deleted).
  • Tapbacks and other reactions (IMAssociatedMessageItem): not_started (was queued, nothing deleted).
  • A message with an unsent part (IMMessageRetractionChatItem): not_started (was queued, nothing deleted).
  • An older message that can't be loaded into the transcript (the selector is missing or throws, or the load times out): not_started, "Couldn't load this message in Messages; open the conversation in Messages and delete it there", instead of a false success.
  • Message not found is now not_started, like the other refusals before dispatch.
  • A recent message: unchanged.

Loading moves the chat's loaded window, so a Messages window showing that chat may jump. deleteIMMessageItems: also exists and deletes permanently, without Recently Deleted. It's left out to keep delete's current semantics.

docs/bridge.md and docs/rpc.md describe the behavior.

Tests

Tests/IMsgHelperTests/DeleteMessageTests.m runs handleDeleteMessage against a fake IMChat that does what IMChat did in the live runs: deleteChatItems: only removes loaded transcript items, a rebuilt item for a text message still deletes it, one for a photo with a caption deletes nothing, and unsent placeholders never delete. It covers:

  • a recent message
  • an older message
  • a photo with a caption, where both parts delete
  • a tapback
  • an unsent message
  • a chat without the loading selector
  • an unknown GUID

Failing first on main (5af52a0); the recent message passes there, as it does live:

$ clang <make test-helper flags> Tests/IMsgHelperTests/DeleteMessageTests.m -o .build/helper-tests/DeleteMessageTests && .build/helper-tests/DeleteMessageTests
FAIL: Success means Messages was given the older message's own transcript item
FAIL: Both the photo and its caption are deleted
FAIL: A tapback is refused before anything is dispatched
FAIL: An unsent message is refused before anything is dispatched
FAIL: Without the loading selector an older message fails closed
FAIL: An unknown message is refused before anything is dispatched
Delete message tests: 6 failure(s)
exit 1

All pass on this branch.

Live proof

Release imsg CLI and helper builds of main (5af52a0) and this branch (0478410; the later 7d66994 only rewords the load-failure error, which no live run reaches), swapped in as the injected helper and run on the same messages in one chat. in_chat comes from chat_message_join, and recently_deleted from chat_recoverable_message_join. "Newer" is the number of later messages in the chat at the time of each delete; it drops between rows and runs because earlier deletes removed newer messages. Phone numbers are replaced and GUIDs shortened.

target 15.6.1 newer 26.7.2 newer main this branch
text 151 101 queued, still in chat Recently Deleted
photo + caption 178 85 queued, still in chat Recently Deleted
threaded reply 105 150 queued, still in chat Recently Deleted; its thread parent stays
thread parent 102 156 queued, still in chat Recently Deleted; its reply stays
tapback 85 95 queued, still in chat not_started, still in chat
unsent 24 96 queued, still in chat not_started, still in chat
recent text 1-3 1-3 Recently Deleted Recently Deleted

The "thread companions" lines at the end of each log are the messages that must survive. On 15.6.1, 5E62202C is the deleted reply's thread parent and 4DD65153 is the deleted parent's reply. On 26.7.2, 307FFBFE is the deleted reply's thread parent, B7CF328C is another reply in that thread, and 39BB0E39 is the deleted parent's reply.

A photo with a caption fails on main even when it's the newest message. On a 26.6.2 (25G83) VM, the same message, with 0 newer messages:

main:

target: photo+caption, 1 file(s), 0 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message FF938A80…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

this branch:

target: photo+caption, 1 file(s), 0 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message FF938A80…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1
macOS 15.6.1 (24G90), main
macOS 15.6.1 (24G90)

target: text, 151 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message 5086AFD0…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: photo+caption, 1 file(s), 178 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message B3536AE9…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: threaded reply, 105 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message 4A91835D…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: thread parent, 102 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message 6DB84455…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: tapback, 85 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message EDE239C2…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: unsent, 24 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message C2768CD4…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: text, 2 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message 022B129B…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

thread companions:
5E62202C parent in_chat=1 recently_deleted=0
4DD65153 reply  in_chat=1 recently_deleted=0
macOS 15.6.1 (24G90), this branch
macOS 15.6.1 (24G90)

target: text, 150 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message 5086AFD0…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: photo+caption, 1 file(s), 176 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message B3536AE9…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: threaded reply, 104 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message 4A91835D…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: thread parent, 101 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message 6DB84455…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: tapback, 84 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message EDE239C2…
error: bridge_v2 delete-message failed (not_started): Dylib error: Tapbacks and other reactions can't be deleted on their own: EDE239C2… The operation did not start; retry is safe.
exit 1
after:  in_chat=1 recently_deleted=0

target: unsent, 23 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message C2768CD4…
error: bridge_v2 delete-message failed (not_started): Dylib error: Messages with an unsent part can't be deleted: C2768CD4… The operation did not start; retry is safe.
exit 1
after:  in_chat=1 recently_deleted=0

target: text, 1 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'iMessage;-;+15551234567' --message E4F70620…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

thread companions:
5E62202C parent in_chat=1 recently_deleted=0
4DD65153 reply  in_chat=1 recently_deleted=0
macOS 26.7.2 (25G332), main
macOS 26.7.2 (25G332)

target: text, 101 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 90E89D8C…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: photo+caption, 1 file(s), 85 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 80204F98…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: threaded reply, 150 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 6D9699D8…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: thread parent, 156 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message A7A3F561…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: tapback, 95 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 4F5B48BE…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: unsent, 96 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 4ECB53BB…
delete-message: queued
exit 0
after:  in_chat=1 recently_deleted=0

target: text, 3 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 022CAC1A…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

thread companions:
307FFBFE parent in_chat=1 recently_deleted=0
B7CF328C reply  in_chat=1 recently_deleted=0
39BB0E39 reply  in_chat=1 recently_deleted=0
macOS 26.7.2 (25G332), this branch
macOS 26.7.2 (25G332)

target: text, 100 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 90E89D8C…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: photo+caption, 1 file(s), 84 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 80204F98…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: threaded reply, 147 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 6D9699D8…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: thread parent, 152 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message A7A3F561…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

target: tapback, 93 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 4F5B48BE…
error: bridge_v2 delete-message failed (not_started): Dylib error: Tapbacks and other reactions can't be deleted on their own: 4F5B48BE… The operation did not start; retry is safe.
exit 1
after:  in_chat=1 recently_deleted=0

target: unsent, 94 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message 4ECB53BB…
error: bridge_v2 delete-message failed (not_started): Dylib error: Messages with an unsent part can't be deleted: 4ECB53BB… The operation did not start; retry is safe.
exit 1
after:  in_chat=1 recently_deleted=0

target: text, 1 newer messages in chat
before: in_chat=1 recently_deleted=0
$ imsg delete-message --chat 'any;-;+15551234567' --message CFF0CDB6…
delete-message: queued
exit 0
after:  in_chat=0 recently_deleted=1

thread companions:
307FFBFE parent in_chat=1 recently_deleted=0
B7CF328C reply  in_chat=1 recently_deleted=0
39BB0E39 reply  in_chat=1 recently_deleted=0

Checks

At 7d66994:

make lint         Done linting! Found 0 violations, 0 serious in 303 files.
make test         ✔ Test run with 505 tests in 5 suites passed
                  ✔ Test run with 350 tests in 5 suites passed
                  Delete message tests: 0 failure(s)
node --test scripts/build-docs-site.test.mjs   pass 2, fail 0
git diff --check  clean

Not run: the Linux gate (no Docker here).

Maintainer follow-up

Added a failing nested-reply regression and fixed transcript selection to require each entry’s own backing-message GUID. Deleting a reply cannot select its containing parent. The regression verifies refusal without an own entry and correct deletion with the parent preserved.

Focused synthetic regressions and independent P0–P2 review pass. Live Messages sends were not used during this round.

@clawsweeper

clawsweeper Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Nov 3, 2026
@clawsweeper

clawsweeper Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex review: needs maintainer review before merge.

What this changes

Delete all loaded transcript parts of a message, loading older messages when necessary and refusing unsupported targets before dispatch.

Example: Delete photo-and-caption message FF938A80… with zero newer messages on macOS 26.6.2.

  • Before: The CLI reports queued, but the message remains in the chat.
  • After: The message leaves the chat and appears in Recently Deleted.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused repair with strong shipped-entry-point evidence, useful regression coverage, and no remaining finding.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): Contributor logs exercise the released CLI and injected helper against existing Messages data on macOS 15.6.1 and 26.7.2, showing successful older-message and multipart deletion, preserved thread companions, unsupported-target refusals, and unchanged recent-message deletion; macOS 26.6.2 additionally covers a newest captioned photo. The latest commit changes only the already-reviewed load-failure diagnostic and its branch structure.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Product

Kind: Bug fix · Worth it: Yes · Fix scope: Complete
User problem: Deleting an older message or a captioned photo can report success while leaving the message in the chat.
Reason: The patch repairs an existing command contract with demonstrated user impact, retains its deletion semantics, and adds no configuration or public API.

Merge readiness

✅ Ready for maintainer review

This PR fixes a demonstrated deletion failure still present on main; the earlier diagnostic finding is resolved, and no actionable defect remains.

Priority: P2
Reviewed head: 7d66994eab85e9fb57a65bc5afd5f8bf5b7f27f1

Before merge

None.

Findings

None.

Agent review details

How this fits together

CLI and RPC deletion requests reach the injected Messages helper, which resolves the chat and message, obtains its transcript parts, and submits them to IMCore for recoverable deletion.

flowchart LR
A[CLI or RPC request] --> B[Injected helper]
B --> C[Resolve chat and message]
C --> D[Find or load transcript parts]
D --> E[Reject unsupported targets]
E --> F[IMCore recoverable deletion]
Loading

Technical review

Best possible solution:

Land the focused transcript-item repair while preserving Recently Deleted behavior and explicit pre-dispatch refusals.

Do we have a high-confidence way to reproduce the issue?

Main's single-item deletion path and the contributor's paired real CLI runs establish the failure; this read-only review ran no target code.

Is this the best way to solve the issue?

The patch addresses the required IMCore input directly, reuses existing lookup helpers, and preserves recoverable deletion instead of switching to permanent deletion.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 5af52a029b3d.

Provenance checked

Testing

Proof path: shipped entry point.

Security

None.

Evidence

What I checked:

Likely related people:

  • Peter Steinberger: Raw commit e01f113 adds Sources/IMsgHelper/MessageMutation.inc:5 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: e01f1133a420; files: Sources/IMsgHelper/MessageMutation.inc)

Review metrics

Metric Value Why it matters
Change distribution Production +81/-5; native tests +200; documentation +4/-1 The production change reuses existing lookup and mutation owners, while the fixture covers distinct success and refusal cases.

Labels

Label changes:

  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P2: Repairs demonstrated false-success behavior in the optional advanced deletion workflow with limited blast radius.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR.
  • proof: sufficient: Contributor real behavior proof is sufficient.

Rating scale

6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior.

Workflow

ClawSweeper edits this one comment on every review. Comment @clawsweeper re-review for a fresh review only. Repair and merge execution lanes are retired.

History

Review history (1 earlier review cycle)
  • reviewed 2026-11-03T12:13:13.679Z sha 0478410 :: needs changes before merge. :: [P3] Explain transcript-loading failures and offer a recovery action

Reviewed November 3, 2026, 7:25 AM ET / 12:25 UTC (Revision 2).

@logananderson

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@clawsweeper clawsweeper Bot added status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Nov 3, 2026
Load older messages into the transcript before deleting every part, and refuse unsupported reactions and unsent placeholders before dispatch (openclaw#379, thanks @logananderson).

Co-authored-by: Logan Anderson <poggus@gmail.com>
@steipete
steipete force-pushed the fix/delete-unloaded-messages branch from 7d66994 to c03bae2 Compare November 5, 2026 06:32
@steipete
steipete merged commit 5dc47b8 into openclaw:main Nov 5, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants