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

> Discover the session persistence strategy in Claude Code Telegram. Learn how a layered architecture with in-memory cache and SQLite ensures durable, isolated conversations bound to user ID and project path.

- Repository: [Richard A/claude-code-telegram](https://github.com/richardatct/claude-code-telegram)
- Tags: internals
- Published: 2026-02-19

---

**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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/storage/facade.py) (lines 34-42).

When initialized in [`src/main.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/src/main.py), the `Storage` class opens a SQLite connection using `settings.DATABASE_URL`. The `SessionRepository` in [`src/storage/repositories.py`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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:

```python
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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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.