Records 029680a — every escalation now funnels through HandoffManager
regardless of which URL it entered through, so /escalate from
EscalateModal produces the full set of artifacts (handoff row,
AppNotification, SSE event, Slack/Teams via notify, per-user emails,
documentation, PSA push) and the bell-icon notification opens the
magic-moment screen end-to-end. Notes the legacy SessionBriefing branch
+ flowpilot_engine.escalate_session as orphaned, scheduled for removal
after pilots have run a couple of weeks on the unified path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 KiB
HANDOFF.md
Last updated: 2026-04-27 22:30 EDT
Active task: Escalation Mode wedge build. See CURRENT_TASK.md for the full status; this file holds the resume point only.
Branch: feat/escalation-metric-endpoint — pushed (latest: 029680a). Draft PR #155 open against main (gitea.resolutionflow.com/chihlasm/resolutionflow/pulls/155). Wedge is feature-complete pending visual QA + the deferred follow-ups in CURRENT_TASK.md. /escalate and /handoff are unified — every escalation goes through HandoffManager and produces the full set of artifacts (handoff row, AppNotification, SSE bus event, Slack/Teams via notify(), per-user emails, documentation, PSA push) regardless of which URL it entered through.
Status
Previous session shipped the two remaining frontend slices: live-arrival SSE subscription in EscalationQueue.tsx, and the magic-moment HandoffContextScreen for senior pickup.
What landed (commits added to the branch):
b8627f4feat(escalations): subscribe EscalationQueue to live SSE arrivals —streamEscalationsinaiSessions.ts(fetch-basedReadableStreamparser; nativeEventSourcecan't send auth headers);HandoffCreatedEvent+EscalationStreamHandlerstypes;EscalationQueue.tsxrewrite withAbortController-managed subscription, exponential-backoff reconnect (1s → 30s cap, resets onready), prepend-on-arrival with locked 200ms slide-in, tab-title(N)prefix whiledocument.hidden,prefers-reduced-motionswap, ARIA live region.f65b657docs(ai): handoff state after frontend SSE slice lands.8e9d22efeat(escalations): magic-moment handoff-context screen on pickup — newHandoffContextScreen.tsx(4 sections; renders gracefully whenai_assessmentis null per the 5s timeout from9bdd995; ARIA dialog + focus on primary CTA + Esc dismiss for re-open overlay;prefers-reduced-motionhonored).FlowPilotSessionPage.tsxintegration: on?pickup=true, fetch the handoff list first (account-scoped via RLS, no claim required), find the latest unclaimed escalate handoff, render the screen and skiploadSession(senior would 404 pre-claim). "Start here" callsclaimHandoff, drops the pickup query, and dismisses —loadSessionthen fires because senior is nowescalated_to_id. Toolbar "Context" button on active sessions re-opens the screen as a dismissible overlay (visible only when senior arrived via the magic-moment flow this session).c194ba4docs(ai): handoff state after magic-moment screen lands.641853afix(escalations): bell-icon notification opens the pickup flow —_build_notification_linkforsession.escalatednow ends with?pickup=trueso notification clicks route through the senior-pickup flow.GET /ai-sessions/{id}now allows account-scoped read forrequesting_escalation/escalatedstatus (RLS already enforces tenant boundary; the owner-only guard was overly restrictive for explicitly-shared in-transit states). Without these two fixes the user observed bell-icon clicks "just clearing the notification" — the navigation was happening but landing on a 404 the senior couldn't escape from.2a2329adocs(ai): handoff state after bell-icon fix; record draft PR #155.029680afeat(escalations): unify/escalatethroughHandoffManager— single canonical path for every escalation.HandoffCreateRequest.target_user_idadded (rejects self-targeting).HandoffManager.create_handofffor intent='escalate' now setssession.escalation_reason+escalated_to_id, builds the legacy AI-enhanced escalation_package via Sonnet (lazy-import from flowpilot_engine, graceful fallback), and merges handoff metadata into it; eager-loadssession.steps+session.userto dodge async lazy-load greenlet errors. NewHandoffManager.finalize_escalationruns_generate_documentation+_push_to_psa+notify()pre-commit so the AppNotification rows and PSA writes land atomically with the handoff.dispatch_escalation_notificationskeeps only fire-and-forget IO (bus publish + per-user emails) post-commit. The/escalateendpoint is a thin shim: owner-only session lookup →create_handoff(intent='escalate')→finalize_escalation→ commit →dispatch_escalation_notifications→ returnSessionCloseResponse.flowpilot_engine.escalate_sessionis no longer called by any endpoint.pickup_sessionaccepts bothrequesting_escalationandescalatedfor in-flight migration. Escalation queue list + sidebar count match either status.
Verified:
tsc -bexit 0 after each frontend commit.- Full backend test suite after unification:
1103 passed in 259.63swith-n auto. - Live SSE handshake against the running dev stack: 200 +
text/event-stream;readyframe on connect;handoff_createdframe with full payload arrived after posting a handoff via the API. Wire format matches the parser exactly. - Live claim flow against the running dev stack:
listHandoffsreturns the unclaimed handoff for a senior pre-claim;claimHandoffflips session status fromescalated→activeand setsescalated_to_id; subsequentGET /ai-sessions/{id}succeeds. - Live access-policy verification: senior (non-owner, non-target) can now
GETan in-transit escalated session detail. - Live unification verification: a single legacy
/escalatecall from a junior produced status='escalated', aSessionDocumentation, aSessionHandoffrow, an attempted PSA push (no_psasince no ticket linked), AND anAppNotificationrow for the team admin with title "Session escalated by Jordan Tech" and link/pilot/{session_id}?pickup=true. The bell-icon click now lands the senior in the magic-moment flow with the actual handoff data.
Not yet verified (would need a real browser session): the slide-in animation visually plays, tab title actually updates, reduced-motion media-query path renders, AbortController cleanup on unmount, exponential backoff after a real network blip, the magic-moment screen layout/typography looks right, dissolve transition feels right. Wire contract + integration semantics are confirmed; visuals are next.
Smoke-test artifact: a single test handoff (0f6149db… on session 50ea20d4…) was claimed during verification and is now an active session owned by the engineer test user. Harmless; useful as visual demo data.
Resume point
- Visual QA via
/qaagainst the dev stack. End-to-end demo flow: junior escalates via EscalateModal → senior gets bell-icon notification → senior clicks the notification (now routes through?pickup=true) → magic-moment screen renders with the rich handoff data → Start here → FlowPilot session view loads. Also: open/escalationsas senior with a second session escalating in the background, watch the slide-in + tab-title flash. The PR description has a checklist mirroring this. - Pick up the deferred follow-ups in
CURRENT_TASK.md. Highest-leverage: suggested-step chips below the chat input (Codex correction, locked design — needs threading throughFlowPilotSession→FlowPilotMessageBar). Next:HandoffManager._generate_snapshotexpansion to include the recent diagnostic timeline pre-claim — though this is lower-priority now that the unified path already merges the legacy enriched escalation_package into the dual-write, so the magic-moment screen has access tosteps_tried/remaining_hypotheses/suggested_next_stepsonce it's wired to read them. - Optional v1: owner-facing
/analytics/escalationspage; Playwright e2e for the GTM Loom demo path. - Eventual cleanup:
flowpilot_engine.escalate_sessionis no longer called by any endpoint and could be deleted; the legacySessionBriefingrender branch inFlowPilotSessionPage.tsxis effectively dead code for any new escalation (magic-moment takes over) but still useful for in-flight legacyrequesting_escalationsessions during the transition window. Both can come out after pilots have run a couple of weeks on the unified path.
Useful breadcrumbs
- SSE endpoint:
backend/app/api/endpoints/session_handoffs.py—stream_escalations. - Pub/sub bus:
backend/app/core/escalation_bus.py. - Frontend SSE consumer:
frontend/src/api/aiSessions.ts→streamEscalations. - Live-arrival queue UI:
frontend/src/components/flowpilot/EscalationQueue.tsx. - Magic-moment screen:
frontend/src/components/flowpilot/HandoffContextScreen.tsx. - Pickup integration:
frontend/src/pages/FlowPilotSessionPage.tsx—magicState,handleStartHere,openHandoffContextOverlay. - Notification dispatch:
backend/app/services/handoff_manager.py—dispatch_escalation_notifications. - Metric endpoint:
backend/app/api/endpoints/flowpilot_analytics.py.
Watch-outs
- Do not reintroduce
client.stream()/ASGITransport tests for infinite SSE responses; test the generator directly. - The bus is acceptable for v1 pilot scale only because Railway is single-replica. Redis pub/sub is the obvious swap when horizontal scaling appears.
streamEscalationsdoesn't drive token refresh on a mid-stream 401 — the Axios interceptor only covers axios calls. Acceptable for v1.- The handoff snapshot today is sparse (
problem_summary, problem_domain, status, step_count, confidence_tierplus optional branch info). The magic-moment screen's "What's been tried" section currently shows engineer notes + step-count affordance, not the actual step timeline. Snapshot expansion is the right fix. HandoffResponse.ai_assessment_data.confidenceis typednumberon the frontend but the backend currently emits'low' | 'medium' | 'high'strings. TheConfidenceBadgecomponent handles both shapes at runtime; the type definition is stale and should be widened tonumber | 'low' | 'medium' | 'high'.- The toolbar "Context" button is hidden on revisited active sessions where the senior didn't arrive via magic-moment this session — known scope cut. Lazy-fetching handoff list on session-load (when status was previously
escalated) is the cleanup.