Yeachan-Heo/oh-my-codex · critical

tmux window topology changed before layout mutation: target=

Error message

tmux window topology changed before layout mutation: target=${teamTarget}; expected=[${expected.join(',')}]; actual=[${actual.join(',')}]; unexpected=[${unexpected.join(',')}]; missing=[${missing.join(',')}]. Team requires an isolated tmux window with no host-owned foreign panes

What it means

Before mutating the team's tmux layout, the code verifies the window still contains exactly the expected pane IDs. A foreign or missing pane means an external process changed the window, violating the isolated-window invariant.

Source

Thrown at src/team/tmux-session.ts:821

  const unexpected = actual.filter((paneId) => !expectedPaneIds.has(paneId));
  const missing = expected.filter((paneId) => !actualPaneIds.has(paneId));
  return new Error(
    `tmux window topology changed before layout mutation: target=${teamTarget}; expected=[${expected.join(',')}]; actual=[${actual.join(',')}]; unexpected=[${unexpected.join(',')}]; missing=[${missing.join(',')}]. Team requires an isolated tmux window with no host-owned foreign panes`,
  );
}

function requireFrozenWindowTopologySync(
  teamTarget: string,
  expectedPanePids: ReadonlyMap<string, number>,
  expectedPaneOwners?: ReadonlyMap<string, string>,
): void {
  const topology = listPanesResult(teamTarget);
  if (topology.error) throw new Error(`failed to read tmux pane topology: ${topology.error}`);

  const actualPaneIds = new Set(topology.panes.map((pane) => pane.paneId));
  if (actualPaneIds.size !== expectedPanePids.size
    || [...expectedPanePids.keys()].some((paneId) => !actualPaneIds.has(paneId))) {
    throw frozenWindowTopologyError(teamTarget, new Set(expectedPanePids.keys()), actualPaneIds);
  }

  for (const [paneId, expectedPanePid] of expectedPanePids) {
    const expectedOwner = expectedPaneOwners?.get(paneId);
    if (expectedOwner) requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedOwner);
    else {
      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: ${proof.paneId}`);
      if (proof.pid !== expectedPanePid) throw new Error(`tmux pane identity changed: ${proof.paneId}`);
    }
  }

  const finalProofs = readExactPaneProofsSync([...expectedPanePids.keys()]);
  for (const [index, [paneId, expectedPanePid]] of [...expectedPanePids.entries()].entries()) {
    const proof = finalProofs[index]!;
    if (proof.status === 'unavailable') throw new ExactPaneProofUnavailableError(proof);
    if (proof.status === 'gone') throw new Error(`tmux pane is not proven live: ${proof.paneId}`);

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Do not interact with the team's tmux window while the team is running
  2. Kill and recreate the team session to rebuild a clean isolated window
  3. Check for stray tmux automation (plugins, hooks) that manipulates panes
  4. Retry the operation after recreating the session
Defensive patterns

Strategy: retry

Validate before calling

const topo = listPanesResult(teamTarget);
const ok = !topo.error && topo.panes.length === expected.size
  && [...expected].every((id) => topo.panes.some((p) => p.paneId === id));

Try / catch

try { mutateLayout(); } catch (e) { if (String(e).includes('topology changed')) { await recreateTeamSession(); mutateLayout(); } else throw e; }

Prevention

When it happens

Trigger: requireFrozenWindowTopology-style pre-check where actual pane IDs differ from expectedPanePids keys: someone split/created/closed a pane in the team window between setup and layout mutation.

Common situations: A user manually typing in or splitting the team's tmux window, another tmux client or script attaching and modifying panes, or a stale pane map after a worker crashed and tmux closed its pane.

Related errors


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