---
phase: 01-core-infrastructure
plan: 03
type: execute
wave: 2
depends_on:
- 01-01
- 01-02
files_modified:
- src/gsd_openclaw/tmux_session.py
- tests/conftest.py
- tests/test_tmux_session.py
autonomous: true
requirements:
- SESS-01
- SESS-02
- SESS-03
- SESS-07
user_setup: []
must_haves:
truths:
- "User can create a new tmux session for a project"
- "User can list all active gsd sessions with their project associations"
- "User can destroy a session when project is complete"
- "Session names follow gsd-{project} format with collision handling"
- "Multiple sessions can run concurrently without interference"
artifacts:
- path: "src/gsd_openclaw/tmux_session.py"
provides: "tmux session lifecycle management"
exports: ["TmuxSessionManager", "TmuxSession"]
min_lines: 120
- path: "tests/test_tmux_session.py"
provides: "Test coverage for tmux operations"
contains: "test_create_session"
key_links:
- from: "src/gsd_openclaw/tmux_session.py"
to: "libtmux.Server"
via: "server = libtmux.Server()"
pattern: "libtmux\\.Server"
- from: "TmuxSessionManager"
to: "state_manager.StateManager"
via: "state persistence after operations"
pattern: "StateManager"
---
Implement tmux session lifecycle management using libtmux.
Purpose: Core functionality for creating, listing, and destroying tmux sessions. This is the primary interface to tmux that all other components will use.
Output: Working tmux_session.py with create/list/destroy operations using libtmux.
@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
@/home/starmen1265/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-core-infrastructure/01-CONTEXT.md
@.planning/phases/01-core-infrastructure/01-RESEARCH.md
**SessionState dataclass:**
```python
@dataclass
class SessionState:
schema_version: str = "1.0"
session_id: str = ""
project_name: str = ""
tmux_session_name: str = ""
tmux_pane_id: str = ""
created_at: str = ""
last_active_at: str = ""
command_count: int = 0
last_output_snapshot: Optional[str] = None
display_name: Optional[str] = None
```
**StateManager class:**
```python
class StateManager:
def save(self, state: SessionState) -> None: ...
def load(self) -> Optional[SessionState]: ...
```
**WorkspaceManager class:**
```python
class WorkspaceManager:
def create(self) -> Workspace: ...
def get_workspace(self) -> Workspace: ...
```
**libtmux API:**
```python
import libtmux
server = libtmux.Server()
# Create session
session = server.new_session(
session_name='gsd-my-project',
attach=False,
window_name='main'
)
# List sessions
for s in server.sessions:
print(f"{s.name}: {s.session_id}")
# Find session
session = server.sessions.get(session_name='gsd-my-project')
# Kill session
session.kill()
```
Task 1: Implement TmuxSession dataclass and basic libtmux wrapper
src/gsd_openclaw/tmux_session.py, tests/conftest.py, tests/test_tmux_session.py
- Test 1: TmuxSession stores session metadata correctly
- Test 2: libtmux Server connection works
- Test 3: Server can list existing sessions
Create TmuxSession dataclass and initial TmuxSessionManager:
```python
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, List
import uuid
import libtmux
@dataclass
class TmuxSession:
"""Represents a managed tmux session."""
session_id: str
project_name: str
tmux_session_name: str
tmux_session: Optional[libtmux.Session] = None # Runtime reference
@property
def exists(self) -> bool:
if self.tmux_session is None:
return False
try:
# Check if session still exists in tmux
self.tmux_session.session_id
return True
except libtmux.exc.LibTmuxException:
return False
class TmuxSessionManager:
"""Manages tmux session lifecycle using libtmux."""
SESSION_PREFIX = "gsd-"
def __init__(self, server: Optional[libtmux.Server] = None):
self.server = server or libtmux.Server()
def _generate_session_name(self, project_name: str) -> str:
"""Generate tmux session name from project name."""
base_name = f"{self.SESSION_PREFIX}{project_name}"
return base_name
def _resolve_collision(self, base_name: str) -> str:
"""Add timestamp suffix if session name exists."""
existing = self.server.sessions.filter(session_name=base_name)
if not existing:
return base_name
suffix = datetime.now().strftime("-%Y%m%d-%H%M%S")
return f"{base_name}{suffix}"
def list_sessions(self) -> List[libtmux.Session]:
"""List all gsd-managed sessions."""
return list(self.server.sessions.filter(
session_name__startswith=self.SESSION_PREFIX
))
```
Add libtmux pytest fixtures to conftest.py for test isolation.
pytest tests/test_tmux_session.py::test_server_connection -x
pytest tests/test_tmux_session.py::test_list_sessions -x
TmuxSession dataclass exists, TmuxSessionManager connects to libtmux, can list sessions
Task 2: Implement session creation with state persistence
src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py
- Test 1: create_session creates tmux session with correct name
- Test 2: create_session persists state to workspace
- Test 3: create_session handles name collision with timestamp suffix
- Test 4: create_session creates workspace directory if missing
- Test 5: create_session returns TmuxSession with all metadata
Implement create_session method:
```python
from gsd_openclaw.state_manager import StateManager, SessionState
from gsd_openclaw.workspace import WorkspaceManager
class TmuxSessionManager:
# ... existing code ...
def create_session(
self,
project_name: str,
workspace_manager: Optional[WorkspaceManager] = None,
display_name: Optional[str] = None
) -> TmuxSession:
"""Create a new tmux session for a project."""
# Ensure workspace exists
ws_manager = workspace_manager or WorkspaceManager(project_name)
ws_manager.create()
# Generate unique session name
base_name = self._generate_session_name(project_name)
tmux_name = self._resolve_collision(base_name)
# Create tmux session (detached)
tmux_session = self.server.new_session(
session_name=tmux_name,
attach=False,
window_name='main'
)
# Create session state
session_id = str(uuid.uuid4())[:8]
now = datetime.now().isoformat()
state = SessionState(
session_id=session_id,
project_name=project_name,
tmux_session_name=tmux_name,
tmux_pane_id=tmux_session.active_pane.pane_id,
created_at=now,
last_active_at=now,
display_name=display_name
)
# Persist state
state_manager = StateManager(project_name)
state_manager.save(state)
return TmuxSession(
session_id=session_id,
project_name=project_name,
tmux_session_name=tmux_name,
tmux_session=tmux_session
)
```
This integrates with StateManager and WorkspaceManager from previous plans.
pytest tests/test_tmux_session.py::test_create_session -x
pytest tests/test_tmux_session.py::test_session_naming_collision -x
create_session creates tmux session, persists state, handles name collisions
Task 3: Implement session destruction with cleanup
src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py
- Test 1: destroy_session kills tmux session
- Test 2: destroy_session removes state file
- Test 3: destroy_session handles already-destroyed session gracefully
- Test 4: destroy_session with cleanup=True removes workspace
Implement destroy_session method:
```python
class TmuxSessionManager:
# ... existing code ...
def destroy_session(
self,
project_name: str,
cleanup_workspace: bool = False
) -> bool:
"""Destroy a tmux session and optionally cleanup workspace."""
state_manager = StateManager(project_name)
state = state_manager.load()
if state is None:
return False # No session to destroy
# Kill tmux session
try:
tmux_session = self.server.sessions.get(
session_name=state.tmux_session_name
)
if tmux_session:
tmux_session.kill()
except libtmux.exc.LibTmuxException:
pass # Session already gone
# Remove state file
if state_manager.state_path.exists():
state_manager.state_path.unlink()
if state_manager.state_path.with_suffix('.json.bak').exists():
state_manager.state_path.with_suffix('.json.bak').unlink()
# Optionally cleanup workspace
if cleanup_workspace:
workspace_manager = WorkspaceManager(project_name)
workspace_manager.destroy()
return True
```
The cleanup_workspace parameter allows keeping the workspace for later resumption if desired.
pytest tests/test_tmux_session.py::test_destroy_session -x
destroy_session kills tmux session, removes state, optionally cleans workspace
Task 4: Implement session listing with project info
src/gsd_openclaw/tmux_session.py, tests/test_tmux_session.py
- Test 1: list_gsd_sessions returns all gsd-* sessions
- Test 2: list_gsd_sessions includes project associations from state files
- Test 3: list_gsd_sessions handles orphan sessions (no state file)
Implement list_gsd_sessions method that combines tmux session info with state:
```python
from dataclasses import dataclass
@dataclass
class SessionInfo:
"""Session info for display."""
tmux_name: str
project_name: str
session_id: str
created_at: str
last_active_at: str
command_count: int
display_name: Optional[str]
is_orphan: bool = False # tmux session exists but no state
class TmuxSessionManager:
# ... existing code ...
def list_gsd_sessions(self) -> List[SessionInfo]:
"""List all gsd sessions with project associations."""
sessions_info = []
# Get all gsd-* tmux sessions
tmux_sessions = self.list_sessions()
for tmux_session in tmux_sessions:
tmux_name = tmux_session.name
# Extract project name from tmux session name
# Format: gsd-{project} or gsd-{project}-{timestamp}
project_name = self._extract_project_name(tmux_name)
# Try to load state
state_manager = StateManager(project_name)
state = state_manager.load()
if state and state.tmux_session_name == tmux_name:
# Matched session
sessions_info.append(SessionInfo(
tmux_name=tmux_name,
project_name=project_name,
session_id=state.session_id,
created_at=state.created_at,
last_active_at=state.last_active_at,
command_count=state.command_count,
display_name=state.display_name,
is_orphan=False
))
else:
# Orphan session (no matching state)
sessions_info.append(SessionInfo(
tmux_name=tmux_name,
project_name=project_name,
session_id="?",
created_at="?",
last_active_at="?",
command_count=0,
display_name=None,
is_orphan=True
))
return sessions_info
def _extract_project_name(self, tmux_name: str) -> str:
"""Extract project name from tmux session name."""
# Remove prefix
name = tmux_name[len(self.SESSION_PREFIX):]
# Remove timestamp suffix if present (format: -YYYYMMDD-HHMMSS)
import re
match = re.match(r'^(.+?)(-\d{8}-\d{6})?$', name)
return match.group(1) if match else name
```
pytest tests/test_tmux_session.py::test_list_gsd_sessions -x
list_gsd_sessions returns session info with project associations, handles orphans
Run full test suite:
```bash
pytest tests/test_tmux_session.py -v
```
Verify integration with state_manager and workspace:
```bash
pytest tests/ -v -k "tmux or state or workspace"
```
- TmuxSession dataclass exists with metadata
- create_session creates tmux session with libtmux
- create_session persists state and handles name collisions
- destroy_session kills tmux session and removes state
- list_gsd_sessions returns session info with project associations
- Concurrent sessions work without interference
- All tests pass: `pytest tests/test_tmux_session.py -v`