macOS: daemon memory gate uses os.freemem(), deferring ~70% of workers and causing a KeepAlive restart loop
- Dominant language
- TypeScript
- Stars
- 72.7k
- Forks
- 8.6k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 83
Description
## Summary
On macOS the daemon's memory gate reads `os.freemem()`, which on Darwin counts only
genuinely-free pages and excludes reclaimable inactive memory. The result is a `freePercent`
of well under 1% on a healthy, idle machine, so the gate defers almost every worker — and the
resulting lack of worker activity then trips the idle self-shutdown, which launchd `KeepAlive`
immediately restarts, producing a restart loop.
## Environment
- macOS (Darwin 25.6.0), 32 GB RAM, Node v26.1.0
- `ruflo` installed globally; daemon run via a launchd agent as
`daemon start --foreground --quiet --ttl 0` (no `--headless`)
## The measurement
```
os.freemem(): 0.15 GB -> 0.5% of 32 GB <-- what the gate uses
memory_pressure: 51% free
vm_stat: 518882 inactive pages = 7.92 GB reclaimable, invisible to os.freemem()
```
`canRunWorker()` computes `freePercent = (os.freemem() / os.totalmem()) * 100` and compares it
against `resourceThresholds.minFreeMemoryPercent`. The Darwin default is `5`
(`const defaultMinFreeMemory = process.platform === 'darwin' ? 5 : 10;`), which is unreachable
in normal macOS operation because inactive pages are never counted as free.
## Impact
Over 24 hours on one machine:
```
completed: 270
deferred: 622
610 deferred: Memory too low
12 deferred: CPU load too high
```
Secondary effect — a restart loop:
```
[09:45:57] Daemon started (PID: 42253, ..., minFreeMemoryPercent: 1%)
[09:45:57] Worker map deferred: Memory too low: 0.5% free
[09:46:57] Daemon self-shutdown: idle for 1800s (no worker activity)
[09:46:58] Daemon started (PID: 42645, ...)
```
The idle detector fires ~60 s after start because deferred workers register no activity;
`KeepAlive` then relaunches. Observed 9 restarts in ~10 minutes.
Lowering `minFreeMemoryPercent` to `1` does not help — 0.5% is still below it. Any positive
floor is unreachable while this metric is used.
## Suggested fix
On Darwin, derive available memory from `vm_stat` (`free + inactive + purgeable + speculative`)
or from `memory_pressure`, rather than `os.freemem()`. Linux `os.freemem()` has a similar but
much less severe skew versus `MemAvailable`.
## Possibly related — NOT confirmed
While investigating I could not get `.claude-flow/config.yaml` to affect the daemon at all:
across three `launchctl kickstart` restarts the startup line kept reporting the previous
threshold, and neither `Daemon config loaded from` nor `yaml parser unavailable` appears
anywhere in a log spanning 2026-06-16 to now, despite the daemon's cwd being the project root.
I note `@claude-flow/cli` is `"type": "module"`, so the `require('yaml')` inside
`readDaemonConfigFromFile` cannot resolve under ESM — but that path would emit the warning, and
it does not. I could not determine where the live values originate, so I am flagging this as an
observation rather than a diagnosis; happy to open it separately if useful.
Contributor guide
Research direction
Start at the daemon's canRunWorker() entry point and inspect how os.freemem(), os.totalmem(), and resourceThresholds.minFreeMemoryPercent determine deferral. Compare Darwin's available-memory signals, such as vm_stat or memory_pressure, and verify that healthy idle macOS machines no longer defer most workers or trigger the idle KeepAlive restart loop.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- backend, operating-systems, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100