Repository navigation
GO-7167 Soft-delete discussion comments - #3324
Merged
requilence merged 19 commits intoOct 12, 2026
Merged
Conversation
Discussion comments become tombstones on delete instead of disappearing, so replies keep their thread. The wire op stays the existing Delete; only discussions materialize it differently.
Separate tombstone-inclusive reads (windows, backfill) from live-only ones (search browse, last-message date); require local targets before pushing modifies or deletes; abort the apply on a tombstone write failure; adjust counters at once instead of only after the reload; pin batches preflight; v2/v1 error mapping for the races; record the inherited pre-commit side-effect limitation.
Read the reply parent through the apply transaction so replayed replies keep their mention; keep pending events of entries a full window evicts; tell the two SSE streams apart for a create and delete in one batch; serve author only when resolved; document the same-profile downgrade boundary; add ChatSearch service tests.
It is a pre-existing bug unrelated to soft delete; record it under the accepted limitations for its own ticket and qualify the mention claim.
ChatMessage.deleted marks a soft-deleted discussion comment. The model stores it only when true and can turn a message into its tombstone form; a create never arrives deleted.
DeleteModeKeep tells applyDelete the handler already rewrote the document inside the apply transaction and it must stay.
CountMessages subtracts tombstones (a sparse index on deleted counts them), mark-unread skips them, the fulltext iterator skips them, and GetLastLiveMessages / GetLastMessagesByCreators return live messages only. GetLastMessages keeps them: it seeds subscription windows.
A discussion's Delete op rewrites the message into its tombstone and keeps it (DeleteModeKeep): author, time, reply target and place stay, content, attachments, reactions and the pin go. The manager's SoftDelete sends the tombstone as an update and drops the message from the counters at once; the post-commit reload still runs. Modifies of a tombstone are dropped at apply time, so delete wins in either order, and chat modifies become update-only so a reaction or pin after a hard delete no longer re-creates a stub. A failed tombstone write is critical.
…s as last message When a full window evicts its oldest entry, unflushed updates (a tombstone, an edit, a reaction, a pin) move to the out-of-window events instead of being dropped; an unflushed Add is still dropped. GetLastMessage returns the newest live message.
Edit, reaction and pin need a live, locally stored target (ErrMessageDeleted / ErrMessageNotFound); a new reply to a deleted comment is refused (ErrReplyToDeletedMessage); deleting a missing or deleted message succeeds without a change. SetMessagesPinned checks every target before pushing any pin. The RPCs report the three as BAD_INPUT.
A fulltext hit whose message is now a tombstone is dropped, and browse without creators reads live messages only (GetLastLiveMessages).
Messages carry deleted: true on a tombstone. Edit, reaction and delete of a deleted comment are a 404 and a reply to one a 400 on reply_to, dry runs included; the middleware's deleted/missing refusals map to the same statuses when a delete races the check. The message_count description says a deleted comment stays but isn't counted.
Messages carry deleted on a tombstone, in reads and stream events, and a reaction the middleware refuses as bad input (a deleted comment) is a 400 instead of a 500.
Status events (read, mention read, synced, reaction read) exist only for in-window messages; a kept one could not be corrected by a later status change in the same batch and was sent stale.
Expected tombstones come from the message as it was before the delete, and its stored order object is compared; Owner and Admin both delete; a repeat delete makes no observer change and no fulltext work; the in-batch counter test covers the mention counter.
The tombstone-aware repository queries are built in one place and the index tests explain those exact queries; tests cover the v1 reaction 400 at the handler, the last-message fallback past a tombstone-only window, an indexer backfill skipping a tombstone, and creatorless browse only (the creator read is live-only in the repository). The served message_count field says a deleted comment isn't counted.
…atus changes A status change (read, mention read, synced, reaction read) of a message outside the window sends no event of its own, but a pending full update of that message is still sent: it now carries the new value instead of the one it was queued with. The unflushed-Add eviction test now gives the evicted message an update too, so it guards the Add check.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Deleting a discussion comment removes it from the store and clients get
Event.Chat.Deletewith only the id. There is nothing left to render the designed "Comment deleted" placeholder from, and every reply under the comment disappears: clients drop a reply whosereplyToMessageIdchain hits a missing message. Android replies to the tapped message and API agents reply to the message they answer, so this applies to any comment in a thread, not just a post. The replies stay in storage and keep syncing.Design:
docs/superpowers/specs/2026-10-12-discussion-soft-delete-design.md.Change
Storage (discussions only). The store
Deleteop is unchanged on the wire. In a discussion,ChatHandler.BeforeDeleteturns the message into a tombstone and keeps it (newstorestate.DeleteModeKeep).createdAt,replyToMessageIdand its place (_o).deleted: true(newChatMessage.deleted = 18, sparse index) and is always read and never a mention.modifiedAtis reset (all clients show "(edited)" otherwise).Apply rules.
ErrCritical).Counters and events.
SoftDeletesendsEvent.Chat.Updatecarrying the tombstone and drops the message from the unread, mention and message counters at once, plus the post-commit reload.messageCountandGetLastMessage(lastMessageDate) skip tombstones.Local guards.
ErrMessageDeleted/ErrMessageNotFound,BAD_INPUT).ErrReplyToDeletedMessage).SetMessagesPinnedchecks every target before pinning any.Search. Full-text hits on tombstones are dropped, and browse reads live messages only. The full-text index removes the comment and never re-adds it.
API v2.
deleted: truewith emptytext./reply_to, dry runs included.message_countdescriptions updated; OpenAPI and the wrapper golden regenerated.API v1.
deletedfield; a reaction to a tombstone returns 400 instead of 500.Compatibility
Deleteop as a hard delete, exactly as today.deleted. It renders a tombstone as a comment with author and date and no body (checked on desktop, iOS and Android; none crash), and the replies stay attached. Client work:post.tsx:439), otherwise the deleter sees the comment and its replies vanish until reload; follow the reply chain to the root;Not in this PR
BeforeCreateskips a message when its full-text enqueue fails.Tests
softdelete_test.gofiles inchatobject,chatrepository,chatsubscriptionandchats, andchat_softdelete_test.goinapi/v2/service, plus storestate, chatmodel, indexer, v1 handler and service tests.go test ./core/... ./pkg/... ./space/... ./util/...passes.