# How Lifetrace Builds and Persists Conversation Context for AI Interactions

> Discover how Lifetrace builds and persists AI conversation context using JSON in SQLite. Learn about its layered architecture for reliable context survival across restarts and distributed calls.

- Repository: [FreeU-group/lifetrace](https://github.com/freeu-group/lifetrace)
- Tags: architecture
- Published: 2026-03-02

---

**Lifetrace stores conversation history as JSON arrays in a SQLite database, using a layered service-repository architecture to ensure AI context survives process restarts and remains available across distributed API calls.**

Lifetrace implements a durable persistence layer for managing stateful AI interactions in the `freeu-group/lifetrace` repository. The system treats every chat session as a persistent entity, separating business logic from data access to guarantee that conversation context is both queryable and efficiently trimmable for large language model (LLM) consumption.

## Service Layer: ChatService Orchestration

Located at **[`lifetrace/services/chat_service.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/services/chat_service.py)**, the `ChatService` class manages the entire lifecycle of conversation context, from session creation to message truncation.

### Session Initialization and Context Creation

When initiating a new AI interaction, `create_new_session()` generates a UUID (or accepts a supplied identifier) and ensures database existence via `ensure_chat_exists()`. The service initializes the conversation context as an empty JSON array:

```python
self.repository.update_chat_context(session_id, json.dumps([]))

```

This guarantees that every session begins with a valid, empty context structure ready for incremental updates.

### Appending and Trimming Messages

The `add_to_session_context()` method implements the core persistence logic. It performs four distinct operations atomically:

1. **Retrieval**: Fetches existing context via `get_session_context()`
2. **Append**: Adds a new dictionary containing `role`, `content`, and timestamp fields
3. **Truncation**: Enforces the `MAX_CONTEXT_LENGTH` constant (set to **50 messages**) to prevent token overflow
4. **Persistence**: Writes the updated JSON string back via `self.repository.update_chat_context()`

The `get_session_context()` method handles deserialization, returning an empty list if the stored data is corrupted or missing, ensuring graceful degradation during AI interactions.

## Data Access Layer: ChatManager and Schema

The repository pattern separates storage concerns from business logic, allowing the conversation context to survive application restarts and remain accessible across multiple processes.

### Repository Implementation

**[`lifetrace/storage/chat_manager.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/storage/chat_manager.py)** implements the `IChatRepository` interface defined in **[`lifetrace/repositories/interfaces.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/repositories/interfaces.py)**. This contract enables dependency injection and testability while enforcing consistent data access patterns.

Key methods include:

- **`update_chat_context(session_id, context)`**: Writes the JSON string to the `context` column and updates the `updated_at` timestamp. If the session row does not exist, the manager auto-creates it with the supplied context, implementing an upsert pattern.
- **`get_chat_context(session_id)`**: Retrieves the raw JSON string from the database, returning `None` if no record exists.

### Database Schema Design

The persistence model resides in **[`lifetrace/storage/models.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/storage/models.py)** (lines 310–323), defining the `Chat` SQLModel table:

```python
class Chat(TimestampMixin, table=True):
    __tablename__: ClassVar[str] = "chats"
    id: int | None = Field(default=None, primary_key=True)
    session_id: str = Field(max_length=100, unique=True)  # Conversation identifier

    chat_type: str | None = Field(default=None, max_length=50)
    title: str | None = Field(default=None, max_length=200)
    context_id: int | None = None
    extra_data: str | None = Field(default=None, sa_column=Column(Text))
    context: str | None = Field(default=None, sa_column=Column(Text))   # JSON-encoded history

    last_message_at: datetime | None = None

```

The **`context`** column stores the entire conversation history as a JSON-encoded list, making the data durable, human-readable in the database, and directly parseable by Python's `json` module during AI context reconstruction.

## LLM Prompt Construction: ContextBuilder

Raw persisted JSON requires transformation before ingestion by language models. **[`lifetrace/llm/context_builder.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/llm/context_builder.py)** bridges this gap, separating storage representation from model-ready formatting.

When constructing prompts, the system:

1. Loads stored context via `ChatService.get_session_context()`
2. Passes the deserialized message list to `ContextBuilder`
3. Generates a formatted prompt string optimized for the specific LLM provider

This architectural boundary allows the persistence layer to remain agnostic of prompt engineering strategies while ensuring AI interactions receive properly formatted conversation history.

## End-to-End Context Flow

The complete lifecycle of conversation context in Lifetrace follows this sequence:

```

User Request → ChatService.add_to_session_context()
    ↓ (Read JSON → Append → Trim to 50 messages)
ChatManager.update_chat_context() → SQLite Chat.context column
    ↓
LLM Inference → ChatService.get_session_context() → ContextBuilder.build_prompt()

```

## Practical Implementation Example

The following code demonstrates creating a persistent session, adding messages, and retrieving context for AI consumption:

```python

# 1. Initialize service components

from lifetrace.services.chat_service import ChatService
from lifetrace.storage.chat_manager import ChatManager
from lifetrace.storage.database import Database
from lifetrace.llm.context_builder import ContextBuilder

db = Database()
chat_repo = ChatManager(db)          # IChatRepository implementation

chat_svc = ChatService(chat_repo)

# 2. Create session with empty context

session_id = chat_svc.create_new_session()  # Returns UUID string

print("Session initialized:", session_id)

# 3. Persist conversation turns

chat_svc.add_to_session_context(session_id, "user", "What's my schedule today?")
chat_svc.add_to_session_context(session_id, "assistant", "You have a meeting at 10 AM.")

# 4. Retrieve for AI processing

history = chat_svc.get_session_context(session_id)
print("Persisted context:", history)

# Output: [{'role': 'user', 'content': "...", 'timestamp': '2026-03-02T12:34:56Z'}, ...]

# 5. Build LLM-ready prompt

builder = ContextBuilder()
prompt = builder.build_summary_context(
    query="Summarize the conversation",
    retrieved_data=history,
)

```

## Summary

- **Persistent Storage**: Conversation context lives in the `context` column of the `chats` table as JSON arrays, ensuring durability across application restarts.
- **Service-Repository Pattern**: `ChatService` handles business logic while `ChatManager` implements the `IChatRepository` interface for loose coupling and testability.
- **Automatic Truncation**: The system enforces a hard limit of 50 messages (`MAX_CONTEXT_LENGTH`) to prevent context window overflow during AI interactions.
- **Separation of Concerns**: `ContextBuilder` transforms raw database JSON into model-specific prompts without polluting the persistence layer.
- **Upsert Semantics**: `update_chat_context()` creates session rows automatically if they don't exist, eliminating race conditions during concurrent session initialization.

## Frequently Asked Questions

### How does Lifetrace prevent conversation context from growing indefinitely?

Lifetrace enforces the `MAX_CONTEXT_LENGTH` constant (set to 50 messages) in `ChatService.add_to_session_context()`. Before persisting, the method trims the message list to this limit, ensuring AI context windows remain within manageable token limits while preserving recent interaction history.

### What database backend stores the conversation context?

The system uses **SQLite** via SQLModel, as implemented in [`lifetrace/storage/models.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/storage/models.py). The `context` column is defined as a SQLAlchemy `Text` type, storing JSON strings that survive process restarts and support complex query patterns through standard SQL.

### Can multiple API calls access the same conversation context simultaneously?

Yes. The architecture separates the `ChatService` (business logic) from `ChatManager` (data access), with the repository layer handling concurrent SQLite operations. The upsert pattern in `update_chat_context()` ensures that simultaneous session initializations don't create duplicate rows, while subsequent reads retrieve the latest persisted state for AI interactions.

### How is the stored JSON context transformed for different LLM providers?

The `ContextBuilder` class in [`lifetrace/llm/context_builder.py`](https://github.com/freeu-group/lifetrace/blob/main/lifetrace/llm/context_builder.py) handles provider-specific formatting. It receives the deserialized Python list from `ChatService.get_session_context()` and applies formatting rules (such as message role mapping or token counting) before returning the final prompt string, keeping the database schema agnostic of model-specific requirements.