Skip to content

fix(conversations): reject soft-deleted conversations from merge#10262

Merged
undivisible merged 2 commits into
BasedHardware:mainfrom
aryanorastar:fix/merge-rejects-deleted-conversation
Jul 24, 2026
Merged

fix(conversations): reject soft-deleted conversations from merge#10262
undivisible merged 2 commits into
BasedHardware:mainfrom
aryanorastar:fix/merge-rejects-deleted-conversation

Conversation

@aryanorastar

@aryanorastar aryanorastar commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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_conversation returns 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's None check and validate_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.py40 passed (2 new: rejects a deleted source; still allows non-deleted sources). The new test_rejects_deleted_conversation fails without the guard (validation would return ok=True).

Product invariants affected

none

Failure-Class: none

Review in cubic

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@Git-on-my-level Git-on-my-level added security-review Touches auth, provider routing, secrets, or security-sensitive surfaces needs-tests PR introduces logic that should be covered by tests labels Jul 22, 2026
aryanorastar added a commit to aryanorastar/omi that referenced this pull request Jul 22, 2026
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>
@aryanorastar

Copy link
Copy Markdown
Contributor Author

Good catch — fixed. perform_merge_async now re-checks for a soft-deleted source right after its background re-fetch, before reading any content: if any source became a tombstone between admission and the worker's fetch, it aborts into _handle_merge_failure rather than building a visible conversation from deleted content. That closes the delete-vs-merge race the admission guard left open.

Regression in test_merge_deleted_source_race.py: asserts the abort (create_processing_conversation never called) when a source is deleted=True after admission, plus a control that a live pair still passes the guard. 46 tests green (incl. the existing merge suites).

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

fix(conversations): reject soft-deleted conversations from merge — bug fix; held (unrelated desktop-swift check red)

@aryanorastar
aryanorastar force-pushed the fix/merge-rejects-deleted-conversation branch from 597dea1 to 8a9740c Compare July 22, 2026 15:57
aryanorastar added a commit to aryanorastar/omi that referenced this pull request Jul 22, 2026
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>
@aryanorastar

Copy link
Copy Markdown
Contributor Author

@Git-on-my-level the TOCTOU race you flagged is fixed in 8a9740cfbe: perform_merge_async now re-checks deleted on every re-fetched source after admission and aborts via _handle_merge_failure before any content read, so a source soft-deleted between admission and the background fetch can't be resurrected. Regression test test_merge_aborts_when_a_source_is_deleted_after_admission drives the exact race; the control test proves a clean merge still passes the guard. Rebased on latest main. Could you re-review?

aryanorastar added a commit to aryanorastar/omi that referenced this pull request Jul 22, 2026
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>
@Git-on-my-level
Git-on-my-level dismissed their stale review July 23, 2026 06:42

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 Git-on-my-level removed the needs-tests PR introduces logic that should be covered by tests label Jul 23, 2026

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

aryanorastar added a commit to aryanorastar/omi that referenced this pull request Jul 23, 2026
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>
@aryanorastar
aryanorastar force-pushed the fix/merge-rejects-deleted-conversation branch from 8a9740c to 2881c66 Compare July 23, 2026 13:56
@aryanorastar

Copy link
Copy Markdown
Contributor Author

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 main, the diff is just the 3 merge files and pr-preflight --suggest reports invariants: none.

The now-failing Hermetic Backend E2E / Merge Gate is unrelated to this PR: the failing case is testing/e2e/test_canonical_memory_pipeline.py::TestSurfaceDefaultAccessMatrix.test_surface_excludes_archive_and_respects_grants[agent_tools] — a canonical memory surface/access-matrix assertion ('coffee fresh short term' short-term memory not surfaced), which this conversation-merge change doesn't touch. It passed on the pre-rebase head and only appeared after rebasing onto current main, so it's a main-side flake/regression in the memory E2E, not this diff.

This PR itself is green on the merge coverage (test_merge_validation.py, test_merge_deleted_source_race.py) and preflight passes 19/19 locally. Could a maintainer re-run the hermetic job (I don't have admin to) or merge past the unrelated memory-E2E failure? kodjima33's approval already stands.

aryanorastar and others added 2 commits July 24, 2026 07:09
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>
@aryanorastar
aryanorastar force-pushed the fix/merge-rejects-deleted-conversation branch from 2881c66 to 9be0da0 Compare July 24, 2026 01:40
aryanorastar added a commit to aryanorastar/omi that referenced this pull request Jul 24, 2026
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>
undivisible added a commit that referenced this pull request Jul 24, 2026
…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. -->
@undivisible
undivisible merged commit fa7dfca into BasedHardware:main Jul 24, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants