Two open sessions, one record

A conversation is open on two surfaces at once — a phone and a browser tab left open since yesterday. The user tells the character something in one of them. Ten minutes later, in the other, the character has no idea. Then the older session sends a message, its state gets written, and now the newer fact is gone from both. The support report says the character forgot something it had already acknowledged, and the transcript of the session that acknowledged it proves it did.

Nothing failed. Two writers shared one record and the last one won.

Concurrency lands on relationship facts, not on the definition

The definition is not the problem here. It is authored, shared, and read-only during a conversation — two sessions reading the same definition is a cache question and nothing more.

Accumulated per-conversation state is a different kind of thing. It is written during conversations, it diverges per conversation from the first turn, and if two sessions are attached to the same record they are both writing it. The unit of writing decides how much a collision destroys.

If the record is one blob — a text field holding the running summary, or a serialised object holding the whole fact set — then any write replaces everything. Session B’s write is built from Session B’s view of the state, which was read before Session A wrote, so B’s write silently reverts A. Not a merge conflict, not an error: a plausible, complete, current-looking record that is missing one fact.

If the record is a set of individually written facts, a collision can only destroy a fact both sessions touched. Two sessions establishing two unrelated details both survive. That is the single largest improvement available here and it is a data-model change, not a locking change.

The running summary is the worst-shaped state for this

A summary is a blob by nature, and it is worse than a blob because it is derived.

When Session B rebuilds a summary, it rebuilds from the history B can see. If A’s turns are not in that history, the new summary is not merely missing A’s facts — it is a coherent account of the relationship in which A’s turns never happened. Write it back and the loss is not recoverable by re-reading, because there is nothing left that says anything is absent.

Three arrangements, all with costs.

One summary per session, plus a shared fact store. Divergence is contained: each session’s narrative stays internally coherent and the durable specifics live in the record that supports per-fact writes. What it costs is that the two sessions genuinely have different memories of the conversational middle, which is visible to anyone using both, and that you now maintain two state mechanisms with different loss profiles instead of one.

Serialise the writer. All state writes for one user go through one queue, and a rebuild reads the current record rather than a stale copy. Collisions become ordering rather than loss. The cost is a write path with a single point of contention, plus a turn that has to either wait for its state write or proceed without knowing whether it landed — and a turn that proceeds anyway has just recreated the stale-read problem in a narrower window.

Accept per-session continuity and reconcile at boundaries. Sessions are independent while open; when one ends, extracted facts are merged into the shared store. Simple, and it moves the whole problem into a merge step where you can afford to be careful. The cost is that a live second session never benefits from the first until it closes, and that a session which never formally ends never reconciles.

Merging is the same problem as a contradiction

Once writes are per-fact, collisions reduce to two values for one fact, which is a problem the system already has to solve — it is what happens when a user contradicts the record. Recency is the obvious rule and it is wrong often enough to matter, because the older session may be sending the older message but establishing the newer fact, and the write clock and the conversational clock are not the same clock.

The distinction that actually helps is provenance. A fact stated explicitly by a user should not be replaced by a fact inferred from tone in another session, regardless of which wrote last. That requires storing provenance and confidence next to every fact — which costs record size, and record size is carried input, so it is paid on every turn in both sessions from then on.

The turn

THE TURN — concurrent sessions

  · One shared record, whole-record writes
                    → simple storage, one field, easy rebuild.

  · Any write replaces everything
                    → the other session's fact is reverted
                      silently. The record looks complete and
                      current while being wrong.

  · Per-fact writes
                    → a collision can only destroy a fact both
                      sessions touched. The largest single
                      improvement, and a data-model change.

  · Provenance and confidence per fact
                    → makes merges resolvable by something
                      better than recency; grows the record.

  · Every field added to the record
                    → PAID EVERY TURN, in both sessions, on
                      every input from then on. Merge
                      correctness is bought with per-turn
                      weight.

  · Per-session summaries
                    → contain divergence and guarantee it. Two
                      sessions remember the middle differently,
                      permanently.

  · No arrangement makes both sessions
    current
                    → a fact established in one is unknown in
                      the other until something propagates it.

Detection

Count concurrent open sessions per user. Most systems do not know this number, and it decides whether any of this is your problem. If it is always one, spend the effort elsewhere; if it has a long tail, the tail is where your unexplained forgetting reports come from.

Log the read version alongside every state write. A write whose read version is not the current version is a stale-read collision, and it is trivially countable. That single counter converts a class of irreproducible complaints into a number on a dashboard.

Diff the record before and after each write, and alarm on fact disappearance. A write that removes facts nobody retracted is the signature failure here. It should be impossible in a per-fact model and it is routine in a blob model, so the alarm also tells you which model you are actually running.

Probe cross-session propagation directly. Establish a fact in one synthetic session, then ask for it in a second session attached to the same record, and record the delay before it appears. That number is your real propagation guarantee, as opposed to the one in the design document.

What this costs and what it doesn’t fix

Per-fact storage, provenance, versions and a serialised writer all add record size or add a step to the write path, and record size is re-supplied on every turn in every attached session. Concurrency correctness here is not a one-off engineering cost; it is a permanent increase in what a turn carries.

And none of it makes two sessions one conversation. They condition on different recent history, so they will answer the same question differently even with a perfectly merged fact store, and the same input reaching a divergent state is still going to produce two different voices. What a good record buys is that the durable specifics survive; the narrative does not, and designing as though it could is the mistake underneath most of these reports.