The Agent Guarded One Caller and Left the Same Bug Behind the Other Door
Monday morning a sparse export job crashed on a null that had supposedly been fixed two weeks earlier. We had shipped a patch for a missing optional field in the user-facing request path, added a guard there, watched the test go green, and called it done. The internal batch job reaches the same helper through a different door, and that door had no guard at all. On Monday it received a sparse record, dereferenced the optional field, and produced the same stack trace the original patch was written to kill.
The fix did not fail at the edge it was shown to. It simply never traveled to the edge it was not shown.
What actually happened
The helper takes an optional field, normalizes it, and returns a record for downstream code. The bug surfaced first through the user-facing request path, so that path became the agent's whole frame. It patched the guard at that one call site, routed the edited test through it, and closed the task. The internal batch job shares the helper but was never part of the prompt, never part of the failing stack, and never appeared in the diff.
A guard placed at a call site protects exactly that caller. Every other caller inherits the original bug.
Why the fix stopped at one caller
The agent reasons from the stack it was handed. A failing test names one entry point, one call site, one branch. The agent has no reason to believe the logic it touches is reached from elsewhere, because nothing in the task points to those places. It fixes the visible symptom at the visible location, which reads like a complete solution.
Call-site patching is local and obvious. Adding a null guard above one call is a small, diffable change that satisfies the immediate test. Fixing the invariant inside the shared layer means changing behavior for everyone, which feels broader and riskier from inside a narrow prompt.
The task named one door, not the building. When you hand an agent a failing stack trace, you have already framed the problem as that stack. It will solve that stack well and has no built-in step asking which other code reaches the same helper.
A call-site guard is a leak, not a fix
There are two places to kill a null: at the door where it enters, or at the point where the helper assumes it exists. Guarding the entry door works for that caller and quietly leaves the invariant unenforced for the rest. The next caller that arrives, especially an internal job written long ago and rarely exercised, inherits the unguarded assumption and crashes on the first sparse record.
The pattern repeats beyond nulls: a timeout added to one HTTP call while the same client is reused elsewhere, an error mapped to a 200 at one endpoint while a parallel endpoint still returns the raw failure, a permission check at one route while a sibling route shares the controller. Each patch greenes the immediate test and leaves a second door open.
The habits that reach every door
Enumerate callers before writing the patch. Before editing, list every entry path that reaches the helper: user routes, admin jobs, scheduled tasks, background workers, scripts that import the module. If the agent only sees one, the patch can only be local.
Add a regression test for each reachable path. A test that routes through the user path proves nothing about the batch job. Exercise the invariant through at least the user path and the internal path, so a fix that only covers one door fails the suite rather than shipping green.
Fix the invariant in the shared layer when possible. Normalize the optional field where the helper consumes it, so every caller gets the guarded behavior at once. A call-site guard is reserved for cases where that caller genuinely needs special handling; it is not a substitute for the shared rule.
Map the blast radius before declaring done. Ask which callers exist and whether the fix lives where all of them can inherit it. A patch that fixes one caller when the logic is shared is a partial fix dressed as a complete one.
The contract version of the same leak
API behavior leaks the same way. You validate a required field on the user-facing endpoint and forget that an internal endpoint, a legacy route, or a generated client reaches the same schema with a looser guard. The prose in a README cannot enumerate every live endpoint, so the check drifts unless it runs against the contract itself. We keep that loop local in Powerduck: the OpenAPI spec is diffed against the live endpoint for every route, so a guard on one door cannot hide the unguarded sibling behind it.
What I check before I call it done
- Did I list every caller and entry path that reaches the shared logic, or only the one in the failing test?
- Is there a regression test that routes through the non-obvious path, not just the hand-edited one?
- Does the fix live in the shared invariant layer, so all callers inherit it, or is it a guard at a single door?
- Would the batch job, admin route, or background worker that shares the code survive a sparse input?
A bug killed at one caller is only killed for that caller. Until the invariant lives where every door can inherit it, the other door is still open and waiting for its sparse record.