How the Open Notebook Chat Graph Maintains Conversation History and Context
Open Notebook persists multi-turn conversations using a LangGraph state machine with SQLite checkpointing, where the ThreadState TypedDict stores message history annotated with add_messages to automatically append new interactions while preserving context across sessions.
The chat functionality in Open Notebook relies on a sophisticated state management system built on LangGraph. By combining an in-memory state representation with persistent SQLite storage, the application ensures that conversation history and notebook context remain available across multiple turns and process restarts, as implemented in the lfnovo/open-notebook repository.
ThreadState: The Schema for Conversation Memory
The foundation of conversation persistence lies in the ThreadState TypedDict defined in open_notebook/graphs/chat.py. This state declaration determines which data travels with every graph execution.
The messages field uses LangGraph's add_messages annotation to automatically handle conversation history:
class ThreadState(TypedDict):
messages: Annotated[list, add_messages] # ← the full message history
notebook: Optional[Notebook] # optional notebook context
context: Optional[str] # optional text-based context
context_config: Optional[dict] # configuration for building context
model_override: Optional[str] # per-request model selection
The add_messages reducer ensures that new AI and user messages append to the existing list rather than replacing it. This creates the in-memory representation of the conversation history that persists throughout the graph's execution cycle.
Persistent Storage with SQLite Checkpointing
While ThreadState maintains conversation data in memory during execution, Open Notebook ensures durability through SQLite-backed checkpointing. The graph compiles with a SqliteSaver instance that automatically persists state to disk.
In open_notebook/graphs/chat.py, the checkpointer initializes as follows:
conn = sqlite3.connect(
LANGGRAPH_CHECKPOINT_FILE,
check_same_thread=False,
)
memory = SqliteSaver(conn)
agent_state = StateGraph(ThreadState)
agent_state.add_node("agent", call_model_with_messages)
agent_state.add_edge(START, "agent")
agent_state.add_edge("agent", END)
graph = agent_state.compile(checkpointer=memory)
The LANGGRAPH_CHECKPOINT_FILE constant, defined in open_notebook/config.py, points to data/sqlite-db/checkpoints.sqlite. Each time the graph runs, LangGraph stores the entire ThreadState in this database. When a new request arrives, the graph restores the latest checkpoint, giving the model access to the full prior messages list and any persisted context or notebook data. This mechanism survives process restarts and enables true multi-turn dialogues.
Message Processing and State Updates
The call_model_with_messages function in open_notebook/graphs/chat.py handles the actual interaction with the language model while maintaining state continuity. This node builds a system prompt from the current state, prepends it to the stored messages, executes the LLM, and returns a structured update that merges back into the checkpoint.
When utilizing the chat service API, you interact with this graph through the execute_chat method:
import asyncio
from api.chat_service import chat_service
async def chat_turn(session_id: str, user_msg: str):
response = await chat_service.execute_chat(
session_id=session_id,
message=user_msg,
context={}, # empty if no extra context
model_override=None,
)
print("Assistant:", response["message"])
# Example usage
asyncio.run(chat_turn("abc123", "What did I write in the last note?"))
The internal implementation constructs the payload as follows:
system_prompt = Prompter(prompt_template="chat/system").render(data=state)
payload = [SystemMessage(content=system_prompt)] + state.get("messages", [])
# ...
ai_message = model.invoke(payload)
# ...
cleaned_message = ai_message.model_copy(update={"content": cleaned_content})
return {"messages": cleaned_message}
The returned dictionary updates the messages field in the state. Because the graph uses the checkpointer, this new AI reply becomes part of the persistent conversation history for the next turn.
Injecting Notebook Context into Conversations
Beyond raw message history, Open Notebook supports rich context injection through optional state fields. The context and context_config fields in ThreadState store notebook-specific information that influences the model's responses.
The API endpoint /api/chat/context (implemented in api/chat_service.py) populates these fields via the ContextBuilder utility from open_notebook/utils/context_builder.py. When executing a chat turn with context, the system passes this data through the execute_chat method:
# Build context for notebook "nb-01"
context = await chat_service.build_context(
notebook_id="nb-01",
context_config={"include_sources": True, "include_insights": True},
)
# Then start a chat session using that context
await chat_service.execute_chat(
session_id="sess-42",
message="Summarize the key insights from this notebook.",
context=context,
)
The resulting context dict stores in ThreadState.context, and the system prompt automatically includes this information on subsequent turns. This allows the model to reference specific notebook content while maintaining awareness of the ongoing conversation history.
Summary
- ThreadState defines the conversation schema using a
TypedDictwithadd_messagesannotation to preserve message history across graph executions. - SQLite checkpointing via
SqliteSaverpersists the full state todata/sqlite-db/checkpoints.sqlite, enabling conversation recovery across process restarts. - State updates occur through the
call_model_with_messagesnode, which returns new messages that merge into the checkpoint for the next turn. - Context injection supports notebook-specific data through
contextandcontext_configfields, populated viaContextBuilderandexecute_chat.
Frequently Asked Questions
How does Open Notebook store chat history persistently?
Open Notebook uses LangGraph's SqliteSaver to store the entire ThreadState in a SQLite database located at data/sqlite-db/checkpoints.sqlite. Each conversation turn creates a checkpoint that survives process restarts, allowing the graph to restore the complete message history and context when a session resumes.
What is the role of ThreadState in the chat graph?
ThreadState is a TypedDict defined in open_notebook/graphs/chat.py that declares the fields traveling with every graph execution. It includes the messages list (annotated with add_messages to handle appends), optional notebook context, and configuration fields. This state object serves as the single source of truth for the conversation's current condition.
How does the chat graph handle notebook-specific context?
The graph stores notebook context in the context and context_config fields of ThreadState. The ContextBuilder utility generates this payload based on notebook content, and the call_model_with_messages function incorporates it into the system prompt. This allows the model to reference specific notes while maintaining conversation continuity.
Can the chat graph survive process restarts?
Yes. Because the graph compiles with a SqliteSaver checkpointer pointing to a persistent SQLite file, the entire conversation state writes to disk after each turn. When the application restarts, the graph restores the latest checkpoint from data/sqlite-db/checkpoints.sqlite, recovering the full message history and any associated context.
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 →