can1357/oh-my-pi · error · Error

GC already running: ${lockPath}

Error message

GC already running: ${lockPath}

What it means

The GC lock acquisition path throws this when a stale lock could not be removed (removeStaleGcLock returned false) — another GC run appears live, so this invocation refuses to start and reports the lock file path.

Source

Thrown at packages/coding-agent/src/cli/gc-cli.ts:1481

	} catch (error) {
		if (codeOf(error) === "ENOENT") return;
	}
}

async function openGcBreakerLock(lockPath: string): Promise<{ path: string; handle: fs.FileHandle }> {
	const breakerPath = `${lockPath}${GC_LOCK_BREAKER_SUFFIX}`;
	for (let attempt = 0; attempt < 2; attempt += 1) {
		const handle = await openNewGcLock(breakerPath);
		if (handle) {
			try {
				await handle.writeFile(`${process.pid}\n${new Date().toISOString()}\n`);
				return { path: breakerPath, handle };
			} catch (error) {
				await releaseGcLockFile(breakerPath, handle);
				throw error;
			}
		}
		if (!(await removeStaleGcLock(breakerPath))) throw new Error(`GC already running: ${lockPath}`);
	}
	throw new Error(`GC already running: ${lockPath}`);
}

async function openGcLock(lockPath: string): Promise<fs.FileHandle> {
	const direct = await openNewGcLock(lockPath);
	if (direct) return direct;

	const breaker = await openGcBreakerLock(lockPath);
	try {
		const raced = await openNewGcLock(lockPath);
		if (raced) return raced;
		if (!(await removeStaleGcLock(lockPath))) throw new Error(`GC already running: ${lockPath}`);
		const takeover = await openNewGcLock(lockPath);
		if (takeover) return takeover;
		throw new Error(`GC already running: ${lockPath}`);
	} finally {
		await releaseGcLockFile(breaker.path, breaker.handle);

View on GitHub (pinned to 9690622007)

Solutions

  1. Wait for the running GC to finish and retry
  2. Confirm no GC process is alive (`ps | grep omp gc`), then delete the lock file at the path in the message and re-run
  3. If the file cannot be removed due to permissions, fix ownership of the lock/state directory

Example fix

# before
omp gc  # GC already running: /path/lock
# after
ps aux | grep 'omp gc'   # confirm nothing is running
rm /path/to/gc.lock
omp gc
Defensive patterns

Strategy: retry

Validate before calling

import * as fs from "node:fs/promises";
// Probe before running GC
try {
  const h = await fs.open(lockPath, "wx"); await h.close(); await fs.rm(lockPath);
} catch (e) {
  if ((e as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`another GC appears to be running (${lockPath})`);
}

Try / catch

try {
  await acquireGcLock(lockPath);
} catch (err) {
  if (err instanceof Error && err.message.startsWith("GC already running")) {
    // back off and retry once the current GC finishes or the lock is cleared as stale
    await Bun.sleep(5000);
    await acquireGcLock(lockPath);
  } else throw err;
}

Prevention

When it happens

Trigger: Acquiring the GC lock when the breaker/lock file exists and cannot be reclaimed as stale: an active concurrent GC holds it, or a stale lock file fails the staleness heuristics (e.g. fresh mtime from a hung process, unlink permission failure).

Common situations: Two `omp gc` invocations racing (cron + manual); a previous GC crashed leaving a lock whose mtime looks fresh; running GC as a different user without permission to remove the lock file.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/34d7e1481da28d65. Report an issue: GitHub.