# 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.