fix: delete steps and elements on message edit#2856
fix: delete steps and elements on message edit#2856tonca wants to merge 5 commits intoChainlit:mainfrom
Conversation
There was a problem hiding this comment.
3 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/chainlit/message.py">
<violation number="1" location="backend/chainlit/message.py:141">
P2: Unhandled exceptions in `remove_children()` can abort `remove()` before UI deletion and ignore best-effort persistence semantics when `fail_on_persist_error` is false.</violation>
</file>
<file name="backend/tests/test_message.py">
<violation number="1" location="backend/tests/test_message.py:828">
P2: Set-based delete-step assertion hides call order and duplicate deletions, weakening regression coverage for ordered descendant cleanup.</violation>
<violation number="2" location="backend/tests/test_message.py:878">
P2: AsyncMock deletion methods are asserted with call-based checks instead of await-based checks, so tests can miss regressions where deletions are no longer awaited.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
1 issue found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="backend/chainlit/message.py">
<violation number="1" location="backend/chainlit/message.py:141">
P1: Unhandled exceptions in `remove_children()` can abort `remove()` and prevent UI step deletion, violating the class’s persistence-error handling behavior.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
backend/chainlit/message.py
Outdated
| await data_layer.delete_step(step_id) | ||
|
|
||
| orphaned_elements = [e for e in elements if e.get("forId") in descendant_id_set] | ||
| await asyncio.gather( |
There was a problem hiding this comment.
since data layers already delete the elements in delete_step method. there might be no need to call delete_element here right ? @cubic-dev-ai
There was a problem hiding this comment.
@Allaoua9 You're likely correct that for most standard data layer implementations (like the LiteralDataLayer), calling delete_step already triggers a cascading deletion of associated elements. In those cases, the explicit call to delete_element is indeed redundant.
However, there are a few reasons why this explicit cleanup is likely included:
- Interface Guarantees: The
BaseDataLayerinterface does not strictly enforce thatdelete_stepmust handle element cleanup. By explicitly deleting the elements in theMessageclass, Chainlit ensures that custom data layer implementations (e.g., a simple file-based or custom SQL implementation) don't accidentally leave "orphaned" elements behind if they haven't implemented cascading deletes. - Completeness: While
delete_stepmight handle elements directly tied to that specific step, theremove_childrenlogic is designed to be a "clean sweep" of the entire descendant tree. Including the elements explicitly makes the intent of the method clear and ensures nothing is missed regardless of the backend's internal logic. - UI Sync: It ensures that the elements are cleared out logically before the final
emitter.delete_stepcall notifies the frontend, providing a consistent state.
In short: it's a safeguard for consistency across different data layer implementations. If your data layer already handles this, these extra calls will simply be no-ops or redundant 404s, but they prevent leaks for users with simpler custom backends.
One thing to note: delete_step definitely does not recursively delete child steps (descendants in the tree) in most backends, which is why the recursive collect_descendants logic for the steps themselves is absolutely necessary.
hayescode
left a comment
There was a problem hiding this comment.
Please make descendant cleanup consistent across all supported data layers before approval. Right now the new deletion path can still leave orphaned persisted records behind.
| ordered_descendant_ids = collect_descendants(self.id) | ||
|
|
||
| for step_id in ordered_descendant_ids: | ||
| await data_layer.delete_step(step_id) |
There was a problem hiding this comment.
This is a persistence-affecting change, so we need consistent behavior across all supported data layers. Right now this path relies on delete_step(), but DynamoDBDataLayer.delete_step() only deletes the step item and does not clean up attached elements/feedback. That means edits can still leave orphaned persisted records behind. Please either delete descendant attachments explicitly here or update every affected data layer and test coverage to support the new semantics.
Problem
When a user edits a message, child steps and elements from previous executions were never deleted from the data layer. This caused all historical steps to accumulate and reappear in the UI after each edit.
Summary by cubic
Deletes descendant steps when a message is edited or removed, preventing old executions from reappearing and keeping the thread clean. Elements are not deleted.
Message.remove_children()to recursively delete descendant steps in leaves-first order, never deleting the root.remove_children()on edit (socket.edit_message) and delete (Message.remove) to keep data consistent.Written for commit 92b3399. Summary will update on new commits.