How the No-Mistakes Daemon Persists Run State Across Restarts Using SQLite
The no-mistakes daemon stores every pipeline run in a local SQLite database with WAL mode and automatic schema migrations, then runs a recovery routine on startup to mark stale runs as failed after crashes.
The kunchenguid/no-mistakes repository implements a reliable persistence layer that ensures pipeline execution state survives daemon restarts and system crashes. By combining SQLite's durability with a deterministic recovery mechanism, the daemon guarantees that no run remains stuck in an ambiguous state after the process terminates unexpectedly.
Database Location and Initialization
The daemon resolves its state file through the paths package, constructing a path to state.sqlite within the user's home directory or the NM_HOME environment override. In internal/paths/paths.go, the DB() method returns this resolved path:
func (p *Paths) DB() string { return filepath.Join(p.root, "state.sqlite") }
When the daemon boots in internal/daemon/daemon.go, it passes this path to db.Open, which configures the connection with production-ready settings. The database opens in WAL (Write-Ahead Logging) mode with foreign-key enforcement and a 5-second busy timeout to handle concurrent access gracefully. Immediately after opening, the function executes the base schema and any pending additive migrations, ensuring backward compatibility when users upgrade the binary.
Schema Design for Run Persistence
The persistence strategy centers on a single runs table defined in internal/db/schema.go. This schema captures all necessary execution context using a ULID-based primary key:
- id:
TEXTprimary key containing a ULID for lexicographical sorting - repo_id, branch, head_sha, base_sha: Git repository context
- status: Current execution state (pending, running, failed, succeeded)
- pr_url, error: Output and failure details
- awaiting_agent_since: Optional timestamp for gated runs
- intent: Capture of user intent for the run
Using ULIDs instead of auto-incrementing integers allows the daemon to generate unique identifiers without database round-trips while maintaining chronological sortability.
Run State Operations
The daemon interacts with the runs table through type-safe helpers in internal/db/run.go. These methods encapsulate timestamp management and status transitions.
Creating and Updating Runs
When a pipeline starts, InsertRun creates a new row with a fresh ULID, current Unix timestamp, and pending status:
newRun, err := d.InsertRun(repoID, branch, headSHA, baseSHA)
if err != nil {
// handle error
}
log.Printf("started run %s", newRun.ID)
Throughout the pipeline lifecycle, dedicated methods update specific fields atomically:
- UpdateRunStatus: Transitions run state (e.g., pending → running)
- UpdateRunErrorStatus: Records failure details
- SetRunAwaitingAgent: Marks a run as parked awaiting an external agent
- ClearRunAwaitingAgent: Removes the awaiting marker when the agent responds
For example, when a gate pauses execution for an agent:
if err := d.SetRunAwaitingAgent(runID); err != nil {
// handle error
}
Querying Active State
The daemon frequently checks for existing runs to prevent duplicate executions. GetActiveRun returns the most recent run with pending or running status, optionally filtered by branch:
active, err := d.GetActiveRun(repoID, "")
if err != nil {
// handle error
}
if active != nil {
fmt.Printf("current active run: %s (status=%s)\n", active.ID, active.Status)
}
Crash Recovery on Startup
The persistence layer ensures consistency after crashes through a recovery routine invoked automatically when the daemon starts. In internal/daemon/daemon.go, the startup sequence calls recoverOnStartup, which delegates to RecoverStaleRuns in internal/db/run.go.
This function performs atomic cleanup of any runs that were interrupted:
- Marks any steps with
runningorpendingstatus asfailed - Clears the
awaiting_agent_sincecolumn to remove stale "parked" flags - Transitions the parent run to
failedwith an error message indicating the daemon crashed
count, err := d.RecoverStaleRuns("daemon crashed during execution")
if err != nil {
slog.Error("failed to recover stale runs", "error", err)
}
if count > 0 {
slog.Info("recovered stale runs", "count", count)
}
This guarantees the database never contains a run that appears active while the process is dead, preventing zombie executions and ensuring users can restart pipelines cleanly.
Thread Safety and Concurrency
SQLite's single-writer model requires careful connection management in multi-threaded environments. The no-mistakes daemon addresses this by limiting the connection pool to one open connection via SetMaxOpenConns(1) in db.Open. All database access flows through the *DB wrapper type, which serializes calls through the underlying sql.DB connection. This design eliminates write contention while allowing concurrent reads through WAL mode's snapshot isolation.
Summary
- SQLite file location: Resolved via
paths.DB()tostate.sqlitein the home directory orNM_HOME - WAL mode: Enabled with foreign keys and 5-second busy timeout for durability and concurrency
- Automatic migrations: Schema updates run on every startup in
internal/db/db.go - ULID identifiers: Text-based primary keys in the
runstable provide chronological sorting without coordination - Recovery routine:
RecoverStaleRunsmarks interrupted runs as failed when the daemon restarts - Thread safety: Single-connection pool ensures SQLite's single-writer semantics are respected
Frequently Asked Questions
Where does the no-mistakes daemon store its SQLite database?
The daemon stores state in state.sqlite located in the user's home directory, or under the path specified by the NM_HOME environment variable. The paths.DB() method in internal/paths/paths.go constructs this absolute path, which the daemon then passes to db.Open during initialization.
How does the daemon handle database schema changes when upgrading?
The db.Open function in internal/db/db.go automatically executes the base schema and any additive migration statements immediately after opening the database. This ensures the schema remains up-to-date regardless of the previous binary version, allowing seamless upgrades without manual migration scripts.
What happens to active runs when the daemon crashes?
When the daemon restarts, it invokes RecoverStaleRuns from internal/db/run.go, which atomically marks any runs with running or pending status as failed and clears the awaiting_agent_since timestamp. This prevents runs from remaining in an ambiguous state and allows users to restart them cleanly.
How does the daemon prevent database corruption from concurrent access?
The connection pool is restricted to a single open connection via SetMaxOpenConns(1), respecting SQLite's single-writer model. Combined with WAL mode and a 5-second busy timeout, this configuration ensures that concurrent read operations do not block the single writer while preventing race conditions during state updates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →