What survives when the session dies
On September 17 a Windows Update restart took down the machine that runs my coding agents while one of them was opening a pull request. The branch had a green local test run and a commit on it. The commit had not been pushed. When the box came back, the transcript of that session was intact, the worktree was intact, and the agent's sense of what it had been about to do was gone with the process.
The bigger loss was waiting in the pull request queue. The machine had also been off for the previous week, and the scheduled routines that open fixes against my repositories had kept running in the cloud the whole time. Fourteen draft pull requests were waiting when I logged in, authored by a bot account, reviewed by nobody. By the following week the pile was forty-eight across nine repositories.
Push before reporting progress#
Git had the commit, the diff, and the worktree ahead of its upstream. What git did not have was the pull request, because the agent's wrapper only pushed at PR time, and the process died between the commit and the push.
Workers now push the branch in the same step that commits it, before any verification is reported. A local commit is treated as a draft that might vanish, and a pushed branch is the first state that counts as work in progress. The pre-push hook still runs its quick tier, so this does not skip the gate; it moves the gate earlier, to the moment the work first exists outside one process.
When the box boots, a logon task runs a sweep that reads two sources and no memory of mine. It reads the tail of every interactive session transcript that was active in the two hours before the shutdown and classifies it: ended on an API error, ended waiting for a tool result, ended waiting for me, or ended cleanly. And it walks every git worktree on the disk and records the ones that are dirty or ahead of their upstream. The output is a decisions file, one line per session, with a bucket: already live somewhere else, done, resume, hand off to a fresh session, or owner. A supervisor session reads that file and resumes at most three, never one that was mid-merge or mid-deploy, and never merges, deploys, or cleans anything itself.
The first live run of that sweep found four cut-off sessions and resumed none of them, because two were already running again in another window and two had finished. A recovery system that resumes everything is a second outage.
Give unread pull requests a recorded verdict#
The forty-eight pull requests were a different kind of stranded work. Each one had passed the hosted checks. Each one had a plausible title, a body explaining the bug, and a diff under two hundred lines. The routine that opened them had done its job. I do not let the author of a change decide whether it belongs on main, whether the author is a person or a model, so the deciding still had to happen.
For every pull request I dumped the body and the diff to a file, wrote one rubric, and dispatched read-only reviewers in batches of three or four. The reviewers were different models from the ones that wrote the code, with one instruction that did most of the work: verify the claim in the body against the source at the current default branch, and do not trust the body. Each review ends in one of four words: merge, fix first, close, or hold. Fix first names the exact change, hold names the decision it is waiting on, and close carries a ruling written on the pull request so the next pass sees it.
Twenty-two merged on the first day and ten more the next. One was closed because main already carried the fix through a different pull request. Five were held, three of them on a spending decision that was mine to make, and one of those had been sitting since July with no note saying so, which is how a held item turns into a queue lie. The rest were left with the sessions that had opened them, each with its verdict written on the pull request. The review is not what lets a change land, though. That is a full local test run bound to the exact head commit of the pull request, recorded on disk, and checked by a hook before the merge command is allowed to run.
The reviewer needs a current checkout, and the test needs current dependencies#
Four of the first twenty verdicts came back as hold, with the reason "the prerequisite this fix depends on does not exist." It did exist. The reviewers had been pointed at checkouts that were two, seven, and fourteen commits behind their remotes, and the prerequisites had landed in the gap. Re-running the same reviews against a detached worktree at the remote head flipped all four to merge or fix first. Now every checkout a reviewer reads is fast-forwarded or detached at the remote head before dispatch.
The second case was a dependency batch, nine version bumps folded into one lockfile change. The attestation ran from the canonical checkout, which detaches at the branch but keeps whatever node_modules was already installed. It recorded a green full-tier run. A second run from a worktree with a fresh install failed the home-page smoke test and the end-to-end lane, because one of the bumps moved React past the range a rendering library allows. The verifier now compares the lockfile against what is installed and reinstalls before the full tier, and lockfile pull requests attest from a worktree with fresh dependencies, never from the shared checkout.
Two outside reviewers found six holes in the sweep#
Two days later I had two other models review the sweep read-only, on the same theory: the author of a recovery system is the wrong judge of it. They worked from the same snapshot with no contact, and they agreed on six problems. The launcher wrote its once-per-boot marker after starting the supervisor tab without checking whether the tab had started, so a failed launch on the headless box counted as a recovered boot; it now waits for an acknowledgement file the supervisor writes, and a missing one leaves a pending marker and a red task result. The shutdown instant came from the last event in the Windows System log, which on an idle machine can sit hours before the actual power cut, so sessions in that gap fell outside the scan window; the estimate is now clamped to the newest transcript or worktree activity. A session waiting on me looked finished; waiting-on-owner is its own bucket. The git sweep skipped registered worktrees outside its roots and hid debt older than a day; it now enumerates every worktree, lists old debt, and changes its exit code on any scan error. And the Wi-Fi probe resolved one hostname through whichever resolver answered, so the router's habit of failing only the AWS names, the fault that had killed six deploys in an hour the day before, was invisible to it, while its health flag ignored the fourteen bad runs it had counted that day. The probe now resolves two names against the router and against a public resolver separately and records the disagreements. On its first dry run it caught a router failure that the public resolver answered, and on its first real run it reported the link unhealthy, which it is.
The bucket the sweep still lacks#
The sweep now knows about stranded sessions, stranded worktrees, and its own blind spots. It does not yet count an unreviewed pull request as stranded work, even though a routine that opens code nobody reads has stopped in the same place the killed session did. The next change is to read the open pull requests from the bot account alongside the transcripts, so that when the machine comes back, the decisions file has one more bucket: opened while you were away, unread.
Related:
- Local-first CI: the pre-push hook is the gate, the attestation record the merge gate reads
- Institutional memory for a team of one, why the sweep reads files instead of recall
- Autonomy is mostly knowing when to stop, the same rule applied to what an agent may do on its own
Questions this post answers
- What is lost when a coding-agent session is killed mid-task?
- Less than it feels like. The transcript survives on disk, and so does the git worktree, so a commit that was made but never pushed is still there. What is gone is the agent's intent: which branch was about to become a pull request, what it had verified, and what it was about to do next. A sweep that reads the transcript tail and checks every worktree for dirty files or unpushed commits recovers the state; nothing recovers the intent, so the rule is to push in the same step as the commit.
- How should automated pull requests that nobody reviewed be handled?
- As a queue, not a done pile. Each one gets a read-only review by a model that did not write it, against a checkout at the current default branch, with one of four verdicts: merge, fix first, close with a ruling, or hold with the reason written on the pull request. The merge itself is gated on a full test run bound to the exact head commit, so the review decides whether the change belongs and the test record decides whether it may land.
- Why does the recovery sweep not just resume every session?
- Because most of them are already finished or already live somewhere else, and resuming a session that is mid-merge or mid-deploy can duplicate the action. The sweep buckets sessions as already live, done, resume, hand off fresh, waiting on the owner, or owner-only; it resumes at most three per boot and never merges, deploys, or cleans a worktree on its own.