Session Persistence Across User and Project Directories: Implementation Strategy in Claude Code Telegram

The claude-code-telegram bot implements session persistence using a layered architecture that combines an in-memory cache with SQLite storage, binding each session to a unique combination of user_id and project_path to maintain isolated, durable conversations across bot restarts.

The RichardAtCT/claude-code-telegram repository provides a Telegram bot interface for Claude Code, requiring robust session management to maintain conversation context. The implementation strategy for session persistence across user and project directories relies on a composite key approach that isolates sessions by both Telegram user ID and filesystem project path, ensuring data survives process restarts while preventing cross-project contamination.

Three-Layer Persistence Architecture

The system coordinates three distinct layers to achieve reliable session persistence. At the top, SessionManager handles orchestration and business logic. The middle layer uses the SessionStorage abstract class, implemented by either InMemorySessionStorage for development or the SQLite-backed Storage façade for production. At the persistence layer, SessionRepository performs CRUD operations against the database.

This architecture enables the bot to check the in-memory cache first for performance, fall back to the database for survival across restarts, and create temporary sessions when no valid state exists.

Session Creation and Retrieval Logic

The entry point for session persistence is SessionManager.get_or_create_session() in src/claude/session.py (lines 74-99). This method implements a cascading lookup strategy:

  1. In-memory check: Verifies if the session_id exists in the active_sessions cache and is not expired (lines 88-95).
  2. Database fallback: If not found in memory, attempts storage.load_session(session_id) to retrieve from SQLite (lines 96-101).
  3. Temporary creation: If no valid session exists, the system enforces per-user session limits using max_sessions_per_user from settings, then creates a temporary session with a temp_<uuid> identifier (lines 155-165).

The temporary session is immediately persisted via await self.storage.save_session(new_session) (line 128) before any external API calls, ensuring no session state is lost if the bot crashes during the Claude request.

Storage Abstraction and Database Integration

The storage layer abstracts the underlying persistence mechanism through two implementations defined in src/claude/session.py (lines 29-63). The InMemorySessionStorage class provides a dictionary-based store suitable for testing, while the production environment uses the Storage façade defined in src/storage/facade.py (lines 34-42).

When initialized in src/main.py, the Storage class opens a SQLite connection using settings.DATABASE_URL. The SessionRepository in src/storage/repositories.py (lines 121-155) handles the actual SQL operations, writing rows that include user_id, project_path, created_at, last_used, and usage metrics.

Session Lifecycle and ID Management

Sessions transition through distinct states during their lifecycle. When Claude returns a response containing a real session_id, SessionManager.update_session() (lines 46-62) performs an atomic swap: it deletes the temporary row from the database, replaces the temporary ID with the real one in the session object, and persists the updated record.

Expiration management occurs through cleanup_expired_sessions (lines 93-104), which queries all sessions via storage.get_all_sessions() and removes entries exceeding settings.session_timeout_hours. The repository supports per-project queries through get_sessions_by_project (lines 216-228), allowing the system to filter sessions by project_path independently of user_id.

SQLite Schema and Data Model

The database schema, initialized by DatabaseManager.initialize(), stores session data in a table mapped by SessionModel in src/storage/models.py. The composite key strategy relies on filtering by both user_id (INTEGER) and project_path (TEXT), enabling a single user to maintain separate conversation contexts for different project directories.

Key columns include:

  • session_id (TEXT PRIMARY KEY): Either the temporary UUID or Claude-provided identifier.
  • user_id (INTEGER): Telegram user identifier.
  • project_path (TEXT): Absolute filesystem path to the project directory.
  • created_at and last_used (TIMESTAMP): Used for expiration calculations.
  • total_cost, total_turns, message_count: Aggregated usage metrics.
  • is_active (BOOLEAN): Soft-delete flag for expired sessions.

Practical Implementation Example

The following pattern demonstrates how bot handlers interact with the session persistence layer:

from pathlib import Path
from src.claude.session import SessionManager
from src.storage.facade import Storage
from src.config.settings import Settings

# Initialization (typically in src/main.py)

settings = Settings()
storage = Storage(settings.DATABASE_URL)
session_manager = SessionManager(settings, storage)

async def handle_message(
    user_id: int, 
    project_path: Path, 
    incoming_session_id: str | None
):
    # Retrieve existing or create temporary session bound to user + project

    session = await session_manager.get_or_create_session(
        user_id=user_id,
        project_path=project_path,
        session_id=incoming_session_id,
    )
    
    # Process Claude request...

    # response = await claude_api.call(session, message)

    
    # Update with real session ID and persist metrics

    # await session_manager.update_session(session.session_id, response)

    return session.session_id

Summary

  • Composite key isolation: Sessions are uniquely identified by the combination of user_id and project_path, preventing cross-project conversation leakage.
  • Temporary session safety: The system creates temp_<uuid> sessions immediately upon request, ensuring persistence exists before external API calls complete.
  • Layered caching: An in-memory active_sessions dictionary provides fast access, while SQLite guarantees durability across process restarts.
  • Automatic expiration: Background cleanup removes stale sessions based on configurable session_timeout_hours.
  • Repository pattern: SessionRepository abstracts SQL operations, supporting both per-user and per-project query patterns.

Frequently Asked Questions

How does the system prevent session collisions between different projects?

The implementation uses project_path as a distinct dimension in the SessionRepository queries. The get_sessions_by_project method (lines 216-228 in src/storage/repositories.py) filters sessions by the absolute filesystem path, while user_id filters by Telegram identity. This composite approach ensures that user 123 working on /projects/app-a maintains a completely separate session from user 123 working on /projects/app-b, stored as distinct rows in the SQLite database.

What happens to active sessions when the bot restarts?

When the bot initializes in src/main.py, it creates a new Storage instance that connects to the existing SQLite file specified by settings.DATABASE_URL. Since all session data is written to disk immediately upon creation (via save_session in SessionRepository), previous sessions survive the restart. The SessionManager reloads sessions into the in-memory active_sessions map on-demand when get_or_create_session() is called and the database fallback succeeds.

How are expired sessions cleaned up?

The cleanup_expired_sessions method scans the database using storage.get_all_sessions(), compares each record's last_used timestamp against settings.session_timeout_hours, and removes expired entries. This prevents database bloat and enforces resource limits. The cleanup runs periodically and uses the is_active boolean flag to implement soft deletion before hard removal from the sessions table.

Why does the system use temporary session IDs before receiving Claude's response?

The temporary temp_<uuid> pattern solves a race condition where the bot must persist session metadata (user_id, project_path, timestamps) before knowing Claude's official session identifier. By creating a temporary row immediately in get_or_create_session(), the system guarantees that usage metrics and context survive even if the Claude API request fails or the bot crashes. Once Claude returns the real session_id, update_session() performs an atomic swap, replacing the temporary record while preserving the session continuity.

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 →