# Phase 1: Core Infrastructure - Research **Researched:** 2026-03-11 **Domain:** tmux session management, state persistence, Python CLI **Confidence:** HIGH ## Summary This phase implements the foundational infrastructure for gsd-openclaw: tmux session lifecycle management with persistent state checkpointing. The core technical stack centers on **libtmux** (v0.53.x) as the Python wrapper for tmux control, combined with JSON state files using atomic write patterns for crash-safe persistence. **Primary recommendation:** Use libtmux for all tmux operations - it provides a clean Pythonic API with Server/Session/Window/Pane objects, built-in pytest fixtures for testing, and is battle-tested via tmuxp. Do not shell out to `tmux` commands directly. ## User Constraints (from CONTEXT.md) ### Locked Decisions **Session Naming:** - Naming format: Project name only (single-user scenario, e.g., `gsd-project-alpha`) - Name collision: Auto-add timestamp suffix (e.g., `-20260311-001`) - Display name: Support user-defined friendly names with mapping: display name -> tmux name - Session limit: Per-user configurable max sessions (default value by planner) **State Storage:** - Location: Per-project independent directory `~/gsd-workspaces/{project}/.gsd/` - Checkpoint frequency: Auto-save every 30 seconds - State content: - Session metadata (ID, project name, creation time) - tmux reference (session name, pane ID) - Activity statistics (last active time, command count) - Last output snapshot - Atomic write: Yes (write to temp file then rename) - Backup strategy: Keep 1 backup (`.gsd/state.json.bak`) **Workspace Structure:** - Root directory: `~/gsd-workspaces/{project}/` - Directory layout: ``` ~/gsd-workspaces/{project}/ ├── .gsd/ │ ├── state.json # Session state │ ├── state.json.bak # State backup │ └── session-dump/ # Session dump ├── code/ # Project code (optional) └── io-logs/ └── {session-id}.log # I/O logs ``` - I/O log retention: 7 days - Session end handling: Delete workspace when destroying session **Recovery Strategy:** - Crash recovery: Auto-detect state file on startup, prompt user to restore - Orphan session handling: Auto-clean tmux sessions without state files - State validation: Validate state file integrity and version compatibility before restore - Backup usage: Auto-try backup if main state file is corrupted ### Claude's Discretion - Exact checkpoint trigger timing (post-command vs timer) - State file schema version number design - Log file naming format - Specific error message wording ### Deferred Ideas (OUT OF SCOPE) - Multi-user prefix (current single-user scenario; can add later for multi-user isolation) - Session pooling and LRU eviction (v2 feature) - Session branching and cloning (v2 feature) - Real-time state sync to OpenClaw control panel (Phase 5) ## Phase Requirements | ID | Description | Research Support | |----|-------------|-----------------| | SESS-01 | User can create tmux session for a project workspace | libtmux `Server.new_session()` API | | SESS-02 | User can list active tmux sessions with project associations | libtmux `Server.sessions` QueryList with filter/get | | SESS-03 | User can destroy a tmux session when project is complete | libtmux `Session.kill()` method | | SESS-04 | Session state persists across server restarts via JSON checkpointing | Atomic write pattern with tempfile + os.replace | | SESS-05 | User can resume interrupted sessions with full context restoration | State validation + libtmux session reattachment | | SESS-06 | Each project has isolated workspace (separate tmux session + directory) | Workspace directory structure pattern | | SESS-07 | Multiple users can run projects concurrently without interference | Session naming with `gsd-` prefix (single-user for v1) | ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | libtmux | 0.53.x | Python API for tmux control | Official Python wrapper, battle-tested via tmuxp, provides Server/Session/Window/Pane objects | | pathlib | stdlib | Path handling | Cross-platform, modern Python path API | | json | stdlib | State serialization | Human-readable, matches GSD patterns | | dataclasses | stdlib | State schema definition | Type-safe state models with minimal boilerplate | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | pytest | 8.x | Testing framework | All unit/integration tests | | pytest-asyncio | 0.23+ | Async test support | If checkpoint timer uses async | | typing | stdlib | Type hints | All function signatures | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | libtmux | subprocess + tmux CLI | libtmux provides typed objects, error handling, pytest fixtures; subprocess requires parsing output | | JSON | YAML/TOML | JSON is faster to parse, stdlib-only, matches GSD state.json pattern | | dataclasses | Pydantic | dataclasses is stdlib, sufficient for this use case; Pydantic adds validation overhead | **Installation:** ```bash pip install libtmux pytest pytest-asyncio ``` ## Architecture Patterns ### Recommended Project Structure ``` gsd-openclaw/ ├── src/ │ └── gsd_openclaw/ │ ├── __init__.py │ ├── state_manager.py # State persistence (load/save/validate) │ ├── tmux_session.py # tmux session lifecycle wrapper │ ├── workspace.py # Workspace directory management │ └── cli.py # CLI entry point (create/list/destroy/resume) ├── tests/ │ ├── conftest.py # pytest fixtures (libtmux server) │ ├── test_state_manager.py # State persistence tests │ ├── test_tmux_session.py # Session lifecycle tests │ └── test_workspace.py # Workspace management tests └── pyproject.toml ``` ### Pattern 1: Atomic State Write **What:** Write state to temp file, then atomically rename to target **When to use:** All state.json writes to prevent corruption on crash **Example:** ```python # Source: https://blog.gocept.com/2013/07/15/reliable-file-updates-with-python/ import json import os import tempfile from pathlib import Path def save_state(state: dict, state_path: Path) -> None: """Atomically write state to JSON file.""" state_path.parent.mkdir(parents=True, exist_ok=True) # Write to temp file in same directory (same filesystem for atomic rename) fd, temp_path = tempfile.mkstemp(dir=state_path.parent, suffix='.tmp') try: with os.fdopen(fd, 'w', encoding='utf-8') as f: json.dump(state, f, indent=2, ensure_ascii=False) # Atomic replace (os.replace is atomic on POSIX) os.replace(temp_path, state_path) except Exception: # Clean up temp file on failure if os.path.exists(temp_path): os.unlink(temp_path) raise ``` ### Pattern 2: libtmux Session Management **What:** Use libtmux Server/Session objects for all tmux operations **When to use:** All tmux session create/list/destroy/attach operations **Example:** ```python # Source: https://github.com/tmux-python/libtmux import libtmux server = libtmux.Server() # Create new session session = server.new_session( session_name='gsd-my-project', attach=False, # Don't attach (detached mode) window_name='main' ) # List sessions for s in server.sessions: print(f"{s.name}: {s.session_id}") # Find specific session session = server.sessions.get(session_name='gsd-my-project') # Kill session session.kill() ``` ### Pattern 3: State Schema with dataclasses **What:** Define typed state schema using dataclasses **When to use:** All state.json structure definition **Example:** ```python from dataclasses import dataclass, asdict from datetime import datetime from typing import Optional @dataclass class SessionState: """State schema for a gsd-openclaw session.""" 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 def to_dict(self) -> dict: return asdict(self) @classmethod def from_dict(cls, data: dict) -> 'SessionState': return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__}) ``` ### Anti-Patterns to Avoid - **Shelling out to tmux CLI:** Use libtmux API instead of `subprocess.run(['tmux', ...])` - **Non-atomic writes:** Never write directly to state.json; always use temp file + rename - **Hardcoded paths:** Use `~/gsd-workspaces/` via `Path.home() / "gsd-workspaces"` - **Session name collisions:** Always check for existing sessions before creating new ones ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | tmux control | subprocess + tmux commands | libtmux | Error handling, typed objects, pytest fixtures built-in | | Session listing | Parse `tmux list-sessions` output | `server.sessions` QueryList | Filter/get methods, type safety | | Atomic writes | Custom file locking | tempfile + os.replace | OS-level atomicity, proven pattern | | Test tmux server | Manual setup/teardown | libtmux pytest fixtures | Auto cleanup, isolated servers | **Key insight:** libtmux's pytest plugin provides `server`, `session`, and `TestServer` fixtures that automatically handle setup/teardown of isolated tmux servers. This eliminates boilerplate and ensures test isolation. ## Common Pitfalls ### Pitfall 1: Session Name Collisions **What goes wrong:** Creating a session with a name that already exists causes tmux error **Why it happens:** tmux requires unique session names per server **How to avoid:** Check `server.sessions.filter(session_name=...)` before creating; append timestamp suffix on collision **Warning signs:** `libtmux.exc.LibTmuxException` on `new_session()` ### Pitfall 2: State File Corruption **What goes wrong:** Partial writes during crash leave corrupted state.json **Why it happens:** Direct writes without atomic pattern can be interrupted **How to avoid:** Always use temp file + os.replace pattern; keep backup file **Warning signs:** `json.JSONDecodeError` on load; state file exists but session doesn't ### Pitfall 3: Orphaned tmux Sessions **What goes wrong:** tmux session exists but state.json is missing or corrupted **Why it happens:** External factors (manual tmux commands, system issues) can create sessions **How to avoid:** Startup scan for sessions with `gsd-` prefix without state files; offer cleanup **Warning signs:** `server.sessions` shows sessions not in state directory ### Pitfall 4: Path Expansion Issues **What goes wrong:** `~` in paths not expanded, or wrong home directory in tests **Why it happens:** String paths vs Path objects; test isolation changes HOME **How to avoid:** Use `Path.home()` for expansion; in tests, use pytest fixtures that set HOME **Warning signs:** FileNotFoundError with literal `~` in path ## Code Examples Verified patterns from official sources: ### Create Session with libtmux ```python # Source: https://github.com/tmux-python/libtmux import libtmux def create_session(project_name: str) -> libtmux.Session: server = libtmux.Server() tmux_name = f"gsd-{project_name}" # Check for collision and add suffix if needed existing = server.sessions.filter(session_name=tmux_name) if existing: from datetime import datetime suffix = datetime.now().strftime("-%Y%m%d-%H%M%S") tmux_name = f"{tmux_name}{suffix}" session = server.new_session( session_name=tmux_name, attach=False, window_name='main' ) return session ``` ### List Sessions with Project Info ```python # Source: https://libtmux.git-pull.com/api/sessions.html def list_gsd_sessions() -> list[dict]: server = libtmux.Server() gsd_sessions = server.sessions.filter(session_name__startswith='gsd-') return [ { 'tmux_name': s.name, 'session_id': s.session_id, 'windows': len(s.windows), 'created': s.session_created, } for s in gsd_sessions ] ``` ### Atomic State Save with Backup ```python # Source: https://blog.gocept.com/2013/07/15/reliable-file-updates-with-python/ import json import os import tempfile from pathlib import Path def save_state_with_backup(state: dict, state_path: Path) -> None: """Save state atomically, maintaining backup.""" state_path.parent.mkdir(parents=True, exist_ok=True) # Rotate backup if main file exists backup_path = state_path.with_suffix('.json.bak') if state_path.exists(): if backup_path.exists(): backup_path.unlink() os.replace(state_path, backup_path) # Atomic write new state fd, temp_path = tempfile.mkstemp(dir=state_path.parent, suffix='.tmp') try: with os.fdopen(fd, 'w', encoding='utf-8') as f: json.dump(state, f, indent=2, ensure_ascii=False) os.replace(temp_path, state_path) except Exception: if os.path.exists(temp_path): os.unlink(temp_path) raise ``` ### State Validation on Load ```python def load_and_validate_state(state_path: Path) -> dict | None: """Load state with validation and fallback to backup.""" if not state_path.exists(): # Try backup backup_path = state_path.with_suffix('.json.bak') if backup_path.exists(): return load_and_validate_state(backup_path) return None try: with open(state_path, 'r', encoding='utf-8') as f: state = json.load(f) # Validate schema version if state.get('schema_version') != '1.0': raise ValueError(f"Unsupported schema version: {state.get('schema_version')}") # Validate required fields required = ['session_id', 'project_name', 'tmux_session_name'] missing = [f for f in required if not state.get(f)] if missing: raise ValueError(f"Missing required fields: {missing}") return state except (json.JSONDecodeError, ValueError) as e: # Try backup on corruption backup_path = state_path.with_suffix('.json.bak') if backup_path.exists() and backup_path != state_path: return load_and_validate_state(backup_path) raise ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | subprocess tmux commands | libtmux typed API | libtmux mature since 2020 | Type safety, testability | | Manual pytest setup | libtmux pytest plugin | libtmux 0.30+ (2024) | Auto cleanup, isolated servers | | String paths | pathlib.Path | Python 3.4+ | Cross-platform, chaining | **Deprecated/outdated:** - `session.attached_pane` / `session.attached_window`: Use `session.active_pane` / `session.active_window` (deprecated in libtmux 0.31) - `pane.split_window()`: Use `pane.split()` (deprecated in libtmux 0.33) ## Open Questions 1. **Checkpoint Timer Implementation** - What we know: 30-second interval is locked in - What's unclear: Threading vs asyncio vs signal-based timer - Recommendation: Use `threading.Timer` for simplicity; asyncio if already using async elsewhere 2. **Schema Version Migration** - What we know: Need schema_version field - What's unclear: Migration strategy for future versions - Recommendation: Keep v1 simple; add migration function when v2 schema needed ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | pytest 8.x | | Config file | pyproject.toml (tool.pytest) | | Quick run command | `pytest tests/ -x -q` | | Full suite command | `pytest tests/ -v --cov=src/gsd_openclaw` | ### Phase Requirements -> Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | SESS-01 | Create tmux session | integration | `pytest tests/test_tmux_session.py::test_create_session -x` | Wave 0 | | SESS-02 | List active sessions | unit | `pytest tests/test_tmux_session.py::test_list_sessions -x` | Wave 0 | | SESS-03 | Destroy session | integration | `pytest tests/test_tmux_session.py::test_destroy_session -x` | Wave 0 | | SESS-04 | State persistence | unit | `pytest tests/test_state_manager.py::test_save_load_state -x` | Wave 0 | | SESS-05 | Session resume | integration | `pytest tests/test_tmux_session.py::test_resume_session -x` | Wave 0 | | SESS-06 | Workspace isolation | unit | `pytest tests/test_workspace.py::test_workspace_isolation -x` | Wave 0 | | SESS-07 | Concurrent user support | unit | `pytest tests/test_tmux_session.py::test_session_naming_collision -x` | Wave 0 | ### Sampling Rate - **Per task commit:** `pytest tests/ -x -q` (quick smoke test) - **Per wave merge:** `pytest tests/ -v` (full test suite) - **Phase gate:** Full suite green + coverage report before `/gsd:verify-work` ### Wave 0 Gaps - [ ] `tests/conftest.py` - libtmux pytest fixtures (server, session, tmp_path) - [ ] `tests/test_state_manager.py` - atomic write, backup, validation tests - [ ] `tests/test_tmux_session.py` - create/list/destroy/resume tests - [ ] `tests/test_workspace.py` - directory structure, cleanup tests - [ ] Framework install: `pip install pytest pytest-cov libtmux` - if none detected ## Sources ### Primary (HIGH confidence) - [libtmux GitHub](https://github.com/tmux-python/libtmux) - Official repository with API examples - [libtmux Sessions API](https://libtmux.git-pull.com/api/sessions.html) - Session class documentation - [libtmux Panes API](https://libtmux.git-pull.com/api/panes.html) - Pane class documentation - [libtmux pytest plugin](https://libtmux.git-pull.com/pytest-plugin/) - Test fixtures documentation - [OpenClaw SKILL.md format](https://github.com/openclaw/openclaw/blob/main/skills/skill-creator/SKILL.md) - Official skill structure ### Secondary (MEDIUM confidence) - [Atomic file updates with Python](https://blog.gocept.com/2013/07/15/reliable-file-updates-with-python/) - Write-replace pattern verified ### Tertiary (LOW confidence) - None - all critical information verified with primary sources ## Metadata **Confidence breakdown:** - Standard stack: HIGH - libtmux is well-documented, actively maintained, battle-tested via tmuxp - Architecture: HIGH - patterns derived from official libtmux docs and established Python practices - Pitfalls: HIGH - based on libtmux documentation warnings and common tmux issues **Research date:** 2026-03-11 **Valid until:** 30 days - libtmux API stable, but pre-1.0 (APIs may change in 2026 per docs)