Skip to content

09. Message Tree & Branching

Maruf Bepary edited this page Jun 17, 2026 · 1 revision

Overview

Traditional chat interfaces model a conversation as a linear sequence: message 1, message 2, message 3, and so on. This works until a user wants to rephrase a question, retry a response, or explore an alternative line of reasoning. In a linear model, any of these actions either overwrites history or requires the user to start a new conversation entirely.

This application uses a tree data structure to represent conversations. Every message is a node. Every node has exactly one parent (except the root) and zero or more children. When a user edits a message, the original remains untouched — a new sibling node is created alongside it. When the AI regenerates a response, a new branch grows from the point of divergence.

The design draws directly from version control systems. Just as Git creates branches rather than overwriting commits, this system creates conversation branches rather than overwriting messages. The user may navigate freely between branches, revisiting abandoned lines of thought or comparing alternative responses side by side.

Three properties make this possible:

Property Stored In Purpose
parentId Database (per message) Links each message to its predecessor
childrenIds Application memory only Lists all direct replies to a message
currentLeafId Database (per chat) Identifies which branch the user is currently viewing

Data Model

The tree is encoded using two entities: the chat record and the message node. Their relationship is intentionally minimal at the database level — the richer structure emerges at the application layer.

Message Node

Each message node carries the following fields:

Field Type Source Purpose
id String Database Unique identifier for the node
role Enum Database One of user, assistant, or system
content String Database The message text
parentId String (nullable) Database ID of the parent node; null for root messages
childrenIds String array Computed at runtime IDs of all direct child nodes
metadata JSON string Database Model ID, tool calls, reasoning tokens, selected tools
createdAt Timestamp Database Creation time of the node

Chat Record

Each chat record holds:

Field Type Purpose
id String Unique chat identifier
title String Display name of the conversation
currentLeafId String (nullable) Points to the deepest message in the active branch
messages Hash map All messages in the conversation, keyed by ID

Storage Representation

Messages are stored in a flat hash map — a dictionary keyed by message ID, not a nested object hierarchy. This provides O(1) lookup for any message by ID and integrates cleanly with immutable state management patterns.

The tree topology is encoded entirely through adjacency-list links: each node's parentId points upward, and each node's childrenIds points downward. There is no nesting.

Foreign Key Design

The parentId field carries no database-level foreign key constraint. This is a deliberate choice. Self-referential foreign keys introduce cascade complexity — deleting a subtree would require careful ordering or deferred constraints. Instead, tree integrity is enforced entirely at the application layer, where the full tree is available in memory and traversal logic can guarantee consistency.

The childrenIds field does not exist in the database at all. It is reconstructed during hydration by scanning all messages and building parent-to-children mappings. This avoids denormalisation and the synchronisation burden that comes with it.

erDiagram
    CHAT {
        string id PK
        string title
        string currentLeafId FK
        timestamp createdAt
        timestamp updatedAt
    }

    MESSAGE {
        string id PK
        string chatId FK
        string role
        string content
        string parentId
        string metadata
        timestamp createdAt
    }

    CHAT ||--o{ MESSAGE : contains
    CHAT ||--o| MESSAGE : "currentLeafId points to"
    MESSAGE ||--o| MESSAGE : "parentId references"
Loading

Tree Structure

A conversation tree grows downward from a root node. Each level alternates between user and assistant roles (with an optional system prompt at the root). When a user edits a message or requests regeneration, a new branch sprouts from the point of divergence.

Linear Conversation (No Branches)

The simplest case: a single path from root to leaf, with no edits or regenerations.

graph TD
    M1["🧑 User: What is photosynthesis?"]
    M2["🤖 Assistant: Photosynthesis is the process by which plants..."]
    M3["🧑 User: Tell me more about chlorophyll"]
    M4["🤖 Assistant: Chlorophyll is a green pigment..."]

    M1 --> M2
    M2 --> M3
    M3 --> M4

    style M4 fill:#4ade80,stroke:#16a34a,color:#000
Loading

The green node is the currentLeafId — the tip of the active branch.

Branching Conversation (After Edits)

When the user edits "Tell me more about chlorophyll" to ask about something different, a sibling node is created. Each sibling may spawn its own subtree.

graph TD
    M1["🧑 User: What is photosynthesis?"]
    M2["🤖 Assistant: Photosynthesis is the process..."]
    M3["🧑 User: Tell me more about chlorophyll"]
    M4["🤖 Assistant: Chlorophyll is a green pigment..."]
    M5["🧑 User: What about cellular respiration?"]
    M6["🤖 Assistant: Cellular respiration is..."]
    M7["🧑 User: How does the Krebs cycle work?"]
    M8["🤖 Assistant: The Krebs cycle involves..."]

    M1 --> M2
    M2 --> M3
    M2 --> M5
    M3 --> M4
    M5 --> M6
    M6 --> M7
    M7 --> M8

    style M3 fill:#94a3b8,stroke:#64748b,color:#000
    style M4 fill:#94a3b8,stroke:#64748b,color:#000
    style M5 fill:#60a5fa,stroke:#2563eb,color:#000
    style M6 fill:#60a5fa,stroke:#2563eb,color:#000
    style M7 fill:#60a5fa,stroke:#2563eb,color:#000
    style M8 fill:#4ade80,stroke:#16a34a,color:#000
Loading
Colour Meaning
Default (no highlight) Shared ancestor nodes
Blue Active branch (currently viewed)
Grey Inactive branch (preserved but not displayed)
Green currentLeafId — the terminal node of the active branch

Active Branch vs Full Tree

The user interface renders only the active branch — a single linear path extracted from the tree. The full tree exists in memory, but the user sees one conversation thread at a time. Branch navigation controls allow switching between sibling branches.

graph TD
    subgraph "Full Tree (in memory)"
        R["Root"]
        A["Branch A — Message"]
        B["Branch B — Message"]
        A1["A child 1"]
        A2["A child 2"]
        B1["B child 1"]
        B2["B child 2"]
        B3["B child 3"]

        R --> A
        R --> B
        A --> A1
        A --> A2
        B --> B1
        B1 --> B2
        B2 --> B3
    end

    subgraph "Rendered Thread (active branch)"
        R2["Root"] --> Br["Branch B — Message"] --> B1r["B child 1"] --> B2r["B child 2"] --> B3r["B child 3"]
    end

    style B3 fill:#4ade80,stroke:#16a34a,color:#000
    style B3r fill:#4ade80,stroke:#16a34a,color:#000
Loading

Hydration: Database to In-Memory Tree

The database stores messages as flat rows — each row contains an id, parentId, and content fields, but no childrenIds. The application must reconstruct the tree topology every time messages are loaded. This process is called hydration.

Two-Pass Reconstruction

Hydration proceeds in exactly two passes:

Pass Action Output
Pass 1 Create message objects from database rows; initialise each with an empty childrenIds array A hash map of message nodes with no parent-child links
Pass 2 Iterate every message; for each with a non-null parentId, append its ID to the parent's childrenIds A fully linked tree with bidirectional traversal

This runs on both the server (during initial page render for a single chat) and the client (during bulk hydration when all chats are loaded into the global store).

flowchart TD
    A["Database rows fetched"] --> B["Pass 1: Create message objects"]
    B --> C["Each message gets empty childrenIds array"]
    C --> D["Messages stored in flat hash map by ID"]
    D --> E["Pass 2: Iterate all messages"]
    E --> F{"Has parentId?"}
    F -- Yes --> G["Push message ID into parent's childrenIds"]
    F -- No --> H["Mark as root node"]
    G --> I["Fully linked tree ready"]
    H --> I

    style A fill:#e0e7ff,stroke:#6366f1,color:#000
    style I fill:#4ade80,stroke:#16a34a,color:#000
Loading

Why Not Store childrenIds?

Storing childrenIds in the database would introduce a denormalisation problem. Every insert or delete would require updating two records (the new message and its parent), creating a synchronisation burden and a window for inconsistency. By computing childrenIds at runtime, the system maintains a single source of truth (parentId) and derives the reverse mapping on demand.


Core Tree Operations

Four pure functions power all tree manipulations. Each operates on the flat hash map and returns either a modified copy or a read-only result. None mutates the original data structure in place.

Operation Purpose Traversal Direction Mutability
Insert Message Adds a node and updates the parent's children list — Immutable (returns new hash map)
Remove Subtree Recursively deletes a node and all descendants Downward (depth-first) Shallow copy, then mutates copy
Reconstruct Thread Walks from leaf to root to extract the active branch Upward (parent chain) Read-only
Get Deepest Leaf Descends from a node following last children to the terminal Downward (last-child) Read-only

Insert Message

Insertion adds a new message node to the tree and, if the message has a parent, registers it as a child of that parent.

The operation follows an immutable update pattern:

  1. A shallow copy of the entire message hash map is created.
  2. The new message is added to the copy.
  3. If the new message has a parentId, the parent node is also shallow-copied, and the new message's ID is appended to the parent's childrenIds.
  4. The modified copy is returned; the original hash map is untouched.

This immutable approach is essential for the state management layer, which relies on reference equality checks to determine whether re-renders are necessary.

graph TD
    subgraph "Before Insert"
        P1["Parent node<br/>childrenIds: [C1]"]
        C1["Child 1"]
        P1 --> C1
    end

    subgraph "After Insert"
        P2["Parent node (copy)<br/>childrenIds: [C1, C2]"]
        C1b["Child 1"]
        C2["Child 2 (new)"]
        P2 --> C1b
        P2 --> C2
    end

    style C2 fill:#4ade80,stroke:#16a34a,color:#000
    style P2 fill:#fbbf24,stroke:#d97706,color:#000
Loading

The yellow node indicates the shallow-copied parent; the green node is the newly inserted message.


Remove Subtree

Subtree removal deletes a target node and every descendant beneath it. The operation proceeds in three steps:

  1. The target node's ID is removed from its parent's childrenIds.
  2. A depth-first traversal collects the target and all its descendants.
  3. Every collected node is deleted from the hash map.

The function returns the parentId of the deleted root, enabling callers to compute a new currentLeafId if the active branch was affected.

graph TD
    subgraph "Before Removal"
        R["Root"]
        A["Node A"]
        B["Node B (target)"]
        B1["B → Child 1"]
        B2["B → Child 2"]
        B2a["B2 → Grandchild"]

        R --> A
        R --> B
        B --> B1
        B --> B2
        B2 --> B2a
    end

    subgraph "After Removal"
        R2["Root"]
        A2["Node A"]
        R2 --> A2
    end

    style B fill:#f87171,stroke:#dc2626,color:#000
    style B1 fill:#f87171,stroke:#dc2626,color:#000
    style B2 fill:#f87171,stroke:#dc2626,color:#000
    style B2a fill:#f87171,stroke:#dc2626,color:#000
Loading

Red nodes are removed. The entire subtree rooted at Node B is deleted, including all descendants regardless of depth.


Reconstruct Thread

Thread reconstruction extracts the active conversation branch from the tree. Starting from the currentLeafId, it walks upward via the parentId chain, prepending each ancestor until it reaches a root node (one with no parent).

The result is a linear, root-to-leaf ordered array — the sequence of messages the user sees in the chat interface.

graph TD
    M1["🧑 User: Question A"]
    M2["🤖 Assistant: Answer A"]
    M3["🧑 User: Follow-up B"]
    M4["🤖 Assistant: Answer B"]
    M5["🧑 User: Follow-up C (edit)"]
    M6["🤖 Assistant: Answer C"]
    M7["🧑 User: Follow-up D"]
    M8["🤖 Assistant: Answer D"]

    M1 --> M2
    M2 --> M3
    M2 --> M5
    M3 --> M4
    M5 --> M6
    M6 --> M7
    M7 --> M8

    style M1 fill:#60a5fa,stroke:#2563eb,color:#000
    style M2 fill:#60a5fa,stroke:#2563eb,color:#000
    style M5 fill:#60a5fa,stroke:#2563eb,color:#000
    style M6 fill:#60a5fa,stroke:#2563eb,color:#000
    style M7 fill:#60a5fa,stroke:#2563eb,color:#000
    style M8 fill:#4ade80,stroke:#16a34a,color:#000

    style M3 fill:#94a3b8,stroke:#64748b,color:#000
    style M4 fill:#94a3b8,stroke:#64748b,color:#000
Loading

The blue path is the reconstructed thread: M1 → M2 → M5 → M6 → M7 → M8. Grey nodes exist in the tree but are not part of the active branch.

The walk direction is upward (leaf to root), but the output is reversed to produce a root-to-leaf sequence for rendering.


Get Deepest Leaf

Starting from any node, this operation descends by always selecting the last element in childrenIds at each level. It continues until it reaches a terminal node — one with no children.

This is used primarily during branch switching. When the user selects a sibling node, the system must determine where the tip of that sibling's subtree lies. The deepest leaf function provides that answer.

graph TD
    N1["Start node"]
    N2["Child A"]
    N3["Child B (last)"]
    N4["B → Child X"]
    N5["B → Child Y (last)"]
    N6["Y → Child Z (last, terminal)"]

    N1 --> N2
    N1 --> N3
    N3 --> N4
    N3 --> N5
    N5 --> N6

    style N1 fill:#fbbf24,stroke:#d97706,color:#000
    style N3 fill:#a78bfa,stroke:#7c3aed,color:#000
    style N5 fill:#a78bfa,stroke:#7c3aed,color:#000
    style N6 fill:#4ade80,stroke:#16a34a,color:#000
Loading
Colour Meaning
Yellow Starting node
Purple Traversal path (last child at each level)
Green Deepest leaf (returned result)

The "last child" heuristic ensures the user lands on the most recently created branch — typically the most relevant continuation.


Branch Creation

Two user actions cause the tree to branch: editing a message and regenerating a response. Both follow the same principle — new nodes are created as siblings rather than replacements — but they differ in where the branching occurs.


Editing a Message

When a user edits a previously sent message, the original message is not modified. Instead, the system creates a new message node with the edited content, placed as a sibling of the original — that is, sharing the same parent.

The flow proceeds as follows:

  1. The user submits an edit to an existing message.
  2. A new message node is created with the edited content and the same parentId as the original.
  3. The parent's childrenIds grows to include the new sibling.
  4. The AI generates a response, which becomes a child of the new edited message.
  5. currentLeafId advances to the AI response, switching the view to the new branch.
  6. The original message and its entire downstream subtree remain intact and navigable.
graph TD
    subgraph "Before Edit"
        P["🧑 User: What is X?"]
        A["🤖 Assistant: X is..."]
        F["🧑 User: Tell me more"]
        A2["🤖 Assistant: More about X..."]

        P --> A
        A --> F
        F --> A2
    end

    subgraph "After Edit"
        P2["🧑 User: What is X?"]
        A3["🤖 Assistant: X is..."]
        F2["🧑 User: Tell me more"]
        A4["🤖 Assistant: More about X..."]
        E["🧑 User: What about Y instead? (edit)"]
        A5["🤖 Assistant: Y is..."]

        P2 --> A3
        A3 --> F2
        A3 --> E
        F2 --> A4
        E --> A5
    end

    style E fill:#60a5fa,stroke:#2563eb,color:#000
    style A5 fill:#4ade80,stroke:#16a34a,color:#000
    style F2 fill:#94a3b8,stroke:#64748b,color:#000
    style A4 fill:#94a3b8,stroke:#64748b,color:#000
Loading

The edited message (blue) becomes a sibling of the original follow-up. The original branch (grey) is preserved but no longer active.


Regenerating a Response

Regeneration differs from editing in one important respect: it creates a branch at the grandparent level rather than the parent level. When a user requests a fresh AI response, the system does not simply create a new assistant message as a sibling of the existing one. Instead, it creates a new user message (with identical content to the original) as a sibling of the original user message, and then generates a fresh assistant response as its child.

This maintains a critical invariant: every assistant message has exactly one user message parent. The role alternation pattern (user → assistant → user → assistant) is never violated.

graph TD
    subgraph "Before Regeneration"
        U1["🧑 User: Explain quantum entanglement"]
        A1["🤖 Assistant: Quantum entanglement is..."]

        U1 --> A1
    end

    subgraph "After Regeneration"
        U1b["🧑 User: Explain quantum entanglement"]
        A1b["🤖 Assistant: Quantum entanglement is... (original)"]
        U2["🧑 User: Explain quantum entanglement (copy)"]
        A2["🤖 Assistant: Entanglement refers to... (regenerated)"]

        U1b --> A1b
        U1b --> U2
        U2 --> A2
    end

    style A1b fill:#94a3b8,stroke:#64748b,color:#000
    style U2 fill:#60a5fa,stroke:#2563eb,color:#000
    style A2 fill:#4ade80,stroke:#16a34a,color:#000
Loading

Design note: This approach ensures that tree traversal logic never encounters consecutive same-role nodes, and that every branch maintains a predictable user→assistant rhythm regardless of depth.


Branch Navigation

When a message has siblings — that is, when its parent has multiple children — the user interface displays navigation controls alongside that message. These controls allow the user to switch between alternative versions of the conversation from that point onward.

Navigation Controls

The controls present three elements:

Element Display Action
Left arrow ← Switch to the previous sibling
Position counter 2 / 3 Shows current sibling index and total sibling count
Right arrow → Switch to the next sibling

Siblings are determined by inspecting the parent node's childrenIds array. The current message's position within that array dictates the counter display. Root messages (those with no parent) are treated as siblings of each other — they share a virtual root.

Navigation Flow

sequenceDiagram
    actor User
    participant UI as Chat Interface
    participant Store as State Store
    participant Tree as Tree Operations
    participant DB as Database

    User->>UI: Clicks sibling arrow (→)
    UI->>Store: Dispatch branch switch
    Store->>Tree: Identify selected sibling ID
    Tree->>Tree: getDeepestLeaf(siblingId)
    Tree-->>Store: Returns deepest leaf ID
    Store->>Store: Set currentLeafId (optimistic)
    Store->>Store: reconstructThread(newLeafId)
    Store-->>UI: Re-render with new branch
    Store->>DB: Persist currentLeafId
    DB-->>Store: Confirmation (or swallowed error)
Loading

The key detail: selecting a sibling does not simply display that sibling's message. It descends to the deepest leaf of the sibling's entire subtree, then reconstructs the full thread from root to that leaf. This ensures the user always sees a complete conversation, not a truncated fragment.


Active Branch Tracking

The currentLeafId field on the chat record is the single source of truth for which branch is currently active. It always points to the deepest message in the viewed branch. Every other piece of branch state — the rendered thread, the sibling counters, the navigation arrows — is derived from this one value.

State Transitions

currentLeafId changes in response to four events:

stateDiagram-v2
    [*] --> Empty: Chat created (no messages)

    Empty --> Leaf1: First message sent
    Leaf1 --> Leaf2: New message appended
    Leaf2 --> Leaf3: New message appended
    Leaf3 --> LeafN: Continued conversation

    LeafN --> BranchLeaf: Branch switched via sibling navigation
    BranchLeaf --> LeafN: Branch switched back

    LeafN --> EditLeaf: Message edited → new branch created
    EditLeaf --> NewLeaf: AI response committed

    LeafN --> RecomputedLeaf: Subtree deleted → recomputed from parent
Loading
Event currentLeafId Behaviour
New message appended Advances to the newly created message
Branch switched Jumps to the deepest leaf of the selected sibling's subtree
Message edited Advances to the AI response on the new branch
Subtree deleted Recomputed: ascend to deleted node's parent, then descend to its deepest remaining leaf
AI response streamed Advances to the committed assistant message once streaming completes

Recomputation After Deletion

When a subtree is deleted and that subtree contained the currentLeafId, the system must find a new leaf:

  1. The deleted node's parentId is retrieved (returned by the removal operation).
  2. From the parent, getDeepestLeaf() descends to the terminal node of the parent's remaining subtree.
  3. currentLeafId is updated to this new terminal node.
  4. The thread is reconstructed from the new leaf.

If the parent has no remaining children, the parent itself becomes the currentLeafId.


Streaming Integration

During AI response generation, the streaming system interacts carefully with the tree to avoid inconsistent states. Communication between client and server uses Server-Sent Events (SSE) — a unidirectional protocol where the server pushes events over a long-lived HTTP connection.

Streaming Lifecycle

sequenceDiagram
    actor User
    participant UI as Chat Interface
    participant Store as State Store
    participant API as Chat API
    participant Tree as Tree Operations
    participant DB as Database

    User->>UI: Sends message
    UI->>Store: Insert user message (optimistic)
    Store->>Tree: insertMessage(userMsg)
    Store->>DB: Persist user message
    UI->>API: POST /api/chat (SSE stream)

    Note over UI: Placeholder node rendered<br/>with synthetic ID

    loop SSE Events
        API-->>UI: text / reasoning / tool-call events
        UI->>Store: Update placeholder content
    end

    API-->>UI: "done" event (with server message ID)
    UI->>Store: Replace placeholder with committed node
    Store->>Tree: insertMessage(assistantMsg)
    Store->>Store: Set currentLeafId → assistantMsg.id
    Store->>DB: Persist assistant message
    Store->>DB: Update currentLeafId
Loading

Key Invariants During Streaming

Phase Tree State Placeholder Behaviour
Before stream starts User message committed to tree No placeholder exists
During streaming Tree unchanged; user message is the leaf Placeholder renders below user message; content updates incrementally
Stream completes ("done") Assistant message committed to tree Placeholder replaced with real node; currentLeafId advances
Stream aborted by user Accumulated partial content committed to tree Partial-content node inserted to maintain tree consistency
Stream error (SSE or network) Error message committed (SSE errors) or no node committed (network failures) Tree remains consistent; the user message is the deepest node

The critical design principle: the streaming system never leaves the tree in an inconsistent state. If the user aborts a stream, accumulated partial content is committed as a valid tree node. If the server returns an error via SSE, the error message text is committed instead of the partial content. In cases of network failure, no additional node is committed — the user message remains the deepest node. In all scenarios, the user may regenerate from the current leaf, creating a new branch.

The placeholder node uses a synthetic ID that is never persisted to the database. It exists only in the rendering layer — React component local state — and is never inserted into the Zustand message tree. When the real message arrives, the placeholder is replaced and the committed node is inserted into the tree atomically.


Optimistic State Management

All tree mutations follow an optimistic update pattern. The user interface reflects changes immediately, without waiting for database confirmation. This eliminates perceived latency and provides a responsive editing experience, even on slow or unreliable network connections.

Optimistic Update Flow

flowchart TD
    A["User triggers action"] --> B["Capture full state snapshot"]
    B --> C["Apply mutation immediately (UI updates)"]
    C --> D["Execute server action (DB persistence)"]
    D --> E{"Server action succeeded?"}
    E -- Yes --> F["Discard snapshot; state is consistent"]
    E -- No --> G["Restore snapshot (full rollback)"]
    G --> H["Display error notification"]

    style A fill:#e0e7ff,stroke:#6366f1,color:#000
    style C fill:#4ade80,stroke:#16a34a,color:#000
    style G fill:#f87171,stroke:#dc2626,color:#000
Loading

Error Handling Modes

Not all operations use the same error handling strategy. The system distinguishes between two modes:

Mode Behaviour on Failure Used For
Strict Full rollback to snapshot; error notification displayed Message deletion, message persistence
Swallow Error logged silently; optimistic state retained currentLeafId updates (branch switching)

The rationale for "swallow" mode on branch switches: currentLeafId is a navigational convenience, not a data integrity field. If the server fails to persist a branch switch, the worst consequence is that a page reload returns the user to a different branch. This trade-off enables an offline-first feel for navigation without risking data loss.

Message deletions, by contrast, use strict mode because a failed deletion that appears successful would leave the user believing messages are gone when they are not — a confusing and potentially data-leaking inconsistency.

Operations by Mode

Operation Mode Rollback Behaviour
Send message Strict Remove optimistically inserted message
Delete message Strict Restore deleted subtree from snapshot
Edit message (create branch) Strict Remove new branch; restore original currentLeafId
Switch branch Swallow Retain optimistic currentLeafId; log error
Rename chat Strict Restore original title
Commit streamed response Strict Remove committed assistant node

Design Decisions & Trade-offs

Decision Rationale Trade-off
No database foreign key on parentId Avoids cascade complexity with self-referential trees; simplifies bulk deletion Application layer must enforce tree integrity; orphaned nodes possible if application logic has bugs
childrenIds computed at runtime Eliminates denormalisation; single source of truth via parentId Two-pass hydration cost on every load; scales linearly with message count
Flat hash map storage O(1) lookup by ID; efficient for immutable state updates; clean Zustand integration Must reconstruct linear thread for rendering; no inherent ordering
Last-child descent for branch switching Surfaces the most recently created continuation of a branch Users cannot land on a mid-branch message; must scroll to find the divergence point
Regeneration creates user+assistant pair Maintains consistent parent-child role alternation throughout the tree Slightly higher storage cost; duplicate user message content across branches
Streaming uses placeholder node Avoids premature tree commits during incomplete generation Temporary visual inconsistency (synthetic ID visible in dev tools, not to users)
Optimistic updates with snapshot rollback Instant UI feedback; eliminates network-dependent latency for all interactions Complexity of maintaining rollback snapshots; potential for brief visual inconsistency on rollback
currentLeafId as sole branch indicator Single source of truth; entire branch state derived from one value Must recompute on every deletion; no history of previously viewed branches

References

  1. OpenAI Platform — Conversation State
  2. Vercel AI SDK — Chatbot UI
  3. Vercel AI SDK — Chatbot Message Persistence
  4. Vercel AI SDK — Generating and Streaming Text
  5. Mike Hillyer — Managing Hierarchical Data in MySQL
  6. PostgreSQL Documentation — WITH Queries (Recursive CTEs)
  7. StackOverflow — Options for Storing Hierarchical Data in a Relational Database
  8. TanStack Query — Optimistic Updates
  9. Zustand Documentation — Updating State
  10. MDN — Using Server-Sent Events
  11. WHATWG HTML Living Standard — Server-Sent Events (§9.2)

Clone this wiki locally