Problem
Observation persistence has the same delete-and-replace transaction shape as relation persistence: markdown persist deletes an entity's observation rows and re-inserts the new set inside the same multi-phase transaction as entity upserts and relation writes. This is the same lock structure that produces the production relation-table deadlock cluster (#1213), with lower observed collision frequency so far — concurrent persists of notes touching overlapping entities can interleave lock acquisition across phases.
No dedicated Logfire fingerprint has been attributed to observation rows yet; some of the 16 relation-cluster fingerprints' retried attempts recorded adjacent integrity/DBAPI failures consistent with contention beyond the relation table. This issue is the proactive completion of the same fix rather than a response to a distinct incident.
Design
Adopt the identical generation-versioned model specified in #1213:
observation rows carry an indexed generation — the note's existing generation token (db_version/content checksum).
- Persist upserts the desired observation set with
ON CONFLICT DO UPDATE SET generation = G guarded by WHERE observation.generation < G; stale writers become no-ops.
- Cleanup of superseded rows (
DELETE ... WHERE entity_id = ? AND generation < G) is a separate short statement after upserts — brief union window, never a gap; observations are derived, eventually consistent state.
- Observation writes commit in their own short transaction, decoupled from entity/relation phases.
Note: observations may lack a natural conflict key today (position/content-based identity) — if so, part of this issue is defining a stable upsert identity (e.g. content-hash within entity) so ON CONFLICT has an arbiter, or an equivalent generation-swap strategy that avoids delete-then-insert in one transaction.
Scope
Core repo: observation repository + markdown persist flow, tenant-DB migration (observation.generation + index + backfill, plus upsert identity if needed). Sequence after (or with) the relation issue so both use one migration window and one persistence-flow refactor.
Acceptance criteria
Problem
Observation persistence has the same delete-and-replace transaction shape as relation persistence: markdown persist deletes an entity's observation rows and re-inserts the new set inside the same multi-phase transaction as entity upserts and relation writes. This is the same lock structure that produces the production
relation-table deadlock cluster (#1213), with lower observed collision frequency so far — concurrent persists of notes touching overlapping entities can interleave lock acquisition across phases.No dedicated Logfire fingerprint has been attributed to observation rows yet; some of the 16 relation-cluster fingerprints' retried attempts recorded adjacent integrity/DBAPI failures consistent with contention beyond the relation table. This issue is the proactive completion of the same fix rather than a response to a distinct incident.
Design
Adopt the identical generation-versioned model specified in #1213:
observationrows carry an indexedgeneration— the note's existing generation token (db_version/content checksum).ON CONFLICT DO UPDATE SET generation = Gguarded byWHERE observation.generation < G; stale writers become no-ops.DELETE ... WHERE entity_id = ? AND generation < G) is a separate short statement after upserts — brief union window, never a gap; observations are derived, eventually consistent state.Note: observations may lack a natural conflict key today (position/content-based identity) — if so, part of this issue is defining a stable upsert identity (e.g. content-hash within entity) so
ON CONFLICThas an arbiter, or an equivalent generation-swap strategy that avoids delete-then-insert in one transaction.Scope
Core repo: observation repository + markdown persist flow, tenant-DB migration (
observation.generation+ index + backfill, plus upsert identity if needed). Sequence after (or with) the relation issue so both use one migration window and one persistence-flow refactor.Acceptance criteria
generation(and upsert identity if introduced) for existing rows.