{"record":{"id":"cf196bc60a96d602","repo":"Yeachan-Heo/oh-my-codex","slug":"tmux-window-topology-changed-before-layout-mutatio","errorCode":null,"errorMessage":"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","messagePattern":"tmux window topology changed before layout mutation: target=(.+?); expected=\\[(.+?)\\]; actual=\\[(.+?)\\]; unexpected=\\[(.+?)\\]; missing=\\[(.+?)\\]\\. Team requires an isolated tmux window with no host-owned foreign panes","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/team/tmux-session.ts","lineNumber":821,"sourceCode":"  const unexpected = actual.filter((paneId) => !expectedPaneIds.has(paneId));\n  const missing = expected.filter((paneId) => !actualPaneIds.has(paneId));\n  return new Error(\n    `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`,\n  );\n}\n\nfunction requireFrozenWindowTopologySync(\n  teamTarget: string,\n  expectedPanePids: ReadonlyMap<string, number>,\n  expectedPaneOwners?: ReadonlyMap<string, string>,\n): void {\n  const topology = listPanesResult(teamTarget);\n  if (topology.error) throw new Error(`failed to read tmux pane topology: ${topology.error}`);\n\n  const actualPaneIds = new Set(topology.panes.map((pane) => pane.paneId));\n  if (actualPaneIds.size !== expectedPanePids.size\n    || [...expectedPanePids.keys()].some((paneId) => !actualPaneIds.has(paneId))) {\n    throw frozenWindowTopologyError(teamTarget, new Set(expectedPanePids.keys()), actualPaneIds);\n  }\n\n  for (const [paneId, expectedPanePid] of expectedPanePids) {\n    const expectedOwner = expectedPaneOwners?.get(paneId);\n    if (expectedOwner) requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedOwner);\n    else {\n      const proof = readExactPaneProofSync(paneId);\n      if (proof.status === 'unavailable') throw new ExactPaneProofUnavailableError(proof);\n      if (proof.status === 'gone') throw new Error(`tmux pane is not proven live: ${proof.paneId}`);\n      if (proof.pid !== expectedPanePid) throw new Error(`tmux pane identity changed: ${proof.paneId}`);\n    }\n  }\n\n  const finalProofs = readExactPaneProofsSync([...expectedPanePids.keys()]);\n  for (const [index, [paneId, expectedPanePid]] of [...expectedPanePids.entries()].entries()) {\n    const proof = finalProofs[index]!;\n    if (proof.status === 'unavailable') throw new ExactPaneProofUnavailableError(proof);\n    if (proof.status === 'gone') throw new Error(`tmux pane is not proven live: ${proof.paneId}`);","sourceCodeStart":803,"sourceCodeEnd":839,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/team/tmux-session.ts#L803-L839","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const topo = listPanesResult(teamTarget);\nconst ok = !topo.error && topo.panes.length === expected.size\n  && [...expected].every((id) => topo.panes.some((p) => p.paneId === id));","typeGuard":null,"tryCatchPattern":"try { mutateLayout(); } catch (e) { if (String(e).includes('topology changed')) { await recreateTeamSession(); mutateLayout(); } else throw e; }","preventionTips":["Never attach to or modify the team's tmux window manually","Run team sessions in a dedicated tmux session name"],"tags":["team","tmux","concurrency","topology"],"backgroundTag":"external-state-mutation","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}