Bounded Daemon Logging with Rotation and Separate Managed Server Output in no-mistakes

The no-mistakes daemon uses the internal/logstore package to enforce strict byte limits across three rotating log streams, preserving file descriptors during rotation through inode-truncation and isolating managed subprocess output via separate RotatingWriter instances.

The kunchenguid/no-mistakes repository employs a disciplined approach to daemon logging that prevents disk exhaustion while maintaining diagnostic clarity. By leveraging the internal/logstore package, the daemon enforces strict byte limits across three distinct log streams, ensuring that long-running processes cannot consume unbounded storage. This architecture separates managed server output from daemon lifecycle messages and preserves file descriptors during rotation through inode-aware truncation.

Log Stream Architecture and Retention Policies

The daemon maintains three distinct log files, each governed by a specific retention policy defined in internal/logstore/rotate.go. These policies prevent unbounded disk growth while preserving recent diagnostic history.

  • Lifecycle Log (logs/daemon.log): Captures daemon-process messages, errors, and high-level events using logstore.LifecyclePolicy(), which enforces a 32 MiB maximum size with 3 rotating backups.
  • Managed Server Log (logs/managed-server.log): Collects stdout and stderr from managed Rovo Dev/OpenCode subprocesses via logstore.ManagedServerPolicy(), limiting output to 16 MiB with 2 backups.
  • Bootstrap Log (logs/daemon-bootstrap.log): Records early-startup diagnostics when the daemon crashes before normal logging initializes, using logstore.BootstrapPolicy() with a 1 MiB limit and 2 backups.

All three streams are opened during daemon initialization in internal/daemon/daemon.go (lines 57–176) using the logstore.Open function, which returns a RotatingWriter configured for the respective policy.

RotatingWriter Implementation and Inode Preservation

At the core of the system is the RotatingWriter struct, a thread-safe io.Writer defined in internal/logstore/rotate.go. It tracks the current file size in a size field and serializes access through a mu mutex to ensure concurrent writes do not corrupt log entries.

The writer implements inode-preserving rotation to prevent breaking existing file descriptors held by child processes. Rather than deleting and recreating the log file, the rotation renames the current file to the backup chain (.1, .2, etc.) and truncates the existing inode in-place. This ensures that any already-open file descriptors—such as those held by the daemon’s managed subprocesses—continue writing to the same path without interruption.

Size-Bounded Write Operations

The Write method implements chunk-aware writing to handle large payloads that might exceed remaining capacity:

func (w *RotatingWriter) Write(p []byte) (int, error) {
    w.mu.Lock()
    defer w.mu.Unlock()
    if w.closed { return 0, os.ErrClosed }

    written := 0
    for len(p) > 0 {
        // Rotate if current size would exceed the limit.
        if w.size >= w.policy.MaxBytes {
            if err := w.rotateLocked(); err != nil {
                return written, err
            }
        }
        remaining := w.policy.MaxBytes - w.size
        chunk := int64(len(p))
        if chunk > remaining { chunk = remaining }
        n, err := w.file.Write(p[:chunk])
        w.size += int64(n)
        written += n
        p = p[chunk:]
        if err != nil { return written, err }
    }
    return written, nil
}

Atomic Rotation Chain

When w.size reaches policy.MaxBytes, the rotateLocked method executes an atomic rotation sequence:

  1. The current file is renamed to .1, shifting existing backups (.1 to .2, etc.) up to the configured Backups count.
  2. The original path is reopened and truncated, preserving the inode while resetting the file size to zero.
  3. The size counter resets, allowing new writes to commence against the fresh file.

This mechanism ensures that the total disk usage remains bounded by (Backups + 1) × MaxBytes for each log stream.

Isolating Managed Server Output

The separation of managed server output from daemon lifecycle events is achieved by instantiating distinct RotatingWriter instances. In internal/daemon/daemon.go, the daemon opens logs/managed-server.log with logstore.ManagedServerPolicy() separately from the lifecycle log.

This architectural decision prevents interleaving of subprocess stdout/stderr with daemon internal messages, enabling operators to locate server-specific issues without parsing mixed streams. The separate writer also ensures that verbose subprocess output cannot evict critical daemon lifecycle entries from their respective retention windows.

Daemon Initialization Example

Components obtain bounded log writers through the logstore.Open API:

// Open the daemon lifecycle log (32 MiB, 3 backups)
lifecycleLog, err := logstore.Open(
    paths.New().Join("logs", "daemon.log"),
    logstore.LifecyclePolicy(),
)
if err != nil {
    log.Fatalf("cannot open lifecycle log: %v", err)
}
defer lifecycleLog.Close()

// Open the managed server log (16 MiB, 2 backups)
managedLog, err := logstore.Open(
    paths.New().Join("logs", "managed-server.log"),
    logstore.ManagedServerPolicy(),
)
if err != nil {
    log.Fatalf("cannot open managed server log: %v", err)
}
defer managedLog.Close()

// Use as io.Writer for any component:
myComponent := &Component{
    Out: lifecycleLog,   // lifecycle messages
    Err: managedLog,     // subprocess stderr
}

The bootstrap log receives special handling in internal/daemon/bootstrap_capture.go, where it captures early diagnostics using logstore.BootstrapPolicy() before the full logging infrastructure initializes.

Summary

  • The internal/logstore package enforces deterministic retention through LifecyclePolicy, ManagedServerPolicy, and BootstrapPolicy, capping total log size at predefined byte limits.
  • RotatingWriter preserves file descriptors during rotation by truncating existing inodes rather than recreating files, ensuring child processes maintain valid write handles.
  • The write loop chunks large payloads to respect size boundaries atomically, preventing single large writes from exceeding policy limits.
  • Separate writer instances for managed server output isolate subprocess streams from daemon lifecycle logs, improving debuggability and preventing log eviction conflicts.

Frequently Asked Questions

How does the rotation mechanism prevent breaking existing file descriptors?

The RotatingWriter preserves the original file inode by truncating it in-place after rotating backups, rather than deleting and recreating the file. As implemented in internal/logstore/rotate.go, this ensures that any already-open file descriptors—such as those held by managed subprocesses—continue writing to the same path without interruption.

What happens when a single write exceeds the remaining bytes in the current log file?

The Write method chunks the payload into segments that fit within the remaining MaxBytes allowance. If the remaining capacity is insufficient, it triggers rotateLocked to rotate the file first, then writes the chunk, repeating this process until the entire payload is consumed.

Why does the managed server log use a separate policy from the daemon lifecycle log?

The managed server log uses ManagedServerPolicy() (16 MiB, 2 backups) to isolate verbose subprocess output from critical daemon lifecycle events (32 MiB, 3 backups). This separation prevents subprocess logs from evicting daemon diagnostic entries and allows operators to debug server issues without parsing interleaved streams.

How is the bootstrap log initialized differently than other logs?

The bootstrap log is captured in internal/daemon/bootstrap_capture.go using logstore.BootstrapPolicy() (1 MiB, 2 backups) during early startup, before the full daemon initialization completes. This ensures that even if the daemon crashes before normal logging is established, diagnostic output is preserved within strict bounds.

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 →