---
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`