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
- Find the live owner and use it (check ps for the Archon process serving this run; or ask the existing endpoint for its pid).
- Stop the existing owner (archon run stop <runId>) before re-acquiring.
- 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
- Use the public run-control API (stop/attach) instead of hand-rolling lock acquisition.
- Never delete lock/socket files while a live owner may exist — probe connectivity first.
- Guard scripts against concurrent invocations on the same run id.
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
- Detached run control endpoint is already owned: ${path}
- owner returned an invalid response
- owner returned an invalid PID
- Refusing detached run control because process ${String(proce
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/fc6cf0974d12db39.
Report an issue: GitHub.