Why Your Coding Agent Got Worse Halfway Through the Session
It was sharp for an hour and now it is contradicting itself and reintroducing bugs you already fixed. That is context degradation, and it is manageable.
A pattern anyone who uses coding agents for real work will recognise. The first hour is excellent. By hour three the agent is contradicting decisions it made earlier, reintroducing a bug you watched it fix, and confidently describing a function signature that changed forty turns ago.
Nothing broke. This is a predictable consequence of how context works, and once you understand the mechanism there are concrete things you can do about it.
What is actually happening
Three separate effects compound.
Position matters. Models attend unevenly across a long context. Information at the very beginning and very end gets weighted more heavily than material in the middle. Your carefully written instructions from turn one are still technically in context and they are competing with forty turns of file contents, tool output, and stack traces.
Stale information is indistinguishable from current information. This is the big one. When the agent read parseConfig() at turn five, that content entered the context. At turn thirty you changed the function. Both versions are now in the conversation, and nothing in the representation marks one as outdated. The model has two contradictory facts with no timestamp.
This is why agents reintroduce bugs. It is not forgetting. It is remembering the old version equally well.
Failed attempts pollute. Every wrong approach, every failing test output, every error trace stays in context. If the agent tried three approaches before finding one that worked, all three are still there, and the wrong ones are lengthy. Anecdotally this is the biggest contributor in debugging sessions specifically, where you accumulate a lot of failure output.
Compaction loses specifics. When the context fills, tools summarise older turns. Summaries preserve narrative and lose detail: exact function names, precise error strings, the specific reason you rejected an approach. After compaction the agent knows you "worked on the config parser" and not that timeout is in seconds and must be an integer.
The symptoms, and what each one means
| Symptom | Likely cause |
|---|---|
| Reintroduces a bug you fixed | Stale file content still in context |
| Contradicts an earlier decision | Compaction lost the reasoning |
| Uses an API signature that changed | Stale read of the file |
| Ignores a constraint you stated once | Position, buried in the middle |
| Loops between two wrong approaches | Both failures in context, neither marked terminal |
| Gets slower and more hedged | Context near capacity |
The looping one is worth calling out because it is the most frustrating. The agent tries A, fails, tries B, fails, tries A again. Both failures are in context with equal weight, and neither is annotated as "already ruled out". Left alone it will cycle indefinitely.
What to do about it
Restart more aggressively than feels natural
The most effective intervention and the one people resist, because starting over feels like losing progress.
You are not losing progress. The code is on disk. What you are discarding is a context window full of stale file reads and failed attempts, which is negative value.
My rule now: when the agent makes the same mistake twice, restart the session. Not the third time, the second. The signal that context has degraded is repetition, and once you see it the session will not recover on its own.
Before restarting, write a handoff. Two minutes of work that makes the new session start where the old one ended:
## Where we are
Refactoring the config loader in src/config/.
## Done
- Moved parsing from loader.ts to parse.ts
- `parseConfig` now returns Result<Config, ParseError>, no longer throws
- Tests in parse.test.ts pass
## Next
- Update the three call sites in server.ts to handle the Result type
- Delete the old try/catch wrappers
## Constraints
- timeout is seconds, integer, not a duration string
- Do NOT touch legacy/pricing.ts
- Tried and rejected: a Zod schema here, because the config
format has conditional fields Zod cannot express cleanly
That last section matters most. Failed approaches with reasons are the thing summaries always lose and the thing that causes loops.
Commit constantly
Every working state gets a commit, with a throwaway message if necessary.
This gives you two things. A recovery point when the agent breaks something, and a diff you can show a fresh session to establish current state cheaply. git diff main is a far better summary of what changed than forty turns of conversation, and it is guaranteed accurate.
If something does go wrong, the reflog makes almost anything committed recoverable, which is the whole reason to commit aggressively.
Put durable constraints in a file, not in chat
Anything the agent must not forget belongs in AGENTS.md, not in a message.
A message is at a fixed position that gets buried. A file gets re-read, is visible to a fresh session, and survives compaction. The whole argument for a good instruction file is that it converts a thing you have to keep saying into a thing that is always available.
Same logic for decisions made mid-session. If you and the agent agree on an approach at turn twenty, write it into a file. Otherwise it is gone at turn eighty.
Keep sessions single purpose
A session that refactors the config loader, then fixes an unrelated test, then adds a feature, has three sets of file contents in context and they interfere.
One task per session. Finish, commit, restart. This feels inefficient and it is faster in wall clock terms, because you avoid the degradation entirely.
Make the agent verify rather than recall
The most reliable fix for stale content is to force a fresh read.
Instead of "update the parseConfig function we discussed", say "read src/config/parse.ts and then update parseConfig". The read puts current content at the end of the context, where attention is strongest, and it overrides the stale copy.
More generally: prefer instructions that make the agent look at ground truth. Run the tests. Read the file. Check the type. Every one of those replaces recall with observation, and observation is the thing that does not degrade.
This is the same property that makes terminal agents better than sidebar assistants in the first place. The verification loop is what keeps them grounded, and a long session degrades precisely because the ratio of recalled information to verified information shifts.
Watch for the context meter
Most tools show context usage. When it passes roughly 70 percent, quality starts dropping before compaction even kicks in.
Treat that number as a signal to wrap up the current task rather than start a new one.
What does not work
Telling it to remember. "Remember that timeout is in seconds" adds one more message at one more position. It does not change how attention works and it does not survive compaction.
Longer context windows. Bigger windows help and they do not fix this, because the problem is not capacity, it is that stale and current information are indistinguishable and both get attended to. A million token window with forty stale file versions in it is not better than a small window with one current version.
Repeating yourself. It occasionally works and mostly it adds noise, which accelerates the underlying problem.
The wider point
I think of a coding agent's context as a workspace rather than a memory. A workspace accumulates: papers you finished with, drafts you rejected, notes that are now wrong. A good workspace gets cleared regularly, and clearing it is not losing work, it is maintenance.
The teams I see getting the most out of these tools are not the ones with the best prompts. They are the ones with fast tests, strict types, good lint rules, and small commits, because all of those produce ground truth the agent can check against instead of relying on what it remembers.
Which is, again, the same conclusion I keep arriving at: the investment that makes AI tooling work is the boring investment in verification, not anything about the model.