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

> Discover how free-claude-code uses tree-based conversation threading with directed acyclic graphs, MessageNode objects, immutable data, O(1) lookups, and async queue processors for efficient chat management.

- Repository: [Ali Khokhar/free-claude-code](https://github.com/Alishahryar1/free-claude-code)
- Tags: internals
- Published: 2026-04-24

---

**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 Layer** ([`messaging/trees/data.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/trees/data.py) [L57-L124]): Defines `MessageNode`, `MessageTree`, and `MessageState` enums for immutable tree structures
- **Repository Layer** ([`messaging/trees/repository.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/trees/repository.py) [L11-L84]): Maintains global indexes for O(1) cross-tree lookups via `TreeRepository`
- **Processing Layer** ([`messaging/trees/processor.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/trees/processor.py) [L16-L63] and [`messaging/trees/queue_manager.py`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/trees/queue_manager.py) [L26-L446]): Orchestrates async execution, queue handling, and cancellation via `TreeQueueProcessor` and `TreeQueueManager`

## 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`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/trees/data.py) [L66-L84]:

```python
@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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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:

```python

# 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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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:

```python
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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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`](https://github.com/Alishahryar1/free-claude-code/blob/main/messaging/trees/queue_manager.py) [L54-L84]:

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

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

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

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

```python

# 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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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`](https://github.com/Alishahryar1/free-claude-code/blob/main/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.