Problem
~/.previewsmcp/serve.log is append-only across the daemon's entire lifetime, with no rotation, no size cap, and no cleanup mechanism. The daemon process redirects its stderr to this file at spawn (Sources/PreviewsCLI/DaemonClient.swift:187-194) using seekToEndOfFile() — every restart appends, every restart accumulates.
The only truncation in the codebase is test-only: Tests/CLIIntegrationTests/DaemonTestLock.swift truncates per-test-process to scope CI failure dumps. Production users get no equivalent.
Why "rotate-on-spawn" is insufficient
The daemon spawns only when DaemonProbe.canConnect() returns false (no existing daemon listening). A single daemon survives across all subsequent CLI invocations until killed, system shutdown, or crash. There's no idle-shutdown.
Realistic lifetimes:
- CI: minutes (one daemon per test run)
- Local dev: hours to weeks of intensive use against the same daemon
Rotate-on-spawn would only help the across-restart case. A developer with a 2-week-old daemon and 1000s of accumulated invocations would see no benefit.
Size growth ballpark
After PR #141's diagnostic stage markers, each previewsmcp run writes ~30 lines (~80 chars each ≈ 2.5KB). Hot-reload sessions amplify: every file change cascades. Round numbers:
- 100 invocations/day → 250 KB/day → 1.7 MB/week → 7 MB/month
- Heavy hot-reload day: 10× that
- A long-lived dev daemon over months: tens to low-hundreds of MB unbounded
Not catastrophic for a CLI tool, but unprincipled — and the file silently grows behind previewsmcp logs, where users don't see it until they go looking.
Options, ranked
1. Runtime size-based rotation (recommended)
Daemon checks serve.log size periodically. The 2s heartbeat loop in runMCPServer (Sources/PreviewsCLI/MCPServer.swift) is a natural piggyback. When size exceeds threshold (e.g., 5 MB):
- Rename
serve.log → serve.log.1 (overwriting any prior .1)
ftruncate(stderr_fd, 0) so the daemon's still-open fd resets to offset 0
Implementation gotcha: the daemon's stderr fd is bound to the inode, not the path. Plain FileHandle(forWritingTo:) + manual seekToEndOfFile (current behavior) means writes after a rename go to the renamed inode. Solutions:
- (a) Open with
O_APPEND in spawnDaemon() so writes always go to the current end-of-file regardless of seek state. After truncate, that's offset 0 of the surviving inode.
- (b) Skip rename; just
ftruncate the live fd. Drops the .1 history entirely. Simpler, less recovery info on failure.
Recommended: (a). One-deep history is cheap and useful for "what happened just before this rotation."
~30 lines, lives in a new LogRotation helper in PreviewsCore. Subsumes "rotate-on-spawn" since the first heartbeat after a spawn that finds an oversized file rotates it.
2. Rotate-on-spawn-if-large
Cheap; ~5 lines in spawnDaemon(). Bounds growth across daemon restarts but doesn't help long-lived daemons. Insufficient on its own per the lifetime analysis above.
3. Idle-shutdown
Daemon exits after N minutes of inactivity. Implicit rotation: each new daemon = fresh log. Behavioral change for users (cold-start latency on next invocation), separate desirability call. ~20 lines in DaemonLifecycle.
4. Configurable retention
PREVIEWSMCP_LOG_MAX_SIZE env var, multi-deep .1/.2/.3 rotation. Overkill given current usage.
Suggested scope
Single PR for option (1) alone. Follow-up only if real users report disk-pressure issues that (1) doesn't address.
Why this is filed separately from PR #141
PR #141 substantially expanded the volume of daemon-side logging (~25 stage-marker call sites added) and added timestamps to every line — both meaningfully accelerate this growth. But the underlying unboundedness has existed since the daemon's stderr-redirect was introduced (PR #133 era). Bundling rotation into PR #141 would overscope what's already a 30+ commit branch.
🤖 Filed from a Claude Code session reviewing PR #141.
Problem
~/.previewsmcp/serve.logis append-only across the daemon's entire lifetime, with no rotation, no size cap, and no cleanup mechanism. The daemon process redirects its stderr to this file at spawn (Sources/PreviewsCLI/DaemonClient.swift:187-194) usingseekToEndOfFile()— every restart appends, every restart accumulates.The only truncation in the codebase is test-only:
Tests/CLIIntegrationTests/DaemonTestLock.swifttruncates per-test-process to scope CI failure dumps. Production users get no equivalent.Why "rotate-on-spawn" is insufficient
The daemon spawns only when
DaemonProbe.canConnect()returns false (no existing daemon listening). A single daemon survives across all subsequent CLI invocations until killed, system shutdown, or crash. There's no idle-shutdown.Realistic lifetimes:
Rotate-on-spawn would only help the across-restart case. A developer with a 2-week-old daemon and 1000s of accumulated invocations would see no benefit.
Size growth ballpark
After PR #141's diagnostic stage markers, each
previewsmcp runwrites ~30 lines (~80 chars each ≈ 2.5KB). Hot-reload sessions amplify: every file change cascades. Round numbers:Not catastrophic for a CLI tool, but unprincipled — and the file silently grows behind
previewsmcp logs, where users don't see it until they go looking.Options, ranked
1. Runtime size-based rotation (recommended)
Daemon checks serve.log size periodically. The 2s heartbeat loop in
runMCPServer(Sources/PreviewsCLI/MCPServer.swift) is a natural piggyback. When size exceeds threshold (e.g., 5 MB):serve.log→serve.log.1(overwriting any prior.1)ftruncate(stderr_fd, 0)so the daemon's still-open fd resets to offset 0Implementation gotcha: the daemon's stderr fd is bound to the inode, not the path. Plain
FileHandle(forWritingTo:)+ manualseekToEndOfFile(current behavior) means writes after a rename go to the renamed inode. Solutions:O_APPENDinspawnDaemon()so writes always go to the current end-of-file regardless of seek state. After truncate, that's offset 0 of the surviving inode.ftruncatethe live fd. Drops the.1history entirely. Simpler, less recovery info on failure.Recommended: (a). One-deep history is cheap and useful for "what happened just before this rotation."
~30 lines, lives in a new
LogRotationhelper inPreviewsCore. Subsumes "rotate-on-spawn" since the first heartbeat after a spawn that finds an oversized file rotates it.2. Rotate-on-spawn-if-large
Cheap; ~5 lines in
spawnDaemon(). Bounds growth across daemon restarts but doesn't help long-lived daemons. Insufficient on its own per the lifetime analysis above.3. Idle-shutdown
Daemon exits after N minutes of inactivity. Implicit rotation: each new daemon = fresh log. Behavioral change for users (cold-start latency on next invocation), separate desirability call. ~20 lines in
DaemonLifecycle.4. Configurable retention
PREVIEWSMCP_LOG_MAX_SIZEenv var, multi-deep.1/.2/.3rotation. Overkill given current usage.Suggested scope
Single PR for option (1) alone. Follow-up only if real users report disk-pressure issues that (1) doesn't address.
Why this is filed separately from PR #141
PR #141 substantially expanded the volume of daemon-side logging (~25 stage-marker call sites added) and added timestamps to every line — both meaningfully accelerate this growth. But the underlying unboundedness has existed since the daemon's stderr-redirect was introduced (PR #133 era). Bundling rotation into PR #141 would overscope what's already a 30+ commit branch.
🤖 Filed from a Claude Code session reviewing PR #141.