Review Every Diff Like a Pull Request
Last lesson ended with a diff sitting in Composer (Cursor's agent mode: you describe a task in plain English, it plans the change and writes it across your files, then hands you a diff to accept or reject), waiting on your call: a cache added to OrderClient.get(id) in src/client.ts, keyed by order id, with a 60-second TTL. You wrote a right-sized prompt, Agent produced a plausible-looking change, and then the lesson stopped short of the one step that actually matters. Plausible-looking is not the same as correct. This lesson is that step.
What a checkpoint actually is
Cursor calls it a checkpoint, and the name invites a wrong guess. It sounds like a save state, maybe even like a git commit Cursor is quietly making on your behalf. It isn't either of those.
A checkpoint is a restore point tied to one specific Composer or Agent turn. Every time you send Agent a request that changes files, Cursor records what those files looked like immediately before that turn, and gives you a way back to exactly that state if you reject what came back. That's the whole mechanism. It's not automatic version control for your project, it doesn't span multiple turns, and it isn't a substitute for git commit. It's scoped to the one edit you're currently looking at, the same way "undo" is scoped to the one keystroke you just typed.
Git history is something you build deliberately, commit by commit, as a record you intend to keep. A checkpoint is closer to a seatbelt: it exists so a single bad turn costs you nothing, not so you have a permanent log of every attempt. Keep committing to git the way you always have. The checkpoint is what makes it safe to try a request, see the diff, and back out clean if the diff is wrong, before anything is worth committing at all.
Reviewing the client.ts diff like a colleague's PR
Here's the diff lesson 5 produced. Open Cursor's diff view for src/client.ts and you'd see this against the file's previous state:
export class OrderClient {
private cache = new Map<string, { order: Order; cachedAt: number }>();
private static readonly CACHE_TTL_MS = 60_000;
get(id: string): Order {
const cached = this.cache.get(id);
if (cached && Date.now() - cached.cachedAt < OrderClient.CACHE_TTL_MS) {
return cached.order;
}
const order = getOrder(id);
this.cache.set(id, { order, cachedAt: Date.now() });
return order;
}
listForCustomer(customerId: string): Order[] {
return listOrders(customerId);
}
}You wouldn't approve a colleague's pull request by skimming it for a plausible shape and clicking merge. You'd check it against what the ticket actually asked for. Do that here, line by line, the same way:
- Does the cache key make sense? The
Mapis keyed byid, the sameidpassed intoget(id). That's what "cache each order by its id" asked for, not a serialized request object or some other key Agent could have invented instead. - Does the TTL match what was asked?
CACHE_TTL_MSis60_000, and the read-path check isDate.now() - cached.cachedAt < OrderClient.CACHE_TTL_MS. Sixty seconds, exactly the window stated in the prompt, not a number Agent picked on its own. - Did it touch anything outside the stated scope?
listForCustomeris untouched.server.tsandorders.tsdon't appear anywhere in this diff. The prompt said don't touch them, and the diff didn't.
Three checks, all pass, and each one took a few seconds because the prompt told you in advance what "correct" was supposed to look like. That's not luck. That's the request from lesson 5 doing its job at review time, not just at request time.
The version you don't see in lesson 5: a first attempt that caches errors too
The diff above is the corrected version. It's worth seeing what a plausible first attempt at the same request can look like, because "plausible" is exactly the problem: a diff that compiles, reads cleanly, and does roughly the right thing can still be wrong in a way that doesn't announce itself.
Imagine Agent's first pass at "add an in-memory cache to OrderClient.get(id)" comes back like this instead:
export class OrderClient {
private cache = new Map<string, { order: Order | Error; cachedAt: number }>();
private static readonly CACHE_TTL_MS = 60_000;
get(id: string): Order {
const cached = this.cache.get(id);
if (cached && Date.now() - cached.cachedAt < OrderClient.CACHE_TTL_MS) {
if (cached.order instanceof Error) throw cached.order;
return cached.order;
}
try {
const order = getOrder(id);
this.cache.set(id, { order, cachedAt: Date.now() });
return order;
} catch (err) {
this.cache.set(id, { order: err as Error, cachedAt: Date.now() });
throw err;
}
}
listForCustomer(customerId: string): Order[] {
return listOrders(customerId);
}
}Skimmed at pull-request speed, this looks like the same change with slightly more error handling, which reads as thorough rather than wrong. It compiles. get(id) still takes a string and still returns an Order on the success path. listForCustomer, server.ts, and orders.ts are still untouched. Every check from the section above still passes.
Run the cache-key and TTL checks again anyway, because that's the discipline, not a one-time pass. The key is still id. The TTL is still 60_000. Nothing on the surface flags this as broken.
orders.ts's getOrder(id) throws for an unknown id. That throw now gets caught and cached, keyed by the bad id, for the full sixty seconds. Call get("nope") once by mistake, typo, retry storm, doesn't matter, and every caller asking for ord_nope for the next sixty seconds gets an instant thrown error from the cache instead of a fresh lookup. A single bad request from one caller poisons the result for every other caller asking about that same id, none of whom made the mistake.
That's the failure mode named at the top of this lesson: nothing about it shows up as a compile error, a lint warning, or an obviously malformed diff. It shows up as a decision, caching a failure the same way it caches a success, that nobody asked for and the prompt never authorized. "Cache each order by its id after a successful lookup" was the instruction. Caching a thrown error isn't a successful lookup. The diff just didn't say so anywhere you'd notice without checking against the actual words in the prompt.
Catching it: reject, correct, re-run
This is where the checkpoint stops being a safety-net metaphor and becomes the thing you actually click. Cursor's checkpoint for this turn is right there in the Composer panel, tied to the request that produced the error-caching version. Reject it. The file reverts to exactly what it looked like before this turn, no partial edit left behind, no manual cleanup.
Then fix the instruction, not the code. Go back to the prompt and close the door this version walked through:
In src/client.ts, add an in-memory cache to
OrderClient.get(id). Cache each order by its id after a
successful lookup only. Do not cache thrown errors. If
get(id) throws for an id, that call should always hit
orders.ts fresh, every time, with no caching of the failure.
If get(id) is called again for the same id within 60 seconds
of a successful lookup, return the cached order without
calling orders.ts. After 60 seconds, treat the entry as
stale and look it up again. Don't change the method's
signature or return type, and don't touch server.ts or
orders.ts.
One clause added, "after a successful lookup only. Do not cache thrown errors," plus a sentence stating what should happen instead. Re-run it, and the diff that comes back is the version from the top of this lesson: a cache keyed by id, a 60-second TTL, and a get(id) that only ever writes to the cache after getOrder(id) returns successfully. A thrown lookup stays uncached, exactly as the corrected prompt now says.
Notice what didn't happen. You didn't hand-edit the broken version to patch out the error-caching branch. You rejected the whole turn, corrected the request, and let Agent produce a clean diff against the corrected instruction. The checkpoint made that a costless choice instead of a fifteen-minute manual surgery on code you didn't write.
Checkpoint
What's ahead
Reading a diff carefully only works when the diff was produced with the right facts in front of Agent to begin with. Every example in this module has been about a request the agent could answer from client.ts alone. Next lesson breaks that pattern on purpose: a request that needs a convention living somewhere else in the repo, and the explicit ways you point Agent at it instead of hoping it already knows.
Feeding Cursor the right context on purpose: @files, @codebase, @docs, and why context is an explicit surface, not an assumption.
Reply here and it goes straight to Rod. Same as replying to one of his emails.