How Open Notebook Handles Chat Session Management and Message History Persistence
Open Notebook implements a dual-storage architecture where chat session metadata lives in SurrealDB while the actual message history persists in a SQLite-backed LangGraph checkpoint system.
Open Notebook is an open-source knowledge management platform that separates conversation metadata from message content to optimize for both query performance and stateful AI interactions. This article examines how the repository handles chat session management and message history persistence by analyzing the actual source code implementation in lfnovo/open-notebook.
Chat Session Architecture Overview
The architecture cleanly separates session metadata from message content. Session records contain only lightweight metadata such as titles, timestamps, and model overrides, while the chronological message flow lives in LangGraph's checkpointed state graph.
This design provides durable session tracking through SurrealDB while enabling fast, resumable conversation state via SQLite checkpointing.
Session Metadata Storage in SurrealDB
The ChatSession class defined in open_notebook/domain/notebook.py represents the persistent session record:
class ChatSession(ObjectModel):
table_name: ClassVar[str] = "chat_session"
nullable_fields: ClassVar[set[str]] = {"model_override"}
title: Optional[str] = None
model_override: Optional[str] = None
When creating a new session via the POST /chat/sessions endpoint, the system:
- Instantiates a
ChatSessionobject - Persists it to SurrealDB via
await session.save() - Creates a
refers_toedge linking the session to its parent notebook viaawait session.relate_to_notebook(request.notebook_id)
Message History Persistence in LangGraph
Unlike session metadata, the actual conversation history is not stored in SurrealDB. Instead, it resides in the LangGraph ThreadState, defined in open_notebook/graphs/chat.py:
class ThreadState(TypedDict):
messages: Annotated[list, add_messages]
notebook: Optional[Notebook]
context: Optional[str]
context_config: Optional[dict]
model_override: Optional[str]
The graph persists this state using SqliteSaver connected to a dedicated checkpoint file:
conn = sqlite3.connect(LANGGRAPH_CHECKPOINT_FILE, check_same_thread=False)
memory = SqliteSaver(conn)
...
graph = agent_state.compile(checkpointer=memory)
This configuration ensures that conversation state survives process restarts while maintaining ACID properties through SQLite.
Creating and Managing Chat Sessions
New sessions are created through the API layer in api/routers/chat.py. The endpoint handles the dual-write pattern: creating the SurrealDB record while initializing the LangGraph thread state.
The client interaction follows this pattern:
import { chat_service } from '@/lib/api/chat';
await chat_service.create_session(notebookId, {
title: 'Research Q&A',
model_override: 'gpt-4o-mini',
});
Behind the scenes, this generates a unique thread ID that serves as the bridge between the SurrealDB session record and the LangGraph checkpoint.
Retrieving Session History
When fetching a session via GET /chat/sessions/{session_id}, the system reconstructs the full conversation by combining both storage layers:
thread_state = await asyncio.to_thread(
chat_graph.get_state,
config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
The endpoint extracts the messages list from thread_state.values["messages"] and transforms it into the ChatSessionWithMessagesResponse model. This delivers the complete message history alongside the session metadata.
Client-side retrieval follows this pattern:
const session = await chat_service.get_session(sessionId);
// `session.messages` now contains HumanMessage / AIMessage objects
Executing Chat Turns
The /chat/execute endpoint manages the message flow through the LangGraph state machine. The implementation in api/routers/chat.py follows this sequence:
- Load the current LangGraph state for the session using
chat_graph.get_state - Append the new
HumanMessageto the state values - Invoke the graph's
agentnode viachat_graph.invoke - Persist the updated session timestamp to SurrealDB
The server-side handling looks like this:
# 1️⃣ Load current state
current_state = await asyncio.to_thread(
chat_graph.get_state,
config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
# 2️⃣ Prepare state values
state_values = current_state.values if current_state else {}
state_values["messages"] = state_values.get("messages", [])
state_values["messages"].append(HumanMessage(content=request.message))
# 3️⃣ Run the LangGraph node (model call)
result = chat_graph.invoke(
input=state_values,
config=RunnableConfig(
configurable={"thread_id": full_session_id,
"model_id": model_override},
),
)
# 4️⃣ Persist session timestamp
await session.save()
The agent node in open_notebook/graphs/chat.py handles the actual LLM interaction, building the system prompt, sending the full message list to the selected model, cleaning the response, and returning an AIMessage.
Utility Functions and Message Counting
For list views requiring message counts without loading full histories, open_notebook/utils/graph_utils.py provides optimized access to the LangGraph checkpoint data. This utility reads the SQLite checkpoint directly to return message counts efficiently, avoiding the overhead of deserializing full conversation states.
Summary
- Dual-storage architecture: Session metadata lives in SurrealDB tables (
chat_session), while message histories persist in SQLite via LangGraph checkpoints. - LangGraph state management: The
ThreadStateTypedDict defines the conversation structure, withSqliteSaverproviding durable persistence throughLANGGRAPH_CHECKPOINT_FILE. - API bridging: Endpoints in
api/routers/chat.pycoordinate between SurrealDB records and LangGraph state using thread IDs as the linkage key. - Immutable history: Messages are appended to the checkpoint state and never modified, ensuring complete conversation audibility.
- Performance optimization: The
graph_utils.pymodule provides lightweight message counting for list views without loading full state.
Frequently Asked Questions
Where is the chat message history actually stored in Open Notebook?
The message history is stored in a SQLite database file configured via LANGGRAPH_CHECKPOINT_FILE, managed by LangGraph's SqliteSaver class. This is separate from the SurrealDB instance that stores session metadata. The SQLite file contains serialized ThreadState objects that include the messages list.
How does Open Notebook link chat sessions to notebooks?
When creating a session, the system creates a refers_to edge in SurrealDB via the relate_to_notebook method on the ChatSession class. This graph relationship connects the session record to its parent notebook while maintaining the session's independent lifecycle in the checkpoint system.
Can chat sessions survive application restarts?
Yes. Because LangGraph uses SqliteSaver with a persistent SQLite connection, the conversation state survives process restarts. The ChatSession record in SurrealDB maintains the thread ID required to retrieve the checkpointed state when the application resumes.
What happens when a user sends a new message?
The /chat/execute endpoint retrieves the current checkpoint state, appends a HumanMessage to the messages list, invokes the LangGraph agent node to generate a response, and stores the resulting AIMessage back into the checkpoint. The session's updated timestamp in SurrealDB is also refreshed to reflect activity.
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 →