Skip to content

fix(backend): cascade every subcollection when a conversation is deleted#10597

Merged
kodjima33 merged 2 commits into
mainfrom
watchdog/delete-conversation-subcollection-cascade
Jul 26, 2026
Merged

fix(backend): cascade every subcollection when a conversation is deleted#10597
kodjima33 merged 2 commits into
mainfrom
watchdog/delete-conversation-subcollection-cascade

Conversation

@kodjima33

@kodjima33 kodjima33 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Bug

Deleting a conversation left most of its child data in Firestore permanently.

database.conversations.delete_conversation() purged only the photos subcollection and then deleted the document. Firestore does not cascade, and a conversation owns more children than photos:

  • deepgram_streaming / soniox_streaming / speechmatics_streaming / fal_whisperx — the per-provider post-processing transcripts written by store_model_segments_result, i.e. verbatim TranscriptSegment text
  • the Hume emotion predictions written by store_model_emotion_predictions_result
  • analytics_markers/conversation_memories_extracted — the marker added five hours ago by feat(desktop): instrument macOS memory activation and extraction outcomes #10543 (try_claim_conversation_memory_analytics), so every conversation with extracted memories now grows one

Everything but photos survived DELETE /v1/conversations/{id} — and survived it permanently. users.delete_user_data() walks each collection with limit().stream(), which returns only existing documents; a deleted conversation is not one, so the wipe never descends into the orphans it left behind. Neither the delete endpoint nor "delete my account" can reach that data afterwards.

Fix

delete_conversation() now enumerates the conversation's children live (conversation_ref.collections()) and purges each recursively before deleting the document, so a subcollection added later is covered by construction rather than by remembering to add another special case.

The recursive purge account deletion already used moves to database/_client.py as delete_collection_recursive(ref, *, client) and is shared by both callers instead of being duplicated. delete_conversation_photos() is unchanged — its other callers (lifecycle.py, merge_conversations.py) still use it.

What I tested

  • New regression test backend/tests/unit/test_delete_conversation_cascade.py (3 tests) — drives production delete_conversation against an in-memory Firestore fake. 2 of 3 fail on the pre-fix code (the transcript segments and the analytics marker survive; a nested child is left dangling) and all 3 pass on the fix.
  • Full backend unit suite via backend/test.sh (740 files, one process per file): exit 0.
  • Adjacent suites re-run individually: test_account_deletion.py (34), test_delete_account_purge_storage.py (5), test_conversation_recordings_purge.py, test_merge_conversations_canonical_delete.py, test_listen_fallback_removal.py, test_dev_api_lock_bypass.py, test_developer_from_segments_idempotency.py, test_conversation_memories_telemetry.py (18) — all pass.
  • make preflight green.

Not exercised against live Firestore (no prod write from a watchdog run); the behaviour is proven through the production code path with the storage layer faked.

Known follow-up (not in this PR)

Conversations deleted before this lands still have orphaned children, and account deletion cannot see them because its walk is query-based. Reaching those needs list_documents() (which does return references to non-existent parents) in the account-deletion walk — a change to the irreversible wipe path that deserves its own review.

Failure-Class: none

🤖 automated by the hourly watchdog — tested and merged

Review in cubic

delete_conversation() purged only the `photos` subcollection before deleting
the document. Firestore does not cascade, and a conversation owns more children
than photos: the per-provider post-processing transcripts
(deepgram_streaming / soniox_streaming / speechmatics_streaming / fal_whisperx,
which store verbatim TranscriptSegment text), the Hume emotion predictions, and
the analytics_markers marker added by the memory-extraction telemetry.

Everything but photos therefore survived a user deleting a conversation — and
survived it permanently: the account-deletion wipe walks *existing* documents
(`limit().stream()`), and a deleted conversation is not one, so its orphaned
children are unreachable by both the delete endpoint and "delete my account".

Enumerate the children live (`conversation_ref.collections()`) and purge each
one recursively, so any subcollection added later is covered by construction.
The recursive purge that account deletion already used moves to database/_client
as `delete_collection_recursive(ref, client=...)` and is now shared by both
callers instead of being duplicated.

## Verification
- backend/tests/unit/test_delete_conversation_cascade.py (new, 3 tests): 2 of 3
  fail against the pre-fix delete_conversation (the transcript segments and the
  analytics marker survive; nested children are left dangling) and all 3 pass on
  the fix.
- Full backend unit suite via `backend/test.sh` (740 files, per-file processes):
  exit 0.
- Adjacent suites re-run individually: test_account_deletion.py (34),
  test_delete_account_purge_storage.py (5), test_conversation_recordings_purge.py,
  test_merge_conversations_canonical_delete.py, test_listen_fallback_removal.py,
  test_dev_api_lock_bypass.py, test_developer_from_segments_idempotency.py,
  test_conversation_memories_telemetry.py (18) — all pass.

Failure-Class: none
Six unit modules replace database._client with a hand-built stub that
enumerates exactly what production imports from it. Moving the recursive
subcollection purge into that module added a name none of them carried, so
importing database.users raised ImportError at collection time.
@kodjima33
kodjima33 force-pushed the watchdog/delete-conversation-subcollection-cascade branch from 0c52aee to 451e832 Compare July 26, 2026 12:18
@kodjima33
kodjima33 merged commit 80b9c4e into main Jul 26, 2026
38 checks passed
@kodjima33
kodjima33 deleted the watchdog/delete-conversation-subcollection-cascade branch July 26, 2026 12:32
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.

1 participant