# How the No-Mistakes Daemon Persists Run State Across Restarts Using SQLite

> Learn how the no-mistakes daemon persists run state across restarts with SQLite. It stores pipeline runs locally and recovers failed runs after crashes for reliable execution.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: internals
- Published: 2026-07-01

---

**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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/paths/paths.go), the `DB()` method returns this resolved path:

```go
func (p *Paths) DB() string { return filepath.Join(p.root, "state.sqlite") }

```

When the daemon boots in [`internal/daemon/daemon.go`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/schema.go). This schema captures all necessary execution context using a ULID-based primary key:

- **id**: `TEXT` primary 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`](https://github.com/kunchenguid/no-mistakes/blob/main/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:

```go
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:

```go
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:

```go
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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/daemon.go), the startup sequence calls `recoverOnStartup`, which delegates to `RecoverStaleRuns` in [`internal/db/run.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/run.go).

This function performs atomic cleanup of any runs that were interrupted:

1. Marks any steps with `running` or `pending` status as `failed`
2. Clears the `awaiting_agent_since` column to remove stale "parked" flags
3. Transitions the parent run to `failed` with an error message indicating the daemon crashed

```go
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()` to `state.sqlite` in the home directory or `NM_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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/db/db.go)
- **ULID identifiers**: Text-based primary keys in the `runs` table provide chronological sorting without coordination
- **Recovery routine**: `RecoverStaleRuns` marks 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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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.