ruvnet/ruflo · warning · ProxyAlreadyRunningError

meta-proxy is already running

Error message

meta-proxy is already running (pid ${pid}). Stop it first with: ruflo proxy stop

What it means

startForeground() refuses to start a second meta-proxy: it reads the PID file and probes it with signal 0 (process.kill(pid, 0)); if a live process answers, ProxyAlreadyRunningError is thrown carrying the running PID. The check is skipped in supervised mode because there the supervisor's own PID was deliberately written by startBackground.

Solutions

  1. Stop the existing instance: `ruflo proxy stop`, then start again
  2. Run `ruflo proxy status` and inspect the live PID — you may just want to leave it running
  3. If the recorded PID belongs to an unrelated recycled process (pid reuse after reboot), follow the status output's stalePidFile hint to clear the stale pid file and retry

Example fix

# before
ruflo proxy start   # already running (pid 1234)
# after
ruflo proxy stop && ruflo proxy start
Defensive patterns

Strategy: try-catch

Validate before calling

import { getProxyStatus } from '@claude-flow/cli/.../proxy/lifecycle.js';
const status = getProxyStatus();
if (!status.running) {
  await startForeground();
} else {
  console.log(`already running, pid ${status.pid}`);
}

Type guard

const isAlreadyRunning = (e: unknown): e is Error & { pid: number } =>
  e instanceof Error && e.name === 'ProxyAlreadyRunningError';

Try / catch

try {
  await startForeground();
} catch (e) {
  if (isAlreadyRunning(e)) return; // idempotent start: already running is success
  throw e;
}

Prevention

When it happens

Trigger: `ruflo proxy start` (foreground, not --service) while a previous meta-proxy still runs; double-invoking start in two terminals; a `--service` instance started earlier still holding the recorded PID.

Common situations: A terminal left with the proxy in the foreground; forgetting an earlier `ruflo proxy start --service` is still alive; a second automation script starting the proxy unconditionally.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/83c86b64092be16e. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/proxy/lifecycle.ts:177

    fs.unlinkSync(proxyLockFilePath());
  } catch {
    /* ignore */
  }
}

/**
 * Foreground start (ADR-307 default) — blocks the caller until the process
 * exits or is interrupted. `stdio: 'inherit'` passes the proxy's own output
 * straight through to the terminal; signals (Ctrl+C) propagate naturally to
 * the child, no manual forwarding needed.
 */
export async function startForeground(supervised = false): Promise<never> {
  const bin = requireBinary();
  const status = getProxyStatus();
  // In service mode startBackground has already written this supervisor's
  // PID. Treating it as a competing proxy makes the supervisor immediately
  // exit before it can spawn meta-proxy.
  if (!supervised && status.running && status.pid) throw new ProxyAlreadyRunningError(status.pid);
  if (status.stalePidFile) clearStalePidFile();

  const child = spawn(bin, [], { stdio: 'inherit', windowsHide: false });
  if (!supervised && child.pid) writePidFile(child.pid);

  const cleanup = () => clearStalePidFile();
  process.on('exit', cleanup);
  if (supervised) {
    const forwardSignal = (signal: NodeJS.Signals) => {
      if (!child.killed) child.kill(signal);
    };
    process.once('SIGTERM', () => forwardSignal('SIGTERM'));
    process.once('SIGINT', () => forwardSignal('SIGINT'));
  }

  await new Promise<void>((resolve) => {
    child.on('exit', () => {
      cleanup();

View on GitHub (pinned to fa13ee4ad6)