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

> Discover how no-mistakes implements bounded daemon logging with rotation and separate managed server output using inode truncation and RotatingWriter instances.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: how-to-guide
- Published: 2026-07-25

---

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

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

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