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>
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
- 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 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:
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.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.- Revival context — if branch was revived, prepend evidence to
rag_context. history— branch'sconversation_messagesminus the last user message. Last 10-15 turns verbatim, older summarized.new_message— the current user message (latest turn).images— image references from current branch uploads.- 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:
- Upload completes, response returned immediately (non-blocking).
- 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 (no LLM), call
_call_aifor summary only if content >2,000 tokens. - 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
BranchAwarePromptBuilderfor context assembly instead of the flat message history - Still call
_call_aifor 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)
- 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 - Manual Alembic migration (per CLAUDE.md lesson 77). Mostly additive — new tables + nullable columns. One non-additive operation:
step_typeCHECK constraint must be dropped and recreated with'fork'added. Useop.drop_constraint/op.create_check_constraint. active_branch_idonai_sessionsis a plain UUID column with no FK constraint (avoids circular FK withsession_branches)- 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
- Dual-write rollback note: Sessions that were escalated via
HandoffManager(while branching was active) will haveescalation_packagein the branching snapshot format (includesbranch_map). The existing escalation queue UI should handle both the old flat format and the branching format gracefully — check forbranch_mapkey presence. This is the one data shape difference that persists after rollback. - Remove
if session.is_branching:guards fromunified_chat_service,flowpilot_engine, and the chat endpoint. The else paths are the original code — unchanged.