Quick answer: When an agent fixes a bug at the call site where the error surfaced, it usually closes that one ticket and leaves the identical defect in the other places the same logic was copied, because the agent's context is anchored to the failing file and it never goes looking for siblings. The durable fix is not to trust a single-file patch for a pattern-level bug; you first locate every occurrence of the logic, decide whether the duplication itself should be collapsed into one shared helper, and then fix it once at the source. If you accept a green test on the reported path and call it done, the next developer hits the same bug in a different endpoint and the agent re-derives the same fix from scratch, which is exactly the loop that makes AI-assisted codebases accumulate near-identical defects.

Agents optimize for the smallest edit that makes the immediate evidence go away, and in a single concrete failure that is a reasonable instinct, but it rests on an assumption that the bug is local. Real codebases violate that assumption constantly: validation rules get copied into several handlers, a retry wrapper is pasted across clients, and a parsing trick appears wherever a particular response had to be handled. The reported failure names one of those locations, and the agent patches it there, while the duplicated copies remain because nothing in the failing test references them.

Why a local fix is rarely the whole fix

The distinction that matters is between a genuinely local bug and a pattern that was duplicated, and the two look identical at the reported site.

SituationCorrect moveRisk of a single-site patch
Truly one-off logicFix in placeNone
Copied validation in N handlersExtract and fix onceDefect survives elsewhere
Shared helper already existsRoute all callers to itDivergent behavior
Framework-provided mechanismUse it, delete copiesDrift after framework updates

The last row is easy to miss: when a framework or the standard library already handles the concern, the right move is to delete the duplicated code rather than repair each copy, because the copies will drift from the maintained implementation the next time behavior changes.

Find every occurrence before you patch

Before editing, search the whole codebase for the structural signature of the bug, not just the literal string, so you catch variants that were renamed or lightly edited. Then treat the list as the real scope of the fix.

# search for the structural pattern, including renamed variants
# example: a hand-rolled timeout check copied across clients
rg -n "Date\.now\(\)\s*-.*>" --type js
rg -n "elapsed|expired|deadline" --type js

# list every call site that imports or inlines the logic
# the fix is only complete when each one is accounted for

Relying on a semantic search rather than a single literal matters because copied code drifts: variable names change and formatting shifts, but the structure stays the same, and a literal grep will miss those siblings while a structural query will not.

Prefer collapsing the duplication over fixing N copies

Where the same logic appears multiple times, the stronger fix is to extract it into one tested unit and route every call site through it, then fix the bug inside that unit once. This both removes the current defect and removes the place where the next copy would live.

// before: three handlers each inline their own expiry check
function isExpired(record, now = Date.now()) {
  return now > record.expiresAt;
}

// after: every caller uses the single tested helper
for (const record of records) {
  if (isExpired(record)) {
    await handleExpiry(record);
  }
}

If a shared helper already exists, the task is simpler and more important: redirect the stragglers to it rather than fixing their inline copies, because leaving two implementations of the same rule guarantees they will diverge the next time someone patches only one.

Make the agent prove the pattern is covered

When you delegate the fix, ask for the full occurrence list and a test that exercises the shared behavior rather than only the reported path, and verify the agent did not just silence the one failing test. A contract or shared module with a focused test turns a pattern-level bug into a single place that cannot silently reappear in a new copy.

The practical rule is to treat any bug that could plausibly have been copied as a pattern-level problem until the codebase proves otherwise. Search structurally first, collapse duplicated logic into one tested helper, and require evidence that every occurrence is covered. A single-file patch that turns one red test green is a good start and a bad finish; it removes the symptom you noticed and preserves the defect everywhere you did not look.

Frequently asked questions

Why does the agent keep fixing the same bug in different files?

Each run is anchored to the failure it is given, and if the logic was copied, the reported location is only one of several. Without a structural search, the agent has no reason to visit the other copies.

Should I fix every copy or extract a helper?

Prefer extracting a single tested helper and routing all callers through it. Fixing each copy leaves the duplication in place and the copies will diverge again on the next change.

How do I catch renamed or edited copies of the logic?

Search for the structural signature, the shape of the operation, rather than a literal variable name, because copied code drifts in naming while keeping the same structure.

What if the framework already handles this?

Delete the copies and use the maintained mechanism. Repairing hand-rolled duplicates just guarantees they fall behind the framework's behavior later.

How do I verify the fix covers the whole pattern?

Get the full occurrence list, confirm each call site routes through the shared unit, and add a test on the shared behavior rather than only the originally failing path.

This is part of the The AI Coding Agent Guide — start there for the full picture of keeping agent edits reliable.