feat(ai-session): add Phase 2 PSA integration, escalation handoff, and session management

Phase 2 of the FlowPilot-First Pivot connecting AI sessions to ConnectWise PSA:

Slice 1 — PSA Ticket Intake:
- FlowPilotEngine accepts psa_ticket intake with graceful CW API fallback
- Ticket picker on intake screen (refactored TicketPickerModal for dual-mode)
- Ticket context card in session sidebar

Slice 2 — Auto Documentation Push:
- PSA documentation service with resolution/escalation note formatting
- Time entry creation via new ConnectWise provider method
- Automatic retry scheduler (APScheduler, 5min interval, 3 retries)
- PSA push status indicators in frontend with manual retry button
- Member mapping warning when CW member not mapped

Slice 3 — Session Pause/Resume & Escalation Handoff:
- Pause/resume endpoints for same-engineer session bookmarking
- Escalation flow: requesting_escalation status, self-escalation blocked
- Enhanced escalation package with LLM-generated hypotheses/suggestions
- Pickup endpoint with continue/fresh resume modes and briefing step
- Escalation queue (sidebar nav + dedicated page)
- SessionBriefing component with continue/fresh choice UI
- EscalateModal with PSA-aware button text

Slice 4 — Mid-Session Ticket Linking:
- Link ticket retroactively with context injection into system prompt
- Link Ticket button in session sidebar

Slice 5 — FlowPilot PSA Settings:
- Settings tab on IntegrationsPage with 7 configurable options
- Stored as flowpilot_settings JSONB on PsaConnection

Database: 2 migrations (flowpilot_settings, psa_post_log changes, status constraint)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 01:30:05 +00:00
parent 2063a799b0
commit bbe590bfec
37 changed files with 3698 additions and 121 deletions

View File

@@ -27,6 +27,8 @@ export interface UseFlowPilotSession {
respondToStep: (response: StepResponseRequest) => Promise<void>
resolveSession: (data: ResolveSessionRequest) => Promise<SessionDocumentation>
escalateSession: (data: EscalateSessionRequest) => Promise<SessionDocumentation>
pauseSession: () => Promise<void>
resumeOwnSession: () => Promise<void>
rateSession: (rating: number, feedback?: string) => Promise<void>
loadSession: (sessionId: string) => Promise<void>
@@ -37,6 +39,9 @@ export interface UseFlowPilotSession {
// Post-close
documentation: SessionDocumentation | null
psaPushStatus: string | null
psaPushError: string | null
memberMappingWarning: string | null
}
export function useFlowPilotSession(): UseFlowPilotSession {
@@ -47,6 +52,9 @@ export function useFlowPilotSession(): UseFlowPilotSession {
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState<string | null>(null)
const [documentation, setDocumentation] = useState<SessionDocumentation | null>(null)
const [psaPushStatus, setPsaPushStatus] = useState<string | null>(null)
const [psaPushError, setPsaPushError] = useState<string | null>(null)
const [memberMappingWarning, setMemberMappingWarning] = useState<string | null>(null)
const startSession = useCallback(async (intake: AISessionCreateRequest) => {
setIsLoading(true)
@@ -73,6 +81,9 @@ export function useFlowPilotSession(): UseFlowPilotSession {
resolution_action: null,
escalation_reason: null,
session_feedback: null,
psa_ticket_id: intake.psa_ticket_id ?? null,
psa_connection_id: intake.psa_connection_id ?? null,
ticket_data: null,
steps: [firstStep],
})
setAllSteps([firstStep])
@@ -120,6 +131,9 @@ export function useFlowPilotSession(): UseFlowPilotSession {
const result = await aiSessionsApi.resolveSession(session.id, data)
setSession(prev => prev ? { ...prev, status: 'resolved' } : null)
setDocumentation(result.documentation)
setPsaPushStatus(result.psa_push_status)
setPsaPushError(result.psa_push_error)
setMemberMappingWarning(result.member_mapping_warning)
setCurrentStep(null)
toast.success('Session resolved')
return result.documentation
@@ -139,6 +153,9 @@ export function useFlowPilotSession(): UseFlowPilotSession {
const result = await aiSessionsApi.escalateSession(session.id, data)
setSession(prev => prev ? { ...prev, status: 'escalated' } : null)
setDocumentation(result.documentation)
setPsaPushStatus(result.psa_push_status)
setPsaPushError(result.psa_push_error)
setMemberMappingWarning(result.member_mapping_warning)
setCurrentStep(null)
toast.success('Session escalated')
return result.documentation
@@ -151,6 +168,28 @@ export function useFlowPilotSession(): UseFlowPilotSession {
}
}, [session])
const pauseSession = useCallback(async () => {
if (!session) return
try {
await aiSessionsApi.pauseSession(session.id)
setSession(prev => prev ? { ...prev, status: 'paused' } : null)
toast.success('Session paused')
} catch {
toast.error('Failed to pause session')
}
}, [session])
const resumeOwnSession = useCallback(async () => {
if (!session) return
try {
await aiSessionsApi.resumeSession(session.id)
setSession(prev => prev ? { ...prev, status: 'active' } : null)
toast.success('Session resumed')
} catch {
toast.error('Failed to resume session')
}
}, [session])
const rateSession = useCallback(async (rating: number, feedback?: string) => {
if (!session) return
try {
@@ -169,7 +208,12 @@ export function useFlowPilotSession(): UseFlowPilotSession {
const detail = await aiSessionsApi.getSession(sessionId)
setSession(detail)
setAllSteps(detail.steps)
setCurrentStep(detail.status === 'active' ? detail.steps[detail.steps.length - 1] ?? null : null)
// Set current step for active and paused sessions (paused can be resumed)
setCurrentStep(
detail.status === 'active' || detail.status === 'paused'
? detail.steps[detail.steps.length - 1] ?? null
: null
)
if (detail.status === 'resolved' || detail.status === 'escalated') {
const doc = await aiSessionsApi.getDocumentation(sessionId)
@@ -199,11 +243,16 @@ export function useFlowPilotSession(): UseFlowPilotSession {
respondToStep,
resolveSession,
escalateSession,
pauseSession,
resumeOwnSession,
rateSession,
loadSession,
isActive,
canResolve,
canEscalate,
documentation,
psaPushStatus,
psaPushError,
memberMappingWarning,
}
}