# How Tuicr Implements Session Persistence and Auto-Save in Rust

> Learn how Tuicr implements session persistence and auto-save in Rust. Discover its use of JSON serialization, atomic file operations, flock locking, and background timers to prevent data loss.

- Repository: [Almog Gavra/tuicr](https://github.com/agavra/tuicr)
- Tags: internals
- Published: 2026-08-07

---

**Tuicr persists review sessions by serializing the `ReviewSession` struct to JSON files under the user's data directory, using atomic file operations with `flock` locking and a background timer that auto-saves every 1000ms to ensure no review state is lost between TUI restarts or CLI interactions.**

Tuicr is a terminal-based code review tool that maintains state across interactive TUI sessions and non-interactive CLI commands. According to the agavra/tuicr source code, the session persistence layer combines Rust structs for in-memory state with atomic filesystem operations to prevent data corruption when multiple processes access the same review.

## Core Architecture of Tuicr Session Persistence

The persistence system spans four primary modules that handle everything from data models to filesystem atomicity.

### The ReviewSession Data Model

At the heart of the system lies `ReviewSession`, defined in [`src/model/review.rs`](https://github.com/agavra/tuicr/blob/main/src/model/review.rs). This struct encapsulates the complete review state, including:

- **File-level metadata**: Per-file `FileReview` instances tracking reviewed hunks
- **Line-level comments**: Structured comment data attached to specific lines
- **Review progress**: Flags indicating which files have been fully reviewed

When the TUI initializes via `App::new()`, it either loads an existing session or creates a fresh in-memory structure that will later synchronize to disk.

### The ReviewStore API Facade

The `ReviewStore` struct in [`src/review_store.rs`](https://github.com/agavra/tuicr/blob/main/src/review_store.rs) provides the public interface for all persistence operations. Key methods include:

- **`get_review(repo_or_slug)`**: Locates and deserializes a session by repository path or PR identifier
- **`add_comment()`**: Merges new comments into an existing session and triggers persistence
- **Listing and enumeration**: Supports the `tuicr review list` CLI command

This facade abstracts the underlying storage mechanics from both the TUI ([`src/app/comments.rs`](https://github.com/agavra/tuicr/blob/main/src/app/comments.rs)) and the CLI entry points.

### Atomic Storage Implementation

Low-level I/O resides in [`src/persistence/storage.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/storage.rs), implementing crash-resistant write semantics. The `storage::save_session` function executes a three-phase commit:

1. **Write to temporary file**: Serializes the `ReviewSession` to a temp path using `serde_json`
2. **Acquire lock**: Uses Unix `flock` (or equivalent) to obtain an exclusive file lock, preventing concurrent writes from the TUI and CLI processes
3. **Atomic rename**: Moves the temp file into place, ensuring readers never encounter partially written JSON

If a stale lock file exists from a crashed process, the storage layer detects and removes it before proceeding, eliminating deadlock scenarios.

### Session Manifest and Indexing

To avoid scanning the entire data directory, Tuicr maintains an [`index.json`](https://github.com/agavra/tuicr/blob/main/index.json) managed by [`src/persistence/manifest.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/manifest.rs). The manifest:

- Maps repository paths and PR slugs to specific session filenames
- Tracks **active sessions** with process IDs and last-seen timestamps
- Provides fast lookup for `manifest::load_index` without filesystem globbing

On graceful TUI exit, the application removes its active session entry from the manifest and deletes empty session files (those with no comments or reviewed flags).

## The Auto-Save Mechanism

Tuicr eliminates manual save operations through a background polling system integrated into the TUI event loop.

### Background Polling and Configuration

The `review_watch_interval_ms` configuration parameter (defaulting to **1000ms**) drives a timer in [`src/app/session.rs`](https://github.com/agavra/tuicr/blob/main/src/app/session.rs). Every interval, the TUI executes an auto-save sequence:

1. **Read current state**: Calls `load_session` to fetch any external modifications
2. **Merge changes**: Incorporates comments added via concurrent `tuicr review add` CLI invocations using `add_comment_to_session`
3. **Atomic write**: Persists the merged state via `storage::save_session`

This merge-before-write strategy ensures that CLI additions surface immediately in the TUI without overwriting in-memory changes.

### Handling Concurrent Modifications

When both the TUI and a CLI process modify the same session, Tuicr prevents lost updates by strictly following read-modify-write semantics. Before each auto-save, the system reloads the JSON from disk, merges the in-memory delta, and writes the combined state. The file lock acquired during `storage::save_session` guarantees that no two writers interleave their JSON serialization.

## Failure Recovery and Lock Handling

The persistence layer implements several resilience patterns for production reliability:

- **Orphaned lock recovery**: If `flock` fails because a lock file exists without a live process, Tuicr removes the stale lock and retries
- **Empty session cleanup**: On normal exit, `Manifest::remove_empty_sessions()` deletes JSON files containing no comments or review progress, preventing disk clutter
- **Crash recovery**: Abnormal termination leaves the session file intact; the next TUI launch loads the recovered state from the data directory (`$XDG_DATA_HOME/tuicr/reviews/` on Unix, `%APPDATA%\tuicr\reviews\` on Windows)

## Practical Code Examples

### Adding a Comment from the TUI

When users save a comment in the interface, `App::save_comment()` delegates to the store:

```rust
// src/app/comments.rs (simplified)
let req = AddCommentRequest {
    file: path.clone(),
    line: Some(line_no),
    body: comment_body.clone(),
    // … additional metadata …
};
add_comment_to_session(&mut self.session, req);
// Autosave timer triggers storage::save_session asynchronously

```

### Appending Comments via CLI

Non-interactive usage modifies the same session file:

```bash

# Adds comment to existing session or creates new

tuicr review add --repo . --input '{"file":"src/main.rs","line":42,"body":"Consider refactoring"}'

```

Behind the scenes, `ReviewStore::add_comment` loads the session JSON, merges the new comment, and invokes `storage::save_session`.

### Loading Sessions Programmatically

```rust
use tuicr::ReviewStore;

fn main() -> Result<()> {
    let store = ReviewStore::new()?;
    // Load by repository path or PR slug
    let session = store.get_review(".")?;
    println!("Loaded {} comments", session.total_comment_count());
    Ok(())
}

```

### Manual Cleanup of Empty Sessions

```rust
use tuicr::persistence::manifest::Manifest;

fn purge_empty() -> Result<()> {
    let mut manifest = Manifest::load()?;
    manifest.remove_empty_sessions()?; // Deletes files with no content
    Ok(())
}

```

## Summary

- **Data model**: `ReviewSession` in [`src/model/review.rs`](https://github.com/agavra/tuicr/blob/main/src/model/review.rs) holds all review state, while `ReviewStore` in [`src/review_store.rs`](https://github.com/agavra/tuicr/blob/main/src/review_store.rs) provides the public API
- **Atomic writes**: [`src/persistence/storage.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/storage.rs) implements `storage::save_session` using temp files, `flock` locking, and atomic rename for crash safety
- **Auto-save**: A 1000ms timer in the TUI ([`src/app/session.rs`](https://github.com/agavra/tuicr/blob/main/src/app/session.rs)) triggers periodic saves, merging external CLI changes before writing
- **Indexing**: [`src/persistence/manifest.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/manifest.rs) maintains [`index.json`](https://github.com/agavra/tuicr/blob/main/index.json) for fast session lookup and tracks active process sessions
- **Concurrency**: File-level locks and read-modify-write cycles prevent data loss when TUI and CLI access the same session simultaneously

## Frequently Asked Questions

### Where does Tuicr store session data on disk?

Tuicr stores JSON session files in the user’s data directory: `$XDG_DATA_HOME/tuicr/reviews/` on Unix systems or `%APPDATA%\tuicr\reviews\` on Windows. An [`index.json`](https://github.com/agavra/tuicr/blob/main/index.json) file in the same directory maps repository paths to specific session filenames.

### How does Tuicr handle concurrent edits from the TUI and CLI?

Before every auto-save, Tuicr reloads the session file via `load_session` to fetch any comments added by `tuicr review add` CLI commands. It merges these external changes into the in-memory `ReviewSession` before calling `storage::save_session`, which acquires an exclusive `flock` to prevent write conflicts.

### What happens if Tuicr crashes during a review?

The session file remains intact on disk due to the atomic write semantics (temp file + rename) in `storage::save_session`. When Tuicr restarts, it loads the recovered state from the data directory. Any orphaned lock files are automatically detected and removed during the next save operation.

### How can I programmatically clean up empty review sessions?

Import the `Manifest` struct from [`src/persistence/manifest.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/manifest.rs) and call `Manifest::load()` followed by `remove_empty_sessions()`. This walks the data directory, deletes JSON files containing no comments or review flags, and updates the index accordingly.