Migrating the shape of the state
You split the fact store: one undifferentiated list becomes typed categories with provenance on each entry. The new code is better in every way. You deploy, and long-running conversations start behaving as though they began yesterday — the character has the definition and nothing else — while brand-new conversations are perfect. The reads are hitting records written in the old shape, finding no typed entries, and returning empty.
The definition survived the change because you wrote it. The accumulated state did not, because you did not.
Authored state and accumulated state migrate differently
There is a hard asymmetry between the two kinds of state a persona system holds.
The definition is authored. If its structure changes, you rewrite it — the source of truth is a person’s intent, and it is still available. Landing that change under a live conversation has its own difficulties, but they are not migration difficulties: nothing is lost, because the material can be re-derived from the author.
Accumulated per-conversation state has no author to ask. It is the only record of what happened, and its shape and its contents cannot be separated by anyone who was not there. A migration that guesses at the new shape is not reformatting data; it is writing new facts. That distinction is the whole subject.
Four ways to change the shape, and what each destroys
Dual-read. Read both shapes, write only the new one, translate old records lazily as they are touched. The safest option and the one to default to: no record is rewritten until a live turn provides the context to rewrite it, and nothing is destroyed in bulk. Two costs, one of them permanent. Every assembly path carries a branch for the old shape, and it carries it until the last old record is retired — which, for conversations that have gone quiet but might resume, is never in practice. The other cost is that the two shapes must be reconcilable at read time, so the new shape cannot be one the old cannot map into.
Batch backfill by translation. Write a translator, run it over everything, delete the old path. Clean, and the risk is entirely in the translator. Translating a flat list into typed categories means classifying each entry, and classification without provenance is inference. Every entry the translator gets wrong becomes a stored fact with full authority and no marker — which is expensive precisely because a wrong fact outlives a missing one. If a translation cannot be made confidently, the correct output is an entry marked low-confidence, not a best guess promoted to a category.
Rebuild from originals. If the original turns were retained, discard the derived state entirely and regenerate it in the new shape. Highest fidelity available, because it goes back past every previous compression rather than translating its output. It is also the most expensive: the work scales with conversation length and it is the same arithmetic as an aggressive rebuild cadence, applied to every conversation at once. And it is only available if originals exist — a system that has only ever kept derived state has already made this decision without noticing.
New shape for new conversations only. No migration, no risk, and permanent bimodality. Two code paths forever, two populations in every metric you compute, and a support surface where the answer to any question is “depends how old the conversation is”. This is the option that looks free on the day and is paid for indefinitely, and it is worth choosing only when the old population is genuinely finite and shrinking.
Double-counting is the failure that survives testing
The bug that gets through review is not an empty read. Empty reads are loud — the character is visibly blank, somebody notices within an hour.
The quiet failure is the same information present in both shapes and read by both paths. A dual-read that falls back to the old record instead of preferring the new one, or a backfill run twice, produces a state containing each fact once from each shape. The assembled input then asserts the same detail repeatedly, which raises its weight against everything else in the state, and a fact stated three times in the input behaves differently from a fact stated once. The symptom is a character that will not stop mentioning something, and nothing about the symptom points at a migration.
The defence is that reads must be preferential and idempotent, not additive: new shape wins where present, old shape only fills gaps, and running the translator twice must be a no-op. Both properties are cheap to build in and effectively impossible to retrofit once records are mixed.
The turn
THE TURN — state migration
· Dual-read, lazy translation
→ nothing is destroyed in bulk; records
convert as conversations touch them.
· The old-shape branch never retires
→ assembly carries both readers
indefinitely, because a quiet
conversation can always resume.
· Batch backfill by translation
→ one shape, one path, done. Every
uncertain translation becomes a stored
fact with unearned authority.
· Rebuild from originals
→ highest fidelity, goes behind every
previous compression. Costs work
proportional to conversation length,
and requires originals.
· New shape for new conversations only
→ zero migration risk, permanent
bimodality in code, metrics and support.
· Whichever shape you land on
→ PAID EVERY TURN. Provenance fields,
typed categories and reconciliation
logic sit in the input or the assembly
path on every message.
· No shape recovers a dropped specific
→ migrating derived state moves it. Only
originals can add anything back.
Detection
Record the shape version on every record and count the populations. Without a version field a mixed population is undetectable, and with one, the migration has a progress bar. This is the cheapest thing on the list and the one most often skipped, because the field looks unnecessary until the day it is the only way to answer any question.
Alarm on fact-count going to zero across a deploy. An empty read after a shape change is the loud failure, and it is trivially detectable as a discontinuity in the count of facts assembled per turn. Compare the same conversations before and after, not the daily average, or new conversations will mask it.
Check for duplicate content within an assembled state. Exact and near-duplicate entries in one record are the double-counting signature. Counting them per turn catches an additive read on the day it ships instead of three weeks later via a complaint about repetition.
Probe old conversations specifically after a shape change. Take conversations that have been dormant longest and run fact-survival probes against them, because they are the population the migration is most likely to have missed and the least likely to be exercised by traffic.
What this costs and what it doesn’t fix
The new shape’s extra fields are carried in the input on every turn, in every conversation, from the migration onward — so a richer state model is a permanent per-message cost and should be justified as one, not as a one-time refactor. The dual-read branch is likewise permanent unless you actually finish and verify the backfill, which most teams do not.
And a migration cannot improve what it moves. If the old records lost specifics to summarisation, the new shape holds the same thinned material in better boxes. The only operation that adds information back is rebuilding from originals, which is why whether you retained them is the decision that quietly determines how much any future change to your state model can accomplish.