Daemon leaks on Linux: `stat -f` is macOS-only + missing PID-file guard

Open
#26 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
55/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
bash, bun, typescript
Domain
cli, tooling

Research direction

Start with bin/macrodata-hook.sh, especially store_lastmod, check_files_changed, is_daemon_running, and start_daemon; compare their behavior with macrodata-daemon.ts on Linux. Run the hook in a Linux environment and verify that mtime checks work and repeated hook calls do not create duplicate daemon processes, including when the PID file is absent or stale.

Written by the indexing model from the issue text.

Description

Summary

On Linux (e.g. WSL2 / Ubuntu), bin/macrodata-hook.sh has two bugs that compound into a daemon leak: every session-start or prompt-submit hook call can spawn a fresh macrodata-daemon.ts that never exits. On a machine that was left running for ~26 days with scheduled dreamtime + morning-prep sessions firing nightly, I ended up with 9 concurrent daemons holding ~14% RSS each, OOM-thrashing and pinning CPU.

Bug 1 — stat -f %m is BSD/macOS syntax

bin/macrodata-hook.sh uses stat -f %m in store_lastmod and check_files_changed. On GNU coreutils (Linux) -f means display filesystem status, not format. The call errors out:

$ stat -f %m ~/some/file
stat: cannot read file system information for '%m': No such file or directory
  File: "/home/..."

Because the error is swallowed by 2>/dev/null, the command substitution returns the empty string. check_files_changed then compares "" against whatever is in .lastmod, always sees a mismatch, and reports "files changed" every prompt. This triggers signal_daemon_reload + re-injects static context on every hook fire.

GNU coreutils uses stat -c %Y. BSD/macOS coreutils uses stat -f %m. A portable helper handles both.

Bug 2 — No guard against duplicate daemon spawning when PID file is missing

is_daemon_running() only returns true when $PIDFILE exists and its PID is live. On my machine ~/.config/macrodata/.daemon.pid doesn't exist (possibly never written by the daemon, or removed, or the daemon is running with a different MACRODATA_ROOT). So is_daemon_running always returns false, and every start_daemon call spawns a new nohup bun run macrodata-daemon.ts & — which gets adopted by init when the parent shell dies and keeps running forever via the croner schedulers.

Over 26 days of scheduled sessions, this accumulated to 9 orphaned daemons:

PID       ETIME        %CPU %MEM CMD
317154    1-04:32:16   3.6  0.3  bun run .../macrodata-daemon.ts
317155    1-04:32:16   3.5  0.3  bun run .../macrodata-daemon.ts
317157    1-04:32:16   3.3  0.3  bun run .../macrodata-daemon.ts
373463    19:31:45     4.6  0.6  bun run .../macrodata-daemon.ts
435928    11:28:12     5.1  12.6 bun run .../macrodata-daemon.ts
453484    52:06        14.6 12.6 bun run .../macrodata-daemon.ts
453533    52:06        14.2 10.6 bun run .../macrodata-daemon.ts
462810    01:47        128  14.0 bun run .../macrodata-daemon.ts
463478    00:13        12.9 2.9  bun run .../macrodata-daemon.ts

Memory was 5.5/7.6 GiB used, 1.7/2 GiB swap used, 15-min load average 252.

Environment

  • Linux 6.6.87.2-microsoft-standard-WSL2 (Ubuntu on WSL2)
  • macrodata 0.2.1 (Claude Code plugin cache)
  • bash, GNU coreutils

Suggested fix

Patch applied locally to bin/macrodata-hook.sh:

  1. Portable mtime helper — try GNU stat -c %Y first, fall back to BSD stat -f %m:

    get_mtime() {
        stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0
    }
    

    Replace all stat -f %m "$X" 2>/dev/null || echo 0 call sites with get_mtime "$X".

  2. pgrep fallback in is_daemon_running — if PID file is missing/stale, check whether any live daemon exists for this user before spawning:

    is_daemon_running() {
        if [ -f "$PIDFILE" ]; then
            local pid=$(cat "$PIDFILE")
            if kill -0 "$pid" 2>/dev/null; then
                return 0
            fi
        fi
        if pgrep -u "$(id -u)" -f "macrodata-daemon\.ts" >/dev/null 2>&1; then
            return 0
        fi
        return 1
    }
    

Happy to open a PR if you'd like — wanted to file the report first in case you'd prefer a different shape (e.g. having the daemon write the PID file more defensively on startup, or adding flock around start_daemon).

Dominant language
TypeScript
Stars
119
Forks
6
Avg merge
1h 32m
Merged PRs (30d)
2

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ascorbic/macrodata

All issues in ascorbic/macrodata

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.