Skip to main content
Most chat UIs treat conversations as linear sequences. But real conversations branch — users edit previous messages, regenerate responses, undo exchanges, and explore alternative paths. This pattern shows how to build a branching conversation system using hydrateMessages, chat.history, and custom actions.

Data model

The standard approach (used by ChatGPT, Open WebUI, LibreChat, and others) stores messages as a tree with parent pointers:
A conversation is a tree of nodes. The active branch is resolved by walking from a leaf node up through parentId pointers to the root, then reversing:
Switching branches means changing which leaf is “active” — the same tree, different path.

Backend setup

Store: tree operations

Define helpers that read and write the node tree. Adapt to your database:

Agent: hydration + actions

Frontend

Sending actions

Wire up edit, undo, and branch switching to the transport:

Branch navigation

To show the < 2/3 > sibling switcher, query the tree for siblings at each fork point. This is a frontend concern — the backend exposes the data, the UI navigates it.
The sibling data (which messages share the same parent) needs to come from your database — query it when loading the chat or include it as client data. The agent only returns the active branch via hydrateMessages.

How it works

Design notes

  • Messages are immutable — edits create siblings, not mutations. This preserves full history for analytics and auditing.
  • The tree lives in your database — the agent loads a linear path from it via hydrateMessages. The agent itself doesn’t know about the tree structure.
  • hydrateMessages + onAction + chat.history are the three primitives. Hydration loads the active path, actions modify the tree, and chat.history.set() syncs the accumulator after tree modifications.
  • Frontend owns navigation — the < 2/3 > UI, sibling queries, and branch switching triggers are client-side concerns. The backend just processes actions and returns responses.

See also