import type { PluginInput } from "@opencode-ai/plugin" import type { TmuxConfig } from "../../config/schema" import type { TrackedSession, CapacityConfig, WindowState } from "./types" import { log, normalizeSDKResponse } from "../../shared" import { isInsideTmux as defaultIsInsideTmux, getCurrentPaneId as defaultGetCurrentPaneId, POLL_INTERVAL_BACKGROUND_MS, SESSION_READY_POLL_INTERVAL_MS, SESSION_READY_TIMEOUT_MS, spawnTmuxWindow, spawnTmuxSession, } from "../../shared/tmux" import { queryWindowState } from "./pane-state-querier" import { decideSpawnActions, decideCloseAction, type SessionMapping } from "./decision-engine" import { executeActions, executeAction } from "./action-executor" import { TmuxPollingManager } from "./polling-manager" import { createTrackedSession, markTrackedSessionClosePending } from "./tracked-session-state" type OpencodeClient = PluginInput["client"] interface SessionCreatedEvent { type: string properties?: { info?: { id?: string; parentID?: string; title?: string } } } interface DeferredSession { sessionId: string title: string queuedAt: Date retryIsolatedContainer: boolean } export interface TmuxUtilDeps { isInsideTmux: () => boolean getCurrentPaneId: () => string | undefined } const defaultTmuxDeps: TmuxUtilDeps = { isInsideTmux: defaultIsInsideTmux, getCurrentPaneId: defaultGetCurrentPaneId, } const DEFERRED_SESSION_TTL_MS = 5 * 60 * 1000 const MAX_DEFERRED_QUEUE_SIZE = 20 const MAX_CLOSE_RETRY_COUNT = 3 const MAX_ISOLATED_CONTAINER_NULL_STATE_COUNT = 2 export class TmuxSessionManager { private client: OpencodeClient private tmuxConfig: TmuxConfig private serverUrl: string private sourcePaneId: string | undefined private sessions = new Map() private pendingSessions = new Set() private spawnQueue: Promise = Promise.resolve() private deferredSessions = new Map() private deferredQueue: string[] = [] private deferredAttachInterval?: ReturnType private deferredAttachTickScheduled = false private nullStateCount = 0 private deps: TmuxUtilDeps private pollingManager: TmuxPollingManager private isolatedContainerPaneId: string | undefined private isolatedWindowPaneId: string | undefined private isolatedContainerNullStateCount = 0 constructor(ctx: PluginInput, tmuxConfig: TmuxConfig, deps: TmuxUtilDeps = defaultTmuxDeps) { this.client = ctx.client this.tmuxConfig = tmuxConfig this.deps = deps const defaultPort = process.env.OPENCODE_PORT ?? "4096" const fallbackUrl = `http://localhost:${defaultPort}` const rawServerUrl = ctx.serverUrl?.toString() try { if (rawServerUrl) { const parsed = new URL(rawServerUrl) const port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80') this.serverUrl = port === '0' ? fallbackUrl : rawServerUrl } else { this.serverUrl = fallbackUrl } } catch (error) { log("[tmux-session-manager] failed to parse server URL, using fallback", { serverUrl: rawServerUrl, error: String(error), }) this.serverUrl = fallbackUrl } this.sourcePaneId = deps.getCurrentPaneId() this.pollingManager = new TmuxPollingManager( this.client, this.sessions, this.closeSessionById.bind(this) ) log("[tmux-session-manager] initialized", { configEnabled: this.tmuxConfig.enabled, tmuxConfig: this.tmuxConfig, serverUrl: this.serverUrl, sourcePaneId: this.sourcePaneId, }) } private isEnabled(): boolean { return this.tmuxConfig.enabled && this.deps.isInsideTmux() } private isIsolated(): boolean { return this.tmuxConfig.isolation === "window" || this.tmuxConfig.isolation === "session" } private getEffectiveSourcePaneId(): string | undefined { if (this.isIsolated() && this.isolatedWindowPaneId) { return this.isolatedWindowPaneId } return this.sourcePaneId } private async spawnInIsolatedContainer( sessionId: string, title: string, ): Promise { if (!this.isIsolated()) return null if (this.isolatedWindowPaneId) { const state = await queryWindowState(this.isolatedWindowPaneId).catch((error) => { log("[tmux-session-manager] failed to query isolated window state", { paneId: this.isolatedWindowPaneId, error: String(error), }) return null }) if (state) { this.isolatedContainerNullStateCount = 0 return null } this.isolatedContainerNullStateCount += 1 log("[tmux-session-manager] isolated container state query returned null", { paneId: this.isolatedWindowPaneId, nullStateCount: this.isolatedContainerNullStateCount, maxNullStateCount: MAX_ISOLATED_CONTAINER_NULL_STATE_COUNT, }) if (this.isolatedContainerNullStateCount < MAX_ISOLATED_CONTAINER_NULL_STATE_COUNT) { return null } this.isolatedContainerPaneId = undefined this.isolatedWindowPaneId = undefined this.isolatedContainerNullStateCount = 0 } const isolation = this.tmuxConfig.isolation log("[tmux-session-manager] creating isolated tmux container", { isolation, sessionId, title }) const result = isolation === "session" ? await spawnTmuxSession(sessionId, title, this.tmuxConfig, this.serverUrl, this.sourcePaneId) : await spawnTmuxWindow(sessionId, title, this.tmuxConfig, this.serverUrl) if (result.success && result.paneId) { this.isolatedContainerPaneId = result.paneId this.isolatedWindowPaneId = result.paneId this.isolatedContainerNullStateCount = 0 log("[tmux-session-manager] isolated container created", { isolation, paneId: result.paneId, }) return result.paneId } log("[tmux-session-manager] failed to create isolated container", { isolation, sessionId }) return null } private getCapacityConfig(): CapacityConfig { return { layout: this.tmuxConfig.layout, mainPaneSize: this.tmuxConfig.main_pane_size, mainPaneMinWidth: this.tmuxConfig.main_pane_min_width, agentPaneWidth: this.tmuxConfig.agent_pane_min_width, } } private getSessionMappings(): SessionMapping[] { return Array.from(this.sessions.values()).map((s) => ({ sessionId: s.sessionId, paneId: s.paneId, createdAt: s.createdAt, })) } private removeTrackedSession(sessionId: string): void { this.sessions.delete(sessionId) if (this.sessions.size === 0) { this.pollingManager.stopPolling() } } private reassignIsolatedContainerAnchor(): void { const nextAnchor = this.sessions.values().next().value if (!nextAnchor) { return } this.isolatedContainerNullStateCount = 0 this.isolatedWindowPaneId = nextAnchor.paneId log("[tmux-session-manager] reassigned isolated container anchor pane", { sessionId: nextAnchor.sessionId, paneId: nextAnchor.paneId, }) } private async cleanupIsolatedContainerAfterSessionDeletion( tracked: TrackedSession, isolatedPaneAlreadyClosed: boolean, state: WindowState, ): Promise { if (tracked.paneId !== this.isolatedWindowPaneId) { return } if (this.sessions.size > 0) { this.reassignIsolatedContainerAnchor() return } const isolatedContainerPaneId = this.isolatedContainerPaneId this.isolatedContainerNullStateCount = 0 this.isolatedContainerPaneId = undefined this.isolatedWindowPaneId = undefined if (!isolatedContainerPaneId) { return } if (isolatedPaneAlreadyClosed && tracked.paneId === isolatedContainerPaneId) { return } try { const result = await executeAction( { type: "close", paneId: isolatedContainerPaneId, sessionId: tracked.sessionId }, { config: this.tmuxConfig, serverUrl: this.serverUrl, windowState: state, sourcePaneId: this.sourcePaneId ?? tracked.paneId, }, ) if (!result.success) { log("[tmux-session-manager] failed to close isolated container pane after anchor session deletion", { sessionId: tracked.sessionId, paneId: isolatedContainerPaneId, }) } } catch (error) { log("[tmux-session-manager] failed to cleanup isolated container pane after anchor session deletion", { sessionId: tracked.sessionId, paneId: isolatedContainerPaneId, error: String(error), }) } } private markSessionClosePending(sessionId: string): void { const tracked = this.sessions.get(sessionId) if (!tracked) return this.sessions.set(sessionId, markTrackedSessionClosePending(tracked)) log("[tmux-session-manager] marked session close pending", { sessionId, paneId: tracked.paneId, closeRetryCount: tracked.closeRetryCount, }) } private async queryWindowStateSafely(): Promise { const paneId = this.getEffectiveSourcePaneId() if (!paneId) return null try { return await queryWindowState(paneId) } catch (error) { log("[tmux-session-manager] failed to query window state for close", { error: String(error), }) return null } } private async closeTrackedSessionPane(args: { tracked: TrackedSession state: WindowState }): Promise { const { tracked, state } = args try { const result = await executeAction( { type: "close", paneId: tracked.paneId, sessionId: tracked.sessionId }, { config: this.tmuxConfig, serverUrl: this.serverUrl, windowState: state, sourcePaneId: this.getEffectiveSourcePaneId(), } ) return result.success } catch (error) { log("[tmux-session-manager] close session pane failed", { sessionId: tracked.sessionId, paneId: tracked.paneId, error: String(error), }) return false } } private async finalizeTrackedSessionClose(args: { tracked: TrackedSession state: WindowState isolatedPaneAlreadyClosed: boolean }): Promise { const { tracked, state, isolatedPaneAlreadyClosed } = args this.removeTrackedSession(tracked.sessionId) await this.cleanupIsolatedContainerAfterSessionDeletion( tracked, isolatedPaneAlreadyClosed, state, ) } private async closeTrackedSession(tracked: TrackedSession): Promise { const state = await this.queryWindowStateSafely() if (!state) return false const closed = await this.closeTrackedSessionPane({ tracked, state }) if (!closed) { return false } await this.finalizeTrackedSessionClose({ tracked, state, isolatedPaneAlreadyClosed: true, }) return true } private async retryPendingCloses(): Promise { const pendingSessions = Array.from(this.sessions.values()).filter( (tracked) => tracked.closePending, ) for (const tracked of pendingSessions) { if (!this.sessions.has(tracked.sessionId)) continue if (tracked.closeRetryCount >= MAX_CLOSE_RETRY_COUNT) { log("[tmux-session-manager] force removing close-pending session after max retries", { sessionId: tracked.sessionId, paneId: tracked.paneId, closeRetryCount: tracked.closeRetryCount, }) this.removeTrackedSession(tracked.sessionId) continue } const closed = await this.closeTrackedSession(tracked) if (closed) { log("[tmux-session-manager] retried close succeeded", { sessionId: tracked.sessionId, paneId: tracked.paneId, closeRetryCount: tracked.closeRetryCount, }) continue } const currentTracked = this.sessions.get(tracked.sessionId) if (!currentTracked || !currentTracked.closePending) { continue } const nextRetryCount = currentTracked.closeRetryCount + 1 if (nextRetryCount >= MAX_CLOSE_RETRY_COUNT) { log("[tmux-session-manager] force removing close-pending session after failed retry", { sessionId: currentTracked.sessionId, paneId: currentTracked.paneId, closeRetryCount: nextRetryCount, }) this.removeTrackedSession(currentTracked.sessionId) continue } this.sessions.set(currentTracked.sessionId, { ...currentTracked, closePending: true, closeRetryCount: nextRetryCount, }) log("[tmux-session-manager] retried close failed", { sessionId: currentTracked.sessionId, paneId: currentTracked.paneId, closeRetryCount: nextRetryCount, }) } } private enqueueDeferredSession( sessionId: string, title: string, retryIsolatedContainer = false, ): void { const existingDeferredSession = this.deferredSessions.get(sessionId) if (existingDeferredSession) { if (retryIsolatedContainer && !existingDeferredSession.retryIsolatedContainer) { this.deferredSessions.set(sessionId, { ...existingDeferredSession, retryIsolatedContainer: true, }) } return } if (this.deferredQueue.length >= MAX_DEFERRED_QUEUE_SIZE) { log("[tmux-session-manager] deferred queue full, dropping session", { sessionId, queueLength: this.deferredQueue.length, maxQueueSize: MAX_DEFERRED_QUEUE_SIZE, }) return } this.deferredSessions.set(sessionId, { sessionId, title, queuedAt: new Date(), retryIsolatedContainer, }) this.deferredQueue.push(sessionId) log("[tmux-session-manager] deferred session queued", { sessionId, queueLength: this.deferredQueue.length, }) this.startDeferredAttachLoop() } private removeDeferredSession(sessionId: string): void { if (!this.deferredSessions.delete(sessionId)) return this.deferredQueue = this.deferredQueue.filter((id) => id !== sessionId) log("[tmux-session-manager] deferred session removed", { sessionId, queueLength: this.deferredQueue.length, }) if (this.deferredQueue.length === 0) { this.stopDeferredAttachLoop() } } private startDeferredAttachLoop(): void { if (this.deferredAttachInterval) return this.nullStateCount = 0 this.deferredAttachInterval = setInterval(() => { if (this.deferredAttachTickScheduled) return this.deferredAttachTickScheduled = true void this.enqueueSpawn(async () => { try { await this.tryAttachDeferredSession() } finally { this.deferredAttachTickScheduled = false } }) }, POLL_INTERVAL_BACKGROUND_MS) log("[tmux-session-manager] deferred attach polling started", { intervalMs: POLL_INTERVAL_BACKGROUND_MS, }) } private stopDeferredAttachLoop(): void { if (!this.deferredAttachInterval) return clearInterval(this.deferredAttachInterval) this.deferredAttachInterval = undefined this.deferredAttachTickScheduled = false this.nullStateCount = 0 log("[tmux-session-manager] deferred attach polling stopped") } private async tryAttachDeferredSession(): Promise { const sessionId = this.deferredQueue[0] if (!sessionId) { this.stopDeferredAttachLoop() return } const deferred = this.deferredSessions.get(sessionId) if (!deferred) { this.deferredQueue.shift() return } if (Date.now() - deferred.queuedAt.getTime() > DEFERRED_SESSION_TTL_MS) { this.deferredQueue.shift() this.deferredSessions.delete(sessionId) log("[tmux-session-manager] deferred session expired", { sessionId, queuedAt: deferred.queuedAt.toISOString(), ttlMs: DEFERRED_SESSION_TTL_MS, queueLength: this.deferredQueue.length, }) if (this.deferredQueue.length === 0) { this.stopDeferredAttachLoop() } return } if (deferred.retryIsolatedContainer) { const isolatedPaneId = await this.spawnInIsolatedContainer(sessionId, deferred.title) if (isolatedPaneId) { const sessionReady = await this.waitForSessionReady(sessionId) this.sessions.set( sessionId, createTrackedSession({ sessionId, paneId: isolatedPaneId, description: deferred.title, }), ) this.removeDeferredSession(sessionId) this.pollingManager.startPolling() log("[tmux-session-manager] deferred session attached in isolated window", { sessionId, paneId: isolatedPaneId, sessionReady, }) return } } const effectiveSourcePaneId = this.getEffectiveSourcePaneId() if (!effectiveSourcePaneId) return const state = await queryWindowState(effectiveSourcePaneId) if (!state) { this.nullStateCount += 1 log("[tmux-session-manager] deferred attach window state is null", { nullStateCount: this.nullStateCount, }) if (this.nullStateCount >= 3) { log("[tmux-session-manager] stopping deferred attach loop after consecutive null states", { nullStateCount: this.nullStateCount, }) this.stopDeferredAttachLoop() } return } this.nullStateCount = 0 const decision = decideSpawnActions( state, sessionId, deferred.title, this.getCapacityConfig(), this.getSessionMappings(), ) if (!decision.canSpawn || decision.actions.length === 0) { log("[tmux-session-manager] deferred session still waiting for capacity", { sessionId, reason: decision.reason, }) return } const result = await executeActions(decision.actions, { config: this.tmuxConfig, serverUrl: this.serverUrl, windowState: state, sourcePaneId: effectiveSourcePaneId, }) if (!result.success || !result.spawnedPaneId) { log("[tmux-session-manager] deferred session attach failed", { sessionId, results: result.results.map((r) => ({ type: r.action.type, success: r.result.success, error: r.result.error, })), }) return } const sessionReady = await this.waitForSessionReady(sessionId) if (!sessionReady) { log("[tmux-session-manager] deferred session not ready after timeout", { sessionId, paneId: result.spawnedPaneId, }) } this.sessions.set( sessionId, createTrackedSession({ sessionId, paneId: result.spawnedPaneId, description: deferred.title, }), ) this.removeDeferredSession(sessionId) this.pollingManager.startPolling() log("[tmux-session-manager] deferred session attached", { sessionId, paneId: result.spawnedPaneId, sessionReady, }) } private async waitForSessionReady(sessionId: string): Promise { const startTime = Date.now() while (Date.now() - startTime < SESSION_READY_TIMEOUT_MS) { try { const statusResult = await this.client.session.status({ path: undefined }) const allStatuses = normalizeSDKResponse(statusResult, {} as Record) if (allStatuses[sessionId]) { log("[tmux-session-manager] session ready", { sessionId, status: allStatuses[sessionId].type, waitedMs: Date.now() - startTime, }) return true } } catch (err) { log("[tmux-session-manager] session status check error", { error: String(err) }) } await new Promise((resolve) => setTimeout(resolve, SESSION_READY_POLL_INTERVAL_MS)) } log("[tmux-session-manager] session ready timeout", { sessionId, timeoutMs: SESSION_READY_TIMEOUT_MS, }) return false } async onSessionCreated(event: SessionCreatedEvent): Promise { const enabled = this.isEnabled() log("[tmux-session-manager] onSessionCreated called", { enabled, tmuxConfigEnabled: this.tmuxConfig.enabled, isInsideTmux: this.deps.isInsideTmux(), eventType: event.type, infoId: event.properties?.info?.id, infoParentID: event.properties?.info?.parentID, }) if (!enabled) return if (event.type !== "session.created") return const info = event.properties?.info if (!info?.id || !info?.parentID) return const sessionId = info.id const title = info.title ?? "Subagent" if (!this.sourcePaneId) { log("[tmux-session-manager] no source pane id") return } await this.retryPendingCloses() if ( this.sessions.has(sessionId) || this.pendingSessions.has(sessionId) || this.deferredSessions.has(sessionId) ) { log("[tmux-session-manager] session already tracked or pending", { sessionId }) return } this.pendingSessions.add(sessionId) await this.enqueueSpawn(async () => { try { const isolatedPaneId = await this.spawnInIsolatedContainer(sessionId, title) if (isolatedPaneId) { const sessionReady = await this.waitForSessionReady(sessionId) this.sessions.set( sessionId, createTrackedSession({ sessionId, paneId: isolatedPaneId, description: title }), ) this.pollingManager.startPolling() log("[tmux-session-manager] first subagent spawned in isolated window", { sessionId, paneId: isolatedPaneId, sessionReady, }) return } if (this.isIsolated() && !this.isolatedWindowPaneId) { log("[tmux-session-manager] isolated container failed, deferring session for retry", { sessionId }) this.enqueueDeferredSession(sessionId, title, true) return } const sourcePaneId = this.getEffectiveSourcePaneId() if (!sourcePaneId) { log("[tmux-session-manager] no effective source pane id") return } const state = await queryWindowState(sourcePaneId) if (!state) { log("[tmux-session-manager] failed to query window state, deferring session") this.enqueueDeferredSession(sessionId, title) return } log("[tmux-session-manager] window state queried", { windowWidth: state.windowWidth, mainPane: state.mainPane?.paneId, agentPaneCount: state.agentPanes.length, agentPanes: state.agentPanes.map((p) => p.paneId), }) const decision = decideSpawnActions( state, sessionId, title, this.getCapacityConfig(), this.getSessionMappings() ) log("[tmux-session-manager] spawn decision", { canSpawn: decision.canSpawn, reason: decision.reason, actionCount: decision.actions.length, actions: decision.actions.map((a) => { if (a.type === "close") return { type: "close", paneId: a.paneId } if (a.type === "replace") return { type: "replace", paneId: a.paneId, newSessionId: a.newSessionId } return { type: "spawn", sessionId: a.sessionId } }), }) if (!decision.canSpawn) { log("[tmux-session-manager] cannot spawn", { reason: decision.reason }) this.enqueueDeferredSession(sessionId, title) return } const result = await executeActions( decision.actions, { config: this.tmuxConfig, serverUrl: this.serverUrl, windowState: state, sourcePaneId, } ) for (const { action, result: actionResult } of result.results) { if (action.type === "close" && actionResult.success) { this.sessions.delete(action.sessionId) log("[tmux-session-manager] removed closed session from cache", { sessionId: action.sessionId, }) } if (action.type === "replace" && actionResult.success) { this.sessions.delete(action.oldSessionId) log("[tmux-session-manager] removed replaced session from cache", { oldSessionId: action.oldSessionId, newSessionId: action.newSessionId, }) } } if (result.success && result.spawnedPaneId) { const sessionReady = await this.waitForSessionReady(sessionId) if (!sessionReady) { log("[tmux-session-manager] session not ready after timeout, tracking anyway", { sessionId, paneId: result.spawnedPaneId, }) } this.sessions.set( sessionId, createTrackedSession({ sessionId, paneId: result.spawnedPaneId, description: title, }), ) log("[tmux-session-manager] pane spawned and tracked", { sessionId, paneId: result.spawnedPaneId, sessionReady, }) this.pollingManager.startPolling() } else { log("[tmux-session-manager] spawn failed", { success: result.success, results: result.results.map((r) => ({ type: r.action.type, success: r.result.success, error: r.result.error, })), }) log("[tmux-session-manager] re-queueing deferred session after spawn failure", { sessionId, }) this.enqueueDeferredSession(sessionId, title) if (result.spawnedPaneId) { await executeAction( { type: "close", paneId: result.spawnedPaneId, sessionId }, { config: this.tmuxConfig, serverUrl: this.serverUrl, windowState: state } ) } return } } finally { this.pendingSessions.delete(sessionId) } }) } private async enqueueSpawn(run: () => Promise): Promise { this.spawnQueue = this.spawnQueue .catch((error) => { log("[tmux-session-manager] recovering spawn queue after previous failure", { error: String(error), }) }) .then(run) .catch((err) => { log("[tmux-session-manager] spawn queue task failed", { error: String(err), }) }) await this.spawnQueue } async onSessionDeleted(event: { sessionID: string }): Promise { if (!this.isEnabled()) return if (!this.getEffectiveSourcePaneId()) return this.removeDeferredSession(event.sessionID) const tracked = this.sessions.get(event.sessionID) if (!tracked) return log("[tmux-session-manager] onSessionDeleted", { sessionId: event.sessionID }) const state = await this.queryWindowStateSafely() if (!state) { this.markSessionClosePending(event.sessionID) return } const closeAction = decideCloseAction(state, event.sessionID, this.getSessionMappings()) if (!closeAction) { await this.finalizeTrackedSessionClose({ tracked, state, isolatedPaneAlreadyClosed: false, }) return } const isolatedPaneAlreadyClosed = closeAction.type === "close" && closeAction.paneId === tracked.paneId try { const result = await executeAction(closeAction, { config: this.tmuxConfig, serverUrl: this.serverUrl, windowState: state, sourcePaneId: this.getEffectiveSourcePaneId(), }) if (!result.success) { this.markSessionClosePending(event.sessionID) return } } catch (error) { log("[tmux-session-manager] failed to close pane for deleted session", { sessionId: event.sessionID, error: String(error), }) this.markSessionClosePending(event.sessionID) return } await this.finalizeTrackedSessionClose({ tracked, state, isolatedPaneAlreadyClosed, }) } private async closeSessionById(sessionId: string): Promise { const tracked = this.sessions.get(sessionId) if (!tracked) return if (tracked.closePending && tracked.closeRetryCount >= MAX_CLOSE_RETRY_COUNT) { log("[tmux-session-manager] force removing close-pending session after max retries", { sessionId, paneId: tracked.paneId, closeRetryCount: tracked.closeRetryCount, }) this.removeTrackedSession(sessionId) return } log("[tmux-session-manager] closing session pane", { sessionId, paneId: tracked.paneId, }) const closed = await this.closeTrackedSession(tracked) if (!closed) { this.markSessionClosePending(sessionId) return } } onEvent(event: { type: string; properties?: Record }): void { this.pollingManager.handleEvent(event) } createEventHandler(): (input: { event: { type: string; properties?: unknown } }) => Promise { return async (input) => { await this.onSessionCreated(input.event as SessionCreatedEvent) } } async cleanup(): Promise { this.stopDeferredAttachLoop() this.deferredQueue = [] this.deferredSessions.clear() this.pollingManager.stopPolling() if (this.sessions.size > 0) { log("[tmux-session-manager] closing all panes", { count: this.sessions.size }) const sessionIds = Array.from(this.sessions.keys()) for (const sessionId of sessionIds) { try { await this.closeSessionById(sessionId) } catch (error) { log("[tmux-session-manager] cleanup error for pane", { sessionId, error: String(error), }) } } } await this.retryPendingCloses() this.isolatedContainerNullStateCount = 0 this.isolatedContainerPaneId = undefined this.isolatedWindowPaneId = undefined log("[tmux-session-manager] cleanup complete") } }