Skip to content
Period 7 / 11

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.

Why this distinction matters

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:

Files changed
src/client.ts
typescript
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 Map is keyed by id, the same id passed into get(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_MS is 60_000, and the read-path check is Date.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? listForCustomer is untouched. server.ts and orders.ts don'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:

typescript
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.

What this diff actually does

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.

Reject and re-run, versus patching a bad diff by hand

Checkpoint

Quick check — What does a Cursor checkpoint actually let you skip?

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.

Continue to Lesson 07

Feeding Cursor the right context on purpose: @files, @codebase, @docs, and why context is an explicit surface, not an assumption.

Have a question about this lesson?

Reply here and it goes straight to Rod. Same as replying to one of his emails.