Chapter 4 of 7 · 13 min read
Deprecated fields and conversation history
On this page
An agent conversation is stateless on the wire. Turn nine does not resume turn
eight; it re-sends turns one through eight in full, including every tool_use
block the model emitted and every tool_result that came back — the first being
the structured block in which a model asks for a call by name and arguments, the
second the block in which your service returns what that call produced — and
then appends the new user message. That is how the model has any memory at all.
It also means that the request carrying your brand-new schema carries eight demonstrations of the old one, positioned much closer to the point of generation than the schema is. Chapters 1 through 3 treated a schema edit as an event with a cost measured in cache windows, which is a matter of minutes. This chapter is about the other clock, which runs for weeks: what a deployed schema change does to conversations that were already open when it landed, and how you find out when the old shape has genuinely stopped arriving.
Every turn re-sends every prior call
The reinforcement compounds, which is what makes the tail long rather than merely present. Each turn where the model copies the old shape adds another instance of the old shape to the history that the next turn will read, so a conversation that has called the tool three times the old way is more likely to make the fourth call the old way than it was to make the third. Topic 5.11, on tool descriptions functioning as few-shot examples, established that in-context examples move tool selection and argument shape harder than a description does; this is the same mechanism operating on content you did not write and cannot edit, because a stored assistant turn is a record of what the model actually emitted and rewriting it would be falsifying the transcript.
Conversation age therefore predicts old-field usage almost perfectly.
Conversations that started after a deploy take the new fields immediately, at a
rate consistent with the schema alone, since they have no contradictory examples
to learn from. Conversations that started before it keep the old shape roughly
in proportion to how many old-shaped calls they already contain. One support
agent that replaced a days_back integer with an explicit since and until
pair had a median session length of about four days and a p95 of eleven, meaning
nineteen sessions in twenty finish sooner than that, and the decay curve of
days_back traffic is that survival curve, offset by whatever fraction of those
sessions touch the tool again.
| Days after deploy | Calls carrying days_back | Dominant source |
|---|---|---|
| 1 | 41% | Live pre-deploy conversations, still short |
| 3 | 18% | Live pre-deploy conversations, thinning |
| 7 | 9% | Long-running sessions plus retry queue |
| 14 | 3.1% | Transcript replays, some retries |
| 21 | 1.4% | Transcript replays only |
| 21 (Monday 09:00–11:00) | 4.0% | Batch replay of weekend escalations |
The first five rows are a survival curve wearing a percentage, and reading them that way lets a second instrument check them. Each figure is roughly half the one a few days before it, which is the shape a population of conversations produces as its members finish, and the curve should therefore run out at about the point the sessions do. It does. The p95 session length is eleven days, so by the fortnight row almost every conversation that was open at the deploy has closed, and that is exactly where the Dominant source column stops saying live and starts saying replay. Two independent things — a session-length statistic measured from the conversation store, and a source attribution measured from call counters — land on the same date without either being used to compute the other, which is the strongest evidence available that the mechanism is the one being claimed rather than a deployment fault that happens to be fading.
The last row is the one that has no decay curve at all. When an escalation comes in with a reproduction attached, an on-call engineer replays the stored transcript against the current service to see the failure again, and a transcript captured two weeks ago contains two-week-old tool calls no matter what day it is replayed on. Replay traffic has a retention window rather than a half-life: as long as thirty days of transcripts are kept and support replays them, the old field arrives from that source at a steady low rate until the last transcript containing it ages out.
A rate that goes back up is the most useful thing in that table, because a decaying population cannot produce one. Conversations only ever finish, so any mechanism made of open conversations gives a curve that falls and keeps falling. A figure that returns to nearly three times the surrounding level, at the same hours on the same day of the week, is therefore made of something else, and periodicity narrows the field fast: infrastructure faults do not repeat on a weekly cycle at nine in the morning, and human working patterns do almost nothing else. The period names the process. A Monday-morning peak points at whatever queue accumulates over a weekend and gets worked through when people come back, and in an agent deployment that is the escalation queue and the replay harness attached to it.
A history the model cannot reconcile
The second-order effect is worse than the persistence, and it shows up when the
compatibility shim is thinner. If days_back had been deleted rather than
deprecated, turn nine would present the model with tool definitions containing
no days_back parameter and eight turns of evidence that the tool is called
with a days_back parameter, and both are in the same context window with equal
claim to being true.
Reading that transcript, a human resolves it instantly, because the deploy is a
fact outside the document and the timestamps make the sequence obvious. The
model has no outside. It has one flat context in which the schema says one thing
and the demonstrated usage says another, and the resolutions it reaches vary.
Some calls come out in the old shape, in defiance of the schema, which then fail
validation. Some come out in the new shape and look fine. And some come out as
hybrids — during the first two days of that migration, around 2% of calls
arrived carrying both days_back: 30 and a since timestamp that had been
derived from it incorrectly, computed against the conversation's start rather
than the current date. Those are the expensive ones, because they validate. A
hybrid call passes the schema check, executes, and returns shipments from the
wrong window, and nothing in the error rate moves.
Hybrids are the predictable outcome of the compatibility shim rather than a surprise, and the chain that produces them is short enough to check. The model is holding two accounts of how the tool is called, both inside one context and neither marked as superseded. Its job at that moment is to emit a continuation that fits what it has read, and the continuation that fits both accounts is the one that satisfies both. Emitting both fields is that continuation. A schema that has been widened to accept either field will accept both, so nothing downstream refuses it, and the call proceeds. Every link there follows from the decision to keep the old field callable, which is the decision that made the migration survivable in the first place — the same unreconciled context, seen from the schema's side rather than the model's.
Conversations that hit this state also tend to degrade in ways that are not about the tool. A model that has produced one rejected call frequently spends the next turn apologising, retrying with a third variant, or narrating its confusion to the user, and the transcript picks up a stretch of low-quality turns that the model then reads on turn ten as further evidence of how this conversation goes. The cost of a hard rename is not one failed call per affected conversation; it is a degraded remainder of every affected conversation.
Deprecation dates assume a caller that can read them
The reflex when a field must change is the API deprecation cycle every backend engineer has run a dozen times: announce it, put a notice in the docs and the response headers, give clients two weeks or two quarters, then delete the field. That cycle rests on an assumption that holds for every HTTP client and fails completely here. It assumes the caller is a piece of software that someone can update, that reads the notice at some point before the deadline, and that once updated stops sending the old shape permanently.
The caller here is a model whose behaviour on this turn is conditioned on a history it emitted before the notice existed. There is no deploy that updates it, because it is not a client with a version; the closest analogue to "updating the client" is starting a new conversation, and that is not something you can do to conversations that are already open. Worse, a portion of the callers are not live at all. A stored transcript from October is a caller that will send the old shape on demand, forever, and no notice period reaches into a file on disk and rewrites what it contains. A deprecation notice in a tool description is read by the model that is generating right now, and it competes with the demonstrations in history, and it does not exist at all for the replay path where the transcript is fed back through a harness rather than through the live agent.
A date and a signal are not the same instrument. A date is a decision made on a whiteboard about when other people will have finished; a signal is a measurement of whether anything is still sending the old field. Retirement here has to be driven by the signal, and the signal has three sources that go quiet at different times and for different reasons.
# Emit one counter per accepted call, tagged with where the old shape came from.
def record_legacy_usage(call, ctx):
if "days_back" not in call.arguments:
return
source = (
"replay" if ctx.is_transcript_replay
else "retry" if ctx.is_queued_retry
else "live"
)
metrics.increment(
"tool.legacy_field",
tags={
"tool": call.name,
"field": "days_back",
"source": source,
"conversation_age_days": min((now() - ctx.conversation_started).days, 30),
},
)The live counter goes to zero when every conversation that predates the deploy
has ended, which you can bound by your session expiry rather than by hope — if
idle conversations are closed after fourteen days, live traffic on the old field
is guaranteed dead fourteen days after the last pre-deploy turn. The retry
counter goes to zero when the queue drains, which for a dead-letter queue — the
holding area a job lands in once it has failed every attempt and is parked for a
human — with a seven-day retention is seven days after the last enqueue. The
replay counter goes to zero only when transcripts containing the old shape
fall out of retention, which for a thirty-day window means thirty days after the
last old-shaped call was written, and the last old-shaped call was written
whenever live finally hit zero. The windows chain, so the honest retirement
date for a thirty-day retention and a fourteen-day session expiry is somewhere
past forty-four days, and the only way to know it has arrived is that all three
counters have read zero for long enough that you would have seen a spike if one
were coming.
The stumble here is taking the largest of those three windows instead of adding two of them, and it is tempting because that is the correct arithmetic almost everywhere else. Independent timers running at once finish when the slowest finishes, so a thirty-day retention alongside a fourteen-day expiry sounds like a thirty-day wait. These two are not running at once. Retention counts from the moment a transcript was written, and a transcript containing the old shape can be written on any day the old shape is still being emitted, which is any day live traffic has not yet gone quiet. The replay clock therefore cannot start until the live clock has stopped, and sequential timers add: fourteen days for the last pre-deploy conversation to close, then thirty more for the last transcript it produced to age out. The same reasoning is what makes a retention increase a schema-migration cost, since lengthening the store lengthens every deprecation that follows it.
The cost of carrying both shapes, and the fields that cannot wait
It does not make the schema smaller. You are carrying both shapes for the whole window, which costs tokens in the tool block on every request, and it costs the model a decision it would not otherwise have to make — topic 5.11's point about parameters that invite the wrong choice applies with full force to a deprecated field sitting there looking callable. The counters tell you when you can stop paying that; they do not reduce it in the meantime.
And it does not apply to a field that must stop working for reasons other than tidiness. The entire premise of watching a signal is that you are willing to keep honouring the old shape until the signal clears, which is a reasonable premise for a rename and an unreasonable one for a parameter that is a security problem. Chapter 7 takes up the case where honouring the old call is the thing you are trying to stop doing.
The general shape to carry out of this is that a tool schema has two kinds of caller and only one of them is a program. The live conversations are callers that adapt, slowly and unreliably, by being shown a new schema next to their own old examples. The stored transcripts are callers that never adapt at all, and they will keep issuing the old shape at whatever rate support replays them until the files themselves expire.
Nothing in this chapter says what the schema should look like while it carries both shapes, how the old field is mapped forward without the handler growing two code paths, or how you word a deprecation that a model reads rather than a human. Those are the mechanics of the migration itself, and they are what makes the difference between a compatibility window that works and one that quietly keeps producing the bug you were trying to fix.
Tool versioning without invalidating the prompt cache — read the current version at https://daviddominguez.dev/learn/en/tool-versioning-without-invalidating-the-prompt-cache/deprecated-fields-and-conversation-history