Full design for branching troubleshooting workspace — extends existing infrastructure without replacing it. Additive tables, nullable columns, dual-write backward compat. Removable if feature is pulled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
25 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
- Extend, don't replace. Reuse existing tables, services, and infrastructure. No parallel code paths.
- Removable. Every branching artifact (tables, columns, services, components) can be deleted without breaking existing functionality.
- Dual-write for backward compat. New handoff system writes to both
session_handoffstable AND existingescalation_package/escalated_to_idfields until the old queue UI is fully migrated. - Branching services never touch the Anthropic SDK. They assemble context and call
_call_aifromassistant_chat_service.py— the same functionunified_chat_servicealready uses.
What This Enables
- Branch Map sidebar — live tree visualization of all diagnostic paths, with status badges (active, dead end, solved, untried) and click-to-switch.
- Fork Cards — in-chat decision points where FlowPilot suggests multiple hypotheses, each becoming a branch.
- Cross-branch AI context — when exploring Branch 3, FlowPilot knows what was tried on Branches 1 and 2.
- Branch revival — new evidence from one branch can reopen a previously dead-end branch.
- Unified handoff (park / escalate) — single snapshot mechanism with intent toggle.
- 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 | _build_escalation_package_enhanced() |
HandoffManager reuses this for snapshot generation |
| 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 FK NULLABLE | NULL | Currently viewed branch |
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.
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 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: UNIQUE(session_id, output_type), check constraints on output_type and status.
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[]) |
Creates ForkPoint + N SessionBranch rows. 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_prompt: str, history: list[dict], new_message: str, images: list[dict]}
Preserves _call_ai's cache breakpoint behavior by separating history from new message.
Assembly order:
- Session context (~2,000 tokens) — problem summary, domain, client info, PSA data
- Cross-branch summaries (~3,000 token cap) — prioritized: active > untried > revived > dead_end
- Revival context — if branch was revived, prepend evidence
- Attachment descriptions (~1,000 tokens) —
ai_descriptionfrom other branches' uploads - Branch messages (remaining budget) — last 10-15 turns verbatim, older summarized
- Token budget enforcement — compress: old messages → dead-end summaries → file content → never drop system prompt, 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. Reuses _build_escalation_package_enhanced() for steps-tried data. |
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:
- Upload completes, response returned immediately.
- Background task (via
asyncio.create_task) calls_call_aiwith image + prompt: "Describe this in one sentence for a troubleshooting context log." - Result written to
file_uploads.ai_description. - For text files: extract content directly, call
_call_aifor summary if >2,000 tokens. - Always runs (not just branching sessions) — useful for search, exports, PSA notes.
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.
Token Budget Strategy
Budget Allocation
| Context Layer | Budget | Strategy |
|---|---|---|
| System prompt + session context | ~2,000 tokens | Fixed |
| Cross-branch summaries | ~3,000 tokens | Scales with branch count. Each summary ~200-500 tokens. Cap at 3,000. |
| Current branch messages | Remaining budget | Last 10-15 turns verbatim. Older summarized. |
| Attachment descriptions | ~1,000 tokens | Included in cross-branch summaries |
Graceful Degradation (in order)
- Summarize old current-branch messages (keep last 8-10 verbatim)
- Trim cross-branch summaries (dead-end → one sentence)
- Drop file content, keep descriptions
- 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 - Single Alembic migration (all additive)
- Unit tests for model creation and relationships
Phase 2: Branch Engine (est. 2-3 days)
BranchManagerserviceBranchAwarePromptBuilderservice- 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)
HandoffManagerservice 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)
ResolutionOutputGeneratorservice- 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_descriptiongeneration - Add
extracted_content+content_summaryfor text files - Tests: upload image → verify
ai_descriptionpopulated
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:
- Drop tables:
session_branches,fork_points,session_handoffs,session_resolution_outputs - Remove columns from
ai_sessions:is_branching,active_branch_id,handoff_count,total_active_seconds,total_parked_seconds - Remove columns from
ai_session_steps:branch_id,is_fork_point,fork_point_id - Remove columns from
file_uploads:uploaded_on_branch_id,uploaded_at_step_id(keepai_description,extracted_content,content_summary— useful independently) - Remove
'fork'from step_type check constraint - Delete service files:
branch_manager.py,branch_aware_prompt_builder.py,handoff_manager.py,resolution_output_generator.py - Delete endpoint files:
session_branches.py,session_handoffs.py,session_resolutions.py - Delete frontend components/hooks/API clients listed above
- Existing escalation flow, upload pipeline, chat service, PSA integration — all untouched