ruvnet / ruvnet/ruflo

macOS: daemon memory gate uses os.freemem(), deferring ~70% of workers and causing a KeepAlive restart loop

Open
#2,935 3 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.