# How ChatSession and ChatMessage Models Store Conversation History and Relate to Notebooks in Open Notebook

> Discover how ChatSession and ChatMessage models store conversation history in Open Notebook. Learn about SurrealDB, LangGraph SQLite, and notebook relations.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: internals
- Published: 2026-07-01

---

**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`](https://github.com/lfnovo/open-notebook/blob/main/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 identifier
- **`model_override`** – Per-session LLM configuration that overrides global defaults
- **Automatic timestamps** – `created` and `updated` fields managed by the base `ObjectModel` class

### Creating Notebook Relationships

The model provides explicit methods to establish graph relationships:

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py), the handler executes:

```python
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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py).

### The Message Flow

When users exchange messages, the chat graph defined in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py) manages persistence through the `call_model_with_messages` node:

1. Retrieves current thread state containing a `messages` list
2. Appends the user's input as a `HumanMessage`
3. Sends accumulated context to the LLM
4. Appends the model's response as an `AIMessage`
5. 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`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py) (and duplicated in [`api/routers/source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/source_chat.py)) is a **Pydantic schema**, not a database entity:

```python
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:

1. **Load metadata** – Fetch the `ChatSession` record from SurrealDB to obtain configuration and relationships
2. **Extract messages** – Call `graph.get_state(thread_id=session_id)` to access the LangGraph checkpoint
3. **Map to DTOs** – Iterate over `thread_state.values["messages"]` and instantiate `ChatMessage` objects

Because SQLite checkpoint access is synchronous, the codebase wraps these calls in `asyncio.to_thread`:

```python

# 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`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/graph_utils.py) provides an efficient helper:

```python
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

- **`ChatSession`** persists in SurrealDB with metadata and `refers_to` edges linking to notebooks or sources, but contains no message content.
- **Actual messages** live in LangGraph's SQLite checkpoint file, referenced by `session_id` as the thread identifier.
- **`ChatMessage`** is a transient Pydantic model used exclusively for API serialization, constructed from LangGraph state at request time.
- **Relationship creation** occurs through `relate_to_notebook()` in [`open_notebook/domain/notebook.py`](https://github.com/lfnovo/open-notebook/blob/main/open_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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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.