can1357/oh-my-pi · error · Error

GC already running

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.

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.

Appendix: 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)