How Tree-Based Conversation Threading Works in free-claude-code

free-claude-code implements tree-based conversation threading by representing every chat as a directed acyclic graph where MessageNode objects form parent-child relationships, coordinated across immutable data structures, O(1) lookup repositories, and async queue processors.

The free-claude-code repository manages complex Claude AI conversations by abandoning linear chat history in favor of a branching tree structure. This approach preserves reply chains, supports parallel conversation branches, and ensures strict ordering guarantees when processing multiple messages within the same conversation thread.

Architecture Overview: Three-Layer Design

The tree-based conversation threading system operates through three tightly-coupled layers defined in the messaging/trees/ directory:

Data Model: MessageNode and MessageTree

At the core of the threading system are two immutable data structures that define the conversation topology.

MessageNode Structure

Each exchange is encapsulated in a MessageNode class defined in messaging/trees/data.py [L66-L84]:

@dataclass(frozen=True)
class MessageNode:
    node_id: str                    # Unique identifier (usually message ID)

    incoming: IncomingMessage       # Raw payload with text, chat_id, user_id

    status_message_id: str         # Claude's "working..." message ID

    state: MessageState            # PENDING, IN_PROGRESS, COMPLETED, ERROR

    parent_id: str | None          # Reference to parent node

    children_ids: list[str]        # References to reply nodes

    created_at: datetime
    updated_at: datetime

The parent_id and children_ids fields establish the directed acyclic graph structure, allowing any message to serve as the root of a sub-conversation branch.

MessageTree and State Management

The MessageTree class (messaging/trees/data.py [L58-L143]) maintains the graph using a hashmap-enforced node dictionary _nodes for O(1) lookup efficiency. Each tree tracks:

  • A FIFO snapshot queue (_SnapshotQueue) preserving message order
  • The currently processing node ID (_current_node_id)
  • An async lock (_lock) guaranteeing atomic state updates across concurrent operations

Repository Layer: Fast Cross-Tree Lookups

The TreeRepository class in messaging/trees/repository.py [L11-L84] solves the inverse lookup problem: given any node ID anywhere in the system, locate its containing tree immediately.

The repository maintains two critical indexes:


# repository.py

class TreeRepository:
    def __init__(self):
        self._trees: dict[str, MessageTree] = {}           # root_id → tree

        self._node_to_tree: dict[str, str] = {}          # node_id → root_id

    
    def get_tree_for_node(self, node_id: str) -> MessageTree | None:
        """O(1) lookup to find which conversation tree owns a specific node"""
        root_id = self._node_to_tree.get(node_id)
        return self._trees.get(root_id) if root_id else None

This design enables operations like resolve_parent_node_id and get_pending_children to traverse conversation branches without scanning entire conversation histories.

Async Processing with TreeQueueProcessor

The TreeQueueProcessor class in messaging/trees/processor.py [L16-L63] isolates event-loop logic from data structures, handling the complexity of async Claude API calls.

Key responsibilities include:

  • Queue management: Determining whether a node can execute immediately or must wait behind existing jobs
  • State transitions: Atomically updating MessageState from PENDING to IN_PROGRESS to COMPLETED
  • Callback integration: Supporting optional queue_update_callback and node_started_callback for UI updates

The processor uses a locking strategy to prevent race conditions:

async def enqueue_and_start(self, tree, node_id, processor):
    async with tree.with_lock():
        if tree.is_processing:
            tree.put_queue_unlocked(node_id)   # Queued behind current job

            return True
        else:
            tree.set_processing_state(node_id, True)
            node = tree.get_node(node_id)
            if node:
                tree.set_current_task(
                    asyncio.create_task(self.process_node(tree, node, processor))
                )
            return False

Managing Conversations via TreeQueueManager

TreeQueueManager in messaging/trees/queue_manager.py [L26-L446] serves as the public facade exposing tree-based conversation threading to the CLI and API routes.

Primary operations include:

  • Tree creation: create_tree instantiates root nodes and registers them in the repository
  • Branching: add_to_tree validates parent nodes and appends children while maintaining the _node_to_tree index
  • Execution: enqueue forwards processing requests to the TreeQueueProcessor
  • Cancellation: cancel_node and cancel_tree provide atomic cleanup using drain_queue_and_mark_cancelled

Tree initialization follows this pattern from messaging/trees/queue_manager.py [L54-L84]:

async def create_tree(self, node_id, incoming, status_message_id):
    async with self._lock:
        root_node = MessageNode(
            node_id=node_id,
            incoming=incoming,
            status_message_id=status_message_id,
            state=MessageState.PENDING,
        )
        tree = MessageTree(root_node)
        self._repository.add_tree(node_id, tree)
        return tree

Practical Implementation Examples

Creating a New Conversation Tree

Initialize a root conversation that serves as the anchor for all subsequent threading operations:

from messaging.trees.queue_manager import TreeQueueManager
from messaging.models import IncomingMessage

manager = TreeQueueManager()

root_msg = IncomingMessage(
    text="Explain quantum computing",
    chat_id="chat-123",
    user_id="user-42",
    message_id="msg-001",
    platform="telegram"
)

tree = await manager.create_tree(
    node_id="msg-001",
    incoming=root_msg,
    status_message_id="status-001"
)

Appending Child Messages

Create conversation branches by specifying parent nodes, enabling threaded replies:

reply_msg = IncomingMessage(
    text="Can you simplify that explanation?",
    chat_id="chat-123",
    user_id="user-42",
    message_id="msg-002",
    platform="telegram",
    reply_to_message_id="msg-001"  # Parent reference

)

tree, child_node = await manager.add_to_tree(
    parent_node_id="msg-001",
    node_id="msg-002",
    incoming=reply_msg,
    status_message_id="status-002"
)

The repository automatically maps msg-002 to the root tree, enabling manager.get_tree_for_node("msg-002") to retrieve the original conversation context.

Enqueueing Nodes for Processing

Submit nodes to the Claude API with automatic queue handling:

async def claude_processor(node_id: str, node: MessageNode):
    response = await call_claude_api(node.incoming.text)
    await tree.update_state(node_id, MessageState.COMPLETED)
    return response

await manager.enqueue(node_id="msg-002", processor=claude_processor)

If the tree is already processing another node, msg-002 enters the FIFO queue; otherwise processing begins immediately.

Cancellation Operations

Handle user interruptions or timeouts with atomic cancellation:


# Cancel specific node and its pending children

await manager.cancel_node("msg-002")

# Terminate entire conversation tree

await manager.cancel_tree(root_id="msg-001")

These operations safely shut down running async tasks and purge the snapshot queue without leaving orphaned database records.

Summary

  • free-claude-code implements tree-based conversation threading using a directed acyclic graph structure where MessageNode objects track parent-child relationships
  • The three-layer architecture separates data immutability (MessageTree), global indexing (TreeRepository), and async execution (TreeQueueProcessor)
  • O(1) node lookups via get_tree_for_node enable efficient traversal of complex conversation branches without linear scans
  • Per-tree FIFO queues and async locks guarantee that only one Claude API call processes per conversation at a time, preventing race conditions
  • The system supports full persistence through to_dict/from_dict serialization, allowing conversation state to survive server restarts

Frequently Asked Questions

What data structure does free-claude-code use for conversation threading?

free-claude-code uses a tree-based directed acyclic graph implemented through the MessageTree class in messaging/trees/data.py. Each node (MessageNode) stores references to its parent and children, enabling branching conversation threads where any message can serve as the root of a sub-discussion.

How does the system prevent simultaneous Claude API calls within the same conversation?

The TreeQueueProcessor in messaging/trees/processor.py enforces sequential processing through a combination of per-tree async locks and FIFO snapshot queues. When enqueue_and_start is called, it checks tree.is_processing; if true, the node enters the queue, otherwise it acquires the lock and begins immediate processing.

Can conversation threads be restored after a server restart?

Yes. The TreeQueueManager provides to_dict and from_dict methods that serialize the entire tree state including node relationships, processing statuses, and queue positions. The TreeRepository supports reconstruction of the _node_to_tree index during startup, enabling full restoration of conversation threading state.

How does the repository locate which conversation tree contains a specific message?

The TreeRepository maintains a _node_to_tree dictionary mapping every node ID to its root tree ID. The get_tree_for_node method performs an O(1) lookup to retrieve the correct MessageTree instance without traversing parent references, making it efficient to locate conversation context for any incoming message regardless of its depth in the thread.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →