Daemon leaks on Linux: `stat -f` is macOS-only + missing PID-file guard
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
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:
-
Portable mtime helper — try GNU
stat -c %Yfirst, fall back to BSDstat -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 0call sites withget_mtime "$X". -
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ascorbic/macrodata
-
Difficulty 1/5 Under an hour Newbie friendliness 70/100
-
Difficulty 1/5 Under an hour Newbie friendliness 74/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
-
Support Cursor Open
Difficulty 5/5 Over a week Newbie friendliness 25/100
All issues in ascorbic/macrodata
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug clawsweeper:linked-pr-open clawsweeper:needs-live-repro clawsweeper:no-new-fix-pr impact:message-loss issue-rating: 🐚 platinum hermit P2 regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 78/100
fullcalendar/fullcalendar#8106 ·