DEV Community

Taylor Wang
Taylor Wang

Posted on

I Treated the Model's Memory as Empty for 48 Hours. The Audit Diff Agreed.

Have you ever asked an AI assistant about a change it made ten minutes earlier, only to get a confident answer that contradicts your git history? I spent 48 hours chasing that failure mode on a free server, and the problem was not the model's intelligence. The real issue was that I treated a stateless HTTP endpoint as if it had a long-term memory. These are field notes from that experiment, using MonkeyCode's free model access and its free server option.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The question I started with

The goal was simple: run a tiny service on a free server, call a free model every thirty minutes, and have it summarize the latest application logs. No external database, no vector store, just the model, a cron job, and a JSONL file. I wanted to know whether a disciplined prompt could hide the fact that every request starts from zero context. The answer, after two days, was a qualified yes, but only after three failures.

What I tried

First, I built a small FastAPI service that accepted a log file and returned a summary. Second, I connected it to MonkeyCode's free server option so the cron job could run somewhere other than my laptop. Third, I wrote a response audit script that stored every prompt and completion as append-only JSONL records.

This is the core artifact I would keep if I repeated the whole experiment:

import json
import hashlib
from difflib import unified_diff
from pathlib import Path

HISTORY = Path("prompt_history.jsonl")

def save_record(record: dict) -> None:
    with HISTORY.open("a") as handle:
        print(json.dumps(record), file=handle)

def short_hash(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()[:8]

def compare_responses(question: str) -> None:
    records = [json.loads(line) for line in HISTORY.read_text().splitlines()]
    matches = [r for r in records if r["question"] == question]
    if len(matches) < 2:
        print("Not enough responses to compare")
        return
    old, new = matches[-2], matches[-1]
    print(f"old {short_hash(old['response'])} -> new {short_hash(new['response'])}")
    for line in unified_diff(old["response"].splitlines(), new["response"].splitlines()):
        print(line)
Enter fullscreen mode Exit fullscreen mode

The audit script let me see when the model's answer drifted even though the question was identical. That drift became the most useful signal in the entire 48 hours.

What broke

I expected rate limits to be the first problem, but they were not. The first thing that broke was my prompt, because I wrote "summarize the latest logs" and then sometimes forgot to attach the logs. The model answered anyway. It produced a plausible summary of logs that did not exist in the request, which is worse than a crash because it looked correct.

The fix was to make the log tail a required part of the request and to fail loudly if it was absent. I added a simple check: if the log block is empty, skip the model call and write a warning to the audit file. That one guard removed most of the false confidence from the pipeline.

The second break was a file corruption bug. My cron job and my manual debugging session both wrote to the same JSONL file, and one interleaved write produced a broken line. On Linux, I fixed it with a file lock:

import fcntl

def locked_append(record: dict) -> None:
    with HISTORY.open("a") as handle:
        fcntl.flock(handle, fcntl.LOCK_EX)
        print(json.dumps(record), file=handle)
        fcntl.flock(handle, fcntl.LOCK_UN)
Enter fullscreen mode Exit fullscreen mode

If you are on Windows, use the msvcrt module instead. The lesson is the same: free servers are still servers, and shared state needs a lock.

The third break was the classic free-server cold start. When the server dozed off, the cron job failed, and my first version did not retry. I added a small backoff wrapper, but I did not bother making it clever; three attempts with a ten-second delay were enough.

What I would repeat

I would repeat the response audit file because it turned vague paranoia into concrete diffs. I would repeat the empty-input guard because it forced the system to fail honestly instead of hallucinating detail. I would also repeat the rule that every model call must carry its own source-of-truth block, even when that block feels redundant.

Here is the decision table I used, and would use again:

Task What I include in the prompt Why
Summarize recent logs Last 50 log lines, bounded Prevents stale context and giant token costs
Give code feedback The diff plus one focused question Reduces noise and irrelevant advice
Generate tests from a spec The full spec, versioned Keeps assertions tied to the source
Answer about past events Logs or notes from that exact time Never rely on the model's memory

Limitations and who should not use this

This was not a benchmark. I did not measure latency, token usage, or accuracy across a statistically meaningful sample, and I am not claiming those numbers are good. The workflow also assumes a human watches the audit trail; if you automate decisions from model output, you are responsible for its mistakes.

Do not use this approach for customer-facing conversations that require genuine statefulness, because a stateless endpoint is the wrong foundation. Do not use it for regulated data unless you can prove which prompt produced which response and who verified the output. And if you cannot name the failure mode you are defending against, you are not ready to put a free model in your path.

If you run a similar 48-hour probe, paste your diff output in the comments. I want to see where your context breaks first.

Top comments (0)