Skip to content
Period 9 / 11

When the Agent Gets It Wrong

OrderClient.get(id) has its cache now. You reviewed the diff, you rolled a bad first attempt back with a checkpoint (Cursor's per-turn undo, a one-click revert to how your files looked before that request), you fed Agent (Cursor's agent mode, which plans and writes a change across your files from a plain-English request) a doc it hadn't read on its own. Four lessons, four separate skills. This one is where you find out they were never separate. You're about to hit a bug that every one of those skills, used the way the last four lessons taught them, walks straight past.

The task: cache listForCustomer the same way

OrderClient has a second method, listForCustomer(customerId: string), and product just asked for a page size on it: callers should be able to cap how many orders come back instead of always getting the whole list. Add that limit and cache the method the same way get(id) was cached in lesson 5. get(id) only ever had one input worth keying on, so caching it was a one-key problem. This method, the moment limit lands, isn't.

The request, right-sized the way lesson 5 taught it:

In src/client.ts, add an optional limit parameter to OrderClient.listForCustomer(customerId: string, limit?: number), passed through to listOrders. Then add an in-memory cache to listForCustomer the same pattern used for get(id): cache the result after a successful lookup, and if listForCustomer is called again within 60 seconds, return the cached result instead of calling orders.ts. Use the same CACHE_TTL_MS constant already on the class. Don't touch server.ts or orders.ts.

Named file, named method, named pattern to follow, a stated TTL source, a stated boundary. By lesson 5's own checklist this prompt does everything right. That's worth sitting with for a second, because it's about to produce a diff that's wrong anyway.

The diff that looks entirely reasonable

Files changed
src/client.ts
typescript
export class OrderClient {
  private cache = new Map<string, { order: Order; cachedAt: number }>();
  private listCache = new Map<string, { orders: 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, limit?: number): Order[] {
    const cached = this.listCache.get(customerId);
    if (cached && Date.now() - cached.cachedAt < OrderClient.CACHE_TTL_MS) {
      return cached.orders;
    }

    const all = listOrders(customerId);
    const orders = limit === undefined ? all : all.slice(0, limit);
    this.listCache.set(customerId, { orders, cachedAt: Date.now() });
    return orders;
  }
}

Read it the way lesson 6 taught you to read a diff. One file touched, exactly as asked. get(id) untouched. The new listCache follows the same shape as the existing cache field: a Map, a timestamp, the same CACHE_TTL_MS reused rather than a new constant invented. limit?: number landed on the signature exactly as requested. server.ts and orders.ts don't appear in the diff. Every box lesson 5 and lesson 6 taught you to check is checked.

The bug is that listCache is keyed on customerId alone. limit is read out of the function argument, applied to slice the result on a cache miss, and then never looked at again. Ask for a customer's first 10 orders, then ask for their first 50, and the second call returns the cached 10, because as far as the cache key is concerned those two calls are identical.

This is what 'cache it the same way' actually asked for

The prompt said "the same pattern used for get(id)," and get(id) only ever had one input worth keying on. Agent generalized the pattern by keying on the one argument that felt analogous to id, and limit never got promoted to part of the key because nothing in the request said it had to be. That's not Agent being careless. It's the request being underspecified in a way that only shows up once you know the method has two arguments that both affect the result — exactly the kind of thing right-sizing (lesson 5) can't catch on its own, because the prompt read as complete when it was written.

Why the diff itself won't tell you

Go back through lesson 6's diff-review checklist against this exact diff. Does the cache key make sense? It's customerId, a string, used as a Map key, which is a completely normal thing to do. Does it follow the established pattern? Yes, line for line. Does it touch files it shouldn't? No. Does the signature change match what was asked? Yes, limit?: number landed exactly where requested. Every question that catches a structurally wrong diff returns a clean answer here, because structurally this diff is fine. TypeScript compiles it without a complaint, because nothing about Map<string, { orders: Order[]; cachedAt: number }> is a type error. Run it once with default limit and it returns the right orders, because on a cold cache, every request is a cache miss and behaves exactly like the uncached version did.

This is the gap lesson 6 named but didn't have a demo to prove yet: a diff only proves what changed. It doesn't prove the change behaves right across the inputs that matter, and "does the diff look correct" is a question about the code's shape, not its behavior. The only way to catch this bug is to actually call listForCustomer twice with two different limit values and look at what comes back.

What each module-2 practice actually checks, and what none of them checks alone

Catching it: a test that exercises the actual behavior

Not a bigger prompt, not a more careful read of the diff. A test that calls the method twice with two different limit values and asserts the results differ where they should.

Files changed
test/orders.test.ts
typescript
import { OrderClient } from "../src/client";

test("listForCustomer caches per limit, not just per customer", () => {
  const client = new OrderClient();

  const firstTen = client.listForCustomer("cus_1", 10);
  const firstFifty = client.listForCustomer("cus_1", 50);

  // A limit of 50 must not return a cached result sized for a
  // limit of 10. If this fails, the cache key is missing `limit`.
  expect(firstFifty.length).not.toBe(firstTen.length);
});

Run that against the buggy diff above and it fails: firstFifty comes back with the same length as firstTen, because the second call hit the cache instead of orders.ts. The failure is the whole point. It's the one signal in this entire lesson that isn't a matter of reading code and judging whether it looks right; it's a direct check against the behavior the request actually needed. A manual version works too, if a full test feels like overkill for a quick check: call listForCustomer("cus_1", 10), then listForCustomer("cus_1", 50), print both lengths, and notice they match when they shouldn't. Same catch, same five seconds, no framework required.

Where this test earns its keep beyond this one bug

.cursor/rules/testing.mdc from lesson 4 already says every new method on OrderClient needs a matching test in test/. This is that test. Writing it isn't extra work bolted onto the debugging story, it's the rule from lesson 4 doing exactly the job it was written for, on the first method where skipping it would have shipped a silent bug.

The fix: a corrected instruction, then a corrected diff

The instruction was underspecified in exactly one place. Naming that place is the whole fix:

The cache key for listForCustomer must include both customerId and limit, not customerId alone. Two calls with the same customerId but different limit values must be cached and returned separately.

Feed that back to Agent, pointed at the same client.ts, and the plausible corrected diff keys the cache on a composite string built from both arguments:

typescript
listForCustomer(customerId: string, limit?: number): Order[] {
  const cacheKey = `${customerId}:${limit ?? "all"}`;
  const cached = this.listCache.get(cacheKey);
  if (cached && Date.now() - cached.cachedAt < OrderClient.CACHE_TTL_MS) {
    return cached.orders;
  }

  const all = listOrders(customerId);
  const orders = limit === undefined ? all : all.slice(0, limit);
  this.listCache.set(cacheKey, { orders, cachedAt: Date.now() });
  return orders;
}

Run the test from the previous section against this version and it passes: cus_1:10 and cus_1:50 are different keys in listCache, so the second call is a real cache miss, and firstFifty comes back as its own lookup instead of a stale copy of firstTen. That pass is the actual proof. Not a cleaner-looking diff, not Agent saying it fixed the issue. The same test, run again, returning a different answer.

Four practices, one discipline

Lesson 4 gave order-api durable context so Agent stopped relearning the project every session. Lesson 5 taught you to bound a request so the diff it produces is small enough to check. Lesson 6 taught you to read that diff the way you'd read a colleague's pull request. Lesson 7 taught you to feed Agent the facts it can't infer on its own. Every one of those is a real practice, and every one of them was satisfied on the way to this lesson's bug. The rule file was in place. The request was scoped. The diff was reviewed and looked exactly like the pattern it was supposed to follow. None of that mattered, because the one thing none of the first four practices does is run the code against more than one input.

That's not a reason to distrust the first four lessons. It's the reason they're a system instead of four independent tips: rules, right-sizing, diff review, and context all raise the odds that what Agent produces is close to correct, and they make the diff small and legible enough that a check is cheap to write. But raising the odds and proving the behavior are different jobs. The check is what proves it, and it's the one step that has to touch the actual running code, not just read about it.

Quick check — The listForCustomer diff compiled, matched the established cache pattern, didn't touch files it shouldn't, and added exactly the signature change that was asked for. Why wasn't that enough?

What's ahead

The cache is correct now, on both methods, and you've got a test in test/orders.test.ts that would catch this exact class of mistake again if it ever came back. The next lesson closes out this module's remaining housekeeping: keeping secrets and build output out of what Cursor indexes in the first place, and turning the rule files from lesson 4 into something a whole team shares instead of something that lives on one machine.

Continue to Lesson 09

.cursorignore for secrets and build output, and turning your project's rules into something the whole team shares.

Have a question about this lesson?

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