How ChatSession and ChatMessage Models Store Conversation History and Relate to Notebooks in Open Notebook
Chat sessions in Open Notebook store metadata in SurrealDB via the ChatSession model while persisting actual message content in LangGraph's SQLite checkpoint file, using a refers_to graph edge to link conversations to notebooks.
Open Notebook implements a hybrid storage architecture that separates session metadata from conversational data. Understanding how ChatSession and ChatMessage interact with notebooks requires examining the division between SurrealDB relationship records and LangGraph's persistent state management.
The ChatSession Model: Metadata and Notebook Linkage
The ChatSession class serves as the authoritative record for conversation metadata and ownership. Defined in open_notebook/domain/notebook.py, this model inherits from ObjectModel and maps to the chat_session table in SurrealDB.
Key fields include:
title– Optional user-defined session identifiermodel_override– Per-session LLM configuration that overrides global defaults- Automatic timestamps –
createdandupdatedfields managed by the baseObjectModelclass
Creating Notebook Relationships
The model provides explicit methods to establish graph relationships:
# From open_notebook/domain/notebook.py
async def relate_to_notebook(self, notebook_id):
await self.relate("refers_to", notebook_id)
When a client creates a new chat session via the POST endpoint in api/routers/chat.py, the handler executes:
session = ChatSession(title=title, model_override=model_override)
await session.save()
await session.relate_to_notebook(notebook_id)
This creates a refers_to edge connecting the ChatSession node to the Notebook node, enabling queries that list all conversations belonging to a specific notebook.
Where Conversation History Actually Lives: LangGraph Checkpoints
Contrary to typical CRUD applications, ChatSession does not store individual messages. The actual conversation history persists in a SQLite checkpoint file managed by LangGraph, configured via LANGGRAPH_CHECKPOINT_FILE in open_notebook/config.py.
The Message Flow
When users exchange messages, the chat graph defined in open_notebook/graphs/chat.py manages persistence through the call_model_with_messages node:
- Retrieves current thread state containing a
messageslist - Appends the user's input as a
HumanMessage - Sends accumulated context to the LLM
- Appends the model's response as an
AIMessage - Commits the updated state to the SQLite checkpoint
This architecture decouples the conversation timeline from the database, allowing LangGraph to handle state serialization and history truncation natively.
The ChatMessage Model: API Serialization Layer
The ChatMessage class defined in api/routers/chat.py (and duplicated in api/routers/source_chat.py) is a Pydantic schema, not a database entity:
class ChatMessage(BaseModel):
id: str # Generated by LangGraph thread state
type: str # "human" or "ai"
content: str # Raw message text
timestamp: Optional[str] = None
These objects are constructed on-the-fly when API endpoints request conversation history. The system never persists ChatMessage instances to SurrealDB; they serve purely as Data Transfer Objects (DTOs) for client communication.
Retrieving Conversation History
To reconstruct a conversation timeline, the API implements a two-stage retrieval process visible in the GET endpoint handlers:
- Load metadata – Fetch the
ChatSessionrecord from SurrealDB to obtain configuration and relationships - Extract messages – Call
graph.get_state(thread_id=session_id)to access the LangGraph checkpoint - Map to DTOs – Iterate over
thread_state.values["messages"]and instantiateChatMessageobjects
Because SQLite checkpoint access is synchronous, the codebase wraps these calls in asyncio.to_thread:
# Pattern found in api/routers/chat.py
thread_state = await asyncio.to_thread(graph.get_state, thread_id=session_id)
messages = thread_state.values["messages"]
Counting Messages Without Loading Content
For UI pagination or quota management, open_notebook/utils/graph_utils.py provides an efficient helper:
async def get_session_message_count(graph, session_id: str) -> int:
# Reads thread state from checkpoint returns len(messages)
state = await asyncio.to_thread(graph.get_state, thread_id=session_id)
return len(state.values.get("messages", []))
This avoids deserializing full message content when only the count is required.
Summary
ChatSessionpersists in SurrealDB with metadata andrefers_toedges linking to notebooks or sources, but contains no message content.- Actual messages live in LangGraph's SQLite checkpoint file, referenced by
session_idas the thread identifier. ChatMessageis a transient Pydantic model used exclusively for API serialization, constructed from LangGraph state at request time.- Relationship creation occurs through
relate_to_notebook()inopen_notebook/domain/notebook.py, establishing graph edges during session initialization. - History retrieval combines SurrealDB session lookups with synchronous SQLite checkpoint reads wrapped in
asyncio.to_thread.
Frequently Asked Questions
Where are chat messages physically stored in Open Notebook?
Chat messages are stored in a SQLite checkpoint file specified by the LANGGRAPH_CHECKPOINT_FILE environment variable, not in SurrealDB. The ChatSession record only stores metadata and relationships, while the LangGraph runtime manages message persistence through its checkpointing system in open_notebook/graphs/chat.py.
How do I programmatically link a new chat session to a notebook?
First instantiate and save a ChatSession object, then call await session.relate_to_notebook(notebook_id) to create the refers_to edge. This pattern is implemented in the POST handler of api/routers/chat.py, which creates the edge immediately after persisting the session record.
What is the purpose of the ChatMessage model if it isn't stored in the database?
ChatMessage serves as a Pydantic serialization schema defined in api/routers/chat.py. It provides a standardized JSON structure for API responses, converting LangGraph's internal message objects (HumanMessage/AIMessage) into client-friendly dictionaries with fields for id, type, content, and timestamp.
How does the system count messages in a session without loading the full conversation?
The utility function get_session_message_count in open_notebook/utils/graph_utils.py accesses the LangGraph checkpoint state directly and returns the length of the messages list. This avoids the overhead of deserializing message content when only the count is needed for pagination or display purposes.
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 →