The Claim Ticket - Making Case Ownership a Recorded Event
The problem
I built a telecom billing-dispute support system that routes disputed-charge cases through three agents in sequence: an Intake agent, a Billing agent, a Resolution agent. Forty synthetic cases, each one a customer disputing a charge, moving from one agent to the next as the case gets worked. In the first version I built, each agent reads the shared case state, appends its notes, and the case moves on. Nobody explicitly confirms that they've picked it up. The whole system runs on an honor system: the next agent's turn happens to run eventually, and that counts as a handoff.
I seeded scheduling gaps into the run on purpose, delaying or skipping an agent's turn the way a real system delays a turn when a queue backs up or a process restarts mid-run. Nothing dramatic. Just the kind of scheduling hiccup that happens constantly in production and never makes it into a demo. Here's what those gaps did to the honor-system version. Some cases sat with nobody actively working them for turn after turn, like a caller on hold whom nobody remembers picking up. A few got worked twice, because two agents each separately assumed they were the one responsible. A few got picked up, partially worked, and never formally closed out, drifting in a kind of ownership limbo where the case history shows activity but no resolution.
I measured this with three numbers: case-drop rate, the percentage of cases with no acknowledged owner within three turns of being handed off; average time-to-resolution; and an orphaned-case count, cases that never reach Resolution at all. All three were bad, and worse, all three were invisible from inside any single agent's own log. Intake looked fine. Billing looked fine. Resolution looked fine. Each agent did exactly what it was built to do on every turn it actually ran. The failure only showed up when I looked at the system from above and asked a question none of the three agents were built to ask themselves: who owns this case right now.
That's what makes this failure mode sneaky. A dropped handoff doesn't throw an error. It doesn't fail a test. It just sits there, in shared state, looking exactly like a case that's still in progress, until someone happens to go looking for it. No one had ever recorded ownership in the first place, which means there was nothing to check a dropped handoff against. That's the gap a case falls through, no matter how well each individual agent does its job.
The pattern
The fix I built makes ownership transfer an explicit, recorded event. Every agent has to call a propose_handoff action naming the case and the agent it's handing off to. The receiving agent has to call accept_handoff before the case counts as in progress for them at all, and has to call complete_handoff with a resolution before the case is considered closed. Every one of those calls writes a row to a handoff ledger: who proposed it, who accepted it, when, and what happened when it closed. Routing itself waits on that ledger. A case doesn't move forward until an accept_handoff has actually been logged for it.
I ran the identical forty cases through the identical scheduling gaps with this ledger in place, and the failure mode changed shape entirely. The scheduling gaps still happen, exactly as before. What's different is what a gap produces. Under the honor system, a gap left a case silently sitting in shared state, with no record that anything had gone wrong. With the ledger, that same gap produces a case sitting visibly unclaimed, timestamped, and queryable. Case-drop rate and orphaned-case count both came down near zero. The ledger was never built to stop a scheduling gap from happening in the first place. What it does is surface the gap the moment it happens, as a specific fact someone can find and act on.
That distinction matters more than it sounds like it should. The ledger doesn't reduce how often something goes wrong underneath it. What it changes is visibility: the moment something does go wrong, it shows up as a specific unclaimed case instead of staying buried in shared state, and that's the property that actually matters for a case someone's waiting on. A telecom customer with a disputed charge doesn't care whether the internal cause was a race condition or a crashed worker. They care whether someone is actually working their case right now, and a claim-acknowledge-complete ledger answers that question directly, at every point in time, straight from the ledger itself. There's no log volume to infer from and no dashboard to interpret.
Design considerations
I built this pattern scoped to exactly two parties handing a single case back and forth, one proposer and one acceptor per handoff. That scoping was a deliberate choice. A lot of the failure-mode writing I've read about multi-agent handoffs describes a different and nastier shape: three or more agents passing a task in a cycle, agent A to B to C and back to A, each one replanning because nobody actually owns the task at any point in the cycle. That's a genuinely different failure. A two-party ledger like the one I built has no concept of prior ownership history across more than one handoff at a time, and it structurally can't see a cycle spanning three or more edges, because it only ever tracks the current holder of a single handoff. Stretching this pattern to cover that shape would be a mistake. That cycle needs its own mechanism, one that tracks a chain of declined or reassigned ownership across the entire loop.
There's also a limit here. A claim ticket tells you a case has an owner. Whether that owner is doing the right thing with the case is a question the ledger was never built to answer. An agent can accept a handoff, sit on the case, and close it with the wrong resolution, and the ledger will show a perfectly clean claim-accept-complete cycle the entire time. Ownership visibility and correctness are two separate properties, and this pattern solves exactly the first one. Checking whether a resolution is actually correct once a case is legitimately owned is a different problem, with a different kind of check, and conflating the two is how a team ends up trusting a system for more than the specific thing it was built to guarantee.
The other calibration question is how many turns to wait before treating a case as dropped. I used three turns as my threshold for measurement. That number is a judgment call tied to how long a legitimate handoff takes in this specific pipeline; it's specific to this system and its rhythm, and I'd expect it to change for a different one. Set it too tight and you'll flag cases that were about to resolve normally. Set it too loose and a case can sit unclaimed for a long time before anyone notices the ledger even has a gap in it. I'd rather tune that number against real turn-time data from the system in question than import a default from somewhere else and assume it fits.