How free-claude-code Implements Session Persistence Across Server Restarts in FastAPI

Session persistence in free-claude-code is implemented via a thread-safe SessionStore class that maintains all conversation state in an atomic JSON file, using debounced writes to minimize I/O and explicit flush operations during FastAPI lifespan shutdown hooks.

The free-claude-code project, an open-source integration layer for Claude Code CLI, must maintain conversation continuity even when the FastAPI server restarts. Rather than using an external database, the project implements a lightweight persistence mechanism that serializes session trees, node mappings, and message logs to disk using a custom JSON-based store.

The SessionStore Architecture

The core persistence logic resides in messaging/session.py within the SessionStore class. This class manages all mutable conversation state through a centralized storage interface.

When instantiated, the store immediately attempts to hydrate itself from the existing JSON file:

self.storage_path = storage_path                 # L25-L27

self._load()                                    # L42

The _load() method (lines 47-95) reads the JSON file and populates three critical in-memory structures: _trees (conversation hierarchies), _node_to_tree (reverse lookups), and _message_log (audit trails). If the file does not exist, the store initializes empty structures and waits for the first mutating operation.

Thread-Safe Mutations and Debounced Writes

All write operations acquire a reentrant lock to prevent race conditions during concurrent updates. When the API calls save_tree(), record_message_id(), or clear_all(), the following sequence executes:

with self._lock:                                 # L35-L42 (save_tree example)

    self._trees[root_id] = tree_data
    # ... additional mutations ...

    self._schedule_save()

The _schedule_save() method (lines 11-31) implements a debounced write strategy using threading.Timer. Rather than writing to disk on every mutation, the store waits 0.5 seconds (_save_debounce_secs) after the last change before flushing. This coalesces rapid updates—such as streaming message chunks—into a single I/O operation.

When the timer fires, _save_from_timer captures a complete snapshot of the current state and passes it to _write_data, which performs an atomic JSON dump:


# Conceptual flow from _save_from_timer

snapshot = self._snapshot()
self._write_data(snapshot)  # Atomic json.dump operation

This debouncing mechanism is critical for performance, as CLI output often generates dozens of events per second.

Graceful Shutdown Handling

To prevent data loss during unexpected restarts, the FastAPI application registers a lifespan context manager in api/app.py that explicitly flushes pending writes:

if message_handler and hasattr(message_handler, "session_store"):
    message_handler.session_store.flush_pending_save()   # L66-L71

The flush_pending_save() method (lines 48-56) cancels any active debounce timer and forces an immediate synchronous write. This ensures that even if the server receives a SIGTERM seconds after the last mutation, no in-memory state is lost.

Restoring State After Server Restarts

When the server initializes, the same SessionStore instance reconstructs the conversation queues before accepting new requests. In api/app.py (lines 19-34), the startup logic retrieves persisted trees and rehydrates the message handler:

saved_trees = session_store.get_all_trees()                # L20-L22

if saved_trees:
    message_handler.replace_tree_queue(
        TreeQueueManager.from_dict(
            {"trees": saved_trees,
             "node_to_tree": session_store.get_node_mapping()},
            # ... additional parameters

        )
    )

Cleaning Up Stale Nodes

During restoration, the system identifies nodes that were in PENDING or IN_PROGRESS states when the previous process died. These cleanup_stale_nodes() operations discard incomplete operations and write the corrected state back to the JSON file. This guarantees that the restored session represents a consistent snapshot, free from phantom partial operations.

Atomicity and Durability Guarantees

The persistence mechanism relies on three architectural qualities to ensure reliability:

  • Atomic snapshots: The _write_data method uses a single json.dump call, ensuring the file contains either the complete previous state or the complete new state—never a partial write.
  • Debounced consistency: The 500ms debounce window prevents file corruption that could occur from overlapping concurrent writes.
  • Explicit checkpointing: The lifespan shutdown hook acts as a forced checkpoint, eliminating the window of data loss that exists between the last debounced write and process termination.

Practical Implementation Examples

Initializing the Session Store

from messaging.session import SessionStore
import os

data_dir = os.path.abspath("./workspace")
os.makedirs(data_dir, exist_ok=True)

store = SessionStore(storage_path=os.path.join(data_dir, "sessions.json"))

Persisting a Conversation Tree

tree_id = "root_123"
tree_data = {
    "root_id": tree_id,
    "nodes": {"node_a": {"content": "System initialization complete"}},
    "metadata": {"timestamp": "2024-01-15T10:30:00Z"}
}
store.save_tree(tree_id, tree_data)   # Triggers debounced save automatically

Recording Message Associations

store.record_message_id(
    platform="slack",
    chat_id="C01ABCXYZ",
    message_id="msg_456",
    direction="outgoing",
    kind="assistant"
)

Forcing Immediate Persistence


# Typically called automatically during shutdown, but available for manual use

store.flush_pending_save()

Summary

  • JSON-based storage: All session state resides in a single file (default: claude_workspace/sessions.json) managed by the SessionStore class in messaging/session.py.
  • Debounced writes: Mutations are coalesced using a 500ms threading.Timer to reduce I/O overhead while maintaining eventual consistency.
  • Graceful shutdown: The FastAPI lifespan hook in api/app.py calls flush_pending_save() to ensure zero data loss on restart.
  • Automatic recovery: On startup, the store reloads conversation trees and cleans stale nodes, presenting a consistent state to the message handler.
  • Thread safety: All mutations are guarded by threading.RLock(), making the store safe for concurrent API access.

Frequently Asked Questions

How does free-claude-code prevent data loss if the server crashes unexpectedly?

The SessionStore uses debounced writes that flush to disk every 500ms after the last mutation. While a crash could lose up to half a second of updates, the atomic JSON write ensures the file never contains corrupted partial data. Additionally, the FastAPI lifespan context attempts to flush pending saves on SIGTERM, covering most graceful restart scenarios.

Where is the session data physically stored?

Session data is stored in a JSON file specified by the storage_path parameter of SessionStore, typically located at settings.claude_workspace/sessions.json relative to the application root. This path is configurable during store initialization in api/app.py.

Is the session store safe for concurrent requests?

Yes. The SessionStore wraps all mutating operations with self._lock (a threading.RLock), ensuring thread-safe access across multiple FastAPI workers or async handlers. The debounced save mechanism further prevents race conditions during disk writes by serializing access through the timer callback.

What happens to in-progress Claude CLI operations when the server restarts?

Nodes in PENDING or IN_PROGRESS states are identified during the startup restoration process in api/app.py and passed to cleanup_stale_nodes(). These incomplete operations are purged from the tree structure, and the cleaned state is written back to the JSON file, ensuring the system resumes from a consistent state.

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 →