> ## Documentation Index
> Fetch the complete documentation index at: https://dev.haico.gr/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-0003: Conversation branching via new threads

> Branch conversations as new threads seeded via checkpointer state, not per-table branch IDs.

# 0003. Branch conversations as new threads seeded via checkpointer state, not per-table branch IDs

## Context and problem statement

Issue #55 needs git-like versioning of a co-construction session: view the turn history as a graph, fork an alternative path at any completed turn, and restore the studio to an earlier state. Conversation state lives in **two stores**. The LangGraph checkpointer versions the `messages` channel per `thread_id`, while the four user-visible surfaces (`documents`, `preferences`, `todos`, `artifacts`) are plain rows keyed by `thread_id` with no version axis (`documents` is even `unique(thread_id)`). Forking only the transcript would leave two branches sharing one workspace, silently corrupting both. The installed `AsyncPostgresSaver` (langgraph-checkpoint-postgres 3.1.0) does not implement `acopy_thread` (the base-class method raises `NotImplementedError`), so there is no library primitive for copying a thread either.

## Decision drivers

* Must not touch the SSE turn loop (`query.py`), the dynamic prompt, or the tool layer; every one of them is keyed by `thread_id` and regression risk is highest there.
* Branch creation must cost O(document + preferences + todos + artifacts), not O(turns × document size): `workspace_snapshots` embeds the full document body per turn, so copying history is quadratic.
* No cross-store atomicity exists (the checkpointer uses its own psycopg connection), so failure handling must be compensating, not transactional.
* Restore must be non-destructive: checkpointer truncation plus multi-table deletes cannot be made atomic and would permanently discard history.

## Considered options

1. **Option A: branch = new `thread_id`, workspace copied as-of the branch point, messages seeded into the new checkpointer thread, parent link in a small `conversation_branches` table.**
2. **Option B: add `branch_id` to every workspace table and make all repositories, tools, the dynamic prompt, and the SSE router branch-aware.**
3. **Option C: LangGraph-native time travel: fork checkpoints in place on the same thread via checkpoint ids.**

## Decision outcome

**Chosen option: Option A**, because a fresh `thread_id` flows through the turn loop, tools, dynamic prompt, and checkpointer config unchanged, and the missing message-copy primitive can be replaced by one verified `aupdate_state` call.

Mechanics (verified by `backend/scripts/spike_branch_seeding.py` against the exact pinned versions: langgraph 1.2.0, langgraph-checkpoint 4.1.0, langgraph-checkpoint-postgres 3.1.0):

* A minimal one-node `StateGraph(MessagesState)` compiled with the same `AsyncPostgresSaver` can **read** a thread written by `create_react_agent` and **seed** a brand-new thread with `aupdate_state(config, {"messages": prefix}, as_node="__start__")`; no LLM client, tools, or prompt are needed for branching.
* The message prefix for turn *k* is every message strictly before the *(k+1)*-th `HumanMessage`.
* The workspace copy resolves "state after turn k" as `snapshot(k+1)` when a later turn exists, else the live rows; artifact rows with `turn_index <= k` are copied so the new thread is self-contained. Snapshots are never copied; history for turns ≤ k is resolved lazily by walking the `conversation_branches` lineage at read time.
* Branch creation is 3-phase (SQL insert with `status='pending'`, then checkpointer seed, then `status='active'`), with compensating cleanup (SQL rows plus best-effort `adelete_thread`) on failure. Readers only see `active` branches.
* Turn numbering continues from the parent (the branch's `conversation_users.turn_index` starts at *k*), keeping turns aligned across siblings for future compare features.

### Positive consequences

* Zero changes to `query.py`, `builder.py`, the tools, or the SSE lifecycle; a branch is "just another conversation" to every existing code path (switching reuses `onLoadConversation` verbatim).
* Branch cost is independent of history length.
* The decision map / graph endpoint is a pure read over existing snapshots plus the new lineage table.

### Negative consequences

* Copied workspaces diverge by design; there is no merge, and cross-branch dedup of identical documents is not attempted.
* The graph read must walk lineage to assemble inherited history (depth capped at 50).
* Seeding semantics depend on the pinned langgraph stack; bumps must re-run `backend/tests/test_branch_seeding_integration.py` (the floor pins in `requirements.txt` were replaced with exact pins for this reason).

## Pros and cons of the options

### Option A: new thread plus seeded state (chosen)

* **+** No blast radius into the turn loop or tools; high feasibility per issue #55's analysis.
* **+** Verified end-to-end by a spike before any plumbing was built.
* **−** Data duplication at the branch point; lineage walk needed for inherited history.

### Option B: `branch_id` on every workspace table

* **+** Cleaner relational model; cheap cross-branch queries.
* **−** Touches the repositories, dynamic prompt, all tools, the SSE router, and the turn contextvar, the highest-regression-risk surface in the codebase. Rejected for v1 (explicitly out of scope in #55).

### Option C: in-place checkpoint forking (LangGraph time travel)

* **+** No message copying at all.
* **−** Versions only the `messages` channel: the four workspace tables have no checkpoint axis, so branches would share one workspace row set, exactly the corruption #55 calls out. Also a confusing UX (two histories inside one conversation).

## Links

* Related ADRs: [0001-two-branch-model](/docs/adr/0001-two-branch-model)
* Related issues / PRs: #55 (also #48, #49, #50 discussions; #54 adjacent)
* External references: LangGraph checkpointer base class (`BaseCheckpointSaver.acopy_thread` raises `NotImplementedError` in langgraph-checkpoint 4.1.0)
