# How tuicr Handles Concurrent Session Writes and External Comment Merging

> Learn how tuicr handles concurrent session writes and external comment merging using exclusive file locks and atomic rename operations for safe, efficient data management.

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

---

**tuicr prevents data races by using exclusive file locks and atomic rename operations, ensuring that multiple processes can safely write to the same review session while automatically merging comments added by external CLI commands.**

The `agavra/tuicr` repository implements a robust persistence layer that allows the TUI and external processes to collaborate on the same code review session without corruption. Understanding how tuicr handles concurrent session writes reveals a battle-tested pattern for atomic file operations in Rust applications.

## Lock-Based Storage Architecture

The core mechanism resides in [`src/persistence/storage.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/storage.rs), where every write operation follows a strict **read-modify-write** cycle protected by filesystem locks.

### Exclusive File Locking with fs2

Before mutating a session, the process creates a side-car lock file (`<slug>.lock`) adjacent to the target JSON. It attempts an exclusive lock using `fs2::FileExt::try_lock_exclusive`. If another process holds the lock, the writer backs off and retries, preventing simultaneous writes to the same session file.

This lock file acts as a mutex across process boundaries. The slug—which encodes the session identity as `local:<repo_path>` or `gh:<owner>/<repo>/pr/<num>`—ensures that different review contexts never interfere with one another.

### Atomic Write Operations

To eliminate corruption risks during crashes, tuicr never writes directly to the target file. Instead, `save_session_by_identity` in [`src/persistence/storage.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/storage.rs) executes a three-phase commit:

1. **Serialize to temporary file**: The updated `ReviewSession` is written to `<slug>.tmp`.
2. **Atomic rename**: `std::fs::rename` moves the temporary file over the original session file. On POSIX systems, this replacement is atomic, meaning readers always see either the old complete file or the new complete file—never a partial write.
3. **Cleanup**: The lock file is removed only after the rename succeeds, signaling that the transaction is complete.

### Stale Lock Recovery

If a process crashes while holding the lock, the lock file persists. Subsequent writers detect this stale lock via a timeout mechanism (`timed out waiting for review storage lock`), remove the orphaned lock file, and proceed with their own write. This guarantees forward progress even after hard failures.

## Merging External Comments in Real Time

External processes—such as `tuicr review add`—use the `ReviewStore` facade in [`src/review_store.rs`](https://github.com/agavra/tuicr/blob/main/src/review_store.rs) to modify sessions independently of the TUI. The storage layer automatically reconciles these changes.

### The Slug-Addressed Session Identity

Sessions are globally identified by a slug string. Both the TUI and CLI calculate this slug deterministically from the repository context, ensuring they address the same physical file in the user-wide review directory (default: `~/.local/share/tuicr/reviews/`).

When `ReviewStore::add_comment` invokes `storage::save_session_by_identity`, the closure receives the **latest persisted session** as its argument. By loading the current state before applying mutations, the function naturally merges external comments with existing data.

### Polling and Hot-Reload Logic

The TUI monitors the session file for changes using a configurable polling interval (`review_watch_interval_ms`, default 1000ms). When `App::reload_session` in [`src/app/session.rs`](https://github.com/agavra/tuicr/blob/main/src/app/session.rs) detects a newer file modification timestamp, it:

1. Reloads the JSON into memory via `load_latest_session_for_context`.
2. Merges new comments into the `App.session` state.
3. Updates the `CommentNavigator` index so new annotations appear instantly in the UI.

This polling mechanism ensures that comments added via the CLI appear in the running TUI within one second, without requiring inter-process communication channels.

## Implementation Walkthrough

The following patterns demonstrate the concurrent-safe APIs used throughout the codebase:

```rust
// External process: Adding a comment via the public API
use tuicr::review_store::ReviewStore;
use std::path::PathBuf;

fn add_external_comment(slug: &str) -> Result<(), Box<dyn std::error::Error>> {
    ReviewStore::default().add_comment(
        slug,
        Comment::new_line(
            PathBuf::from("src/main.rs"),
            42,
            "Consider extracting this into a helper function".to_string(),
        ),
    )?;
    Ok(())
}

```

```rust
// TUI internal: Persisting pending edits with automatic merge
use crate::persistence::storage::{save_session_by_identity, slug_for_session};

fn save_current_comment(app: &mut App) -> anyhow::Result<()> {
    let slug = slug_for_session(&app.session)?;
    
    save_session_by_identity(&slug, |persisted_session| {
        // persisted_session contains the latest data from disk,
        // including any external comments added since last load
        persisted_session.add_comment(app.pending_comment.clone());
        Ok(persisted_session.clone())
    })?;
    
    app.pending_comment.clear();
    Ok(())
}

```

The `save_session_by_identity` function signature enforces the read-modify-write pattern by requiring a closure that receives the current session and returns the modified version, preventing accidental blind writes.

## Summary

- **Exclusive file locks** (`<slug>.lock`) prevent concurrent writes using `fs2::FileExt::try_lock_exclusive` as implemented in [`src/persistence/storage.rs`](https://github.com/agavra/tuicr/blob/main/src/persistence/storage.rs).
- **Atomic commits** via temporary files and `std::fs::rename` ensure crash-resistant persistence.
- **Stale lock detection** recovers automatically from crashed processes using timeout-based lock validation.
- **Slug-based addressing** guarantees that sessions are uniquely identified across TUI and CLI contexts.
- **Polling-based reloading** (`review_watch_interval_ms`) merges external comments into the running TUI via `App::reload_session` in [`src/app/session.rs`](https://github.com/agavra/tuicr/blob/main/src/app/session.rs).

## Frequently Asked Questions

### What happens if two tuicr instances try to save simultaneously?

The first process acquires the exclusive lock on `<slug>.lock` and proceeds with the read-modify-write cycle. The second process blocks briefly, then retries. Because `save_session_by_identity` reloads the session state inside the locked section, the second write automatically incorporates any changes committed by the first process, preventing lost updates.

### How does tuicr avoid corrupting session files if the process crashes mid-write?

All writes go to a temporary file (`<slug>.tmp`) first. Only after the entire JSON payload is successfully serialized does the code call `std::fs::rename` to move the temp file over the target. Since POSIX renames are atomic, a crash at any point leaves either the original valid file or the new valid file—never a truncated or garbled intermediate state.

### Can I run the TUI and CLI commands in parallel on the same review?

Yes. The TUI polls the storage directory every second (configurable via `review_watch_interval_ms`) and calls `load_latest_session_for_context` to detect changes. When the CLI writes a new comment through `ReviewStore`, the TUI picks up the updated file timestamp on its next poll cycle and reloads the session, displaying the new comment immediately without restart.

### Where are the unit tests for concurrent write handling?

The test suite in [`src/app/tests/persistence_merge_tests.rs`](https://github.com/agavra/tuicr/blob/main/src/app/tests/persistence_merge_tests.rs) simulates simultaneous writes from multiple processes and asserts that the final session contains the union of all comments. Additionally, [`src/app/tests/submit_flow_tests.rs`](https://github.com/agavra/tuicr/blob/main/src/app/tests/submit_flow_tests.rs) exercises the full comment submission lifecycle, validating lock acquisition and atomic rename behavior under various failure scenarios.