--- phase: 01-core-infrastructure plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/gsd_openclaw/__init__.py - src/gsd_openclaw/state_manager.py - tests/conftest.py - tests/test_state_manager.py autonomous: true requirements: - SESS-04 user_setup: [] must_haves: truths: - "State can be saved to JSON file atomically" - "State can be loaded from JSON file" - "Corrupted state falls back to backup automatically" - "State schema includes session metadata, tmux references, and activity stats" artifacts: - path: "src/gsd_openclaw/state_manager.py" provides: "State persistence with atomic writes" exports: ["SessionState", "StateManager", "save_state", "load_state"] min_lines: 80 - path: "tests/test_state_manager.py" provides: "Test coverage for state operations" contains: "test_save_load_state" key_links: - from: "src/gsd_openclaw/state_manager.py" to: "~/gsd-workspaces/{project}/.gsd/state.json" via: "atomic write (tempfile + os.replace)" pattern: "tempfile\\.mkstemp.*os\\.replace" --- Implement state persistence layer with atomic writes and backup rotation for crash-safe session checkpointing. Purpose: Provides the foundation for all session state management. Other components depend on this for storing and recovering session metadata. Output: Working state_manager.py with atomic save, load with validation, and automatic backup rotation. @/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 @.planning/phases/01-core-infrastructure/01-VALIDATION.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 ``` **Atomic write pattern:** ```python def save_state(state: dict, state_path: Path) -> None: 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 ``` Task 1: Create state schema with dataclasses src/gsd_openclaw/__init__.py, src/gsd_openclaw/state_manager.py - Test 1: SessionState can be created with all fields - Test 2: SessionState.to_dict() returns complete dict - Test 3: SessionState.from_dict() reconstructs from dict - Test 4: SessionState ignores unknown fields in from_dict() Create the SessionState dataclass with all fields from CONTEXT.md: - schema_version (default "1.0") - session_id, project_name, tmux_session_name, tmux_pane_id - created_at, last_active_at (ISO format strings) - command_count (int) - last_output_snapshot, display_name (Optional[str]) Implement to_dict() using dataclasses.asdict() and from_dict() with field filtering. Also create the package __init__.py with version and exports. Do NOT add validation logic yet - that comes in Task 3. pytest tests/test_state_manager.py::test_session_state_creation -x pytest tests/test_state_manager.py::test_session_state_serialization -x SessionState dataclass exists with all fields, to_dict() and from_dict() methods work correctly Task 2: Implement atomic state save with backup rotation src/gsd_openclaw/state_manager.py, tests/test_state_manager.py - Test 1: save_state creates file atomically (temp file + rename) - Test 2: save_state creates parent directories if missing - Test 3: save_state rotates backup (state.json -> state.json.bak) - Test 4: save_state cleans up temp file on failure - Test 5: Temp file is created in same directory as target (for atomic rename) Implement save_state(state: SessionState, state_path: Path) function: 1. Create parent directories with mkdir(parents=True, exist_ok=True) 2. If state_path exists, rotate backup: - Delete old state.json.bak if exists - Rename state_path to state.json.bak 3. Write to temp file using tempfile.mkstemp(dir=state_path.parent, suffix='.tmp') 4. Use os.fdopen to write JSON with indent=2, ensure_ascii=False 5. Atomic replace with os.replace(temp_path, state_path) 6. On exception, clean up temp file if it exists Use the pattern from RESEARCH.md - this is the atomic write pattern proven to prevent corruption. Do NOT use file locking - os.replace is atomic on POSIX systems. pytest tests/test_state_manager.py::test_atomic_write -x pytest tests/test_state_manager.py::test_backup_rotation -x State saves atomically, backup rotates correctly, temp files cleaned up on failure Task 3: Implement state load with validation and fallback src/gsd_openclaw/state_manager.py, tests/test_state_manager.py, tests/conftest.py - Test 1: load_state returns None for missing file - Test 2: load_state returns SessionState for valid file - Test 3: load_state raises ValueError for unsupported schema version - Test 4: load_state raises ValueError for missing required fields - Test 5: load_state falls back to backup when main file is corrupted (JSONDecodeError) - Test 6: load_state falls back to backup when validation fails Implement load_state(state_path: Path) -> Optional[SessionState] function: 1. If state_path doesn't exist, try backup (state_path.with_suffix('.json.bak')) 2. If neither exists, return None 3. Read JSON with proper encoding (utf-8) 4. Validate: - schema_version == "1.0" (raise ValueError if not) - Required fields present: session_id, project_name, tmux_session_name 5. On JSONDecodeError or ValueError, try backup (if backup != state_path) 6. Return SessionState.from_dict(data) Also create tests/conftest.py with pytest fixtures: - tmp_state_dir fixture using pytest's tmp_path - sample_state fixture returning a valid SessionState instance pytest tests/test_state_manager.py::test_save_load_state -x pytest tests/test_state_manager.py::test_state_validation -x pytest tests/test_state_manager.py::test_backup_fallback -x State loads with validation, corrupted files fall back to backup, missing files return None Task 4: Create StateManager class with workspace path resolution src/gsd_openclaw/state_manager.py, tests/test_state_manager.py - Test 1: StateManager.get_state_path returns correct path for project - Test 2: StateManager.save persists state to correct location - Test 3: StateManager.load retrieves state from correct location Create StateManager class that encapsulates state operations: ```python class StateManager: WORKSPACES_ROOT = Path.home() / "gsd-workspaces" def __init__(self, project_name: str): self.project_name = project_name self.workspace_path = self.WORKSPACES_ROOT / project_name self.state_path = self.workspace_path / ".gsd" / "state.json" def save(self, state: SessionState) -> None: save_state(state, self.state_path) def load(self) -> Optional[SessionState]: return load_state(self.state_path) def get_state_path(self) -> Path: return self.state_path ``` This provides a clean interface for other components to use state persistence without managing paths directly. pytest tests/test_state_manager.py::test_state_manager_class -x StateManager class provides clean interface for state persistence with correct workspace paths Run full test suite to verify all state operations: ```bash pytest tests/test_state_manager.py -v ``` All tests must pass. Coverage should include: - Atomic write pattern - Backup rotation - Validation logic - Fallback to backup - StateManager convenience methods - SessionState dataclass with all required fields exists - Atomic save_state function prevents corruption - load_state validates schema and required fields - Backup rotation works (state.json.bak) - Corrupted files fall back to backup automatically - StateManager class provides clean interface - All tests pass: `pytest tests/test_state_manager.py -v` After completion, create `.planning/phases/01-core-infrastructure/01-01-SUMMARY.md`