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
- Do not interact with the team's tmux window while the team is running
- Kill and recreate the team session to rebuild a clean isolated window
- Check for stray tmux automation (plugins, hooks) that manipulates panes
- 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
- Never attach to or modify the team's tmux window manually
- Run team sessions in a dedicated tmux session name
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
- auth directory must not be a symlink: ${dir}
- auth path is not a directory: ${dir}
- ${label} is not a file: ${path}
- timed out waiting for tmux extended-keys lease lock: ${lockP
- shutdown_shared_session_${kind.replaceAll(' ', '_')}_owner_c
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/cf196bc60a96d602.
Report an issue: GitHub.