Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions v3/@claude-flow/cli/src/services/worker-daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { EventEmitter } from 'events';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import os from 'os';
import {
HeadlessWorkerExecutor,
HEADLESS_WORKER_TYPES,
Expand Down Expand Up @@ -105,6 +106,34 @@ const DEFAULT_WORKERS: WorkerConfigInternal[] = [
// Worker timeout (5 minutes max per worker)
const DEFAULT_WORKER_TIMEOUT_MS = 5 * 60 * 1000;

/**
* Compute platform-aware resource thresholds.
*
* The original hardcoded values (maxCpuLoad: 2.0, minFreeMemoryPercent: 20)
* permanently block all daemon workers on macOS because:
*
* - CPU: os.loadavg()[0] reports aggregate load across ALL cores. A 16-core
* Mac at 15% utilization per core reports loadavg ~2.4, exceeding the 2.0
* threshold. Scaling by core count (cores × 3) allows workers to run under
* normal system load while still blocking under genuine resource pressure.
*
* - Memory: macOS aggressively caches files in RAM. os.freemem() reports only
* truly unused memory (typically 5-8%), not reclaimable cache. The 20%
* threshold is unreachable under normal macOS operation. A 1% threshold on
* Darwin reflects actual memory pressure, not cache utilization.
*
* See: https://github.com/ruvnet/claude-flow/issues/1077
*/
function getDefaultResourceThresholds(): DaemonConfig['resourceThresholds'] {
const cpuCount = os.cpus().length;
const isDarwin = process.platform === 'darwin';

return {
maxCpuLoad: cpuCount * 3,
minFreeMemoryPercent: isDarwin ? 1 : 5,
};
}

/**
* Worker Daemon - Manages background workers with Node.js
*/
Expand Down Expand Up @@ -134,10 +163,7 @@ export class WorkerDaemon extends EventEmitter {
stateFile: config?.stateFile ?? join(claudeFlowDir, 'daemon-state.json'),
maxConcurrent: config?.maxConcurrent ?? 2, // P0 fix: Limit concurrent workers
workerTimeoutMs: config?.workerTimeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS,
resourceThresholds: config?.resourceThresholds ?? {
maxCpuLoad: 2.0,
minFreeMemoryPercent: 20,
},
resourceThresholds: config?.resourceThresholds ?? getDefaultResourceThresholds(),
workers: config?.workers ?? DEFAULT_WORKERS,
};

Expand Down Expand Up @@ -233,7 +259,6 @@ export class WorkerDaemon extends EventEmitter {
* Check if system resources allow worker execution
*/
private async canRunWorker(): Promise<{ allowed: boolean; reason?: string }> {
const os = await import('os');
const cpuLoad = os.loadavg()[0];
const totalMem = os.totalmem();
const freeMem = os.freemem();
Expand Down Expand Up @@ -326,6 +351,9 @@ export class WorkerDaemon extends EventEmitter {
this.startedAt = new Date();
this.emit('started', { pid: process.pid, startedAt: this.startedAt });

const { maxCpuLoad, minFreeMemoryPercent } = this.config.resourceThresholds;
this.log('info', `Resource thresholds: maxCpuLoad=${maxCpuLoad} (${os.cpus().length} cores), minFreeMemoryPercent=${minFreeMemoryPercent}% (${process.platform})`);

// Schedule all enabled workers
for (const workerConfig of this.config.workers) {
if (workerConfig.enabled) {
Expand Down