Skip to content

8 min read

A handoff is a decision request, not a transcript

On this page

The stopping policy fires at 03:07. The engineer on the rota opens the notification at 08:40, and what happens in the next ten minutes is decided entirely by what the run left behind.

The common version is a Slack message reading "run r-4471 stopped after 14 attempts, see trace" with a link to 4,000 lines of tool calls. Opening it means scrolling for the last failure, working out which of the 291 changed lines are load-bearing, checking out the branch to see whether it even builds, and reconstructing from the reasoning what the agent thought it was doing. Measured on a team that did this for three months, the median time from opening that message to taking any action was thirty-five minutes, and roughly a third of the time the action was to delete the branch and do the task by hand.

The run knew almost everything the engineer spent those thirty-five minutes rediscovering. It knew the failing check set, it knew what each of the fourteen attempts had tried, it knew which files it had touched, it knew what it had been uncertain about. None of that reached the person, because the handoff was implemented as a link rather than as a document, and a link makes the reader do the reconstruction.

The document the policy should emit

markdown
**r-4471 — auth extraction — STOPPED at step s07, attempt 14**

## Decision needed
Should `parseSessionToken` continue to accept a lowercase `session=` cookie name?

- **Yes (preserve today's behaviour):** 6 callers in `internal/api` must pass the
  case-insensitive matcher explicitly. ~40 min of work, I have a draft on the branch.
- **No (case-sensitive, as the RFC and the new package assume):** one line reverts,
  9 tests pass, and any client still sending `session=` breaks. Access logs show
  0.4% of requests in the last 24h use the lowercase form — mostly `curl` from
  two internal jobs.

I could not decide this because it is a compatibility question about live traffic,
not a code question.

## State of the tree
- `main` untouched. Nothing deployed. No credentials used beyond the repo.
- Certified work: steps s01–s06, on `agent/r-4471`, HEAD `8f3c1ad`, builds clean,
  full suite green (14m02s, 0 failures).
- Uncertified work: step s07's last patch (291 lines) is NOT on the branch. It is
  in `refs/stash` under `s07 attempt 14 @ 03:06` and in the run artifacts.
  The branch is at the s06 boundary, so `git checkout agent/r-4471` gives you a
  working tree.

## What was attempted
| # | Approach | Failing after | Changed vs previous |
| --- | --- | --- | --- |
| 10 | pass matcher through the wrapper | 9 (same set) | — |
| 11 | normalise the header at the middleware | 11 (2 new, 0 resolved) | 82% new |
| 12 | case-insensitive lookup inside parseSessionToken | 9 (same set) | 74% new |
| 13 | accept both names, prefer canonical | 9 (same set) | 78% new |
| 14 | recovery: split s07 into s07a/s07b, retry s07a only | 9 (same set) | narrower |

The nine failing checks are the same nine identifiers in every attempt:
`TestTokenRoundTrip`, `TestRefreshFlow`, and 7 cases in `TestSessionCookieMatrix`.
All nine assert the lowercase form. They were written in 2019 and no ticket
explains them.

## Unresolved risks
- The reflection-based admin router calls `parseSessionToken` and is not covered
  by any test in s07's check. Whichever way you decide, that path needs a manual look.
- s04 is certified by `go test ./internal/billing/...`, which does not exercise
  retries. If retry behaviour changed there, nothing would have caught it.

## Cost
412k tokens, 3h51m wall clock, 14 attempts of a 20 budget. Stopped by the
unmoving-failure-set rule after one bounded recovery.

Two hundred and forty words, produced entirely from the ledger and the per-iteration records. The engineer reading it at 08:40 has a decision to make in the first four lines and enough context to make it in the next twenty. On the team that switched to this format, median time to action fell to about four minutes, and the useful proxy metric — how often the reader opened the raw trace at all — fell to under one call in five.

What each element costs if it is missing

The order is not stylistic. Each element is there because leaving it out transfers a specific piece of work to the person reading.

ElementIf it is missing, the reader mustTypical cost
The decision, stated first with optionsinfer from the failure what question is open, then work out the options themselves10–20 min, and they may answer a different question
Where the certified work is, by branch and SHAcheck out the branch, build it, discover what state it is in5–10 min, plus the risk of reviewing an uncertified tree
What was done with the uncertified patchsearch for lost work, or assume there is none5 min, or silently redoing 291 lines
Attempts with evidencere-derive which approaches were already ruled out15+ min, and they often retry attempt 11
Unresolved risksdiscover the uncovered path in review, or in productionunbounded
Cost and remaining budgetguess whether resuming is even affordable2 min, and bad resume decisions

The decision goes first because it is the only element that determines what the reader does; everything below it is evidence supporting a choice they have already been shown. Putting the state of the tree first, which is the natural engineering instinct, produces a document that reads as a status report and buries the request.

The line about the uncertified patch deserves particular attention, because the state the human inherits is the largest single cost in the handoff and it is a decision the run makes, not a fact it reports. Three dispositions are available: revert to the last certified step and discard the uncertified work; revert the branch but preserve the patch somewhere named; or leave the working tree exactly as the run left it. The third is the default in most implementations and the worst, because the sandbox is usually reclaimed within hours and because a half-applied step is the state chapter three showed is hardest to reason about. Preserving on a named ref is the sensible default:

bash
git stash push -u -m "r-4471 s07 attempt 14 @ 03:06"
git checkout s06 -- internal/api/
git push origin agent/r-4471
aws s3 cp .agent/ledger.json s3://agent-runs/r-4471/ledger.json

Whatever the run chooses, the handoff has to say which of the three happened in plain words, because an engineer who cannot tell whether the diff in front of them is certified work or abandoned work will treat all of it as suspect and review the lot.

Testing a handoff policy is more tractable than it sounds. Take twenty stored failing runs, generate handoffs from their records, give them to engineers who did not run them, and measure two things: minutes to first action, and whether the reader needed the raw trace. A handoff format that requires the trace more than one time in five is not carrying enough evidence, and one that has never been read by anyone other than its author is untested regardless of how complete it looks.

What does not belong in it

The transcript is the obvious exclusion, and it is excluded rather than summarised. A link to it belongs at the bottom for the one reader in five who needs to check something specific, and the document must be complete without it, because a handoff that gestures at the trace has moved the reconstruction back onto the reader in a politer format.

The model's narrative reasoning does not belong either. "I considered whether the middleware was the right place and concluded it was not" is unverifiable, occupies the space where evidence should be, and reads as more authoritative than it is. What replaces it is the attempts table, where the same information appears as an approach with a measured outcome. The hypothesis field from the ledger is the one place a belief is allowed, and it earns its place by being labelled as a lead rather than a finding.

Nor does a decision the run has already made on the reader's behalf. Handoffs that open with "I recommend making the parser case-sensitive" get skimmed and approved, which converts a compatibility question about live traffic into a rubber stamp. Stating the options with their consequences and the traffic figure is more work to write and it is the difference between asking for a decision and asking for permission.

A handoff is also not a pull request description, though it often becomes one. The PR describes finished work for a reviewer checking correctness; the handoff describes unfinished work for someone choosing a direction, and the two have different first paragraphs. Where they usefully merge is delivery: opening a draft PR on agent/r-4471 with the handoff as its body puts the document next to the diff, gives it a stable URL, and means the eventual decision is recorded in the place the work lives. The alternative — a chat message — is fine for the notification and poor as the artifact, because chat has no state and the question stays open.

One quality of the document is easy to miss: it has an expiry. It describes a tree at a SHA, and if somebody merges to main in the meantime the options may no longer cost what the document says. Stamping the base commit and the time, as the state section does, lets a reader three days later work out whether they are looking at a live question.

Whether long horizons are the right thing to want

The demo that prompts this question is always impressive: an agent completing a multi-hour refactor unattended, with a VP observing correctly that engineer time is the constraint and the machine may as well work overnight. The conditions under which that is a good bet are specific and mostly concern what exists before the run starts.

An overnight run is worth it when the task decomposes into steps whose certifying checks are strong enough that a certified step can be trusted without being read, since otherwise the morning's work is reviewing everything the night produced. It needs the ledger to be durable outside the sandbox, the trajectory instrumentation running, and a stopping policy that will halt the run at around a fifth of the budget when it stops moving. The blast radius has to be bounded to a branch, with no deploys and no production credentials in the environment. And someone must have budgeted review time for the morning, because a run that finishes successfully still produces a diff, and unreviewed agent output accumulates faster than it gets read.

The harder question is whether to push the horizon at all, and the honest answer is that the return on horizon is capped by the strength of the per-step checks. Every mechanism in this topic — decomposition, reconstruction from the tree, thrash detection, the stopping rule — depends on a check that separates a step that worked from one that did not, and none of them makes a weak check stronger. A team whose suite covers sixty percent of the code it is asking an agent to change does not have a horizon problem. Extending the horizon in that situation multiplies unreviewed work, which is why the diagnostic work on long-horizon agentic systems keeps finding that the failures are not model capability so much as lost state and unverifiable intermediate work.

Given a fixed engineering budget, three alternatives usually beat buying more horizon. Investing in verification — faster suites, characterisation harnesses over the untested areas, checks that are hermetic enough to certify a step — raises the ceiling on everything at once, and it is work that pays off whether or not the agent is involved. Shorter supervised cycles put a human at the boundary between steps, which costs attention and converts an eight-hour unreviewable diff into six one-hour reviewable ones. And several short runs in parallel on independent steps beat one long run, because their failures are independent, each produces a separately reviewable unit, and the parallel version has a much better worst case: one branch is wrong rather than one long trajectory being wrong from step four onwards.

Where longer horizons genuinely win is where the task is long by nature rather than by ambition — a forty-package codemod, a dependency upgrade with a mechanical fix per call site, a test migration — because those have strong checks by construction and a human in the loop every twenty minutes adds nothing but latency. That is a narrower class than the demos suggest, and it is a class worth serving well.

The pieces are now assembled in an order that can be built in. Take a real task that would take an afternoon, decompose it into steps that each end in a check you can name, write the ledger to a file the sandbox cannot take with it, and run it. The first thing you will learn is which of your steps have no check, and that list is the most useful artifact in this topic.

Long-horizon code tasks — read the current version at https://daviddominguez.dev/learn/en/long-horizon-code-tasks/a-handoff-is-a-decision-request-not-a-transcript

Back to top