Short tasks don't need any of this. If one fails, run it again. The problem starts when a task takes over an hour.

Sessions end for all kinds of reasons: the context fills, the execution environment is reclaimed, a person stops it, the network drops. You can't prevent that. What you can prevent is losing everything that was finished when it happens.

1. The Real Problem Is That Progress Lives in the Conversation

By default, where does the fact "six items are done" live? Only in the transcript. When the session ends, it goes with it.

Worse, the output usually goes too. If the agent planned to write all eight items and commit once at the end, an interruption at item five and a half leaves nothing at all.

This shape: interruption = total loss write 1 → write 2 → ... → write 8 → commit → push ↑ die here and you have zero This shape: interruption = progress preserved 1 → verify → commit + push 2 → verify → commit + push ... ↑ die here and everything before survives

The principle is old and simple: put every completed unit into durable storage immediately. For agent work, durable storage means a commit and a push.

2. What Each Interruption Point Actually Costs

Treating "interrupted" as one event doesn't produce a design. What you lose depends on where the run stopped, and so does what the next session has to do.

Stopped at What's lost Next session must ────────────────────────────────────────────────────────────────────────────── Before planning nothing start over (no loss) Plan made, not committed the reasoning, the order re-choose — different answer Plan committed, mid item 1 one draft redo that item Item 1 verified, not committed one item + verify time redo that item Committed, not pushed that commit, if reclaimed redo that item Pushed nothing continue from the next item

The table collapses into two moves. Commit the plan first to erase row two; attach commit and push to each item to shorten rows four and five. Every other row already costs at most a single item.

Row two is the expensive one. A draft rewritten comes out roughly the same, but "why these eight, in this order" comes out differently every time you regenerate it. What you end up with is two sessions each executing half of a different plan.

3. Commit the Plan First

Then go one step further and persist the plan before any output.

The scheduled posts on this blog are drafted in batches by an agent. The first thing that agent does is not write a post — it writes and commits a .content-queue.md.

## Batch 2026-08-06 | # | publish | track | slug | done | |---|------------|-------|-------------------------------|------| | 1 | 2026-08-22 | A | eleventy-scheduled-publishing | [x] | | 2 | 2026-08-25 | B | claude-md-agent-rules | [x] | | 3 | 2026-08-27 | C | zone2-cardio-heart-rate-guide | [ ] | | 4 | 2026-08-30 | A | locale-symmetry-build-guard | [ ] |

With that file present, the next session reconstructs the state without any transcript: what was decided, how far it got, what remains — all of it in the repository.

There's a useful side effect. Writing the plan down first means the agent can't quietly change direction mid-run. Start without one and item six is often something nobody intended.

Plans are cheaper than output and disappear first.

A post can be rewritten. "Why these eight, in this order" is much harder to reconstruct. Persist the cheap, fragile thing first.

What a queue file has to contain

To restore state, the queue file has to answer every question the next session will ask. Six fields turned out to be necessary.

Field What breaks without it ──────────────────────────────────────────────────────────────── Batch identifier two batches blur; nobody knows where this one ends Branch name the next session cuts a different branch Item list what was decided is gone Per-item done mark finished work gets done again Batch kind new vs. follow-up is unknown; wrong work starts Closing mark a finished batch gets picked up as unfinished

The last two were added later. Without an explicit "this batch is closed," the next session has only "no incomplete items" to go on — and that is indistinguishable from a batch whose plan hasn't been written yet. One line removes the ambiguity.

People read this file too, which is why it's a Markdown table rather than JSON. Exactly one value has to be machine-read — the checkbox. Everything else is evidence the next session reasons from, and prose carries that better.

4. One Commit Per Work Item

When to commit is the whole game, and the rule is one line: the moment an item passes verification, commit and push it.

Critically, the output and the progress marker go in the same commit.

git add src/ko/posts/{slug}.njk \ src/en/posts/{slug}.njk \ .content-queue.md ← the checkbox too git commit -m "feature: ..." git push

Split them and you create a window where they disagree. Die with the post committed but the checkbox unticked and the next session writes that post again. One commit closes the window.

Push every time as well. When the execution environment itself is reclaimed, local commits go with it — a local commit is not a checkpoint.

How big should an item be?

"One item, one commit" only holds if items are the right size. Three tests decided ours.

  • Can it be verified alone? If the build only passes once another item lands, those two are one item. That's exactly why a ko/en pair is always a single item here — commit one side and the build fails.
  • Does it fit in one session? An item large enough to span sessions defeats per-item commits. Split it.
  • Is redoing it cheap? An interruption costs the whole item. If that cost makes you wince, that's the signal to split it too.

Cutting too fine has its own cost: maintaining the queue starts to outweigh the work. Our rule of thumb was ten to forty minutes per item. Shorter, merge; longer, split.

5. Separate START from RESUME

For resumption to work, the agent has to know whether this is new work or continuation. So we made the run mode explicit.

MODE: START If an unfinished batch exists, finish it before starting a new one. Otherwise, plan a new batch. MODE: RESUME No unfinished batch → do nothing and exit immediately. Unfinished batch → check out its branch and continue from the first incomplete item. Never redo an item already marked done.

That first RESUME line matters more than it looks. With nothing to do, the correct behavior is to do nothing and stop.

Left unstated, an agent will find something to do — producing work reads as more helpful than returning empty-handed. So it starts a batch nobody asked for. On a task that fires automatically, that repeats quietly, forever.

"If there's nothing to do, exit immediately" has to be written down.

It is not the default behavior. A few harmless-looking empty runs later, you have output nobody planned.

6. What RESUME Checks, and In What Order

What RESUME actually does is five decisions. The order matters enough that we wrote it out.

1. Fetch the remote first git fetch --all └ don't trust local state. the last session ran elsewhere 2. Read the queue file is there an unfinished batch? └ no → stop here. do not invent a new batch 3. Check the batch's branch does a branch of that name exist? └ yes → check it out. do not create another 4. Read the done marks skip every [x] item 5. Cross-check against files do checkbox and file disagree? └ yes → believe the file

Skip step 1 and the failure is silent. A fresh execution environment has the repository cloned at the default branch, and the branch the last session created exists only on the remote. Judge "no unfinished batch" from local state alone and the whole batch restarts from scratch.

Step 3 came from experience as well. With a date in the branch name, a second session waking the same day cut a new branch with -2 appended. Both sets of output survived, but we ended up with two pull requests and a queue file that had forked. Stating "if a branch of that name exists, continue on it" made the problem disappear.

7. Completion Must Be Observable

Resumption requires deciding "is this item done?" from files, not from a transcript.

Two signals back each other up here:

  • The checkbox — [x] in the queue file is the primary record.
  • File existence — if src/{ko,en}/posts/{slug}.njk exists, it was really produced.

When they disagree, believe the files. The checkbox is a record; the file is the fact. Disagreement is rare anyway, since they ship in one commit.

This structure also buys idempotency. Run the same session twice and the second run skips everything already complete. On anything that fires on a schedule, duplicate runs are guaranteed — so this isn't optional.

8. Verify Per Item Too

Having split commits by item, split verification the same way. Write all eight and build once at the end, and a failure means hunting for which item caused it. Worse, an interruption leaves unverified output behind.

write item 1 → npm run build → pass → commit + push ↘ fail → fix, rerun → commit only on pass

The rule is never commit an item that hasn't passed. Then every commit in the repository is a state where the build is green, and whatever survives an interruption is entirely valid.

This site fits the pattern well because the verification lives in the build. When checking is one command, running it per item costs almost nothing.

The cost of verification sets the ceiling on per-item commits. Our build runs in under a second, so running it every item is free. Flip that around: a ten-minute verification makes per-item checking untenable. Then you split the suite — fast checks (syntax, types, the item's own tests) per item, the slow full run once at the end of the batch. The dividing line is "can this check tell me which item broke it?"

9. How to Record an Item That Failed

Everything so far assumes things go well. In practice, one item sometimes refuses to work. The worst response is skipping it quietly: the next session reads the queue, sees "not done," and walks into the same wall.

So the done mark has three states, not two.

[x] finished — never redo [ ] not started — the next session picks it up [!] attempted and blocked — reason recorded below └ what failed, how far it got, what would unblock it

That third state lets the next session decide whether to retry or wait for a human. "The build failed" is worth another attempt; "the source text doesn't exist yet" will fail identically.

Recording failures in the queue file is deliberate too. Logs die with the session and an issue tracker is a separate system the next session won't open. It has to live in the file the successor is guaranteed to read.

10. What to Put in the Rules File

Surprisingly little is needed to keep this running. The agent rules file says roughly this:

- Commit the plan first. The plan lands before any output. - The moment an item passes verification, commit and push it. Do not batch several items into one commit. - Mark that item done in the queue file in the same commit. - Never redo an item already marked done. - On RESUME with no incomplete items, exit immediately.

Without that second line, an agent will batch its commits — to a human eye that's the tidier history. But once you assume interruption, frequent checkpoints beat a tidy log.

11. Common Questions

Doesn't committing a queue file clutter the history?
It adds a commit per item, yes. But those commits are the work log: what was done, in what order, when — sitting right next to the output. It's cheaper than building a dashboard, and more importantly it lives where the next session will actually look.

Why not an issue tracker or a project board?
You can use one. The single requirement is that the agent can read and write it mid-task. A board reads better for humans, but every item means an API call to update state, and a failed call desynchronizes it. A file in the repository ships in the same commit as the output, so the two structurally cannot disagree.

Can several agents work the same queue at once?
Not with this design. Two agents committing to one branch collide on push, and both can claim the same item. Parallelism needs a branch per item, or a claim commit that marks an item "in progress" the moment it's taken. We assumed one runner — the schedule fires once a day, so there was nothing to parallelize.

What if an item is marked done but the output is missing?
Believe the files and redo it. This combination only arises when output and marker land in separate commits; one commit makes it impossible. The reverse (output present, marker missing) is the same class of bug and is the worse of the two — the next session rewrites a file that already exists.

Interruptions are rare for us. Is this worth it?
Compare the costs. The design asks for one plan commit and a git push per item. A single interruption costs everything done so far. Low probability against a large loss still favors paying the premium — and on work that runs while nobody is watching, interruptions aren't rare.

What if the session didn't die but produced something wrong?
That's a scoping problem, not a resumption problem. Per-item commits do help — bad output is isolated to one commit and reverts cleanly. But keeping wrong output from reaching production is a separate concern, and that's what branches and pull requests are for.

12. In Summary

These are old principles: checkpoint your batch jobs, make units of work atomic, make re-runs safe. Agents don't change any of that.

  • Keep progress in the repository. In the conversation, it dies with the session.
  • Commit the plan before the output. The plan is the hardest part to regenerate.
  • One item = one verification = one commit = one push, with output and progress marker together.
  • Make completion decidable from files. That's what makes re-runs safe.
  • Say explicitly that no work means exit. It isn't the default.
  • Resumption starts with fetching the remote. Local state is not the last session's state.
  • Record a blocked item as blocked. Skip it quietly and the next session hits the same wall.

If you're handing an agent long tasks, ask what would survive if this session ended right now. If the answer is "nothing," the structure needs fixing before the task does.