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:parentId pointers to the root, then reversing:
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.historyare the three primitives. Hydration loads the active path, actions modify the tree, andchat.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
hydrateMessages— backend-controlled message history- Actions — custom actions with
actionSchemaandonAction chat.history— imperative history mutations- Database persistence — basic persistence pattern (linear)

