fix: cascade delete audio, memories, and tasks when deleting conversation#5573
fix: cascade delete audio, memories, and tasks when deleting conversation#5573
Conversation
… conversation Audio files are always deleted. Memories and action items are optionally deleted via query params, with vectors cleaned up in background tasks. Closes #4868
New dialog shows a checkbox to optionally delete associated memories and tasks when deleting a conversation. Defaults to unchecked.
Greptile SummaryThis PR implements cascade deletion of audio files, memories, action items, and Pinecone vectors when a conversation is deleted, and adds a "Don't show again" checkbox to the swipe-to-delete confirmation dialog. The delete confirmation message is updated across all 30+ supported locales to warn users that associated data will also be removed. Key changes:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User
participant FL as Flutter (confirmDismiss)
participant P as ConversationProvider
participant API as Backend DELETE /v1/conversations/{id}?cascade=true
participant FS as Firestore
participant BG as BackgroundTask
participant S3 as GCS Audio
participant PC as Pinecone
U->>FL: Swipe to delete
alt showDeleteConfirmation == false
FL-->>P: return true (skip offline check!)
else showDeleteConfirmation == true
FL->>FL: Check connectivity
alt Offline
FL-->>U: Show error dialog
else Online
FL-->>U: Show confirm dialog (with "Don't show again" checkbox)
U->>FL: Confirm
end
end
FL->>P: deleteConversationLocally()
P->>P: Remove from local state
P->>P: Schedule server delete (3s delay)
Note over P,API: After 3 seconds (or next swipe)
P->>API: DELETE conversation?cascade=true
API->>FS: delete_conversation (sync)
API->>PC: delete_vector (sync)
API->>FS: get_memory_ids_for_conversation (sync)
API->>FS: delete_memories_for_conversation (sync)
API->>FS: delete_action_items_for_conversation (sync)
API->>BG: add_task(delete_conversation_audio_files)
API->>BG: add_task(delete_memory_vector) × N
BG->>S3: Delete audio files
BG->>PC: Delete memory vectors
|
| confirmDismiss: (direction) async { | ||
| HapticFeedback.mediumImpact(); | ||
| bool showDeleteConfirmation = SharedPreferencesUtil().showConversationDeleteConfirmation; | ||
|
|
||
| if (!showDeleteConfirmation) return Future.value(true); | ||
|
|
There was a problem hiding this comment.
showConversationDeleteConfirmation preference silently dropped
The previous implementation respected a user preference (SharedPreferencesUtil().showConversationDeleteConfirmation) that allowed skipping the confirmation dialog on swipe-to-delete. That preference still exists in preferences.dart but is no longer read here, meaning users who had set it to false (no confirmation) will now always see the dialog — a silent behavioral regression.
If the intent is to always require confirmation for the new checkbox UX, the preference should either be migrated/removed from SharedPreferencesUtil or the code should explicitly document that the preference is intentionally ignored, rather than leaving a dead settings key.
| confirmDismiss: (direction) async { | |
| HapticFeedback.mediumImpact(); | |
| bool showDeleteConfirmation = SharedPreferencesUtil().showConversationDeleteConfirmation; | |
| if (!showDeleteConfirmation) return Future.value(true); | |
| confirmDismiss: (direction) async { | |
| HapticFeedback.mediumImpact(); |
| @@ -221,13 +206,27 @@ | |||
| ), | |||
| context: context, | |||
| ); | |||
| return false; | |||
| } | |||
There was a problem hiding this comment.
Offline error dialog not awaited — Dismissible snaps back immediately
showDialog is called without await, so return false executes synchronously and the Dismissible animation snaps the item back while the error dialog appears simultaneously. In the previous code the confirmDismiss future was the dialog future itself, meaning the item stayed in its swipe position until the dialog was dismissed. Consider awaiting the dialog so the two events are sequenced:
| if (!connectivityProvider.isConnected) { | |
| await showDialog( | |
| builder: (c) => getDialog( | |
| context, | |
| () => Navigator.pop(context), | |
| () => Navigator.pop(context), | |
| context.l10n.unableToDeleteConversation, | |
| context.l10n.pleaseCheckInternetConnectionAndTryAgain, | |
| singleButton: true, | |
| okButtonText: context.l10n.ok, | |
| ), | |
| context: context, | |
| ); | |
| return false; | |
| } |
|
|
||
| return {"status": "Ok"} | ||
|
|
||
|
|
There was a problem hiding this comment.
204 No Content response must not return a body
The endpoint is decorated with status_code=204 but the function returns {"status": "Ok"}. HTTP 204 (No Content) must have no response body. FastAPI/Starlette typically strips the body, but the return type is misleading and some proxy layers may surface this. The response body should either be removed or the status code changed to 200.
| return |
|
|
||
| if delete_memories: | ||
| memory_ids = memories_db.get_memory_ids_for_conversation(uid, conversation_id) | ||
| memories_db.delete_memories_for_conversation(uid, conversation_id) | ||
| for memory_id in memory_ids: | ||
| background_tasks.add_task(delete_memory_vector, uid, memory_id) | ||
|
|
There was a problem hiding this comment.
Memory vectors deleted after Firestore records — orphan risk on crash
delete_memories_for_conversation is called synchronously (removing Firestore records), and the Pinecone vector deletes are then scheduled as background tasks. If the process dies after the Firestore commit but before the background tasks complete, the Pinecone vectors for those memories will be permanently orphaned because the document IDs are now gone and can't be re-fetched.
Consider either:
- Fetching and passing the memory IDs into background tasks that handle both Firestore deletion and vector deletion atomically, or
- Moving the Firestore deletion itself into a background task alongside the vector deletion so both fail or succeed together.
…onversation Remove optional query params — associated data is now always deleted.
Always delete associated data — no checkbox needed. Show informational message.
Backend now always deletes associated data.
…ider Simplify delete flow — no longer need per-conversation option tracking.
Use bool return from simplified dialog.
Remove deleteOptions state — dialog now returns simple bool.
…edData key Updated message to inform users that memories, tasks, and audio files will be deleted. Removed unused deleteAssociatedData key from all locales.
|
@greptile-apps re-review |
backend/routers/conversations.py
Outdated
| # Always delete associated memories and their vectors | ||
| memory_ids = memories_db.get_memory_ids_for_conversation(uid, conversation_id) | ||
| memories_db.delete_memories_for_conversation(uid, conversation_id) | ||
| for memory_id in memory_ids: | ||
| background_tasks.add_task(delete_memory_vector, uid, memory_id) | ||
|
|
||
| # Always delete associated action items | ||
| action_items_db.delete_action_items_for_conversation(uid, conversation_id) |
There was a problem hiding this comment.
Memories and action items always deleted — opt-out never implemented
The PR description states "Optionally deletes associated memories and tasks via a new checkbox in the delete confirmation dialog" and "Backend accepts delete_memories and delete_action_items query params", but neither is implemented. Memories and action items are unconditionally destroyed for every conversation deletion, with no way to opt out.
This is a breaking behavioral change for users who expect to delete a conversation (e.g., to free audio storage) while retaining the AI-extracted memories and tasks derived from it. The previous implementation preserved memories on conversation deletion.
The UI message was updated to say "This will also delete associated memories, tasks, and audio files", but the dialog still lacks the described checkbox — users are simply told everything will be deleted and cannot choose otherwise.
If unconditional deletion is the intended design, the PR description and test plan should be corrected, and the decision to remove user control should be explicit. If the checkbox was meant to be included, it needs to be added to showDeleteConversationDialog and the backend should accept delete_memories / delete_action_items query parameters to honour the user's choice.
app/lib/widgets/dialog.dart
Outdated
| Future<bool> showDeleteConversationDialog(BuildContext context) async { | ||
| return await showDialog<bool>( | ||
| context: context, | ||
| builder: (ctx) { | ||
| final actions = [ | ||
| TextButton( | ||
| onPressed: () => Navigator.of(ctx).pop(false), | ||
| child: Text(context.l10n.cancel, style: const TextStyle(color: Colors.white)), | ||
| ), | ||
| TextButton( | ||
| onPressed: () => Navigator.of(ctx).pop(true), | ||
| child: Text(context.l10n.confirm, style: const TextStyle(color: Colors.red)), | ||
| ), | ||
| ]; | ||
|
|
||
| if (PlatformService.isApple) { | ||
| return CupertinoAlertDialog( | ||
| title: Text(context.l10n.deleteConversationTitle), | ||
| content: Text(context.l10n.deleteConversationMessage), | ||
| actions: actions, | ||
| ); | ||
| } | ||
| return AlertDialog( | ||
| title: Text(context.l10n.deleteConversationTitle), | ||
| content: Text(context.l10n.deleteConversationMessage), | ||
| actions: actions, | ||
| ); | ||
| }, | ||
| ) ?? | ||
| false; | ||
| } |
There was a problem hiding this comment.
context vs ctx — stale outer context used for l10n inside builder
The builder closes over the outer context (the one passed into showDeleteConversationDialog) to call context.l10n.*, while the inner ctx is only used for Navigator.of(ctx).pop(...). If the widget that owns context is unmounted while the dialog is animating open (e.g., a race on the detail page), accessing context.l10n inside the builder will use a detached BuildContext.
The safer pattern is to resolve all l10n strings before opening the dialog (or inside the builder using ctx through Localizations.of<AppLocalizations>(ctx, AppLocalizations)!):
builder: (ctx) {
final l10n = Localizations.of<AppLocalizations>(ctx, AppLocalizations)!;
final actions = [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(l10n.cancel, style: const TextStyle(color: Colors.white)),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(l10n.confirm, style: const TextStyle(color: Colors.red)),
),
];
...
}No need for a separate function — the dialog is a simple confirm/cancel.
Use getDialog directly and check showConversationDeleteConfirmation.
Return showDialog result directly in else branch.
Checkbox in the swipe-to-delete dialog lets users disable future confirmation prompts via showConversationDeleteConfirmation preference.
…cascade message Users who previously disabled the delete confirmation dialog will see it once after this update, so they're aware that memories and tasks are now also deleted. The migration flag ensures this only happens once.
…ompatibility Old app versions calling DELETE without ?cascade=true keep existing behavior (conversation + vectors only). New app sends cascade=true to also delete audio, memories, tasks, and memory vectors.
|
@greptile-apps re-review |
Summary
delete_memoriesanddelete_action_itemsquery paramsCloses #4868
Test plan
🤖 Generated with Claude Code