How SqliteSaver Checkpoints and Persists Chat Message History in Open-Notebook
Open-Notebook uses LangGraph's SqliteSaver to automatically serialize chat message history to a SQLite database file, enabling persistent conversation threads across API requests without manual database management.
Open-Notebook leverages LangGraph's checkpointing system to maintain persistent chat histories across user sessions. The implementation utilizes SqliteSaver from the langgraph.checkpoint.sqlite module to store thread states—including the complete message list—in a local SQLite database. This approach ensures that every state transition in the chat workflow is automatically persisted and can be retrieved using a unique thread identifier.
Configuration and Database Location
The SQLite checkpoint file path is defined centrally in the configuration module. This ensures all graph instances reference the same persistent storage location.
In open_notebook/config.py, the checkpoint file is configured to live within the data folder:
# open_notebook/config.py
LANGGRAPH_CHECKPOINT_FILE = f"{DATA_FOLDER}/sqlite-db/checkpoints.sqlite"
The application creates this directory at startup using os.makedirs, guaranteeing that ./data/sqlite-db/checkpoints.sqlite exists before any graph operations begin. This single SQLite file holds all thread states for the entire application.
Instantiating the SqliteSaver
Each chat graph that requires persistence constructs a SqliteSaver instance using a shared SQLite connection. The connection is opened with check_same_thread=False to allow compatibility with LangGraph's internal threading model.
In open_notebook/graphs/chat.py, the saver is instantiated as follows:
# open_notebook/graphs/chat.py
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
from open_notebook.config import LANGGRAPH_CHECKPOINT_FILE
conn = sqlite3.connect(LANGGRAPH_CHECKPOINT_FILE, check_same_thread=False)
memory = SqliteSaver(conn) # ← checkpointer instance
The same pattern appears in open_notebook/graphs/source_chat.py, ensuring consistent persistence behavior across different chat workflows. This connection object becomes the bridge between the graph's state and the SQLite database.
Compiling the Graph with Checkpointing
Persistence is enabled by passing the SqliteSaver instance to the graph's compile method. This wires the graph so that every state transition triggers an automatic database write.
From open_notebook/graphs/chat.py:
# open_notebook/graphs/chat.py
from langgraph.graph import StateGraph, START, END
agent_state = StateGraph(ThreadState)
# ... nodes and edges defined here ...
graph = agent_state.compile(checkpointer=memory) # ← persistence enabled
Once compiled with the checkpointer, the graph automatically calls SqliteSaver.save_state (internal to LangGraph) after each node execution. This serializes the entire state object—including the messages list—into the SQLite database under the corresponding thread identifier.
Automatic State Serialization
When a chat node processes a message, it returns a dictionary containing the updated state. LangGraph's runtime merges this into the current state and immediately persists it to SQLite.
Inside the chat node (call_model_with_messages), updating the message history looks like this:
# Inside open_notebook/graphs/chat.py node function
state_values["messages"].append(HumanMessage(content=request.message))
# Returning the updated dict triggers the checkpoint save
return {"messages": state_values["messages"]}
The SqliteSaver captures the complete ThreadState, ensuring that the full conversation history—including all previous Human and AI messages—is preserved after each interaction.
Retrieving Persisted Chat History
API endpoints retrieve saved history by calling graph.get_state. Because SqliteSaver provides only a synchronous get_state method, Open-Notebook wraps these calls using asyncio.to_thread to prevent blocking the FastAPI event loop.
In api/routers/chat.py, the retrieval pattern is:
# api/routers/chat.py
import asyncio
from langchain_core.runnables import RunnableConfig
thread_state = await asyncio.to_thread(
chat_graph.get_state,
config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
messages = thread_state.values["messages"] # Complete chat history
A similar utility pattern appears in open_notebook/utils/graph_utils.py for counting stored messages:
# open_notebook/utils/graph_utils.py
thread_state = await asyncio.to_thread(
graph.get_state,
config=RunnableConfig(configurable={"thread_id": session_id}),
)
message_count = len(thread_state.values["messages"])
Thread Isolation and Session Management
The key to multi-session persistence is the thread_id parameter passed within the RunnableConfig. This identifier scopes all checkpoints to specific chat sessions, allowing multiple concurrent conversations to maintain independent histories within the same SQLite file.
When a new chat session starts, a unique thread_id is generated. All subsequent calls to get_state or graph invocations using this ID access the exact same checkpointed state, effectively restoring the conversation context from the database.
Summary
Open-Notebook implements persistent chat history through LangGraph's checkpointing mechanism:
- Configuration: The SQLite file path is defined in
open_notebook/config.pyasLANGGRAPH_CHECKPOINT_FILE - Instantiation:
SqliteSaveris created with asqlite3connection inopen_notebook/graphs/chat.pyandsource_chat.py - Compilation: The graph is compiled with
checkpointer=memoryto enable automatic state serialization - Persistence: Each node return triggers
SqliteSaver.save_state, storing the complete message list under the thread ID - Retrieval: API endpoints use
asyncio.to_threadwithgraph.get_stateto fetch history asynchronously - Isolation: The
thread_idparameter ensures separate conversation histories coexist in the same database file
Because the saver operates at the graph layer, no additional manual database code is required to maintain chat history.
Frequently Asked Questions
Where is the chat history physically stored in Open-Notebook?
The chat history is stored in a SQLite database file located at ./data/sqlite-db/checkpoints.sqlite relative to the application root. This path is defined in open_notebook/config.py and created automatically at startup. All thread states and message histories persist to this single file.
How does Open-Notebook handle the synchronous SqliteSaver in an async FastAPI application?
Open-Notebook uses asyncio.to_thread to execute the synchronous get_state method of SqliteSaver in a separate thread pool. This pattern appears in api/routers/chat.py and open_notebook/utils/graph_utils.py, preventing the SQLite operations from blocking the main event loop while maintaining async compatibility.
What triggers the actual database write when saving chat messages?
The database write is triggered automatically by LangGraph's runtime when a node returns an updated state dictionary. In open_notebook/graphs/chat.py, when the call_model_with_messages node returns {"messages": updated_list}, the compiled graph (which was initialized with checkpointer=memory) automatically invokes the internal save_state method to serialize the thread state to SQLite.
Can multiple chat sessions use the same SqliteSaver instance?
Yes, multiple chat sessions share the same SqliteSaver instance and SQLite file. Session isolation is maintained through the thread_id parameter in the RunnableConfig. Each unique thread_id scopes its own checkpoint history within the database, allowing concurrent sessions to operate independently without data collision.
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 →