The worst bug report a chat product can get is not "it crashed". It is: "I told it something five messages ago, I can scroll up and see myself telling it, and it is acting like I never said it." The user attaches a screenshot. The message is right there. The model, meanwhile, is answering from a history in which that message does not exist. Nobody is lying. That is what makes it awful.

How you end up with two conversations
Nobody designs this. It accrues. In Foxl Desktop, the UI renders chat from SQLite tables - messages, tool_calls, reasoning - written incrementally as each streamed token and tool event lands. That store exists because the UI needs granular, ordered, resumable rows. Separately, the agent runtime persists a snapshot of its own in-memory message array once per invocation, so a cold process restart can rebuild the model's context without replaying the world. That store exists because the agent needs its exact wire-format history, content blocks and all.
Both stores are called "the conversation". Both are written by code that is locally correct. And the moment you have two writers describing one thing, you do not have redundancy. You have a fork that has not diverged yet.
Three quiet leaks
Ours diverged in three places, and each one is instructive because each was a reasonable line of code doing its job.
First: the runtime used a sliding window that trimmed old turns from the head of the in-memory array to keep prompts bounded. Sensible. But the snapshot was taken from that same array - so the trim, meant as a per-request view, got baked into the persisted history. Restart the process and the model's memory now permanently starts at turn 41. The UI table still had turns 1 through 40. The user could see them. The model could not.
Second: when a user cancelled a streaming response, the SDK injected a synthetic "cancelled by user" turn into the agent's array to keep its bookkeeping consistent. That phantom turn went into the snapshot. It never went into the UI tables, because the UI correctly showed nothing - the user cancelled. Two stores, each self-consistent, describing different pasts.
Third, the mirror image: the snapshot was saved at invocation end. A crash or a hard kill mid-turn meant the UI tables had the partial turn - rows are written as events land - and the snapshot did not. The store with the finer write granularity won by default, silently.
Notice the shape of all three. No exception was thrown. No invariant checker fired, because there was no invariant - nothing in the system even expressed the claim that the two stores should agree. Each store passed its own tests. The disagreement was only observable as behavior: a model that seemed forgetful, or haunted by turns nobody saw.
Demote one store
The fix that actually held was not synchronizing the writers - we tried variations of that, and every new write path was a new place to leak. The fix was structural: decide which store is the truth, and make the other one mechanically derivable from it. The UI tables won, for a simple reason: that is the store the user audits. Every screenshot in every bug report is a photograph of that store. If the system must disagree with someone, it must never be the person holding the receipt.

So the snapshot became a cache. On a cold rebuild, the agent's history is re-derived from the same tables the UI reads, deterministically - same input rows, byte-identical output array, which matters because prompt caching keys off those bytes. The snapshot still exists as a fast path, but it holds no information of its own anymore. It cannot drift, because it has nothing to drift from; disagreement is now a cache miss, not a haunting.
You already have two stores
This is not a chat-app problem. Every system past a certain age has second stores: the cache, the denormalized column, the search index, the read replica, the materialized view, the "temporary" JSON file someone added during an incident in 2024. Having them is not the failure. The failure is having two writers - two code paths that each believe they are recording the truth, with no function that maps one store onto the other and no rule for who wins when they differ.
The test is cheap to state: for every pair of stores describing the same entity, can you point at code - not a person, code - that produces one from the other? If reconciling them would require a meeting, they are not two copies of one thing. They are two things, and one of them is wrong, and you do not know which, and neither does your model.
The user always reads one of the stores. Make it the true one, and make everything else admit it is a cache.