Quick answer: An agent has no memory of a fix it made in a previous session, so nothing structurally stops it from reintroducing the exact bug you already paid to remove. The guard that actually holds is not a better prompt or a longer chat history; it is a checked-in regression test that fails the moment the old behavior comes back. Treat every bug fix as a request to encode the failure into a permanent, executable assertion, because the agent will happily undo prose fixes but cannot quietly undo a red test in CI.

Teams first notice this as a strange kind of deja vu. A payment endpoint that was double-charging customers gets fixed in March. In June, during an unrelated refactor, the agent rewrites the surrounding handler and the duplicate charge returns — sometimes character-for-character, sometimes with a new name that makes it look like fresh work. The root cause is not incompetence. Each session starts from the current code and an instruction, not from the history of why the code looks the way it does. When the structural reason for a guard is not visible in the code, the agent sees the guard as noise and removes it.

Why the fix does not survive

There are three distinct mechanisms, and most teams conflate them. Naming the one you actually have matters because the fix is different in each case.

1. The fix lived only in the conversation

If the reasoning happened in chat and the code only captured the end result, the next session inherits a correct-looking line with no explanation. An agent optimizing for simplicity reads a defensive null check, a retry limit, or a sort-before-dedupe as redundant and deletes it. The decision was preserved; the justification was not. This is the most common and the easiest to fix.

2. The guard is present but untested

A code comment says // do not remove: prevents duplicate charges. Comments are advisory. When the agent regenerates the function, it may keep the comment and drop the mechanism, or drop both. A comment cannot fail a build. Until the invariant is asserted by a test, the guard is a suggestion that survives only as long as no one edits the file.

3. The fix was structural and the refactor changed the structure

Some fixes depend on a boundary — idempotency keys handled in a specific layer, a transaction wrapping two writes, a queue deduplicating events. A refactor that moves logic between layers can reintroduce the bug without touching the original lines at all. Here even a unit test on the old function passes while the end-to-end behavior regresses, because the thing that guaranteed the invariant moved.

What durable protection looks like

The pattern that works is the same one senior engineers use for each other: make the invariant executable and keep it close to the code.

  1. Write the regression test before considering the bug closed. The test must reproduce the original failure first (red), then pass with the fix. This proves the test actually guards the bug and is not an assertion that always passes.
  2. Name the test after the invariant, not the implementation. prevents a second charge for the same idempotency key survives a refactor where calls dedupeHelper once does not.
  3. Encode the why in a short comment only for non-obvious constraints. The comment supports the test; it does not replace it.
  4. Add an end-to-end check for boundary-dependent fixes. If the bug lived across a layer transition, a unit test is insufficient — drive the real path and assert the observable outcome.
// Red first: this fails when the duplicate-charge behavior returns
it("rejects a second capture with the same idempotency key", async () => {
  const key = "chk_2026_06_10_001";
  const first = await charge({ amount: 4999, idempotencyKey: key });
  const second = await charge({ amount: 4999, idempotencyKey: key });

  expect(first.status).toBe(201);
  expect(second.status).toBe(201);
  expect(second.body.id).toBe(first.body.id); // same record, not a new charge
  expect(await countCaptures()).toBe(1);
});

Notice the assertions pin the observable result (one capture, the same identifier) rather than the internal helper. An agent can rename the helper or move the logic, but the moment a second capture is created the test goes red and blocks the merge.

Fixes that rely on memory vs. fixes that rely on the build

Protection mechanismSurvives a new session?Survives a refactor?Cost to keep
Reasoning kept in chatNoNoNone, but worthless
Code commentSometimesNoLow
Named regression unit testYesYes, within the unitModerate
End-to-end invariant testYesYes, across layersHigher
Type or schema constraintYesYes for shapeLow once in place

The last row is underused. Where a bug is really about shape — a field that must never be undefined, a response that must match the contract — a generated type or a schema check from the OpenAPI spec is stronger than a hand-written test because it cannot be deleted in a targeted edit without a type error. Keeping the contract as the source of truth means the agent inherits constraints it does not have to remember.

A practical workflow for the next fix

When an agent or a human fixes a bug, add three artifacts in the same commit: the corrected code, the regression test that fails without it, and a one-line comment only if the constraint is non-obvious. Then run the test against the previous commit to confirm it would have caught the original failure. This turns a one-time repair into a permanent fence. Teams that make this a merge requirement stop seeing fixed bugs reappear, because every session — human or agent — is forced through the same executable gate.

Frequently asked questions

Will a longer context window or uploading the whole history stop this?

It reduces the frequency but does not remove the problem. Context is finite and eventually summarized, and the agent still has no guarantee that past reasoning applies after a structural change. A regression test enforces the invariant regardless of what the model remembers. Use context for nuance; use the build for guarantees.

How do I know whether an existing fix is actually protected?

Revert the fix locally and run the suite. If no test fails, the fix is unprotected regardless of how many comments explain it. Reintroduce the bug deliberately in a throwaway branch, observe the red test, then discard the branch. That exercise tells you exactly which guards are real.

Doesn't writing a test for every bug slow the agent down?

It adds minutes at the point of the fix and saves hours of re-diagnosing the same incident later. The expensive event is not the test authoring; it is a regression reaching production a second time. Once the test exists, it guards every future session for free, including refactors you have not anticipated.

What if the bug is about an external API changing, not our code?

Add a contract test that pins the response shape you depend on, driven by the OpenAPI document or a recorded fixture. When the upstream changes the shape, the test fails at the boundary instead of surfacing as a silent runtime bug. The same principle applies: encode the assumption so the build, not memory, enforces it.

This is part of the AI Coding Agent Guide — start there for the full picture of how to verify agent work and keep fixes durable.