How Tuicr Implements Session Persistence and Auto-Save in Rust

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. 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 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) and the CLI entry points.

Atomic Storage Implementation

Low-level I/O resides in 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 managed by 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. 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:

// 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:


# 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

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

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 holds all review state, while ReviewStore in src/review_store.rs provides the public API
  • Atomic writes: 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) triggers periodic saves, merging external CLI changes before writing
  • Indexing: src/persistence/manifest.rs maintains 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →