Repairing What the Agent Already Did

10 min read

O20
Operation · AgentOps: Deploy & Operate

Repairing what the agent already did.

The agent incident you will actually have is not an outage — it is a Tuesday on which someone notices that a tool has been writing something subtly wrong since the 3rd, and the containment machinery you built fires in four seconds against a fault that landed three weeks ago. Everything published about agent incidents is about stopping the loop; almost nothing is about the four thousand wrong actions already committed, and repairing those is a different discipline, because each one was a judgement rather than a row, so you cannot roll back a table — you have to decide again, at scale, under the same conditions that produced the mistake.

STEP 1

Agent faults arrive weeks before you detect them, because they are semantically wrong and syntactically fine.

A crashed job pages you. A malformed write fails a constraint. An agent that has quietly decided the wrong thing produces well-formed records that satisfy every schema you have, and it produces them at whatever rate the queue supplies. Detection is not free and it is not fast, so plan the repair path on the assumption that discovery lags the first bad action by days at minimum.

Three classes show up, and they need different searches:

  • Wrong-but-valid writes. The refund was issued at the wrong tier, the ticket was routed to the wrong queue, the record was updated with a plausible value derived from the wrong field. Everything validates. Only a domain check finds it.
  • Right actions with wrong reasoning. These are the ones that pass a spot audit and fail a full one. The outcome happens to be correct on the cases someone reviewed, which means the sample understated the population — the failure is in the policy the agent applied, so its prevalence is a function of case mix rather than of the agent.
  • Actions not taken. The invisible class, and usually the largest. An agent that stopped escalating a category of case leaves no record anywhere; you find it by counting what should exist, not by inspecting what does. If your monitoring only looks at emitted actions, this class is structurally undetectable — see quality regression detection for the counting side.

Write down your detection lag as a number, once, from the last real incident: hours from first bad action to first human suspicion. Every design choice on this page is sized by that number, and most teams discover it is measured in weeks. That figure, times your action rate, is the size of the repair you should be able to perform.

STEP 2

Scope by decision, not by row — and if you cannot enumerate the affected runs in one query, that is the finding.

The first question in the room is always "how many are affected?", and the first answer is always "we're pulling that together". The gap between those two is the whole cost of the incident, because everything downstream — the notification, the regulator conversation, the fix — waits on the denominator.

Row-level forensics do not get you there. The wrong refunds do not differ structurally from the right ones; what they share is a decision made under a particular prompt version, a particular tool version, a particular retrieval result, in a particular window. So the query you need is over decisions, and it needs those fields to exist:

run_id
started_at, finished_at
principal / on_behalf_of
prompt_version, model_id, tool_versions[]
inputs_digest, retrieved_doc_ids[]
decision (the classification, not the prose)
actions[]  -> external_ref, reversible: yes|compensable|no

With that, scoping is a filter and takes minutes. Without it, scoping is an archaeology project against provider logs with a 30-day retention window, and the honest version of your incident report says "approximately". Note the last field especially: reversibility recorded at the time of the action, not inferred afterwards by whoever is on call at 2am. This is the same ledger that idempotency and retries asks for to make writes safe, doing a second job — and the decision-level fields are what decision receipts and audit exists to standardise.

Scope wide and narrow twice. Start from the widest defensible boundary — everything that ran under the suspect prompt version — then narrow with domain predicates, and keep both numbers. The wide number is your exposure; the narrow number is your work list; reporting only the narrow one, before you have justified the narrowing, is how a second incident gets born.

STEP 3

Reversal is a taxonomy, not a button.

"Roll it back" is a sentence borrowed from databases, and it survives contact with an agent only for the part of the damage that lives in a database you own. Sort the affected actions into four buckets, because each has a different cost, a different owner and a different lead time:

  • Internal state you own. Recompute or restore. Cheap, and the only bucket where the word rollback means what people think it means.
  • External effects with a compensating action. A refund can be re-charged, a cancelled order re-placed, a ticket re-opened, a permission re-granted. This is the bucket that eats the time: compensation is a business process with rate limits, approval requirements and its own failure modes, and running four thousand of them is itself an agent workload with all of the same risks.
  • Irreversible effects. An email that arrived. A payment that settled. A message posted to a customer's channel. Data disclosed to someone who should not have seen it. Nothing here is undone; the only available action is notification and, where applicable, serious-incident reporting. The size of this bucket was fixed by design decisions made months ago, which is the argument for confirming irreversible actions in the first place — see human in the loop.
  • Derived state. The bucket everyone forgets. Wrong actions were summarised into reports, embedded into an index, written into agent memory, cached as answers, and read by other agents that then acted on them. Repairing the source without rebuilding the derivatives leaves the error alive in a second population — the mechanics for the index are in re-indexing and embedding migrations, and for memory in erasure against agent memory.

Do the sort before you touch anything. The ordering that minimises harm is usually: freeze, then notify the irreversible bucket, then compensate, then rebuild derivatives — and it is almost never the ordering that feels most productive at hour one, which is to start fixing the easy internal rows.

STEP 4

Re-deciding needs the policy that was in force, not the one you just fixed.

For most of the affected set you do not know the correct outcome; you only know the one you got is suspect. Producing the right answer means running the decision again — and the naive version of that, re-running today's agent over last month's cases, is wrong in a way that is easy to miss.

Two reasons. First, the case must be re-decided against the rules that applied at the time: the pricing table of the 3rd, the eligibility policy before the amendment, the customer's status as it then was. A correct decision today can be the wrong repair for a decision owed then. Second, you need to distinguish "the agent was broken" from "the agent was right and something else was broken", and you can only do that by replaying the original inputs — which requires that you pinned and recorded prompt, model and tool versions per run, exactly as rollout, versioning and pinning argues, and that the inputs are still reachable rather than having been overwritten in place.

Three rules make the re-decision trustworthy:

  • Replay against pinned versions and archived inputs. If retrieval fed the decision, the retrieved documents are part of the input and must be replayed too, not re-fetched — a re-fetch silently substitutes today's corpus and turns your repair into a new experiment.
  • Do not let the same machine grade its own repair unsupervised. Sample. The rate depends on consequence, but "the fixed agent re-ran everything and reported it was fine" is the shape of the second incident. Review a stratified sample by hand before the batch runs, and another after.
  • Record the repair as a decision in its own right. Same fields, linked to the run it supersedes. You will be asked, later, why account 44219 was refunded twice, and the answer must be a record rather than a recollection.
STEP 5

Run the backfill like a migration, not like an incident.

By this point the emergency is over: the loop is stopped, the scope is known, the outcomes are decided. What remains is a bulk mutation against production, and the failure mode of bulk mutations under time pressure is well documented and entirely avoidable. Treat it as a migration with the usual ceremony, even though it feels urgent.

  • Dry run producing a diff, reviewed by a human before anything writes. The diff is also the artefact you show the business owner to get the go-ahead, so it pays for itself twice.
  • Derive the idempotency key from the original run id. Repairs get interrupted, retried, and re-run by a second person who did not know the first one had started. A key of repair:v1:<run_id> makes the double refund impossible instead of unlikely.
  • Bounded batches with a kill switch and per-batch verification. Same instincts as kill switches, applied to your own repair job — which is, after all, an automated process making thousands of consequential writes with no one watching each one.
  • Respect the downstream rate limits. Four thousand compensating API calls against a payment processor is a traffic event; four thousand emails in ten minutes is a deliverability event that will also get your domain flagged.
  • Consolidate customer contact. One accurate message per affected person, not one per wrong action. A customer who receives nine apology emails learns more about your incident than you intended to disclose, and the person who fielded it will remember the nine.

Split the repair into two jobs with different risk profiles: a compute job that decides what should have happened and writes its conclusions to a staging table nobody reads, and an apply job that executes from that table. The compute job can be re-run freely, reviewed, and diffed; the apply job is small, dumb, idempotent and interruptible. Fusing them is what turns a repair into an incident.

STEP 6

The cheap preparation that decides whether any of this is possible.

Every capability above is a property of decisions you make while nothing is wrong. Retrofitting them during an incident is not an option, so the useful question is which ones are cheap enough to just have.

  • A side-effect ledger, per run, with a reversibility class on every entry. The single highest-leverage artefact on this page. It is a write-time record: what external thing did we touch, what is its reference, and how would we undo it? Recording that at design time takes one field on a tool definition; reconstructing it during an incident takes a week.
  • An undo path designed with every write tool. When a tool is added, its reversal is specified alongside it — a compensating call, a restore, or an explicit "none, this is irreversible", which is a useful thing to have to write down out loud during a design review.
  • Retention that outlives your detection lag. Traces on a 14-day window with a three-week detection lag mean the incident is unscopable by construction. Retention for agent decisions is a repair capability before it is a compliance one — the trade-off is in trace sampling and retention, and note that sampled traces make repair impossible even when they make monitoring affordable.
  • A repair credential, scoped and separate. The repair job needs powers the agent never had — reversing settled transactions, writing to closed records — and the agent needs powers the repair job should not have. Two identities, two scopes, both audited.
  • One rehearsal. Pick a boring wrong-write scenario, inject it in staging, and run the whole path: detect, scope, classify, replay, batch, notify. The first time you learn that your retrieved documents were never archived should not be the day it matters.

If you do one thing, make reversibility a recorded property rather than an assumption. Every write tool declares, at definition time, how its effect is undone — restore, compensating call, or irreversible — and every run keeps a side-effect ledger stamped with that class. That single discipline converts the two questions that decide an agent incident from research projects into queries: how many are affected, and how many of those can we actually put back. Everything else on this page — replay against pinned versions, staged compute-then-apply, consolidated notification — is straightforward engineering once you can answer those two. And the count of irreversible actions in that ledger is the real number: it is your maximum exposure, it was set by design decisions rather than by the incident, and it is the only one you can still change today.