# Conversational Branching — Design Spec

> **Date:** 2026-03-24
> **Status:** Draft
> **Branch:** `feat/conversational-branching` (to be created)
> **Source:** `docs/ConversationalBranching_DataModel_Spec.docx` (original spec)

---

## Executive Summary

Conversational Branching transforms FlowPilot from a linear AI chat into a branching troubleshooting workspace. Engineers explore multiple diagnostic hypotheses as first-class branches, with full AI context awareness across all paths.

The design is **additive and removable** — all branching state lives in new tables and nullable columns. If the feature is pulled, drop the tables, remove the columns, and the app works exactly as it does today.

---

## Design Principles

1. **Extend, don't replace.** Reuse existing tables, services, and infrastructure. No parallel code paths.
2. **Removable.** Every branching artifact (tables, columns, services, components) can be deleted without breaking existing functionality.
3. **Dual-write for backward compat.** New handoff system writes to both `session_handoffs` table AND existing `escalation_package`/`escalated_to_id` fields until the old queue UI is fully migrated.
4. **Branching services never touch the Anthropic SDK.** They assemble context and call `_call_ai` from `assistant_chat_service.py` — the same function `unified_chat_service` already uses.

---

## What This Enables

1. **Branch Map sidebar** — live tree visualization of all diagnostic paths, with status badges (active, dead end, solved, untried) and click-to-switch.
2. **Fork Cards** — in-chat decision points where FlowPilot suggests multiple hypotheses, each becoming a branch.
3. **Cross-branch AI context** — when exploring Branch 3, FlowPilot knows what was tried on Branches 1 and 2.
4. **Branch revival** — new evidence from one branch can reopen a previously dead-end branch.
5. **Unified handoff (park / escalate)** — single snapshot mechanism with intent toggle.
6. **Three-output resolution package** — PSA ticket notes, KB article draft, and client-facing summary.

---

## Reuse Map

### What branching reuses (NOT duplicated):

| Capability | Existing Source | How Branching Uses It |
|---|---|---|
| LLM calls | `_call_ai` in `assistant_chat_service.py` | All branching LLM calls go through this |
| Image content blocks | `_call_ai` multimodal support | Current branch images included in messages via existing format |
| Token counting | `AISessionStep.input_tokens/output_tokens` | Unchanged — tokens tracked per step per branch |
| RAG search | `rag_service.py` | Branch messages use same RAG pipeline |
| Image upload + S3 storage | `file_uploads` table + `storage_service.py` | Extended with 5 new columns, no new table |
| PSA push | `psa_documentation_service.py` | Resolution outputs and handoff notes push through existing service |
| Escalation package structure | `_build_escalation_package_enhanced()` pattern | HandoffManager builds its own branch-aware snapshot (the existing function is not branch-aware and uses a different AI call path). The snapshot JSONB follows the same general structure for compatibility. |
| Escalation queue | Existing `ai_sessions` query + frontend | Dual-write keeps old queue working |
| Session lifecycle | `flowpilot_engine.py` resolve/escalate/pause | Extended, not replaced |

### What's new:

| Capability | Notes |
|---|---|
| `session_branches` table | Core branching entity |
| `fork_points` table | Decision point metadata (kept separate for expandability) |
| `session_handoffs` table | Unified park/escalate with history |
| `session_resolution_outputs` table | Three independent output deliverables |
| `BranchManager` service | Branch lifecycle CRUD + context summary generation |
| `BranchAwarePromptBuilder` service | Cross-branch context assembly |
| `HandoffManager` service | Snapshot, assessment, claim, PSA push |
| `ResolutionOutputGenerator` service | PSA notes, KB article, client summary generation |
| AI description pipeline | Async `ai_description` generation on every file upload |

---

## Data Model

### Modified existing tables

**`ai_sessions` — add columns:**

| Column | Type | Default | Notes |
|---|---|---|---|
| `is_branching` | BOOLEAN | FALSE | Whether branching is active |
| `active_branch_id` | UUID NULLABLE (no FK — soft pointer to avoid circular FK) | NULL | Currently viewed branch. No FK constraint to `session_branches` because of circular reference (session → branch → session). Application-level integrity. |
| `handoff_count` | INTEGER | 0 | Times handed off |
| `total_active_seconds` | INTEGER | 0 | Cumulative active time |
| `total_parked_seconds` | INTEGER | 0 | Cumulative parked time |

**`ai_session_steps` — add columns:**

| Column | Type | Default | Notes |
|---|---|---|---|
| `branch_id` | UUID FK NULLABLE | NULL | NULL = pre-branching/root |
| `is_fork_point` | BOOLEAN | FALSE | Whether this step triggered a fork |
| `fork_point_id` | UUID FK NULLABLE | NULL | References `fork_points.id` |

**`file_uploads` — add columns:**

| Column | Type | Default | Notes |
|---|---|---|---|
| `ai_description` | TEXT NULLABLE | NULL | AI-generated one-sentence description |
| `extracted_content` | TEXT NULLABLE | NULL | Extracted text from logs/configs |
| `content_summary` | TEXT NULLABLE | NULL | AI summary for long files |
| `uploaded_on_branch_id` | UUID FK NULLABLE | NULL | Which branch the file was uploaded from |
| `uploaded_at_step_id` | UUID FK NULLABLE | NULL | Which step triggered the upload |

All columns nullable. Existing rows unaffected. `ai_description` is always generated on upload (not just branching sessions) — useful for search, exports, PSA notes, Knowledge Flywheel.

Also add `'fork'` to `ai_session_steps.step_type` check constraint. **Note:** modifying a PostgreSQL CHECK constraint requires `DROP CONSTRAINT` then `ADD CONSTRAINT` — this is NOT an additive operation. The Alembic migration must be written manually per CLAUDE.md lesson 77, using `op.drop_constraint('ck_ai_session_steps_step_type')` then `op.create_check_constraint(...)` with the new values list.

### New tables

**`session_branches`**

| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `session_id` | UUID FK → `ai_sessions.id` CASCADE | |
| `parent_branch_id` | UUID FK → self, NULLABLE | NULL = root branch |
| `fork_point_step_id` | UUID FK → `ai_session_steps.id`, NULLABLE | Step where this branch forked |
| `branch_order` | INTEGER | Display order among siblings (1-based) |
| `label` | VARCHAR(200) | "Network connectivity", "Print spooler service" |
| `status` | VARCHAR(20) | `active`, `dead_end`, `solved`, `untried`, `revived` |
| `status_reason` | TEXT NULLABLE | AI-generated reason for status |
| `status_changed_at` | TIMESTAMP NULLABLE | |
| `status_changed_by` | UUID FK → `users.id`, ondelete SET NULL, NULLABLE | |
| `conversation_messages` | JSONB | LLM message history scoped to this branch |
| `context_summary` | JSONB | `{tried: [], concluded: str, artifacts: []}` |
| `evidence_from_branch_id` | UUID FK NULLABLE | If revived, evidence source |
| `evidence_description` | TEXT NULLABLE | What triggered revival |
| `created_at` | TIMESTAMP | |
| `updated_at` | TIMESTAMP | |

Indexes: `session_id`, `parent_branch_id`, `(session_id, status)`, `(session_id, branch_order)`.
Check constraints: `status IN (...)`, `branch_order > 0`.

**`fork_points`**

| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `session_id` | UUID FK → `ai_sessions.id` | |
| `parent_branch_id` | UUID FK → `session_branches.id` | Branch this fork occurs in |
| `trigger_step_id` | UUID FK → `ai_session_steps.id`, NULLABLE | Step that triggered fork |
| `fork_reason` | TEXT | AI explanation |
| `options` | JSONB | `[{label, description, branch_id, status}]` |
| `created_at` | TIMESTAMP | |

**`session_handoffs`**

| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `session_id` | UUID FK → `ai_sessions.id` | |
| `handed_off_by` | UUID FK → `users.id` | |
| `intent` | VARCHAR(20) | `park` or `escalate` |
| `source_branch_id` | UUID FK NULLABLE | Active branch at handoff |
| `snapshot` | JSONB | Branch map, status, next step, waiting on, watch out |
| `ai_assessment` | TEXT NULLABLE | Diagnostic assessment (escalate only) |
| `ai_assessment_data` | JSONB NULLABLE | `{likely_cause, suggested_steps, confidence}` |
| `artifacts` | JSONB NULLABLE | `[{name, type, reference}]` |
| `engineer_notes` | TEXT NULLABLE | |
| `priority` | VARCHAR(20) | `normal` or `elevated` |
| `claimed_by` | UUID FK NULLABLE | |
| `claimed_at` | TIMESTAMP NULLABLE | |
| `psa_note_pushed` | BOOLEAN DEFAULT FALSE | |
| `psa_note_id` | VARCHAR(100) NULLABLE | |
| `notification_sent` | BOOLEAN DEFAULT FALSE | |
| `created_at` | TIMESTAMP | |

Check constraints: `intent IN ('park', 'escalate')`, `priority IN ('normal', 'elevated')`.
Dual-write: on create, also populates `ai_sessions.escalation_package` and `escalated_to_id`.

**`session_resolution_outputs`**

| Column | Type | Notes |
|---|---|---|
| `id` | UUID PK | |
| `session_id` | UUID FK → `ai_sessions.id` | |
| `output_type` | VARCHAR(30) | `psa_ticket_notes`, `knowledge_base`, `client_summary` |
| `generated_content` | TEXT | AI-generated output |
| `structured_data` | JSONB NULLABLE | For KB: `{symptoms, root_cause, steps, tags}` |
| `edited_content` | TEXT NULLABLE | Engineer's edited version |
| `status` | VARCHAR(20) | `draft`, `approved`, `pushed`, `rejected` |
| `pushed_to` | VARCHAR(50) NULLABLE | `psa`, `kb_library`, `clipboard`, `email` |
| `pushed_at` | TIMESTAMP NULLABLE | |
| `pushed_reference` | VARCHAR(200) NULLABLE | External ID after push |
| `generated_by_model` | VARCHAR(50) | |
| `created_at` | TIMESTAMP | |
| `updated_at` | TIMESTAMP | |

Constraints: check constraints on `output_type` and `status`. Use `INSERT ... ON CONFLICT (session_id, output_type) DO UPDATE` (upsert) in `generate_all()` so outputs can be regenerated if a session is re-opened after resolution. `UNIQUE(session_id, output_type)` enforces one-of-each but allows replacement.

### Entity Relationships

```
AISession 1──* SessionBranch (session has many branches)
AISession 1──* AISessionStep (unchanged)
AISession 1──* SessionHandoff (can be handed off multiple times)
AISession 1──3 SessionResolutionOutput (one per output type)
SessionBranch 1──* AISessionStep (branch owns steps via branch_id)
SessionBranch 1──* SessionBranch (parent → children, self-referential)
SessionBranch 1──* ForkPoint (branch contains fork points)
ForkPoint 1──* SessionBranch (each option becomes a branch)
SessionHandoff *──1 User (handed_off_by, claimed_by)
FileUpload *──1 SessionBranch (optional, via uploaded_on_branch_id)
```

---

## Service Layer

### Integration Architecture

```
┌─────────────────────────────────────────────────────────────┐
│  LAYER 3: BRANCHING SERVICES (NEW)                          │
│                                                             │
│  BranchManager         — Fork, switch, mark status, revive, │
│                          tree query, context summary gen     │
│                                                             │
│  BranchAwarePromptBuilder — Assembles system prompt +       │
│                              messages + images with cross-   │
│                              branch context, returns dict    │
│                              for _call_ai                    │
│                                                             │
│  HandoffManager        — Snapshot, AI assessment, claim,     │
│                          briefing, PSA push, queue query     │
│                                                             │
│  ResolutionOutputGen   — PSA notes, KB article, client       │
│                          summary, push to destination        │
└────────────────────────────┬────────────────────────────────┘
                             │ calls _call_ai directly
                             ▼
┌─────────────────────────────────────────────────────────────┐
│  LAYER 2: EXISTING CHAT INFRASTRUCTURE                      │
│                                                             │
│  _call_ai / _call_anthropic_cached                          │
│    — Anthropic API, prompt caching, image blocks, MCP       │
│                                                             │
│  unified_chat_service  — Session-level chat (linear)        │
│  flowpilot_engine      — Step lifecycle, resolve, escalate  │
│  rag_service           — Flow library search                │
│  psa_documentation_service — ConnectWise note push          │
└────────────────────────────┬────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│  LAYER 1: INFRASTRUCTURE (EXISTS)                           │
│                                                             │
│  Railway Object Storage — S3 bucket, presigned URLs         │
│  PostgreSQL             — All data                          │
│  Anthropic API          — Claude with Vision                │
└─────────────────────────────────────────────────────────────┘
```

### `services/branch_manager.py`

Branch lifecycle management. Pure data operations + one LLM call pattern (context summary).

| Method | What it does | LLM call? |
|---|---|---|
| `create_root_branch(session_id)` | Creates root branch, sets `is_branching=True`, copies `session.conversation_messages` into root branch. Session-level field kept as pre-branching snapshot. | No |
| `create_fork(session_id, parent_branch_id, trigger_step_id, fork_reason, options[])` | Pre-generates all branch UUIDs in Python, then inserts `ForkPoint` (with branch_ids in options JSONB) + N `SessionBranch` rows in a single transaction. Sets `is_fork_point=True` on trigger step. Unexplored options get status `untried`. | No |
| `switch_branch(session_id, target_branch_id)` | Updates `session.active_branch_id`. Returns branch with context. | No |
| `mark_branch_status(branch_id, status, reason)` | Updates status. Generates `context_summary` via `_call_ai`. | Yes — summary |
| `revive_branch(branch_id, evidence_from_branch_id, evidence_description)` | Sets status `revived`, records evidence source, prepends revival context to branch messages. | No |
| `get_branch_tree(session_id)` | Full tree with status, labels, step counts, summaries. | No |
| `build_cross_branch_context(branch_id)` | Reads `context_summary` from all sibling branches, returns formatted text. | No |

### `services/branch_aware_prompt_builder.py`

Pure function — takes data, returns assembled prompt components. No DB access, no LLM calls.

**Single method:** `build(branch, sibling_summaries, session_context, attachments, token_budget)`

Returns: `{system_base: str, rag_context: str, history: list[dict], new_message: str, images: list[dict]}`

**Important:** Return keys match `_call_ai`'s parameter names exactly. `system_base` is the stable system prompt (cached by Anthropic). `rag_context` contains the cross-branch summaries + attachment descriptions (NOT cached — changes per query). This preserves prompt caching: the base prompt is a cache hit across turns, while cross-branch context varies.

Callers invoke: `_call_ai(**builder.build(...))`.

**Data fetching responsibility:** The API endpoint (or a coordinator method `BranchManager.build_prompt_inputs(session_id, branch_id, db)`) pre-fetches all data from the DB — branch messages, sibling summaries via `build_cross_branch_context()`, session context, attachment descriptions — then passes the assembled data to `build()`. The builder itself does no DB queries.

**Assembly order:**
1. `system_base` — `ASSISTANT_SYSTEM_PROMPT` + session context (~2,000 tokens). Problem summary, domain, client info, PSA data. This is stable across turns and gets cached.
2. `rag_context` — cross-branch summaries (~3,000 token cap, prioritized: active > untried > revived > dead_end) + attachment descriptions from other branches (~1,000 tokens). This changes per query and is NOT cached.
3. Revival context — if branch was revived, prepend evidence to `rag_context`.
4. `history` — branch's `conversation_messages` minus the last user message. Last 10-15 turns verbatim, older summarized.
5. `new_message` — the current user message (latest turn).
6. `images` — image references from current branch uploads.
7. Token budget enforcement — compress in order: old messages → dead-end summaries → file content → never drop system_base, last 5 messages, branch status map.

### `services/handoff_manager.py`

Unified park/escalate with dual-write backward compatibility.

| Method | What it does | LLM call? |
|---|---|---|
| `create_handoff(session_id, intent, engineer_notes, user_id)` | Creates `SessionHandoff`. Calls `generate_snapshot()`. If escalate, calls `generate_ai_assessment()`. Dual-writes to `session.escalation_package` + `escalated_to_id`. | Escalate only |
| `generate_snapshot(session_id)` | Serializes branch tree into snapshot JSONB. Builds its own branch-aware steps-tried data (the existing `_build_escalation_package_enhanced()` is not branch-aware — it iterates all steps without branch attribution and uses a different AI call path). Follows the same general snapshot structure for compatibility with the existing escalation queue. | No |
| `generate_ai_assessment(session_id)` | Full session + branch context → diagnostic assessment. | Yes |
| `generate_briefing(handoff_id, claiming_user_id)` | Natural-language handoff summary for claiming engineer. | Yes |
| `claim_session(handoff_id, claiming_user_id)` | Updates `claimed_by/at`, sets session `active`. Dual-writes `escalation_package`. | No |
| `push_to_psa(handoff_id)` | Calls existing `psa_documentation_service`. | No |
| `get_queue(team_id, filters)` | DB query for parked + escalated sessions. | No |

### `services/resolution_output_generator.py`

Three LLM calls on resolve, each through `_call_ai`.

| Method | What it does | LLM call? |
|---|---|---|
| `generate_all(session_id)` | Creates 3 `SessionResolutionOutput` rows. | 3 calls |
| `generate_psa_notes(session_id)` | Structured ticket notes with full branch history. | Yes |
| `generate_kb_article(session_id)` | KB draft — dead-end branches become "rule out first" guidance. `structured_data` has `{symptoms, root_cause, steps, tags}`. | Yes |
| `generate_client_summary(session_id)` | Non-technical summary for end user. | Yes |
| `push_output(output_id, destination)` | Routes: `psa` → `psa_documentation_service`, `kb_library` → flow/step library, `clipboard` → returns content. | No |

### AI Description Pipeline (upload extension)

Not a new service — extends the existing upload endpoint in `uploads.py`:

1. Upload completes, response returned immediately (non-blocking).
2. Background task (via `asyncio.create_task`) calls `_call_ai` with image + prompt: "Describe this in one sentence for a troubleshooting context log."
3. Result written to `file_uploads.ai_description`.
4. For text files: extract content directly (no LLM), call `_call_ai` for summary only if content >2,000 tokens.
5. Always runs (not just branching sessions) — useful for search, exports, PSA notes.

**Safeguards:** The background task must catch and log all exceptions without crashing the upload response. If `_call_ai` fails (rate limit, timeout), `ai_description` stays NULL — the upload is still usable, just without cross-branch context. Cost is ~$0.005 per image on Sonnet (~1,650 input tokens + 50 output tokens). No additional rate limiting needed beyond the existing upload rate limit (10/minute).

---

## API Endpoints

All under existing `/api` prefix, JWT auth, team-scoped.

### Branch Management

| Method | Endpoint | Description |
|---|---|---|
| GET | `/ai-sessions/{id}/branches` | List all branches (tree structure) |
| POST | `/ai-sessions/{id}/branches/fork` | Create fork point with N branches |
| PATCH | `/ai-sessions/{id}/branches/{bid}` | Update branch status |
| POST | `/ai-sessions/{id}/branches/{bid}/switch` | Switch active branch |
| POST | `/ai-sessions/{id}/branches/{bid}/revive` | Revive dead-end with evidence |
| POST | `/ai-sessions/{id}/branches/{bid}/message` | Send message on a specific branch |

### Handoff (Park / Escalate)

| Method | Endpoint | Description |
|---|---|---|
| POST | `/ai-sessions/{id}/handoff` | Create handoff (park or escalate) |
| GET | `/ai-sessions/{id}/handoffs` | Handoff history for session |
| POST | `/ai-sessions/{id}/handoffs/{hid}/claim` | Claim a handed-off session |
| GET | `/ai-sessions/queue` | Team queue (parked + escalated) |

### Resolution Outputs

| Method | Endpoint | Description |
|---|---|---|
| POST | `/ai-sessions/{id}/resolve` | Resolve + auto-generate 3 outputs |
| GET | `/ai-sessions/{id}/outputs` | Get all resolution outputs |
| PATCH | `/ai-sessions/{id}/outputs/{oid}` | Edit output before pushing |
| POST | `/ai-sessions/{id}/outputs/{oid}/push` | Push to destination |

Note: endpoints nest under `/ai-sessions` (not `/api/v1/sessions` as the original spec proposed) to match existing routing conventions.

---

## Integration Surface — Existing Code Changes

**Critical:** The following existing code paths must check `session.is_branching` to avoid data divergence:

### `unified_chat_service.send_chat_message()`
Currently appends messages to `session.conversation_messages`. When `is_branching=True`, this must instead:
- Route the message to `session_branches[active_branch_id].conversation_messages`
- Use `BranchAwarePromptBuilder` for context assembly instead of the flat message history
- Still call `_call_ai` for the actual LLM interaction (same call path)

### `flowpilot_engine` step creation
Currently creates `AISessionStep` with no `branch_id`. When `is_branching=True`, must set `branch_id` to the active branch.

### Existing `/ai-sessions/{id}/chat` endpoint
Must detect `is_branching` and delegate to the branch message endpoint logic. Linear sessions continue through the existing path unchanged.

**Pattern:** Each integration point is a simple `if session.is_branching:` guard that delegates to branching services. The existing code path is the `else` — completely untouched. If the feature is rolled back, remove the guards and the else paths remain.

---

## Token Budget Strategy

### Budget Allocation

| Context Layer | Budget | Strategy |
|---|---|---|
| System prompt + session context | ~2,000 tokens | Fixed. Lives in `system_base` (cached). |
| Cross-branch summaries + attachment descriptions | ~4,000 tokens combined | Scales with branch count. Each branch summary ~200-500 tokens + attachment descriptions ~100 tokens each. Lives in `rag_context` (not cached). Cap at 4,000 total. |
| Current branch messages | Remaining budget | Last 10-15 turns verbatim. Older summarized. |

### Graceful Degradation (in order)

1. Summarize old current-branch messages (keep last 8-10 verbatim)
2. Trim cross-branch summaries (dead-end → one sentence)
3. Drop file content, keep descriptions
4. **Never drop:** system prompt, problem summary, last 5 messages, branch status map

### Branch Limits by Plan

| Plan | Max Branches | Rationale |
|---|---|---|
| Free | 2 | Experience branching, limit cost |
| Pro | 5 | Covers most scenarios |
| Team | 10 | Complex multi-path issues |
| Enterprise | Unlimited | |

---

## Frontend Components

| Component | Location | Description |
|---|---|---|
| `BranchMap` | `components/session/BranchMap.tsx` | Sidebar tree visualization with status badges |
| `BranchNode` | `components/session/BranchNode.tsx` | Individual node in branch map |
| `ForkCard` | `components/session/ForkCard.tsx` | In-chat fork decision point |
| `BranchTransitionBar` | `components/session/BranchTransitionBar.tsx` | Context bar on branch switch |
| `BranchRevivalCard` | `components/session/BranchRevivalCard.tsx` | Evidence card for revival |
| `HandoffModal` | `components/session/HandoffModal.tsx` | Unified park/escalate modal |
| `ResolutionOutputPanel` | `components/session/ResolutionOutputPanel.tsx` | Three-tab resolution view |
| `SessionQueuePage` | `pages/SessionQueuePage.tsx` | Team queue for parked/escalated |

| Hook | Location | Description |
|---|---|---|
| `useBranching` | `hooks/useBranching.ts` | Branch state management |
| `useHandoff` | `hooks/useHandoff.ts` | Handoff flow state |
| `useResolutionOutputs` | `hooks/useResolutionOutputs.ts` | Resolution output state |

| API Client | Location | Description |
|---|---|---|
| `branches.ts` | `api/branches.ts` | Branch API client |
| `handoffs.ts` | `api/handoffs.ts` | Handoff API client |
| `resolutions.ts` | `api/resolutions.ts` | Resolution output API client |

---

## Backend File Locations

```
backend/app/
├── models/
│   ├── session_branch.py          # SessionBranch model
│   ├── fork_point.py              # ForkPoint model
│   ├── session_handoff.py         # SessionHandoff model
│   └── session_resolution_output.py  # SessionResolutionOutput model
├── schemas/
│   ├── session_branch.py          # Pydantic schemas
│   ├── session_handoff.py         # Pydantic schemas
│   └── session_resolution.py      # Pydantic schemas
├── services/
│   ├── branch_manager.py          # Branch lifecycle
│   ├── branch_aware_prompt_builder.py  # Cross-branch context assembly
│   ├── handoff_manager.py         # Park/escalate
│   └── resolution_output_generator.py  # 3-output generator
├── api/endpoints/
│   ├── session_branches.py        # Branch API router
│   ├── session_handoffs.py        # Handoff API router
│   └── session_resolutions.py     # Resolution output API router
└── alembic/versions/
    └── xxx_add_branching_tables.py  # Single migration
```

---

## Implementation Phases

### Phase 1: Data Foundation (est. 2 days)
- Create 4 new models + Pydantic schemas
- Add columns to `ai_sessions`, `ai_session_steps`, `file_uploads`
- Manual Alembic migration (per CLAUDE.md lesson 77). Mostly additive — new tables + nullable columns. One non-additive operation: `step_type` CHECK constraint must be dropped and recreated with `'fork'` added. Use `op.drop_constraint` / `op.create_check_constraint`.
- `active_branch_id` on `ai_sessions` is a plain UUID column with no FK constraint (avoids circular FK with `session_branches`)
- Unit tests for model creation and relationships

### Phase 2: Branch Engine (est. 2-3 days)
- `BranchManager` service
- `BranchAwarePromptBuilder` service
- Branch API endpoints
- Integration with FlowPilot engine (branch_id on step creation)
- Integration tests: create → fork → explore → dead-end → switch → verify cross-branch context

### Phase 3: Handoff System (est. 1.5-2 days)
- `HandoffManager` service with dual-write
- Handoff API endpoints + queue endpoint
- PSA push integration (reuses existing service)
- Tests: park → verify snapshot. Escalate → verify assessment. Claim from queue.

### Phase 4: Resolution Outputs (est. 1.5-2 days)
- `ResolutionOutputGenerator` service
- Resolution API endpoints + push logic
- KB article generation with dead-end branch "rule out" guidance
- Tests: resolve multi-branch session → verify 3 outputs → edit → push

### Phase 5: AI Description Pipeline (est. 0.5 day)
- Extend upload endpoint with async `ai_description` generation
- Add `extracted_content` + `content_summary` for text files
- Tests: upload image → verify `ai_description` populated

### Phase 6: Frontend (est. 3-4 days)
- Branch Map sidebar (tree vis, status badges, click-to-switch)
- Fork Card component (in-chat decision point)
- Branch transition animation
- Handoff modal (unified park/escalate)
- Session queue page
- Resolution output panel (three-tab view + edit + push)
- Branch revival UI

---

## Removability Checklist

If this feature is pulled:

1. Drop tables: `session_branches`, `fork_points`, `session_handoffs`, `session_resolution_outputs`
2. Remove columns from `ai_sessions`: `is_branching`, `active_branch_id`, `handoff_count`, `total_active_seconds`, `total_parked_seconds`
3. Remove columns from `ai_session_steps`: `branch_id`, `is_fork_point`, `fork_point_id`
4. Remove columns from `file_uploads`: `uploaded_on_branch_id`, `uploaded_at_step_id` (keep `ai_description`, `extracted_content`, `content_summary` — useful independently)
5. Remove `'fork'` from step_type check constraint
6. Delete service files: `branch_manager.py`, `branch_aware_prompt_builder.py`, `handoff_manager.py`, `resolution_output_generator.py`
7. Delete endpoint files: `session_branches.py`, `session_handoffs.py`, `session_resolutions.py`
8. Delete frontend components/hooks/API clients listed above
9. Existing escalation flow, upload pipeline, chat service, PSA integration — **all untouched**
10. **Dual-write rollback note:** Sessions that were escalated via `HandoffManager` (while branching was active) will have `escalation_package` in the branching snapshot format (includes `branch_map`). The existing escalation queue UI should handle both the old flat format and the branching format gracefully — check for `branch_map` key presence. This is the one data shape difference that persists after rollback.
11. Remove `if session.is_branching:` guards from `unified_chat_service`, `flowpilot_engine`, and the chat endpoint. The else paths are the original code — unchanged.
