DEV Community

niuniu
niuniu

Posted on

The Agent That Forgot Everything: A Debugging Postmortem

If you have followed the agent architecture threads this week, you have seen the same phrase repeated: agents should remember decisions, not just data. I agreed with that idea until my own agent forgot everything at 3 AM on a free server. The debugging session that followed taught me more than any essay, because the failure was ordinary and the fix was boring. Both are worth sharing.

Picture the setup: a small support agent that answers questions about your project's documentation. Locally it works beautifully; the agent remembers your name and recalls that you asked about authentication ten minutes ago. You deploy it to a free server, point it at a free model endpoint, and go to sleep. At 3 AM the first alert arrives, and the agent is answering every request as if it has never met the user.

The symptom looked like a model problem, because the replies were coherent but generic, as if the conversation had been reset. A quick restart fixed it for an hour, which made it look like a memory leak or a rate limit. I checked the obvious suspects first: token usage, request counts, and the model endpoint's error logs. Nothing was over the limit, and the logs showed clean 200 responses.

The root cause was embarrassingly simple. The agent kept its conversation history in a Python list in memory, and the free server recycled the process whenever memory pressure crossed a threshold. Every recycle wiped the history, so the agent woke up with amnesia. The generic replies were not the model's fault; they were the result of sending each request with an empty context.

The second bug was hiding behind the first. Even when the process survived, the history grew without bound, and every request re-sent the entire conversation. After a few hundred messages the context window filled up, and the model started dropping the oldest turns, which produced the same amnesia in miniature. Two bugs, one symptom: state was being treated as an afterthought.

The fix was to treat memory as a first-class citizen. I moved the history out of RAM into a small SQLite database, keyed by session, and added a trimming policy that keeps the last twenty turns plus a rolling summary of everything before that. Here is the core of the fix:

import sqlite3
from datetime import datetime, timezone

conn = sqlite3.connect("agent_state.db")
conn.execute(
    "CREATE TABLE IF NOT EXISTS turns ("
    "session_id TEXT, role TEXT, content TEXT, created_at TEXT)"
)

def remember(session_id, role, content):
    conn.execute(
        "INSERT INTO turns VALUES (?, ?, ?, ?)",
        (session_id, role, content, datetime.now(timezone.utc).isoformat()),
    )
    conn.commit()

def recent_turns(session_id, limit=20):
    rows = conn.execute(
        "SELECT role, content FROM turns "
        "WHERE session_id = ? ORDER BY rowid DESC LIMIT ?",
        (session_id, limit),
    ).fetchall()
    return list(reversed(rows))
Enter fullscreen mode Exit fullscreen mode

With that in place, a restart no longer erased the conversation, because the state lived on disk instead of in the process. The trimming policy solved the second bug, because the agent now sends the last twenty turns and a short summary of the older ones. Token cost became predictable, and the request size stopped growing.

The reproducible test is simple enough to run anywhere. Start the buggy version, send a message, and kill the process with kill -9. Restart it and ask what the user just said; the buggy version draws a blank. Apply the fix, repeat the same steps, and the agent answers correctly. Then send fifty messages in a loop and watch the request size in the logs; the fixed version stays flat while the buggy one grows until it breaks.

All of this happened on a free server with a free model endpoint, which is where the economics get interesting. The free tier I used for this experiment came from MonkeyCode, an open source project that offers free model access and a free server option; at the time of writing the allowance included ten million tokens. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The point is not that the free tier is perfect, because it is not, but that constrained infrastructure forces better design, and that discipline caught a bug that would have stayed hidden on a beefy production box.

Let me be clear about who should not use this approach. If your workload needs a guaranteed uptime SLA, a free server is the wrong home, because process recycling is a feature, not a bug. If you handle sensitive data with strict residency requirements, a shared free tier should give you pause. And if your conversation histories are long and your users are impatient, you need a real database and a real deployment, not a SQLite file and a hobby process. The technique I described is a debugging lesson, not a production architecture.

The architecture essays call this a reasoning ledger, a place where the agent records decisions rather than raw data. My retrospective agrees with the concept, with one amendment: a ledger is only useful if it survives a restart. The lesson is not that agents are hard; the lesson is that state is the first thing you should debug, because it fails in the most human way, which is amnesia. If you want to reproduce this failure yourself, the MonkeyCode project is open source, so clone it, deploy it to the free server, and try to break it. You will learn more from the breakage than from the README.

Top comments (0)