← All posts

Memory

Why AI coding agents keep making the same mistakes

The frustrating thing about an agent repeating a mistake is that it's behaving perfectly. Given what it can see, the rejected approach really is the obvious one. The failure is that nothing recorded why it was rejected.

Here's the interaction, in whatever form you've hit it. You ask an agent to improve something. It confidently proposes a change. You recognise the change, because you spent most of a day last quarter discovering it doesn't work. You explain why. It agrees immediately, apologises, and does the right thing.

Next month, different session, same suggestion.

It's tempting to read this as the model being stubborn or shallow. It isn't. The agent is reasoning correctly from what it can see, and what it can see is a codebase that contains the fix but no record of the failure. From that starting point, proposing the obvious change is the right move. The information that would change the answer isn't anywhere.

Five patterns

These recur across projects, and they share a structure worth noticing: in every one, the knowledge that would prevent the mistake is absent from the code by nature, not merely undocumented.

1. The re-proposed rejected approach

You evaluated three ways to do something and picked the least obvious one for a non-obvious reason. The code shows the choice; nothing shows the evaluation. Every fresh look at that code sees a strange decision and helpfully offers to fix it.

This is the single most expensive pattern, because the reasoning is impossible to recover from the artefact. You cannot infer “we tried the queue-based importer and the provider's ordering guarantees don't hold under retry” from a codebase that contains no queue.

2. The re-diagnosed bug

An intermittent failure takes an hour to corner. The fix is three lines. Six weeks later a similar symptom appears elsewhere and the whole investigation runs again from scratch — because the fix was recorded in git and the diagnosis wasn't recorded anywhere.

Fixes are cheap and diagnoses are expensive, and we habitually store only the cheap half.

3. The undone defensive code

Something in the code looks redundant. It looks redundant because it exists to handle a case that happened once, in production, at an inconvenient hour. An agent doing a tidy-up pass removes it. It stays removed until the case recurs.

Comments help here and only partially: a comment can say “don't remove this” but rarely carries the full incident, and comments get removed alongside the code they defend.

4. The re-discovered environment constraint

Production has a property that development doesn't — a rate limit measured per account rather than per connection, a proxy that strips a header, a database without a search index. Work in dev, fail in production, discover it again.

This one is worse than it looks because the constraint is invisible in both the code and the local environment. There is no way to know it except to have been told.

5. The drifted convention

The project does errors a particular way for a particular reason. An agent, reading a file that happens to be one of the older ones, matches a pattern you've been migrating away from and produces something internally consistent and locally wrong.

Why the code can't fix this

Every one of these is a case where the useful information is counterfactual: what would have happened, what did happen once, what was considered and discarded. A codebase is a record of what is. Counterfactuals have no representation in it.

Git gets closer, since a revert or a reverted revert is at least a trace. But commit messages are written for the change, not for the future reader trying to decide whether to make that change again, and nobody searches git history before proposing a refactor. The information is technically present and practically unreachable.

QuestionAnswerable from the code?
What does this do?Yes
What changed, and when?Yes, from git
Why this design over the alternative?No
What was tried and failed?No
Which of these two similar functions is load-bearing?No
What breaks in production but not locally?No

What actually fixes it

Store the negative results. That's it — and it's underrated precisely because negative results feel like they have no value once you've moved past them.

They have the most value, because they're the only knowledge with no other home. A successful approach is in the code. A failed one is nowhere.

The note that prevents pattern 1

upsert_note({
  project: "ingest",
  title: "Rejected: queue-based importer — provider ordering breaks under retry",
  tags: ["rejected-approach", "decision", "ingest"],
  status: "active",
  body: "Evaluated a queue-based importer in March. Rejected.\n" +
        "The provider guarantees ordering per connection, not per\n" +
        "account, so a retried message can land after a later one and\n" +
        "we apply a stale update. Reproduced in staging.\n" +
        "Current design (serial cursor, src/ingest/cursor.js) looks\n" +
        "primitive on purpose. Don't 'modernise' it to a queue without\n" +
        "solving ordering first."
})

Note what that includes: the alternative, the specific reason, the evidence, and an explicit instruction for the next reader. The last line matters — an agent that retrieves this doesn't just learn a fact, it learns what to do with it.

The rule that makes it happen

None of this works as a resolution to be more diligent. It works as a rule in the instruction file, so the agent does it without being asked each time:

When you rule out an approach, record it: what you tried, why it
failed, and what you did instead. Rejected approaches are the highest
value thing to store, because nothing else records them.

When you find a non-obvious root cause, record the cause, not just
the fix. The fix is in git; the reasoning isn't.

When production behaves differently from development, record the
difference as a constraint.

And the other half, which is what turns a store into a defence:

Before proposing a significant change to existing code, search
project memory for prior decisions and rejected approaches
concerning it.

What changes

With that in place, the interaction at the top of this post goes differently. The agent, before proposing the change, searches. It finds the March note. Instead of proposing the queue, it says: this looks like it should be a queue, but there's a note saying ordering breaks under retry — do you want me to look at whether that's still true?

That's a much better conversation, and it's a strictly better use of the agent: it's now reasoning about whether an old constraint still holds, rather than rediscovering that it exists.

It also compounds in a specific way. Each stored failure is permanent — it never has to be learned a third time — and the set of things your agents get wrong shrinks monotonically rather than resetting every session.

Setting it up

Connect an MCP memory server and add the rules above to your instruction file. For Claude Code that's:

claude mcp add --transport http knownbase https://knownbase.dev/mcp

Then seed it with the failures you already remember. Sit down for fifteen minutes and write out every “oh, we tried that” you can recall. That list is more valuable than anything the store will accumulate in its first month, and it's sitting in your head right now.

Related reading

FAQ

Why does my AI agent keep suggesting things I already rejected?

Because the rejection isn't recorded anywhere it can see. The code contains the approach you kept, not the ones you discarded, so from the agent's position the discarded option genuinely looks like an improvement. It's reasoning correctly from incomplete information.

Won't a comment in the code fix this?

Partly, for knowledge that has a natural home next to a specific line. It fails for anything project-wide, anything about an approach that isn't in the code at all, and anything long enough that a comment would be unreasonable. Comments also get removed along with the code they defend.

Isn't this what git history is for?

Git records what changed and, at best, a sentence about why. It doesn't record the alternatives considered, and nobody — human or agent — searches commit history before proposing a refactor. The information is present and unreachable.

What's the single highest-value thing to store?

Rejected approaches. They're the only category of knowledge with no other home: a successful approach is visible in the code, a failed one exists nowhere at all once the session ends.

Stop re-learning the same failures

Store the rejected approaches and root causes your agents keep rediscovering, and search them before the next change. Free plan, no card required.

Create free workspace