# How the Open Notebook Chat Graph Maintains Message History and Notebook Context Across Sessions

> Learn how the Open Notebook chat system uses LangGraph and SQLite to maintain message history and notebook context across sessions, storing data by session ID.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: deep-dive
- Published: 2026-06-19

---

**The Open Notebook chat system uses LangGraph with a SQLite checkpoint (`SqliteSaver`) to persist thread-isolated state, storing messages and notebook references keyed by the session's SurrealDB record ID.**

The `lfnovo/open-notebook` repository implements a sophisticated conversational AI layer that preserves full dialogue history and notebook context across disconnected HTTP requests. By leveraging LangGraph's stateful graph architecture and SQLite-based persistence, the system ensures that every chat session maintains continuity without requiring in-memory state management.

## Thread-Isolated State Architecture

The foundation of cross-session persistence lies in the **`ThreadState`** schema defined in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py). This TypedDict structure encapsulates everything required to reconstruct a conversation's context:

```python
class ThreadState(TypedDict):
    messages: Annotated[list, add_messages]   # Stored chat history

    notebook: Optional[Notebook]              # Linked notebook object

    context: Optional[str]                    # Optional raw context string

    context_config: Optional[dict]            # Context-generation configuration

    model_override: Optional[str]             # Per-session model selection

```

Each chat session receives a unique **thread ID** derived from the SurrealDB record ID of the `chat_session` object (e.g., `chat_session:abc123`). This identifier serves as the primary key for state retrieval from the checkpoint store.

## SQLite Checkpoint Persistence

The chat graph is compiled with a **`SqliteSaver`** backend that writes state to a local SQLite file specified by `LANGGRAPH_CHECKPOINT_FILE`. When the API endpoint `/chat/execute` in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py) receives a request, it reconstructs the conversation state by querying this checkpoint:

```python

# Retrieve persisted state for this specific thread

current_state = await asyncio.to_thread(
    chat_graph.get_state,
    config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
state_values = current_state.values if current_state else {}
state_values["messages"] = state_values.get("messages", [])
state_values["notebook"] = notebook
state_values["context"] = request.context

```

This mechanism ensures that even if the server restarts between requests, the **full message history** (as LangChain `HumanMessage` and `AIMessage` objects) remains available in the SQLite database.

## Notebook Context Injection

Beyond message history, the system persists **notebook references** across sessions. When a chat session is created, the notebook relationship is recorded in SurrealDB via `session.relate_to_notebook`. During each `/chat/execute` call, the API loads the notebook object and injects it into the state:

```python
state_values["notebook"] = notebook

```

Because the entire `ThreadState` dictionary is checkpointed after each graph invocation, the notebook reference survives indefinitely. The system prompt template (`[Chat/system]`) can embed this notebook metadata, allowing the LLM to reference specific research materials throughout the conversation.

## Message Processing and State Updates

The graph consists of a single node named `"agent"` that executes **`call_model_with_messages`**. This function receives the current `ThreadState`, constructs a system prompt with notebook context, and invokes the LLM:

```python
ai_message = model.invoke(payload)  # LLM generates response

cleaned_message = ai_message.model_copy(
    update={"content": cleaned_content}
)
return {"messages": cleaned_message}  # State mutation

```

When `graph.invoke` completes, LangGraph automatically writes the updated state—including the new `AIMessage`—back to the SQLite checkpoint. Subsequent requests for the same `thread_id` retrieve this augmented message list, creating a seamless conversational experience.

## Querying History Without Mutation

The utility function in [`open_notebook/utils/graph_utils.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/graph_utils.py) enables read-only inspection of message counts without modifying state:

```python
thread_state = await asyncio.to_thread(
    graph.get_state,
    config=RunnableConfig(configurable={"thread_id": session_id}),
)
if thread_state and thread_state.values and "messages" in thread_state.values:
    return len(thread_state.values["messages"])

```

This helper supports the `/chat/sessions` endpoint, which reports `message_count` for each session by reading directly from the checkpoint store.

## Summary

- **LangGraph with SQLite checkpointing** (`SqliteSaver`) provides durable persistence for chat state across server restarts and HTTP requests.
- **`ThreadState`** in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py) encapsulates messages, notebook references, and configuration in a thread-isolated structure.
- **Thread IDs** derived from SurrealDB `chat_session` record IDs ensure each conversation maintains separate state.
- **Automatic checkpointing** occurs after every graph invocation, persisting new AI messages and maintaining full conversation history.
- **Notebook context** survives across sessions because the notebook object is stored within the checkpointed state and reinjected on each request.

## Frequently Asked Questions

### How does the chat graph handle server restarts without losing conversation history?

The system uses a **SQLite checkpoint file** (`LANGGRAPH_CHECKPOINT_FILE`) managed by LangGraph's `SqliteSaver`. Since state persists to disk rather than memory, restarting the server preserves all thread states. When the `/chat/execute` endpoint receives a request, it loads the existing state from this SQLite database using the session's `thread_id`, ensuring conversations resume exactly where they left off.

### Can multiple chat sessions access the same notebook simultaneously?

Yes. Each chat session maintains its own **thread-isolated state** with a unique `thread_id`, but multiple sessions can reference the same notebook object. The notebook is injected into each session's `ThreadState` independently, allowing different conversations to explore the same research materials while maintaining separate message histories and context configurations.

### What happens if a chat session references a notebook that gets deleted?

The checkpoint store maintains a snapshot of the notebook object within the `ThreadState` at the time of each graph invocation. However, the system typically loads the notebook fresh from SurrealDB on each request in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py). If the notebook is deleted, the next request would fail to load the object and likely raise an error during state preparation, though previously checkpointed states would retain the last known notebook reference.

### How does the system count messages without loading the entire conversation?

The `get_message_count` utility in [`open_notebook/utils/graph_utils.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/graph_utils.py) uses **`graph.get_state`** to retrieve only the state metadata for a specific `thread_id`, then checks the length of the `messages` list within `state_values`. This operation reads from the SQLite checkpoint without invoking the full graph or mutating state, providing an efficient way to report message counts for the `/chat/sessions` listing endpoint.