coleam00/Archon · error

Detached run control endpoint is already owned: ${endpointPa

Error message

Detached run control endpoint is already owned: ${endpointPath}

What it means

acquireOwnerLock() creates the owner lock file exclusively (openSync 'wx'); on EEXIST it checks whether the control endpoint is still connectable. If a live process answers on the endpoint, the run is already owned and the first of two identical 'already owned' errors is thrown. Only after a failed connect (and one poll grace period for startup races) may the stale lock be removed.

Source

Thrown at packages/cli/src/utils/detached-run-control.ts:170

    throw new Error(`Detached run control endpoint is already owned: ${path}`);
  }

  // A crashed Unix owner can leave the socket pathname behind. Connection refusal,
  // not age, is the proof that no process owns it; remove only that stale pathname.
  rmSync(path, { force: true });
  await listen(server, path);
}

async function acquireOwnerLock(runId: string, endpointPath: string): Promise<number> {
  const lockPath = detachedRunControlLockPath(runId);
  try {
    return openSync(lockPath, 'wx', 0o600);
  } catch (error) {
    if (!isNodeError(error) || error.code !== 'EEXIST') throw error;
  }

  if (await canConnect(endpointPath)) {
    throw new Error(`Detached run control endpoint is already owned: ${endpointPath}`);
  }
  // A lock is written immediately before listen. Give that narrow startup window
  // one chance to become reachable before treating both files as crash residue.
  await new Promise<void>(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
  if (await canConnect(endpointPath)) {
    throw new Error(`Detached run control endpoint is already owned: ${endpointPath}`);
  }

  rmSync(lockPath, { force: true });
  if (process.platform !== 'win32') rmSync(endpointPath, { force: true });
  return openSync(lockPath, 'wx', 0o600);
}

function releaseOwnerLock(lockPath: string, lockFd: number): void {
  const owned = fstatSync(lockFd);
  closeSync(lockFd);
  try {
    const current = statSync(lockPath);

View on GitHub (pinned to 0773b97458)

Solutions

  1. Find the live owner and use it (check ps for the Archon process serving this run; or ask the existing endpoint for its pid).
  2. Stop the existing owner (archon run stop <runId>) before re-acquiring.
  3. If the process shown is genuinely dead but the socket still accepts, investigate for another process holding the port; only then clean the stale files.

Example fix

// before
archon run attach <runId>   # owner alive -> already owned
// after
archon run stop <runId>
archon run attach <runId>
Defensive patterns

Strategy: try-catch

Validate before calling

const ownerAlive = await canConnect(endpointPath);
if (ownerAlive && existsSync(lockPath)) {
  console.log('Run already owned; do not acquire lock.');
}

Try / catch

try {
  const fd = await acquireOwnerLock(runId, endpointPath);
} catch (err) {
  if (err instanceof DetachedRunOwnerUnavailableError || (err instanceof Error && err.message.includes('already owned'))) {
    // attach to existing owner or stop it first
  } else throw err;
}

Prevention

When it happens

Trigger: lockFd is called for a run whose lock file exists and whose control socket accepts a connection when acquireOwnerLock probes it.

Common situations: Attaching to a run whose owner process is still alive from a previous session; a wrapper script re-running the same detached run; leftover lock plus a still-running owner you forgot about.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/fc6cf0974d12db39. Report an issue: GitHub.