fix(conversations): reject soft-deleted conversations from merge#10262
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks for the narrow fix and for adding regression coverage around the validator path. The new validate_merge_compatibility check is directionally right and the current unit test file passes locally (python -m pytest tests/unit/test_merge_validation.py -q → 40 passed).
I do think one privacy/data-deletion edge still needs to be closed before merge: perform_merge_async re-fetches the source conversations later, after the endpoint validation and after the background task has been queued. If one of those source docs becomes a soft-deleted tombstone between admission and the background worker's fetch, the worker still proceeds to merge its transcript/photos/audio into the new visible conversation. That leaves the delete-vs-merge race described in the PR body partially open.
Please add a final deleted-source guard in the background merge path after the worker fetches the source docs and before it merges raw data/copies artifacts, with a focused regression test for “source becomes deleted=True after initial validation/admission”. The failure path should abort the merge rather than creating a visible merged conversation from deleted content.
Once that background-path guard is in place, this looks like a good, well-scoped privacy fix.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
Addresses review on BasedHardware#10262. validate_merge_compatibility guards admission, but perform_merge_async re-fetches the sources in the background task; a source that becomes a soft-deleted tombstone between admission and that fetch (the delete-vs- merge race) would still be merged. Add a deleted-source guard right after the background fetch, before any content is read, aborting into _handle_merge_failure rather than building a visible conversation from deleted content. Regression: test_merge_deleted_source_race.py — abort when a source is deleted after admission (create_processing_conversation never called), plus a control that a non-deleted pair passes the guard. Verified: pytest test_merge_deleted_source_race.py test_merge_validation.py test_merge_audio_chunk_copy_failure.py -> 46 passed. Failure-Class: none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch — fixed. Regression in |
kodjima33
left a comment
There was a problem hiding this comment.
fix(conversations): reject soft-deleted conversations from merge — bug fix; held (unrelated desktop-swift check red)
597dea1 to
8a9740c
Compare
Addresses review on BasedHardware#10262. validate_merge_compatibility guards admission, but perform_merge_async re-fetches the sources in the background task; a source that becomes a soft-deleted tombstone between admission and that fetch (the delete-vs- merge race) would still be merged. Add a deleted-source guard right after the background fetch, before any content is read, aborting into _handle_merge_failure rather than building a visible conversation from deleted content. Regression: test_merge_deleted_source_race.py — abort when a source is deleted after admission (create_processing_conversation never called), plus a control that a non-deleted pair passes the guard. Verified: pytest test_merge_deleted_source_race.py test_merge_validation.py test_merge_audio_chunk_copy_failure.py -> 46 passed. Failure-Class: none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@Git-on-my-level the TOCTOU race you flagged is fixed in |
reprocess_conversation fetches through _get_valid_conversation_by_id, which does not filter soft-deleted tombstones (get_conversation returns them; the helper only 404s on a missing doc), then runs process_conversation with force_process=True — regenerating structured data, action items, memories and embeddings. Reprocessing a tombstone (via a direct API call, or a delete-vs- reprocess race) therefore resurrects content the user deleted back into their memories and derived data. Reject a deleted conversation while still allowing a discarded one, which reprocess intentionally revives. Third instance of the tombstone-eligibility contract (sync BasedHardware#10119, merge BasedHardware#10262); extract the shared is_soft_deleted predicate and converge eligible_merge_target onto it. Verified: pytest test_reprocess_tombstone_guard.py test_sync_merge_target_selection.py test_merge_validation.py -> 49 passed (behaviour-preserving for eligible_merge_target). Failure-Class: none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolved on current head 8a9740c: background merge now re-checks deleted sources after worker fetch and before content merge; regression coverage added.
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks for the follow-up. I re-reviewed the current head (8a9740cfbe) and the blocker from my earlier review looks resolved: perform_merge_async now re-checks the re-fetched source conversations for deleted=True before transcript/photo/audio merge work starts, and aborts through _handle_merge_failure instead of creating a visible merged conversation from tombstoned content.
I also ran the relevant backend merge unit coverage locally with secrets stripped:
python -m pytest tests/unit/test_merge_validation.py tests/unit/test_merge_deleted_source_race.py tests/unit/test_merge_audio_chunk_copy_failure.py tests/unit/test_merge_conversations_canonical_delete.py -q
Result: 49 passed, 8 warnings.
I’m leaving this as a positive maintainer signal rather than a formal approval because this touches a privacy/data-deletion invariant and the PR is still labeled for security review, but from my pass the requested TOCTOU guard and regression coverage are now in place.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
Addresses review on BasedHardware#10262. validate_merge_compatibility guards admission, but perform_merge_async re-fetches the sources in the background task; a source that becomes a soft-deleted tombstone between admission and that fetch (the delete-vs- merge race) would still be merged. Add a deleted-source guard right after the background fetch, before any content is read, aborting into _handle_merge_failure rather than building a visible conversation from deleted content. Regression: test_merge_deleted_source_race.py — abort when a source is deleted after admission (create_processing_conversation never called), plus a control that a non-deleted pair passes the guard. Verified: pytest test_merge_deleted_source_race.py test_merge_validation.py test_merge_audio_chunk_copy_failure.py -> 46 passed. Failure-Class: none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8a9740c to
2881c66
Compare
|
Status after the rebase: the PR Metadata Preflight red is resolved — it was a stale-base artifact (CI diffed the merge commit against a merge-base 434 commits behind, so it saw 334 files and demanded 8 spurious invariant citations). Rebased onto current The now-failing Hermetic Backend E2E / Merge Gate is unrelated to this PR: the failing case is This PR itself is green on the merge coverage ( |
The user-initiated merge path validated locked + completed but never soft-delete. get_conversation returns tombstones unfiltered and POST /v1/conversations/merge only 404s on a missing (None) doc, so a deleted conversation id — passed by an API client, or arriving via a delete-vs-merge race — flowed straight through and merged. That resurrects deleted content into a new visible conversation: the inverse of the tombstone contract the sync merge path already enforces (conversations_db.eligible_merge_target, BasedHardware#10119). Add the soft-delete rejection to validate_merge_compatibility, mirroring the existing locked/completed gates, plus a hermetic regression test. Verified: python -m pytest tests/unit/test_merge_validation.py -> 40 passed. Failure-Class: none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review on BasedHardware#10262. validate_merge_compatibility guards admission, but perform_merge_async re-fetches the sources in the background task; a source that becomes a soft-deleted tombstone between admission and that fetch (the delete-vs- merge race) would still be merged. Add a deleted-source guard right after the background fetch, before any content is read, aborting into _handle_merge_failure rather than building a visible conversation from deleted content. Regression: test_merge_deleted_source_race.py — abort when a source is deleted after admission (create_processing_conversation never called), plus a control that a non-deleted pair passes the guard. Verified: pytest test_merge_deleted_source_race.py test_merge_validation.py test_merge_audio_chunk_copy_failure.py -> 46 passed. Failure-Class: none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2881c66 to
9be0da0
Compare
reprocess_conversation fetches through _get_valid_conversation_by_id, which does not filter soft-deleted tombstones (get_conversation returns them; the helper only 404s on a missing doc), then runs process_conversation with force_process=True — regenerating structured data, action items, memories and embeddings. Reprocessing a tombstone (via a direct API call, or a delete-vs- reprocess race) therefore resurrects content the user deleted back into their memories and derived data. Reject a deleted conversation while still allowing a discarded one, which reprocess intentionally revives. Third instance of the tombstone-eligibility contract (sync BasedHardware#10119, merge BasedHardware#10262); extract the shared is_soft_deleted predicate and converge eligible_merge_target onto it. Verified: pytest test_reprocess_tombstone_guard.py test_sync_merge_target_selection.py test_merge_validation.py -> 49 passed (behaviour-preserving for eligible_merge_target). Failure-Class: none Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…10270) ## What A soft-delete/tombstone data-integrity gap: `POST /v1/conversations/{id}/reprocess` regenerates a soft-deleted conversation's derived data — resurrecting content the user deleted. `reprocess_conversation` fetches through `_get_valid_conversation_by_id`, which does not filter soft-deleted tombstones (`get_conversation` returns them; the helper only 404s on a missing doc). It then runs `process_conversation(force_process=True, is_reprocess=True)`, which regenerates structured data, action items, **memories** and embeddings. So reprocessing a deleted conversation — via a direct API call, or a delete-vs-reprocess race — re-derives the user's memories / action items / embeddings from content they deleted. ## Fix Reject a **deleted** conversation in `reprocess_conversation` (404), while still allowing a **discarded** one — which reprocess intentionally revives (per its docstring, and the `eligible_merge_target` contract). Checked on the raw doc because the `Conversation` model does not carry `deleted`. ## Repairing the failure-class boundary This is the **third instance** of one tombstone-eligibility contract — *a soft-deleted tombstone must not have content operations applied, or deleted data resurfaces*: - sync merge target — #10119 (`eligible_merge_target`) - user-initiated merge — #10262 - reprocess — this PR Per AGENTS.md ("if two or more recent fixes share the cause, add a reusable guard surface"), rather than a third point-fix I extracted the shared **`is_soft_deleted`** predicate and converged `eligible_merge_target` onto it (behaviour-preserving), so the reprocess guard and the sync guard share one definition. #10262's inline merge check can adopt it too — a small follow-up rather than re-touching that merged path here; formalizing a named failure class is a reasonable registry follow-up. ## Tests (hermetic) - `test_reprocess_tombstone_guard.py` — the predicate (deleted → tombstone; discarded / plain / None → not), reprocess **rejects** a deleted conversation (404, `process_conversation` never called), and still **allows** a discarded one. - Existing `eligible_merge_target` and merge-validation tests stay green (behaviour-preserving refactor). **49 passed.** ## Product invariants affected none Failure-Class: none <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/BasedHardware/omi/pull/10270?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
What
The user-initiated conversation merge (
POST /v1/conversations/merge) validated that sources are unlocked and completed, but never that they are not soft-deleted.conversations_db.get_conversationreturns tombstones unfiltered, and the endpoint only 404s on a missing (None) doc. So a soft-deleted conversation id — passed directly by an API client, or arriving via a delete-vs-merge race — passed both the endpoint'sNonecheck andvalidate_merge_compatibility, and was merged.Merging a tombstone resurrects deleted content into a new visible conversation: the merge reads the deleted source's transcript/photos/audio, builds a new visible conversation from them, then re-deletes the sources. That is the inverse of the tombstone contract the sync merge path already enforces (
conversations_db.eligible_merge_target, #10119 — "a soft-deleted tombstone must never absorb new segments").Fix
Reject soft-deleted sources in
validate_merge_compatibility, mirroring the existing locked/completed gates. This is the direct sibling of #10119: that guarded the automatic sync merge target; this guards the user-initiated merge path. Narrow and behavior-preserving for every non-deleted source.Verification
python -m pytest tests/unit/test_merge_validation.py→ 40 passed (2 new: rejects a deleted source; still allows non-deleted sources). The newtest_rejects_deleted_conversationfails without the guard (validation would returnok=True).Product invariants affected
none
Failure-Class: none