How Lifetrace Builds and Persists Conversation Context for AI Interactions

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, 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:

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 implements the IChatRepository interface defined in 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 (lines 310–323), defining the Chat SQLModel table:

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 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:


# 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. 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 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.

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 →