Skip to content

fix: cascade delete audio, memories, and tasks when deleting conversation#5573

Merged
mdmohsin7 merged 23 commits intomainfrom
fix/delete-conversation-cascade-cleanup
Mar 13, 2026
Merged

fix: cascade delete audio, memories, and tasks when deleting conversation#5573
mdmohsin7 merged 23 commits intomainfrom
fix/delete-conversation-cascade-cleanup

Conversation

@mdmohsin7
Copy link
Member

@mdmohsin7 mdmohsin7 commented Mar 11, 2026

Summary

  • When deleting a conversation, always deletes associated audio files, memories, tasks, and vectors — no optional toggle
  • Updated delete confirmation dialog description to inform users that memories, tasks, and audio files will also be deleted
  • Added "Don't show again" checkbox to swipe-to-delete dialog so users can skip future confirmations
  • Memory vectors are cleaned up from Pinecone as a background task
  • Backend endpoint simplified — removed delete_memories and delete_action_items query params

Closes #4868

Test plan

  • Swipe-to-delete a conversation → dialog shows with updated description and "Don't show again" checkbox
  • Confirm delete → conversation, audio, memories, and tasks all deleted
  • Check "Don't show again" + confirm → future swipe deletes skip the dialog
  • Delete from conversation detail page menu → same dialog (without checkbox)
  • Cancel dialog or tap outside → no deletion occurs
  • Offline → shows error dialog, no deletion
  • Undo (3-second snackbar) works correctly after swipe delete

🤖 Generated with Claude Code

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This 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:

  • Backend: DELETE /v1/conversations/{id} gains a cascade query param; when true, it synchronously deletes Firestore records (memories, action items) and queues audio file + Pinecone vector deletions as background tasks.
  • Flutter client: deleteConversationServer hardcodes cascade=true, making cascade permanent for all in-app deletions. A SharedPreferences migration resets the "skip confirmation" flag on first launch so existing users see the updated warning once.
  • UX: The swipe-to-delete dialog is rebuilt inline with a StatefulBuilder + Checkbox; the detail-page delete dialog is unchanged (no checkbox there).
  • Regression: The offline connectivity check in confirmDismiss is placed after the early-return for showDeleteConfirmation == false, so users who have checked "Don't show again" can swipe-delete while offline with no error feedback — the conversation disappears locally but the server deletion silently fails.

Confidence Score: 3/5

  • Not safe to merge as-is — the offline bypass regression allows silent data loss (local state desync) and breaks a stated test-plan requirement.
  • The cascade delete logic itself is sound and the localization updates are thorough. However, the offline connectivity check being placed after the showDeleteConfirmation early-return is a clear regression: users who have opted out of the confirmation dialog can silently delete while offline, with the conversation vanishing from the local list but remaining on the server. Combined with the pre-existing issues flagged in earlier rounds (204 with body, orphan Pinecone vectors), the backend is also not fully consistent. These issues prevent a safe merge without fixes.
  • app/lib/pages/conversations/widgets/conversation_list_item.dart (offline bypass around line 196) requires the most urgent attention before merging.

Important Files Changed

Filename Overview
backend/routers/conversations.py Adds cascade delete logic (audio, memories, action items, vectors) behind a cascade query param. Known issues: 204 status code returns a response body (semantics violation), Firestore memory records are deleted synchronously before async Pinecone vector cleanup (orphan risk on crash), and the endpoint is not guarded — all synchronous deletions run sequentially without a transaction, so a crash mid-sequence leaves partial data.
app/lib/pages/conversations/widgets/conversation_list_item.dart Replaces the reusable getDialog call with an inline StatefulBuilder dialog that includes a "Don't show again" checkbox; the checkbox correctly updates SharedPreferences on confirm. Critical regression: the offline connectivity guard is placed after the showDeleteConfirmation early-return, so users who checked "Don't show again" can silently delete conversations while offline.
app/lib/backend/preferences.dart Introduces a one-time migration in the showConversationDeleteConfirmation getter: on first read after update, it forces the preference back to true so existing users see the updated cascade-delete warning. Side-effect-in-getter is unconventional but intentional; works correctly for the migration goal.
app/lib/backend/http/api/conversations.dart Hardcodes cascade=true on the DELETE endpoint URL so every in-app deletion always cascades. Simple, intentional change; the response-code check (statusCode == 204) is compatible with how FastAPI strips 204 bodies.
app/lib/widgets/dialog.dart Minor whitespace-only reformatting of the TextButton in getDialog; no behavioral change.
app/lib/pages/conversation_detail/page.dart Removes an unnecessary nested block {{...}} around two lines in the delete confirm callback; no behavioral change.
app/lib/l10n/app_en.arb Updates deleteConversationMessage to describe cascade effects and adds dontShowAgain key. The new key is missing its @dontShowAgain metadata block, which could cause the doc comment to be lost on future flutter gen-l10n runs.

Sequence Diagram

sequenceDiagram
    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
Loading

Comments Outside Diff (1)

  1. app/lib/pages/conversations/widgets/conversation_list_item.dart, line 193-196 (link)

    Offline check bypassed when "Don't show again" is active

    When showConversationDeleteConfirmation is false (user has previously checked "Don't show again"), confirmDismiss returns true immediately on line 196 — before the connectivity check at line 198 is reached. If the user is offline, the Dismissible will dismiss and onDismissed will call deleteConversationLocally, which removes the conversation from local state immediately and schedules a server deletion 3 seconds later. That server call (deleteConversationServer) silently returns false on network failure (its return value is ignored in deleteConversationOnServer). The result is the conversation disappears from the local UI but remains on the server, reappearing the next time the list is fetched from the server.

    The test plan item "Offline → shows error dialog, no deletion occurs" will fail whenever showConversationDeleteConfirmation is false.

    The connectivity check should run regardless of whether the dialog is suppressed:

Last reviewed commit: 20d2ab5

Comment on lines 191 to 193
confirmDismiss: (direction) async {
HapticFeedback.mediumImpact();
bool showDeleteConfirmation = SharedPreferencesUtil().showConversationDeleteConfirmation;

if (!showDeleteConfirmation) return Future.value(true);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
confirmDismiss: (direction) async {
HapticFeedback.mediumImpact();
bool showDeleteConfirmation = SharedPreferencesUtil().showConversationDeleteConfirmation;
if (!showDeleteConfirmation) return Future.value(true);
confirmDismiss: (direction) async {
HapticFeedback.mediumImpact();

Comment on lines +196 to +210
@@ -221,13 +206,27 @@
),
context: context,
);
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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"}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return

Comment on lines +221 to +227

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)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Fetching and passing the memory IDs into background tasks that handle both Firestore deletion and vector deletion atomically, or
  2. 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.
@mdmohsin7
Copy link
Member Author

@greptile-apps re-review

Comment on lines +218 to +225
# 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +7 to +37
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@mdmohsin7 mdmohsin7 changed the title fix: delete associated audio, memories, and tasks when deleting conversation fix: cascade delete audio, memories, and tasks when deleting conversation Mar 13, 2026
…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.
@mdmohsin7
Copy link
Member Author

@greptile-apps re-review

@mdmohsin7 mdmohsin7 merged commit 54580bf into main Mar 13, 2026
2 checks passed
@mdmohsin7 mdmohsin7 deleted the fix/delete-conversation-cascade-cleanup branch March 13, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deleting summaries does not delete the underlying transcript or audio file

1 participant