JSONL ledgers in git as the state layer for an autonomous agent: patterns that survive crashes and retries
Our autonomous agent has been running a small publishing business for three months: it posts, replies, follows, publishes articles, and tracks every decision it makes. The state layer behind all of that is not Postgres, not SQLite, not Redis. It is a directory of JSONL files committed to git. This choice gets us laughed at occasionally, so this post is the honest case for it — the patterns that make append-only text files survive crashes, retries, concurrent writers, and an LLM's enthusiasm for re-running things it already ran. Why files-in-git at all Three properties turned out to matter more than query power: Every state change is a diff. When the agent follows someone, replies to a thread, or publishes an article, the evidence lands in git log with a timestamp and an author. Auditing an autonomous system is the hard part of running one; with ledgers in git, the audit trail is the storage engine. Scheduled jobs and interactive sessions share state with no server. Our GitHub Actions jobs check out the repo, read the ledgers, act, commit. The interactive session pulls before deciding anything. The merge boundary is git's problem, which is a well-understood problem. The LLM can read its own state natively. An agent that can grep its full decision history is meaningfully smarter than one that needs a query layer written for it. Pattern 1: append-only, with one exception Almost every ledger is append-only: one JSON object per line, new facts go at the end. Append-only means a crashed write corrupts at most the final line, and recovery is "drop the broken tail," not "restore from backup." The exception: consumption ledgers (a stock of pre-written posts, a queue of follow candidates) need a consumedAt stamp on existing rows. For those we load-modify-rewrite the whole file — acceptable because the files are small — with one hard rule: a consumed mark is never overwritten. The update function refuses to touch a row whose consumedAt is already set. Retry-safety comes from t