You start a coding agent on a free server. You close the laptop. You come back to a half-finished change. The interesting question is not "why did it crash". It is "why couldn't it resume".
A crash is cheap. A crash with no state record is expensive. This article is a myth-busting FAQ about that gap, plus a small contract you can copy.
The setup this article assumes
- An agent that runs shell commands inside a repository.
- A host you do not fully control: free tier, shared VM, or container.
- A model budget measured in tokens, not in wall-clock time.
If any of those is false, parts of this will not apply. The contract is illustrative, not a benchmark.
Myth 1: "The token quota is the limit I hit first"
The claim. Free allowance is the binding constraint. Spend it carefully and you are fine.
What you can actually observe. Four counters move, not one. Tokens grow per step. Wall-clock grows per step. Failing commands grow per retry. Human review grows per diff.
Corrected model. Tokens are a budget unit, not a capacity unit. The first wall is usually a command that fails deterministically and gets retried.
| What you assume binds | What often binds first | Where to look |
|---|---|---|
| Token allowance | a command that fails the same way each time | repeated exit_code != 0 for one step_id
|
| Model speed | build and test wall-clock |
seconds field per step |
| Running more workers | queueing on one shared CPU | steps that slow down as workers increase |
Two habits follow from this:
- Measure tokens per successful step, not per step.
- Count repeated failures separately. A step that never succeeds burns both budgets.
Myth 2: "The server is up, so my agent is running"
The claim. If the box responds to ping, work is progressing.
Corrected model. There are three lifetimes, and they are not the same:
- Host lifetime: the VM or container exists.
- Process lifetime: your worker process exists.
- Session lifetime: the agent still holds its plan in memory.
Closing a tab kills the session. A restart kills the process. Neither tells you what already ran. Progress must be recorded outside the process, or the process lifetime becomes your data-loss window.
A minimal supervisor, illustrative only and not hardened:
# /etc/systemd/system/agent-worker.service
[Unit]
Description=agent worker
After=network-online.target
[Service]
WorkingDirectory=/srv/work/repo
ExecStart=/usr/bin/python3 /srv/work/agent/run_steps.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
Restart=on-failure restarts the process. It does not restore your plan. That is the point.
Myth 3: "Open source means I can trust what it does"
The claim. I can read the code, so the agent is safe to run.
Corrected model. Auditability is not isolation. Reading source tells you what the code can do. It does not stop a bad git push or an accidental rm -rf in your working tree.
Pair source access with three cheap controls:
- A disposable workspace per run (
git worktree add). - No push credentials in the agent's environment.
- A read-only copy of anything you cannot regenerate.
Myth 4: "A log is a checkpoint"
The claim. I print every command to a file, so I can resume.
Corrected model. A log you can read is not a state you can resume. Resuming needs four fields per step: a stable step_id, a fingerprint of the command, the exit code, and the working directory.
Drop the fingerprint and you get a silent bug. You edit step build, keep the same id, restart, and the runner skips it because the id says "done".
The artifact: a checkpoint contract you can copy
The script below is illustrative and unexecuted. Treat it as a template, not a tested library.
# agent/run_steps.py — illustrative, not executed
import hashlib, json, pathlib, subprocess, sys, time
LOG = pathlib.Path("agent/steps.jsonl")
def fingerprint(cmd: str, cwd: str) -> str:
h = hashlib.sha256()
h.update(cmd.encode())
h.update(cwd.encode())
return h.hexdigest()[:16]
def finished() -> set[str]:
if not LOG.exists():
return set()
done = set()
for line in LOG.read_text().splitlines():
rec = json.loads(line)
if rec["exit_code"] == 0:
done.add(rec["step_id"])
return done
def run(step_id: str, cmd: str, cwd: str = ".") -> None:
if step_id in finished():
print(f"skip {step_id}")
return
t0 = time.time()
p = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
rec = {
"step_id": step_id,
"fingerprint": fingerprint(cmd, cwd),
"cmd": cmd,
"cwd": cwd,
"exit_code": p.returncode,
"seconds": round(time.time() - t0, 1),
"stdout_tail": p.stdout[-400:],
"stderr_tail": p.stderr[-400:],
}
LOG.parent.mkdir(parents=True, exist_ok=True)
with LOG.open("a") as f:
f.write(json.dumps(rec) + "\n")
if p.returncode != 0:
sys.exit(p.returncode)
if __name__ == "__main__":
run("deps", "npm ci")
run("build", "npm run build")
run("tests", "npm test")
Two honest limitations:
- Skip logic trusts
step_idalone. Bump the id when a command changes, or compare fingerprints on resume. - The contract is at-least-once, not exactly-once. A step that crashed mid-way may run twice.
How to probe it in an hour
This is a test plan, not a reported result. Run it yourself and keep the output.
- Run the steps once. Keep
agent/steps.jsonl. -
kill -9the worker mid-run. - Restart it. Expect
skiplines for finished steps only. -
git diffthe worktree. Confirm no step repeated a side effect. - Change a command but keep its
step_id. Confirm it does not skip. - Count
secondsper step. That is your real wall-clock floor, not model latency.
If step 5 skips, your checkpoint is a log, not a contract. Fix the fingerprint check before adding more steps.
Where a free tier actually helps
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The operator describes MonkeyCode as an open-source project with free model access, stated as a 10-million-token allowance at the time of writing, plus a free server option. Terms, quotas, and availability can change, so confirm current details on the project's official page before you plan around them.
Used honestly, those two things map onto this article's two halves:
- Free model access is the endpoint your step runner calls.
- The free server is the host that runs the worker and the supervisor unit.
Two cautions, and they are the myths again. A token allowance is not throughput. A free server is not durable storage, so keep steps.jsonl somewhere you can retrieve it after the host disappears. If you want a low-risk first test, run the hour-long probe there before pointing it at anything you care about.
Who should not use this approach
- Workloads needing exactly-once side effects: payments, schema migrations, email sends. This contract is at-least-once; add idempotency keys first.
- Teams with audit obligations: add signed, append-only logs and retention rules.
- Projects with data residency constraints: confirm where checkpoints and model calls land before running anything.
- Anyone who needs a guaranteed uptime number: a free host is a test host, not an SLA.
Takeaway
Your agent rarely dies from a model error. It dies from unresumable state. Record step ids, command fingerprints, exit codes, and working directories. Then a restart is a skip line, not a lost afternoon.
Top comments (0)