Files
resolutionflow/docs/superpowers/specs/2026-03-24-conversational-branching-design.md
chihlasm 3309b6d1ce docs: fix 12 review issues in branching design spec
Critical fixes:
- _call_ai signature: return keys now match params (system_base, rag_context)
- Cross-branch context goes in rag_context (not system_base) to preserve caching
- HandoffManager builds own branch-aware snapshot (not reusing non-branch-aware fn)
- Added Integration Surface section: unified_chat_service + flowpilot_engine
  must check is_branching to avoid data divergence

Major fixes:
- active_branch_id: plain UUID, no FK (avoids circular FK)
- Resolution outputs: upsert on regeneration, not unique violation
- AI description pipeline: error handling + cost safeguards documented
- create_fork: pre-generates branch UUIDs for single-transaction insert

Minor fixes:
- status_changed_by FK target specified (users.id)
- Token budget: attachment descriptions inside cross-branch cap (4K combined)
- BranchAwarePromptBuilder: caller responsibility documented
- Removability: dual-write escalation_package shape divergence noted

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 07:46:39 +00:00

30 KiB

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_baseASSISTANT_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: psapsa_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.