Skip to content

Lessons Learned & Recurring Patterns from running an autonomous coding agent #5006

Description

@rysweet

Lessons Learned & Recurring Patterns from Running an Autonomous Coding Agent

This document distills the most important, repeatable lessons from a long stretch of operating Simard — an autonomous software agent that runs continuously as a background service, picks up engineering work on its own, writes code, opens pull requests, and (when the work passes review and CI) merges them. It was written after reviewing the entire operating history: 133 saved session snapshots and roughly 40 long-term notes accumulated over many restarts and one full host migration.

The goal is to avoid re-learning the same lessons. Most incidents in this history were not new problems — they were repeats of a pattern already seen earlier. If you hit a new failure, check it against the categories below first.


A quick glossary (so the rest reads plainly)

  • The agent / the daemon — Simard itself: a long-running background program (a "daemon" is just a service that runs continuously). It decides what to work on, launches sub-tasks, and manages its own deployments.
  • Decision cycle — the agent's main repeating loop: look at the current state, figure out what matters, decide an action, do it, repeat. (Internally this is an "OODA" loop: Observe–Orient–Decide–Act.)
  • Memory store — an embedded graph database (named lbug) compiled directly into the agent's binary. It holds the agent's long-term memory and its list of goals.
  • Workflow / recipe — a scripted, multi-step automation that runs the full engineering pipeline for one task: design → write tests → implement → review → open a PR. The program that executes it is the "recipe runner."
  • Working copy (worktree) — a separate checked-out copy of the git repository, used so multiple tasks can build in parallel without stepping on each other. (Git calls these "worktrees.")
  • Lease / claim — a short-lived "I am allowed to act right now" record stored in a database, meant only for work that is currently in progress.
  • Self-deploy — the agent building a new version of itself and installing it to replace the running version.

1. Automate judgment as reasoning behind a thin, simple rail — not as piles of if/then rules

What happened. The agent's hardest bugs came from stacking several independent, individually-reasonable rules on top of the same shared state. In the worst case the agent got stuck busy — always doing something, but never actually making progress — because three separate mechanisms fought each other every idle cycle:

  1. a rule that forced a "new cycle" and, in doing so, dropped the current work assignment,
  2. a cleanup rule that reclaimed any task without an active working copy, and
  3. a delivery step that kept finding the goal had "disappeared" before it could act on it.

Each rule was sensible alone. Together they produced zero forward progress and repeated errors.

Lesson.

  • For decisions that involve judgment — what to work on next, whether to start or stop, whether something is healthy, when to clean up — use one reasoned decision behind a thin, deterministic guardrail, rather than a growing pile of counters, thresholds, timeouts, and competing rules.
  • But know the boundary: plain infrastructure (installers, symlinks, test isolation) should stay simple and rule-based. "Use reasoning" applies to judgment, not to plumbing.
  • Prefer changing behavior by editing the automation scripts and prompts rather than adding new code, where that's possible.

2. Parsing text output from tools/AI is fragile — pass meaning, not scraped text

What happened. A recurring source of breakage was reading the text output of one step and trying to parse it — even when that output looked like structured JSON pulled from a program's console output. Removing this pattern took a whole multi-week effort, then a follow-up sweep across many more places that had the same habit.

Lesson.

  • If the thing that consumes a step's output is another automated/AI step, hand it the actual result object or meaning, and let the next step interpret it — don't print text and re-parse it.
  • These antipatterns spread. When you find one instance, search the whole codebase for siblings and fix them together. Fixing one at a time ("whack-a-mole") leaves the rest to fail later.

3. "Argument list too long" — send big inputs through a file or standard input, never the command line

What happened. A frequent crash was the operating-system error "Argument list too long." It appeared whenever a large prompt or block of context was passed as a command-line argument. The classic trigger was a shell command that inlined a whole file into the arguments (for example, embedding $(cat somefile) directly into the command).

Lesson. Any large input to a command-line tool must be delivered through a file the tool opens, or through standard input — never as a command-line argument or environment variable. When you see this error, audit every place that launches that tool; it's usually a systemic habit, not a one-off.

4. A system that upgrades itself needs an outside "escape hatch," and its safety checks come in layers

What happened. The feature that lets the agent build and install a new version of itself froze repeatedly. It was fixed four separate times, and each fix revealed the next problem underneath:

  1. a timing issue where the file was still in use when it tried to swap it,
  2. two upgrades colliding over the same temporary state directory,
  3. a health check that called a command that didn't exist, and
  4. a pre-deploy test that wasn't isolated and failed spuriously.

There was also a chicken-and-egg trap: the safety checks that must pass before a new version can deploy run inside the currently-running version. So if those checks themselves are broken, the running version can't deploy the very fix that would repair them. The only way out was a one-time manual install from outside the normal self-upgrade path.

Lessons.

  • Any system that modifies itself must have an out-of-band recovery path (a way to install a fix manually), or a broken self-check becomes a permanent trap.
  • Pre-deploy safety checks form a stack — fix each with real evidence and expect another layer beneath it. Resist shortcuts like loosening timing or muting flaky tests; those hide the next real bug.
  • Confirm a deploy by the exact code it's running, not by the version number. The version string stayed the same across many code changes, so it was a useless signal; check the actual commit/build identity of the running process instead.

5. Old copies of the program can silently win — always verify what's actually running after a deploy

What happened. The installer put the new program in one location, but older leftover copies existed in other directories that the system searched first. As a result, running the program launched a months-old build without any error. Separately, a local "redeploy" script did not first update the local code to the latest, so it quietly rebuilt stale code.

Lessons.

  • After every deploy, verify that the version and exact build of the running program match what you just deployed, and fail loudly if they don't.
  • Don't trust a redeploy script to update your source first. Explicitly fast-forward to the latest code and confirm the built binary actually contains your change.

6. Demand evidence before believing a failure — most "failures" were false alarms

What happened. A large share of apparent failures turned out to be measurement artifacts:

  • Searching logs for words like "panic," "refused," or "error" matched the reasoning transcripts and memory-recall word lists the agent prints — not actual crashes or errors.
  • One "health check failed" alarm was really a stale reading from a five-minute window belonging to the previous version, not a real regression.

Lessons.

  • Detect real crashes only with a precise check (e.g., the specific "panicked at" log line from the service journal), and filter out the agent's own transcript/recall chatter before drawing conclusions.
  • Distinguish a stale snapshot (a time-windowed health reading) from a real, current problem. Collect evidence first; never act on a guess.

7. A database compiled into your binary is high-risk — and formal modeling was worth it

What happened. The embedded memory database was a persistent source of trouble over the history: a memory-corruption crash, duplicate-symbol build failures when compiled from source, a disruptive version migration, a bug that wiped the store, contention when multiple writers hit it at once, and a write-ahead-log fix. Stabilizing it required pinning to a controlled fork of the database and writing formal specifications (using TLA+, a language for precisely modeling concurrent systems) for the multi-writer design.

Lessons.

  • A native database linked directly into your program carries real risk (build/ABI issues, corruption, concurrency bugs). Pin the dependency deliberately, and always keep a rollback binary plus a pre-change backup of the data.
  • Formal modeling paid off for the tricky concurrent (multi-writer) design — worth the effort for correctness-critical components.
  • [Still open] Memory recall is slow: it currently fans out one lookup per item (tens of thousands of round-trips per recall, adding minutes to each decision cycle). It needs a bulk index instead.

8. Don't edit a running service's in-memory state from the outside — use a durable channel

What happened. Adding a goal via the command line while the agent was running was silently discarded. The agent keeps its goal list in memory and writes it back to storage every cycle, only reloading from storage at startup — so it overwrote the externally-added goal on the next cycle.

Lessons.

  • The reliable way to give a running agent new work is a durable, external record it reads deliberately (here: GitHub issues), not by poking its live in-memory state.
  • Any state shared between a live service and an outside writer needs an explicit merge/inbox protocol, or the service will overwrite the outsider every time.

9. Temporary locks and leases must clean themselves up on restart — never carry them across a move

What happened. Short-lived "who is allowed to act" records repeatedly blocked progress. The sharpest example came during the host migration: 3,976 leftover permission records copied from the old machine blocked every decision cycle on the new machine. They had been created in normal (read-write) mode, but the new machine was running in a safe read-only mode — and because the permission identity included that mode flag, every attempt collided with a stale record. Worse, these records were dated far in the future, so the routine "delete expired records" cleanup never removed them.

Lessons.

  • Temporary runtime records (permission leases, task claims) are only meaningful for work that is currently in progress. They must be cleared and rebuilt on startup, and must never be copied or trusted across a restart, migration, or configuration change.
  • Never migrate runtime lock/lease tables along with real data. (A code fix to automatically clear these records at startup is now in progress.)

10. Discipline for running many automated workflows at once

  • Cap concurrency. Launching about ten workflows at the exact same moment caused them to hang fighting over startup resources (not CPU or memory). Stagger launches and keep the number modest.
  • Clean up finished workflows. Workflow processes often keep running after their PR has already merged. Each cycle, match running workflows against merged PRs and stop the leftovers. Never relaunch a workflow whose PR already merged — check first.
  • Remove abandoned working copies before restarting a workflow, or the restart fails because the working copy already exists.
  • Never nest working copies inside each other — that's unsupported and causes corruption. (Running many workflow processes is fine; nesting their checkouts is not.)
  • Always branch new work from the latest main. A workflow branches from whatever the source checkout currently points at; if that's stale, the resulting PR conflicts with main. Update the source checkout first.
  • Never kill the agent's own managed workflows, and when stopping a process, target it by its exact numeric process ID — never by name-matching, which can hit the wrong thing.

11. Merge rules (non-negotiable)

  • Never bypass the safety gates. Do not skip the pre-commit checks and do not force-merge with admin override. Every PR goes: automated workflow → a rigorous review loop until all concerns are resolved → a final "ready to merge" checklist → all CI checks green → merge.
  • No band-aids. Fix root causes; reject re-introducing counters, thresholds, and grace-timers to paper over the judgment-vs-rules problem from Lesson 1.
  • Notify the owner on every merge and deploy (email + chat) with a short explanation of the problem and the PR that fixed it — even for fully autonomous merges.
  • Watch for API rate limits. One class of GitHub API calls can be exhausted while another still works; read CI status and post comments through the still-available endpoints when that happens.

12. Day-to-day operating hygiene

  • Never block by polling or sleeping in the foreground — start long jobs in the background and wait for completion notifications.
  • Keep health checks simple and precise: is the service active, and are there any real crash log lines — nothing fuzzier.
  • Don't commit point-in-time reports (investigation write-ups, test-run summaries, one-off findings) into the repository. Keep repo docs durable and accurate as features change; capture snapshots as issues or notes instead.
  • Keep the tech stack pure to its design: this agent is a native program with an embedded memory database — no unrelated language runtimes or alternate databases bolted on.
  • Keep the agent's identity out of the source code. Baking personality/goal text into the codebase previously caused it to "bleed" into unrelated behavior; keep that configuration separate from code.
  • Measure before claiming the machine is busy. The hosts are large (64 CPUs, ~500 GB RAM) and comfortably run ten-plus parallel workflows; check the actual load instead of assuming.

13. Playbook for moving the agent to a new host

Concrete lessons from migrating the agent from its old machine to a new one:

  • When there's no direct SSH access and network addresses collide, drive the new host through the cloud provider's "run a command on the VM" mechanism, and move bulk data through cloud storage with short-lived access links.
  • When the system disk is small, put the agent's data and source directories on a larger attached disk and link to them.
  • For the chat integration, register the new host as an additional linked device (approve a QR code). Do not copy the old host's device identity — that can de-register the device and force a phone re-verification.
  • Watch version requirements of bundled tools (the chat tool needed a specific, recent Java version), and run such helpers as durable background services.
  • The main config file is essential — the agent crash-loops without it.
  • Clear the temporary permission records after copying the database (see Lesson 9).
  • Deploy the latest code, and always keep a rollback binary plus a pre-migration data backup.

How to use this document

When a new failure appears, check it against these 13 categories first — most incidents in this history were repeats of an earlier pattern. The highest-leverage, most durable fixes were:

  1. Automate judgment as reasoning behind a thin rail, not piles of competing rules (Lesson 1).
  2. Make temporary locks/leases self-heal on startup and never carry them across a move (Lesson 9).
  3. Stop parsing scraped text between automated steps; pass meaning instead (Lesson 2).
  4. Verify what's actually running after every deploy by exact build identity, not version number (Lessons 4–5).

Written as a durable knowledge capture across the full operating history, at the owner's request.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions