Summary. Two related concurrency bugs let transcript-segment edits silently disappear. Surfaced during review of #9353 (web transcript editing). The web PR ships client-side mitigations; this tracks the durable, cross-surface fix in the backend (the endpoints are shared by all clients).
1. update_conversation_segment_text is a non-transactional read-modify-write
backend/database/conversations.py → 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. The same read-modify-write pattern is in update_conversation_segments (used by the speaker-assign endpoints), so speaker tagging has the same race.
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 (client.transaction() / @firestore.transactional) so the read+write is atomic and retried on contention. Apply to both update_conversation_segment_text and update_conversation_segments.
2. Reprocess overwrites the transcript from a stale snapshot
reprocess_conversation (backend/routers/conversations.py) loads the conversation once (_get_valid_conversation_by_id), then process_conversation persists the conversation. An edit that lands after the snapshot load is clobbered when reprocess writes back.
Proposed fix: reprocess should not rewrite transcript_segments — it only regenerates derived data (structured summary, action items, vectors). Either re-read the latest transcript at write time, or narrow reprocess's persistence to derived fields only.
Already mitigated client-side (web, #9353)
The web UI now serializes segment saves (one PATCH in flight at a time) and makes editing and reprocess mutually exclusive, which removes the single-client race. It cannot fix the cross-client/cross-tab case or the reprocess root cause — those require the backend changes above.
Summary. Two related concurrency bugs let transcript-segment edits silently disappear. Surfaced during review of #9353 (web transcript editing). The web PR ships client-side mitigations; this tracks the durable, cross-surface fix in the backend (the endpoints are shared by all clients).
1.
update_conversation_segment_textis a non-transactional read-modify-writebackend/database/conversations.py→update_conversation_segment_textdoesdoc_ref.get()→ mutate one segment in the decodedtranscript_segmentsarray →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. The same read-modify-write pattern is inupdate_conversation_segments(used by the speaker-assign endpoints), so speaker tagging has the same race.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 (
client.transaction()/@firestore.transactional) so the read+write is atomic and retried on contention. Apply to bothupdate_conversation_segment_textandupdate_conversation_segments.2. Reprocess overwrites the transcript from a stale snapshot
reprocess_conversation(backend/routers/conversations.py) loads the conversation once (_get_valid_conversation_by_id), thenprocess_conversationpersists the conversation. An edit that lands after the snapshot load is clobbered when reprocess writes back.Proposed fix: reprocess should not rewrite
transcript_segments— it only regenerates derived data (structured summary, action items, vectors). Either re-read the latest transcript at write time, or narrow reprocess's persistence to derived fields only.Already mitigated client-side (web, #9353)
The web UI now serializes segment saves (one PATCH in flight at a time) and makes editing and reprocess mutually exclusive, which removes the single-client race. It cannot fix the cross-client/cross-tab case or the reprocess root cause — those require the backend changes above.