Lesson 2: Checkpoints: Writing the Execution Scene to Disk
Learning goals:
- Name the six fields that belong in checkpoint.json, and for each one say what resume runs into when it's missing
- Tell apart the two save points inside a single loop turn (after the model names a tool, after the tool result is recorded) and explain what writing only one of them sets you up for
- Write a
saveCheckpoint that can't corrupt the checkpoint file itself — write a temp file, then rename atomically, instead of overwriting in place
Prerequisites: You've read Lesson 1 and can tell memory from execution state; you're comfortable with the messages array and the stop_reason-driven loop skeleton from "Agent Harness Fundamentals: Loops and Control" | Prev: Lesson 1 << | Next: Lesson 3 >>
The execution scene lives in memory by default
Lesson 1 pulled memory and execution state apart: memory is what you feed the model, execution state is the running scene the harness itself is holding — the messages array, the turn counter, the tool call whose result hasn't been recorded yet. By default that scene exists only in the process's memory. When the process dies it goes with it, and even with every other file on disk intact, the task can only start over from zero.
Writing that scene to disk — turning it into something a restarted process can read back — is what a checkpoint is. What actually holds up long-task reliability in practice usually isn't asking the model to absorb every failure on its own; it's pairing "the adaptability of AI agents built on Claude with deterministic safeguards like retry logic and regular checkpoints"1. This lesson covers the checkpoint half: what belongs in one, where in the loop to write it, and how to perform the write itself — because a checkpoint written wrong can leave you worse off than no checkpoint at all.
What to save: the six fields in checkpoint.json
A checkpoint isn't "dump everything in memory to a file." It's "record what resuming the loop needs — no more, no less." Every later lesson in this course runs off the same protocol:
- version: the protocol version number. This format will change eventually (compression for
messages, a new shape for pendingToolUse), and version lets the resume path ask "do I recognize this checkpoint?" before anything else — on a version it doesn't know, it should refuse to load and fail loudly rather than grit its teeth and parse ahead.
- task: the original user task, in words. After a restart the harness code doesn't remember what it was doing; all it can read is this file on disk. Without
task, the harness can't even say which task the checkpoint belongs to, never mind report resume progress back to the user.
- turns: how many turns have already run. It's what decides whether to trip the stopping conditions from "Agent Harness Fundamentals: Loops and Control" (a maximum-turn cap, say), and it's the number resume keeps counting from instead of restarting at zero.
- tokensUsed: cumulative token spend. The compaction threshold from "Context Engineering: Spending Finite Attention Where It Counts" fires off this number. Leave it out of the checkpoint and resume either pretends the count starts at zero — putting every compaction decision out of step — or has to re-estimate usage for every message in
messages, and in most setups the historical usage figures simply aren't available anymore.
- messages: the whole conversation scene, every user / assistant / tool_result message the model has seen. It's the largest thing in the checkpoint and the one thing you can't skip: the model has no memory of its own, and everything it knows about what happened earlier is this array you hand it on the next request. Drop it and what you resume isn't "carry on" — it's a brand-new task starting from zero while dragging along every side effect the old run already produced.
- pendingToolUse: either
null, or a record shaped like { id, name, input } — a tool the model has named whose result isn't recorded yet. What you do with this field is Lesson 3's business, where resume reconciles against it; here you only need to know it's the checkpoint's designated slot for marking a half-finished state. To keep its shape simple, every example in this lesson assumes one tool_use block per turn; when a turn issues several concurrent tool calls, make it an array — the reasoning is the same.
When to save: two save points per turn
Feed those fields into the loop and the timing turns out not to be as simple as "write once at the end of every turn." There are two save points:
Point A sits after the model response arrives and before the tool runs: record the response's tool_use block into pendingToolUse, then write. Point B sits after the tool result has been appended to messages: set pendingToolUse back to null, then write again.
Is B alone good enough? The exposure is the window between A and B — the model has named a tool and the tool is running, or has finished but its result hasn't made it into messages and hasn't been written to disk. If the process dies in that window, the last checkpoint on disk is still the one B wrote on the previous turn, and it knows nothing about this turn's call: it isn't that some detail got lost, it's that this tool call left no trace on disk whatsoever. Lesson 3 reconciles on resume — did that tool really finish, does it need re-running — and what it reconciles against is precisely the pendingToolUse that A wrote. This lesson just digs the hole; the exercises in lesson 6 put a B-only checkpoint in front of you and have you diagnose what goes wrong on resume.
How to save: you can't overwrite in place
The obvious approach is to JSON.stringify the state object and fs.writeFileSync it straight over the old checkpoint.json. That's fine when the process exits normally — but "exits normally" is exactly the case checkpoints aren't for. Checkpoints exist for the process getting killed at any moment, for power loss, for the container being evicted. Writing a file isn't an atomic operation. If the process is interrupted mid-write, the checkpoint.json left on disk may be half-written: not the old version, not the new one, just truncated JSON. The next resume throws on JSON.parse, and that file was the task's only copy of the scene — there's no older version to fall back on.
The move is "write a temp file, then rename atomically." Write the complete contents into checkpoint.json.tmp; if you crash halfway through that step, the only casualty is the temp file, and the real checkpoint.json is still the intact older version from before the crash, which resume reads fine. Once the .tmp file is complete, fs.renameSync it onto the real filename. On the same filesystem, rename is a one-step atomic replace: the OS either points the directory entry at the new file in full or leaves it pointing at the old one. There is no half-renamed state in between.
Product reference: what a checkpoint looks like in Claude Code
The protocol this lesson teaches is for unattended long tasks, at a granularity of two save points per loop turn. For contrast, look at where a real product — Claude Code — puts the word "checkpoint": "checkpointing automatically captures the state of your code before each user prompt."2 "Every user prompt creates a new checkpoint"2, and "Claude Code saves checkpoints with the conversation, so you can still run /rewind after you resume a session"2.
The scenario it serves isn't this one. Claude Code's checkpoints are built for a human-in-the-loop session — the user may stop things at any moment, try an approach, decide to go back to before some message and take another run at it — so the natural unit is "the user said something." What you're building here is for unattended long tasks: nobody is standing by to call a halt, the unit is "the loop went around once," and within a single turn it splits again into save points A and B, because a crash can land between "the model named a tool" and "the result got recorded." The two aren't solving the same problem. Putting them side by side is mostly to make one thing clear: how fine to cut a checkpoint, and how often to write one, depends on what the checkpoint is serving. There isn't only one answer.
Checkpoints aren't free
Checkpoints cost something. Under this lesson's protocol, one loop turn writes to disk twice. For a short task that finishes in three or five turns, that's pure overhead — the process runs to completion and those checkpoint files never get read. Whether to put this machinery into your own harness is worth measuring against the rule that "you should consider adding complexity only when it demonstrably improves outcomes."3 The longer the task and the higher the cost of a crash, the better that trade gets; for something that finishes in a few seconds, you probably won't need it.
💻 Exercises
Recap
- The execution scene lives in memory by default and dies with the process. What checkpoints are for is sparing a long task from re-running from scratch after every crash, so it can pick up where it broke instead1
- checkpoint.json holds six fields:
version, task, turns, tokensUsed, messages, pendingToolUse. messages is the biggest piece, and without it the model has nothing to go on about what happened earlier; pendingToolUse is the dangling-call marker Lesson 3 reconciles against
- One loop turn has two save points: A after the model names a tool and before the tool runs, B after the tool result is fully recorded in
messages. Saving at B only leaves a blind spot across the window where the model has named a tool that hasn't finished
- Overwriting the checkpoint file in place isn't safe. The process can be killed at any moment, and a crash mid-write turns the only copy of the scene into half a JSON document. Write a
.tmp file first and rename it into place with fs.renameSync — that's what guarantees whatever is on disk at any instant is one complete version
- Claude Code's checkpoints work at a different granularity — captured automatically before each user prompt2, serving a human-in-the-loop session. What this lesson builds is for unattended long tasks. The cut points differ, but both answer the same question: when something goes wrong, where do you go back to?
- Checkpoints aren't free. Two disk writes per turn is pure overhead on a short task, and whether it's worth adding comes down to whether it demonstrably improves the outcome, not to assuming more is better3
>> Lesson 3: Resuming from a Checkpoint: Restarting the Loop