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:
- Retrieval: Fetches existing context via
get_session_context() - Append: Adds a new dictionary containing
role,content, and timestamp fields - Truncation: Enforces the
MAX_CONTEXT_LENGTHconstant (set to 50 messages) to prevent token overflow - 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 thecontextcolumn and updates theupdated_attimestamp. 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, returningNoneif 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:
- Loads stored context via
ChatService.get_session_context() - Passes the deserialized message list to
ContextBuilder - 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
contextcolumn of thechatstable as JSON arrays, ensuring durability across application restarts. - Service-Repository Pattern:
ChatServicehandles business logic whileChatManagerimplements theIChatRepositoryinterface 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:
ContextBuildertransforms 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →