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

  1. Re-discover the current pane PID and decide deliberately whether the new occupant is yours to kill
  2. Disable respawn (`remain-on-exit off` or set `respawn-pane` hooks off) for managed panes
  3. 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

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


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/e99c25d38ac35aae. Report an issue: GitHub.