Yeachan-Heo/oh-my-codex · critical · Error
tmux pane identity changed: ${paneId}
Error message
tmux pane identity changed: ${paneId} What it means
The pane is live but its current PID differs from expectedPid passed to requireLiveExactPaneSync. tmux recycled the pane ID for a new process; targeting it would affect an unrelated pane, so the identity check throws.
Source
Thrown at src/team/tmux-session.ts:587
if (!hudMatches) throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
const finalProof = readExactPaneProofSync(debt.pane_id);
if (finalProof.status !== 'live' || finalProof.pid !== debt.pane_pid) {
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
}
removeRestoredHudCleanupDebtSync(record.path);
}
/**
* Kill only a pane that a fresh global snapshot proves live. Callers treat
* gone/dead rows as already cleaned and unavailable snapshots as fail-closed.
*/
function requireLiveExactPaneSync(paneId: string, expectedPid?: number): string {
const proof = readExactPaneProofSync(paneId);
if (proof.status === 'unavailable') throw new ExactPaneProofUnavailableError(proof);
if (proof.status === 'gone') throw new Error(`tmux pane is not proven live: ${paneId}`);
if (expectedPid !== undefined && proof.pid !== expectedPid) {
throw new Error(`tmux pane identity changed: ${paneId}`);
}
return proof.paneId;
}
function killExactPaneSync(paneId: string, expectedPid?: number, assertAuthorization?: () => void): void {
const proof = readExactPaneProofSync(paneId);
if (proof.status === 'unavailable') throw new ExactPaneProofUnavailableError(proof);
if (proof.status === 'gone') return;
if (expectedPid !== undefined && proof.pid !== expectedPid) {
throw new Error(`tmux pane identity changed: ${paneId}`);
}
assertAuthorization?.();
// Authorization can read tmux. Re-prove immediately after it so a recycled
// pane ID cannot be targeted by the subsequent kill.
const finalProof = readExactPaneProofSync(proof.paneId);
if (finalProof.status === 'unavailable') throw new ExactPaneProofUnavailableError(finalProof);
if (finalProof.status === 'gone') return;
if (finalProof.pid !== proof.pid || (expectedPid !== undefined && finalProof.pid !== expectedPid)) {View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Re-discover the current pane PID and decide deliberately whether the new occupant is yours to kill
- Disable respawn (`remain-on-exit off` or set `respawn-pane` hooks off) for managed panes
- Refresh persisted PID records whenever panes are respawned
Defensive patterns
Strategy: validation
Validate before calling
const proof = readExactPaneProofSync(paneId); if (proof.status === 'live' && expectedPid !== undefined && proof.pid !== expectedPid) reevaluateOccupant(paneId);
Type guard
const panePidMatches = (p: ExactPaneProof, pid?: number): boolean => p.status === 'live' && (pid === undefined || p.pid === pid);
Try / catch
catch (e) { if (/pane identity changed/.test(e.message)) { refreshPidRecords(); return; } throw e; } Prevention
- Persist (paneId, panePid) pairs and refresh them on every respawn
- Turn off respawn for managed worker panes
When it happens
Trigger: kill/cleanup calls that pass a frozen PID (e.g. workerPanePidsByIndex) after the pane was respawned or killed-and-recreated with the same %id.
Common situations: Respawn-on-exit panes; agent worker restarted; recovery code using stale PID records after a server restart.
Related errors
- tmux pane identity changed: ${target}
- tmux source authority changed before effect
- invalid auth slot name: use 1-64 letters, numbers, '.', '_'
- invalid auth slot path
- auth directory must not be a symlink: ${dir}
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/e99c25d38ac35aae.
Report an issue: GitHub.