Summary
POST /v3/memories is returning retryable 503 Service temporarily unavailable responses for beta/dev Desktop traffic because the legacy memory-ledger transaction performs a Firestore read after it has staged writes.
This is a follow-up to #9662's broader transaction-contract work, but is a distinct, reproducible ordering regression introduced while preserving canonical state-head fields in #9739.
User impact
- Desktop Sentry group:
OMI-DESKTOP-2F3.
- 23 reported events across 7 users between 2026-07-14 20:36:58Z and 2026-07-15 00:48:52Z.
- Full event breadcrumbs show the failed request is
POST https://api.omiapi.com/v3/memories (the beta/dev API), not the production sync-backfill endpoint.
- The effect is a failed new-memory write. The route returns before post-processing/vector upsert; do not claim successful persistence or data corruption from these failures.
Confirmed runtime evidence
At an exact Sentry failure time, dev Cloud Run backend returned 503 for /v3/memories in ~91 ms. The request neighborhood contains:
ERROR:routers.memories:Firestore create_memory failed
ReadAfterWriteError: Attempted read after write in a transaction.
The same route failure persisted across successive dev backend revisions; it is not explained by a single traffic shift or by the unrelated production backend-sync-backfill OOMs.
Root cause
#9739 added _state_head_write_payload() to preserve trusted canonical state-head fields when a legacy ledger write advances its independent head.
In backend/database/memory_ledger.py, both ledger transaction variants can execute in this order:
- Read
memory_state/head, the existing memory projection, and the commit document.
- Call
projection_writer(transaction), which stages a set() of the memory document.
- Stage the commit write.
- Evaluate
_state_head_write_payload(...) for the final state-head write.
- If trusted state-head fields are absent,
_state_head_write_payload(...) reads memory_state/apply_control using the same transaction.
Firestore forbids step 5 after steps 2–3. It raises ReadAfterWriteError, which routers/memories.py:create_memory() catches and translates to HTTP 503.
The existing unit tests exercise the state-preservation semantics but their fake transaction does not enforce Firestore's read-before-write ordering, so they did not catch this.
Prescribed fix
Keep #9739's state-preservation behavior, but perform all possible transaction reads before the first write:
- In both
_append_commit_transaction and _append_commit_with_builder_transaction, build the final state-head payload (including any apply_control fallback read) before calling projection_writer(transaction) or any transaction.set(...).
- Reuse that already-built payload for the final
state_ref write. Do not remove canonical-head preservation or silently fall back to an untrusted synthetic head.
- Add a regression test with a strict transaction fake (or Firestore emulator) that raises if
get() occurs after set():
- initial state lacks trusted canonical head fields;
- an
apply_control fallback is needed;
- a legacy
create_memory ledger write succeeds;
- the resulting state preserves the validated canonical fields.
- Cover both the direct append and builder append paths if both retain the helper.
Acceptance criteria
Related
Summary
POST /v3/memoriesis returning retryable503 Service temporarily unavailableresponses for beta/dev Desktop traffic because the legacy memory-ledger transaction performs a Firestore read after it has staged writes.This is a follow-up to #9662's broader transaction-contract work, but is a distinct, reproducible ordering regression introduced while preserving canonical state-head fields in #9739.
User impact
OMI-DESKTOP-2F3.POST https://api.omiapi.com/v3/memories(the beta/dev API), not the production sync-backfill endpoint.Confirmed runtime evidence
At an exact Sentry failure time, dev Cloud Run
backendreturned503for/v3/memoriesin ~91 ms. The request neighborhood contains:The same route failure persisted across successive dev backend revisions; it is not explained by a single traffic shift or by the unrelated production
backend-sync-backfillOOMs.Root cause
#9739 added
_state_head_write_payload()to preserve trusted canonical state-head fields when a legacy ledger write advances its independent head.In
backend/database/memory_ledger.py, both ledger transaction variants can execute in this order:memory_state/head, the existing memory projection, and the commit document.projection_writer(transaction), which stages aset()of the memory document._state_head_write_payload(...)for the final state-head write._state_head_write_payload(...)readsmemory_state/apply_controlusing the same transaction.Firestore forbids step 5 after steps 2–3. It raises
ReadAfterWriteError, whichrouters/memories.py:create_memory()catches and translates to HTTP 503.The existing unit tests exercise the state-preservation semantics but their fake transaction does not enforce Firestore's read-before-write ordering, so they did not catch this.
Prescribed fix
Keep #9739's state-preservation behavior, but perform all possible transaction reads before the first write:
_append_commit_transactionand_append_commit_with_builder_transaction, build the final state-head payload (including anyapply_controlfallback read) before callingprojection_writer(transaction)or anytransaction.set(...).state_refwrite. Do not remove canonical-head preservation or silently fall back to an untrusted synthetic head.get()occurs afterset():apply_controlfallback is needed;create_memoryledger write succeeds;Acceptance criteria
POST /v3/memoriesno longer producesReadAfterWriteError/ 503 for this path in dev.Related