Thursday, 14:07, and my AI coding agent had just extracted a helper function from a 200-line file without a complaint. Thirty seconds later I asked for a follow-up — rename the return type — and it produced a rewrite of a different file, with a type that had never existed in our conversation. My first instinct was to blame the model: weak memory, cheap free tier, typical. My second instinct, after three wasted minutes, was the one that actually worked. I looked at the wire.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. I was testing MonkeyCode, an open-source AI coding assistant, on its free model access and free server option, because no side project of mine deserves a paid subscription yet. The failure had nothing to do with model intelligence and everything to do with how the client assembled the conversation history. That distinction is the entire point of this post.
The model is not the first suspect
Whenever an agent "forgets" something, the tempting diagnosis is a weak memory, and we move on. But an LLM can only answer the payload you give it; if the history is malformed, the smartest model in the world behaves like a goldfish. Right now the DEV feed is full of two adjacent arguments: AI has promoted every developer to a reviewer, and benchmarks measure the harness more than the model. Both are fine debates, and both skip the boring layer I actually hit — the conversation array.
So before you decide whether the model can reason, check whether the client even sent it the material to reason about. In my case the server answered, the connection stayed open, and the response was confidently wrong in a way that mirrored my own previous turns. That is a payload symptom, not a reasoning one.
Reproduce it, then prove it
After the second failure I built a minimal reproduction, because a hunch is not a bug report. The script simulates an interrupted streaming turn — exactly what happens when you press Escape mid-response or the network blinks for a second.
// repro.mjs — interrupt a stream, then send the next request
const history = [
{ role: 'system', content: 'You are a coding assistant.' },
{ role: 'user', content: 'Extract the helper from utils.ts' },
];
// The assistant starts streaming, and the turn is aborted at token 4.
history.push({ role: 'assistant', content: "Here's the ex" });
// The user follows up, and the client appends the new request.
history.push({ role: 'user', content: 'Now rename the return type to Result' });
for (const m of history) {
console.log(`[${m.role}] ${JSON.stringify(m.content)}`);
}
Output:
[system] 'You are a coding assistant.'
[user] 'Extract the helper from utils.ts'
[assistant] "Here's the ex"
[user] 'Now rename the return type to Result'
This looks harmless, doesn't it? Then the model receives an assistant turn that ends mid-word, and it must continue as if that fragment were its own previous thought. Some models recover; others drift into completions that are unrelated to your request. The variant I actually caught in my logs was even nastier: a completely empty assistant turn, pushed into history by an aborted stream, which broke the role alternation entirely.
Bad: system -> user -> assistant('') -> user
Worse: system -> user -> assistant("Here's the ex") -> user
Good: system -> user -> assistant(complete) -> user
The payload audit
Once I suspected the payload, the workflow became a checklist instead of a guessing game:
- Log every outgoing request body, redacted, to a JSONL file.
- After each turn print the role sequence: system, user, assistant, user...
- Flag empty content, duplicated roles, and assistant fragments shorter than a word.
- Reproduce with the exact transcript, never from memory.
Then the fix, at the client boundary rather than in the prompt:
type Turn = { role: 'system' | 'user' | 'assistant'; content: string };
function normalizeHistory(history: Turn[]): Turn[] {
return history
.filter((t) => t.content.trim().length > 0) // drop aborted/empty turns
.filter((t, i, arr) => {
const next = arr[i + 1];
return !(t.role === 'user' && next?.role === 'user'); // keep strict alternation
});
}
This is plain TypeScript, not prompt engineering; it fixes the transport, and the fix is testable. Add a regression test that aborts a stream, builds the next payload, and asserts the shape of the history. I would argue that one test is worth more than a thousand carefully worded prompts, because it tests the thing that actually broke.
What the free server taught me
Why does this belong in a post about free tiers? Because a hosted free server is a black box by design, and that is a feature. With a local model I would have checked server logs and called it a day; with MonkeyCode's free server option I had no server access at all, so I had to instrument the only boundary I owned — the request itself. That constraint produced better debugging habits than any tutorial I have read lately.
The free allowance also changed my sense of waste. Ten million tokens sounds infinite until you start resending half-written turns and doubled messages; every malformed request is quota burned for nothing, so a payload audit becomes a budget audit. Meanwhile, when the failure is genuinely server-side — a 429, a cold start, a reset connection — the payload is irrelevant, and you need a different fallback pipeline altogether. This retrospective is specifically about the middle case: the server answered, and the answer was wrong in a way that mirrored your own history.
The reusable checklist
If you take one thing from this post, take this sequence:
- Reproduce with a transcript, not a memory.
- Inspect the outgoing payload before inspecting the model's "memory".
- Check the role order, empty turns, and truncated fragments.
- Fix the client boundary and add a regression test.
- If the payload is clean, escalate to the provider with evidence.
And who should ignore this approach? If your coding agent behaves well, skip the logger. If the service itself is down, this won't help. The technique is for the uncomfortable middle: confident wrong answers, sessions that seem to lose memory at the worst moment, and free tiers that behave differently from your local setup.
The agent forgot the first message because the client handed it a broken conversation, not because the model's memory is a sieve. So the next time your assistant seems stupid, ask one question before you blame the model: what exactly did we send? The answer is usually on your side of the wire, and it is usually a boring bug you can fix — and test — today. If you are trying MonkeyCode's free model access, spend the first ten minutes auditing a payload instead of polishing a prompt; there is plenty of allowance left to learn the difference.
Top comments (0)