How the no-mistakes Daemon Ensures a Singleton Instance: OS-Level File Locking Explained

The no-mistakes daemon guarantees a singleton instance by acquiring an exclusive, non-blocking OS-level file lock on <NM_HOME>/daemon.lock before any initialization work, automatically releasing it when the process exits to prevent duplicate daemons.

The no-mistakes daemon uses a robust singleton pattern to ensure only one live process manages a given NM_HOME directory at any time. According to the kunchenguid/no-mistakes source code, this implementation relies on kernel-enforced file locking rather than PID files, eliminating stale lock scenarios while providing clear diagnostic feedback when conflicts occur.

The Singleton Lock Architecture

OS-Level File Locking Mechanism

At the core of the singleton guarantee is the acquireSingletonLock function defined in internal/daemon/lock.go. This function opens the lock file at <NM_HOME>/daemon.lock and attempts to obtain an exclusive, non-blocking lock using platform-specific helpers (tryLockFile and unlockFile).

The kernel automatically releases this lock if the owning process exits, crashes, or is killed. This kernel-enforced cleanup removes the need for separate "is the holder still alive?" checks that traditional PID-file approaches require, making the mechanism significantly more reliable.

Diagnostic Record for Observability

Once the lock is acquired, the daemon writes a small JSON diagnostic record into the lock file containing its PID and start time. As implemented in lines 53–62 of internal/daemon/lock.go, this record provides human-readable context:

record := lockHolderRecord{PID: os.Getpid(), StartedAt: time.Now().UTC()}
if data, err := json.Marshal(record); err == nil {
    _ = f.Truncate(0)
    _, _ = f.WriteAt(data, 0)
    _ = f.Sync()
}

Important: This JSON metadata is strictly for diagnostics. The actual safety guarantee comes from the exclusive OS lock itself, not the contents of the file.

Integration with the Daemon Lifecycle

Early Acquisition in RunWithOptions

The singleton guard is placed at the very beginning of RunWithOptions in internal/daemon/daemon.go (lines 19–28). This early acquisition ensures the lock is held before any destructive startup work—such as stale-run recovery, orphan-worktree cleanup, or IPC socket binding—begins execution.

func RunWithOptions(p *paths.Paths, d *db.DB, stepFactory StepFactory) error {
    // Singleton guard: only one live daemon may own this NM_HOME at a time.
    lock, err := acquireSingletonLock(p)
    if err != nil {
        return err
    }
    defer lock.Release()
    // ... rest of daemon startup …
}

If another daemon already holds the lock, acquireSingletonLock returns ErrSingletonLockHeld wrapped with the holder's PID and start time. This allows the second daemon to abort immediately with a clear error message, preventing race conditions that could corrupt run metadata or delete active worktrees.

Automatic Release and Safety Guarantees

The daemon defers lock.Release() immediately after acquisition (lines 31–33 of daemon.go). The Release method unlocks and closes the file descriptor, and is safe to call on a nil receiver—a property validated in internal/daemon/lock_test.go lines 74–80. This safety ensures the deferred cleanup executes correctly even if lock acquisition fails, preventing resource leaks.

Error Handling Implementation

When the singleton lock is already held, the daemon surfaces detailed diagnostics to the user. The error handling around lines 28–32 of daemon.go wraps ErrSingletonLockHeld with the competing process's PID and start time, enabling administrators to identify the running instance without needing external process inspection tools.

Summary

  • Kernel-enforced exclusivity: The acquireSingletonLock function in internal/daemon/lock.go uses OS-level file locking that automatically cleans up on process termination.
  • Early validation: Lock acquisition occurs at the start of RunWithOptionsbefore any destructive operations, preventing race conditions.
  • Rich diagnostics: The lock file contains a JSON record with PID and start time, wrapped in ErrSingletonLockHeld when conflicts occur.
  • Safe cleanup: The Release method handles nil receivers safely, ensuring deferred unlocks execute correctly via defer lock.Release().

Frequently Asked Questions

What happens if the no-mistakes daemon crashes while holding the singleton lock?

The kernel automatically releases the file lock when the process exits, regardless of whether the exit is clean or due to a crash. This behavior eliminates the stale lock problems common to PID-file-based singleton implementations, ensuring the next daemon startup can acquire the lock immediately without manual intervention.

Can two daemons run simultaneously on different NM_HOME directories?

Yes. The singleton lock is scoped to a specific NM_HOME directory via the <NM_HOME>/daemon.lock file path. Two daemons targeting different NM_HOME directories operate on separate lock files and therefore do not conflict, allowing concurrent daemon instances for different data roots.

How does the daemon distinguish between a held lock and other file errors?

The acquireSingletonLock function returns the specific sentinel error ErrSingletonLockHeld when the lock is already acquired by another process. This error includes the holder's diagnostic information (PID and start time), allowing calling code to distinguish singleton conflicts from permission errors or disk failures and present appropriate user messages.

Is the singleton lock mechanism portable across operating systems?

Yes. The implementation uses platform-specific helper functions (tryLockFile and unlockFile) that abstract OS differences while providing the same exclusive, non-blocking lock semantics. The internal/daemon/lock.go file handles these platform variations, ensuring consistent singleton behavior across supported operating systems.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →