There is a comment in my intake agent that I did not enjoy writing. It sits inside a test scenario whose whole job is to prove the bot never invents a date for a callback, and it records that the scenario passed while the bot was telling a real person it would happen “usually inside a few days”. The guard matched a list of spellings. The test matched a list of spellings. That particular preposition, a plain synonym of the one both lists knew, was on neither.
I wrote both lists in one sitting, working from the same mental inventory of ways to phrase a deadline. The test was the guard again, in the second person, and it could only ever agree with it.
Here is the claim: a check written by the author of the code inherits the blind spot of the code, and care makes this worse rather than better, because care is the thing being duplicated. The one oracle that escapes it is a record of what the system already did, in front of people, before its author had a theory about it.
Why the expensive check was the right one for a year
The agent runs a structured intake conversation. Eleven profile fields, gathered in a few minutes of free talk rather than a form, matched against a live catalogue, then handed to a person. I have written before about the shape it settled into: code owns the policy, the model does language, and every claim the model makes is checked against what the tools actually returned.
That checking layer is where the cost lives. It had grown to about a hundred and twenty named mechanisms, each with a live incident behind it and a comment naming the incident. Fifty-three of them had gone a fortnight of production traffic without firing once.
Verification stood on two legs. Around seventeen hundred unit tests, fast and free, each proving one mechanism works alone. And sixty-three scripted personas driven through the live agent over HTTP, which cost real tokens and prove a mechanism works next to the others. That second thing is the failure mode that bites: both self-inflicted regressions of last month’s redesign were caught by a persona run and by nothing else.
[past-max]So the personas earned their keep. What I stopped noticing is what they are made of: an invented candidate, an invented script, and a list of strings the reply must avoid. All three are my own judgement wearing a costume.
They are also stochastic. I ran three of them twice this week against identical code and got two passes, then three. A signal I want behind a deploy has to move only when the code moves.
An oracle has to come from outside the author
The decision layer here is already pure. composeReply takes two JSON-serialisable arguments and returns a string; the intake planner takes one object and returns which question to ask next. Neither reads a clock, a database, or a global.
Which means every turn the bot had ever taken was reproducible, and I had been throwing the inputs away. The database kept the final text and a comma-separated list of the mechanisms that touched it. That is enough to see that a guard fired and never enough to see why, so every hypothesis about a live defect had to be retested by synthesising a fresh conversation with a live model.
The change was one table: a row per turn holding the arguments the pure functions were called with, the turn-level verdicts, and the session flags as the turn started. Then a script that reruns them on today’s code and diffs the result against the text that actually reached the candidate.
It reports two verdicts, because two things can move. The text can differ. Or the text can come out identical while a different set of mechanisms produced it, which means a guard died quietly and its absence happened not to show. No diff of the final message will ever surface that second case.
The first thing it verified was itself
The script reads its database connection from the environment, and on every machine except the production box that resolves to a local Docker container holding a dozen turns from my own testing. So the first time I ran it, to confirm records were arriving at all, it replayed those twelve and told me behaviour on real traffic was unchanged.
A verification that cannot say what it verified is the same green-over-nothing the corpus exists to remove. The source line now prints on every run with the host named and the word LOCAL attached when it is local, and the tick at the bottom refuses to appear under a hundred replayed turns. It says why, in words, instead of staying quiet.
A layer that only subtracts cannot produce a true sentence
Every honesty check in the composer deletes. Two layers can put a true sentence back: the one that states what the search actually found, and the one that supplies a neutral opening when the checks have emptied it. Both ran before the checks did.
On this turn the model wrote its own version of “nothing matches that exactly, but here are the closest ones”. The framing layer stood down, correctly, because the draft already said it. Then a later guard deleted that whole sentence for its trailing question mark. The framing layer had judged a draft that no longer existed by the time the message went out, and someone asking for one specific thing got four near-misses and no word that the thing itself was unavailable.
Additive layers run last now, after everything that can delete. Four lines of reordering. I would have read transcripts for a year without finding it, because the reply that shipped looks perfectly reasonable on its own.
Yesterday’s traffic is cheaper than an invented conversation
Turn records only accumulate forward, and I wanted the corpus this week rather than in a fortnight. What the replay compares is today’s pure functions against the text captured at the time, and both halves hold whether a candidate lived that turn or a script re-drove their transcript an hour ago. So I re-drove them: a hundred and five real conversations pulled through the bearer export and fed back into a local instance, one candidate message at a time.
Sixteen hundred and forty-eight turns. Four hours and twenty-two minutes. Eleven dollars and thirty-eight cents. Ninety-nine per cent of those turns replay; the rest are deterministic short-circuits that reach neither pure function, and the script counts them as skipped with a stated reason rather than folding them into the pass rate.
[devil's-advocate]Against that, one persona is a few dollars for a single invented conversation that takes a different branch each run.
The first thing a new oracle finds is the person who built it
I rebuilt the composer’s checking layer this week with Claude Code doing the typing, and the corpus found four defects in that work before any of them reached a person. Two are worth repeating, because a unit test would not have been looking for either.
The first was in the recording itself. The captured planner input held a live reference to the session’s ask-counter, which increments later in the same turn, and the row serialises at the end. So the corpus stored a count one higher than the planner had actually seen, and the replay reported a divergence on a code path that was fine. I had fixed exactly this shape a day earlier for a different field, and fixed it as a one-off instead of as a rule, which is the accumulation habit the whole system is supposed to be curing.
The deleted no-exact-match sentence was the second.
The other two: a guard deleting a genuine question because the question contained the words that guard hunts for, and a label the model puts on its own output being trusted over the content sitting beside it, which deleted a handoff announcement on its way to someone who had just been signed up.
[max]What a replay cannot see
Two boundaries, both sharp.
It verifies new logic over recorded inputs. It says nothing about a new input. When I added a field to the composer’s arguments, the recorded turns did not carry it, the replay took the old branch, and it reported that nothing had changed about a change it had never reached. That was recoverable only because the corpus stores more than the composer’s arguments, so the missing value could be rebuilt with the same pure function production now calls. Next time it may not be.
And it is blind to the model’s own choices, because it replays recorded model output. Whether the model still produces sane structure after I change what I ask of it is a question only a live run answers. That is what the personas are for now: three of them, chosen because they exercise the model’s half, in place of sixty-three chosen because conversations vary.
The measurement that started all of this is still sitting where I left it. Across a hundred and eight conversations, sixty-three contain a question the candidate had already answered. It is the largest defect in the system by volume, I have had the number for four days, and it is still the largest defect in the system by volume.