# src/multiplexer/ ## Responsibility Provides a unified abstraction for tmux, Zellij, Herdr, cmux, and kitty to spawn, manage, and close panes for child OpenCode agent sessions. ## Design ### Core Abstractions - **Multiplexer Interface** (`types.ts`): Defines the contract for terminal multiplexer implementations with methods for pane lifecycle management and layout application. - **Concrete Implementations**: - `TmuxMultiplexer`: tmux-specific implementation using `tmux` CLI commands - `ZellijMultiplexer`: zellij-specific implementation using zellij plugin API - `HerdrMultiplexer`: herdr-specific implementation using `herdr` CLI commands - `KittyMultiplexer`: kitty-specific implementation using `kitten @` CLI commands - `CmuxMultiplexer`: cmux UUID surface implementation using the cmux CLI - **Shared Utilities** (`shared.ts`): `quoteShellArg`, `buildOpencodeAttachCommand`, and `findBinary` — extracted from the three adapters to eliminate copy-paste duplication. - **Session Manager** (`session-manager.ts`): Tracks child session lifecycle and coordinates pane operations via event-driven architecture. - **cmux lifecycle** (`cmux/session-lifecycle.ts`): Owns readiness, deferred spawning, stable-idle polling, activity generations, reliable close retries, tracked orphan cooldown, cleanup, timers, and ownership. The generic manager delegates its five public lifecycle entries for cmux. - **cmux state/policy** (`cmux/session-state.ts`, `cmux/close-policy.ts`): Shared single-record storage and pure close-intent transitions. Pane records are removed only after confirmed close. The store uses a global symbol so a same-directory lifecycle can take over tracked orphans after hot reload; retry budgets remain finite per lifecycle instance. - **Factory** (`factory.ts`): Creates appropriate multiplexer instance based on configuration and environment detection. ### Key Interfaces ```typescript export interface Multiplexer { readonly type: 'tmux' | 'zellij' | 'herdr' | 'cmux' | 'kitty'; isAvailable(): Promise; isInsideSession(): boolean; spawnPane(sessionId: string, description: string, serverUrl: string, directory: string): Promise; closePane(paneId: string): Promise; applyLayout(layout: MultiplexerLayout, mainPaneSize: number): Promise; } ``` ### State Management The session manager uses a **shared global state** pattern to coordinate across plugin instances: - `sessions`: Map of active tracked sessions (sessionId → pane metadata) - `knownSessions`: Map of sessions that have been created but may not have active panes - `spawningSessions`: Set of sessions currently being spawned (prevents duplicate spawns) - `closingSessions`: Map of ongoing close operations (prevents race conditions) - `deferredIdleCloses`: Set of sessions that should be closed on idle but have running background jobs ### Event-Driven Architecture The session manager reacts to OpenCode session events: - `session.created`: Spawns a new pane for the child session - `session.status`: Handles idle/busy state transitions - `session.deleted`: Cleans up pane when session is deleted ## Flow ### Session Creation Flow ``` 1. OpenCode creates child session → emits 'session.created' event 2. MultiplexerSessionManager.onSessionCreated() ├─ Checks if multiplexer is enabled ├─ Validates event properties (sessionId, parentId) ├─ Checks if session is already tracked or spawning ├─ Records session in knownSessions ├─ Spawns pane via multiplexer.spawnPane() │ ├─ Validates server is running │ ├─ Creates new pane with: │ │ ├─ Command: opencode attach --session-id │ │ ├─ Working directory: project directory │ │ └─ Title: session description │ └─ Returns paneId ├─ Validates pane creation succeeded ├─ Records session in sessions map with pane metadata └─ Starts polling loop if not already running 3. The selected tmux, Zellij, Herdr, or cmux implementation creates the pane or surface. cmux delegates lifecycle reliability to `CmuxSessionLifecycle`. ``` ### Session Completion Flow ``` 1. Child session becomes idle → emits 'session.idle' or 'session.status' event 2. MultiplexerSessionManager.onSessionStatus() ├─ Checks if session is tracked ├─ If idle: │ ├─ Checks for running background jobs │ ├─ If background job running: defers close │ └─ Otherwise: closes pane via multiplexer.closePane() │ ├─ Removes from sessions map │ ├─ Calls tmux/zellij kill-pane command │ └─ Logs completion └─ If busy: respawns pane (same flow as creation) 3. Session deleted → emits 'session.deleted' event 4. MultiplexerSessionManager.onSessionDeleted() ├─ Removes from knownSessions └─ Closes pane (same flow as idle) ``` ### Polling Loop - Runs every `POLL_INTERVAL_BACKGROUND_MS` (default: 5000ms) - Fetches session statuses from OpenCode server - Closes any idle sessions that aren't tracked by this instance - Stops when no sessions remain ## Integration ### Consumers - **Main Plugin** (`src/index.ts`): Initializes multiplexer session manager during plugin startup - **Council Agents** (`src/agents/council.ts`, `src/agents/council-agents.ts`): Use session manager for child session pane management - **Background Job Board** (`src/utils/background-job-board.ts`): Coordinates with session manager to defer pane closing when background jobs are running ### Dependencies - **Config Schema** (`src/config/schema.ts`): Provides `MultiplexerConfig` with type, layout, and size settings - **Logger** (`src/utils/logger.ts`): Logs multiplexer operations for debugging - **OpenCode Server**: Provides session lifecycle events and status API ### Configuration ```typescript interface MultiplexerConfig { type: 'tmux' | 'zellij' | 'herdr' | 'cmux' | 'kitty' | 'auto' | 'none'; layout: MultiplexerLayout; // 'tiled' | 'main-horizontal' | 'main-vertical' | 'grid' main_pane_size?: number; // Percentage for main pane (0-100) zellij_pane_mode?: string; // Zellij-specific pane mode } ``` ### Environment Detection - **Auto Mode**: Detects tmux (`TMUX`), Zellij (`ZELLIJ`), kitty (`KITTY_PID`), Herdr (`HERDR_ENV`/`HERDR_PANE_ID`), or cmux (complete `CMUX_SOCKET_PATH`, `CMUX_WORKSPACE_ID`, and `CMUX_SURFACE_ID` identity). - **Availability Check**: Validates multiplexer binary is available before use ## Implementation Details ### Tmux Implementation - Uses `tmux` CLI commands via `spawn()` utility - Creates panes with descriptive titles and working directories - Applies layouts using `tmux select-layout` and `tmux resize-pane` - Graceful shutdown: sends Ctrl+C before killing pane to allow clean process termination ### Zellij Implementation - Uses zellij plugin API via `zellij` CLI - Creates panes with plugin-based OpenCode integration - Layout management via zellij's built-in layout system - Session attachment via zellij's pane-specific attach mechanism ### Error Handling - Server health checks before pane creation - Graceful degradation when multiplexer is unavailable - Logging at each lifecycle stage for observability - State consistency maintained via shared global state with proper locking ## Testing - Factory tests (`factory.test.ts`): Validates multiplexer creation and configuration - Session manager tests (`session-manager.test.ts`): Tests event handling and pane lifecycle - Integration tests verify pane cleanup on session deletion ## Files | File | Purpose | |------|---------| | `index.ts` | Public API exports | | `types.ts` | Core interfaces and shared utilities | | `shared.ts` | Shared infrastructure (quoteShellArg, buildOpencodeAttachCommand, findBinary) | | `factory.ts` | Multiplexer instance creation | | `session-manager.ts` | Session lifecycle management | | `tmux/index.ts` | tmux-specific implementation | | `zellij/index.ts` | zellij-specific implementation | | `herdr/index.ts` | herdr-specific implementation | | `kitty/index.ts` | kitty-specific implementation | | `cmux/index.ts` | cmux adapter and encoded surface handles | | `cmux/session-lifecycle.ts` | cmux event, polling, spawn, close, orphan, and cleanup ownership | | `cmux/session-state.ts` | process-global cmux session registry | | `cmux/close-policy.ts` | pure cmux close-intent transitions and retry budgets |