Skip to content

fix(conversations): transactional segment-text edits to stop lost updates (#9392) - #9574

Merged
kodjima33 merged 1 commit into
mainfrom
issues-improver/9392-transactional-segment-text
Jul 13, 2026
Merged

fix(conversations): transactional segment-text edits to stop lost updates (#9392)#9574
kodjima33 merged 1 commit into
mainfrom
issues-improver/9392-transactional-segment-text

Conversation

@kodjima33

@kodjima33 kodjima33 commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

What

Wrap update_conversation_segment_text (backend/database/conversations.py) in a Firestore transaction so concurrent transcript-segment text edits can't silently lose-update each other.

Why

From #9392:

update_conversation_segment_text does doc_ref.get() → mutate one segment in the decoded transcript_segments array → doc_ref.update() (rewrites the whole array). Two concurrent edits (even to different segments) both read the pre-edit array; the later write overwrites the earlier one → lost update.

Repro: open the same conversation in two tabs and edit different segments within the same ~200ms window → one edit is lost (both return 200).

Proposed fix: wrap the read-modify-write in a Firestore transaction.

The web PR (#9353) only serializes saves within a single client; the cross-tab / cross-client race requires this backend fix.

Change

  • The read (doc_ref.get) and the whole-array write now run inside a @firestore.transactional inner function — the same pattern already used by upsert_conversation in this file — so the read is retried on write contention and the array rewrite is atomic.
  • Return-code contract (ok / not_found / locked / segment_not_found) is unchanged, so patch_conversation_segment_text in routers/conversations.py behaves identically.

Root cause & durable guard

  • Root cause: a shared read-modify-write over the entire transcript_segments array with no transaction.
  • Durable guard: atomic Firestore transaction + regression tests that drive the real db function through a controllable transaction seam and assert the edit is written via transaction.update while the untouched segment is preserved.

Verified

Added 4 tests to backend/tests/unit/test_conversation_revision_contract.py (edit lands + other segment preserved / missing-segment no-write / locked / not_found), run with the backend venv:

ENCRYPTION_SECRET=... .venv/bin/python -m pytest tests/unit/test_conversation_revision_contract.py -q
14 passed in 4.37s

(10 pre-existing + 4 new; the new ones deselected-run green under -k segment.)

Scope note (deliberately deferred)

#9392 also covers two larger, separate surfaces left as follow-ups so this PR stays reviewable and revertable:

  1. the speaker-assign race — read happens in the router, write via update_conversation_segments (used by 6 call sites incl. the sync pipeline); fixing it correctly means moving the mutation inside a transaction per endpoint.
  2. reprocess_conversation overwriting the transcript from a stale snapshot (touches process_conversation persistence).

Auto-generated from issue feedback by the mini issues-improver. Review before merge.

Review in cubic

…lost updates

update_conversation_segment_text did a non-transactional read-modify-write:
doc.get() -> mutate one segment in the decoded transcript_segments array ->
doc.update() rewriting the whole array. Two concurrent edits (even to different
segments) both read the pre-edit array, and the later write clobbered the
earlier one -- both returned 200 but one edit was silently lost.

Wrap the read + write in a Firestore transaction using the same
@firestore.transactional inner-function pattern already used by
upsert_conversation in this file, so the read is retried on contention and the
whole-array rewrite is atomic. Return-code contract (ok/not_found/locked/
segment_not_found) is unchanged, so the router behavior is identical.

Root cause: shared read-modify-write over the whole transcript_segments array
without a transaction. Durable guard: atomic transaction + regression tests
asserting the write goes through transaction.update and the untouched segment
is preserved.

Verified: added 4 tests in test_conversation_revision_contract.py exercising the
real db function through a controllable transaction seam (edit lands + other
segment preserved, missing-segment no-write, locked, not_found). Ran with the
backend venv:
  ENCRYPTION_SECRET=... .venv/bin/python -m pytest \
    tests/unit/test_conversation_revision_contract.py -q  -> 14 passed

Scope note: the speaker-assign race (router-side read + update_conversation_
segments) and the reprocess stale-snapshot overwrite from #9392 are larger,
separate surfaces left as follow-ups.

fixes #9392

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kodjima33
kodjima33 merged commit 6276daf into main Jul 13, 2026
25 of 28 checks passed
@kodjima33
kodjima33 deleted the issues-improver/9392-transactional-segment-text branch July 13, 2026 14:33
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