docs(ai): handoff state after magic-moment screen lands
Marks the magic-moment handoff-context screen as shipped, points the next session at visual QA + push + draft PR, and captures the deferred follow-ups (suggested-step chips, snapshot expansion, toolbar button on revisits, owner analytics, Playwright e2e). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
**Task:** Build **Escalation Mode** — the wedge for ResolutionFlow's GTM (first paying-customer push). When a junior tech escalates a FlowPilot session, the senior tech sees structured handoff context in seconds instead of running a 5-minute verbal "tell me what you tried" call.
|
||||
|
||||
**Status:** in-flight on `feat/escalation-metric-endpoint`. Backend is **feature-complete and test-stabilized**. **Frontend SSE subscription is shipped** (`EscalationQueue.tsx` now subscribes via fetch-based ReadableStream, prepends new arrivals with the locked 200ms slide-in, flashes tab title when backgrounded, respects `prefers-reduced-motion`, exponential-backoff reconnect). **Next:** magic-moment handoff-context screen, then push + draft PR.
|
||||
**Status:** in-flight on `feat/escalation-metric-endpoint`. Backend is **feature-complete and test-stabilized**. **Frontend live-arrival SSE subscription is shipped** (`EscalationQueue.tsx` subscribes via fetch-based ReadableStream, prepends new arrivals with the locked 200ms slide-in, flashes tab title when backgrounded, respects `prefers-reduced-motion`, exponential-backoff reconnect). **Magic-moment handoff-context screen is shipped** (`HandoffContextScreen.tsx` + integration in `FlowPilotSessionPage.tsx` — renders on Pick Up before claim, claims on "Start here", re-openable from toolbar, gracefully handles null AI assessment). **Next:** push + draft PR, then optional analytics page + Playwright e2e + chat-input suggested-step chips.
|
||||
|
||||
**Plan:** [`docs/plans/2026-04-27-escalation-mode-wedge-design.md`](../docs/plans/2026-04-27-escalation-mode-wedge-design.md). Reviewed by `/office-hours`, `/plan-eng-review`, `/plan-design-review`, `/codex review`. Eng + Design CLEARED. Codex's two-metric correction + claim role gate + per-channel notification model + SSE bus diagnostics all applied.
|
||||
|
||||
@@ -22,15 +22,20 @@
|
||||
| `bc15952` | Codex: stabilize SSE backend tests — `Depends(..., scope="function")` releases auth DB deps before the long-lived stream body; SSE handshake test calls the generator directly; AI-assessment stub fixture; bus normalizes string vs UUID account_id |
|
||||
| `fff8338` | Doc-only: track escalation assessment latency follow-up |
|
||||
| `9bdd995` | Bound escalation assessment latency to `ESCALATION_AI_ASSESSMENT_TIMEOUT_SECONDS` (default 5s); handoff still creates if assessment times out |
|
||||
| _pending_ | Frontend SSE subscription in `EscalationQueue.tsx` — fetch-based `ReadableStream` reader, `handoff_created` triggers refetch + prepend with locked 200ms slide-in, exponential-backoff reconnect, tab-title flash when backgrounded, `prefers-reduced-motion` honored, ARIA live-region |
|
||||
| `b8627f4` | Frontend SSE subscription in `EscalationQueue.tsx` — fetch-based `ReadableStream` reader; `handoff_created` triggers refetch + prepend with locked 200ms slide-in; exponential-backoff reconnect; tab-title flash when backgrounded; `prefers-reduced-motion` honored; ARIA live-region |
|
||||
| `f65b657` | Handoff state docs after frontend SSE slice lands |
|
||||
| `8e9d22e` | Magic-moment handoff-context screen on pickup — `HandoffContextScreen.tsx` (4 sections, graceful null AI assessment, focus management, prefers-reduced-motion); `FlowPilotSessionPage.tsx` integration (pre-claim handoff fetch, claim on Start here, toolbar re-open overlay) |
|
||||
|
||||
**Test status:** focused subset (`test_escalation_bus`, `test_handoff_manager`, `test_session_handoffs_api`, `test_flowpilot_analytics_escalations`) → `32 passed in 18.91s` with `-n auto`. Frontend `tsc -b` clean. Live-arrival smoke test against the running dev stack confirmed the SSE handshake delivers the `ready` frame on connect and a `handoff_created` frame with the expected payload after posting a handoff. Branch not pushed.
|
||||
**Test status:** focused subset (`test_escalation_bus`, `test_handoff_manager`, `test_session_handoffs_api`, `test_flowpilot_analytics_escalations`) → `32 passed in 18.91s` with `-n auto`. Frontend `tsc -b` clean. End-to-end smoke test against the running dev stack confirmed: SSE handshake delivers `ready` frame on connect and `handoff_created` after a posted handoff; `listHandoffs` returns the unclaimed handoff for a senior pre-claim; `claimHandoff` flips session status from `escalated` → `active` and `escalated_to_id` is set so subsequent GET succeeds. Branch not pushed.
|
||||
|
||||
## Remaining work on this branch
|
||||
|
||||
1. **Magic-moment handoff-context screen** — 4-section view (problem summary / what's been tried / AI assessment / Start here CTA) that loads on Pick Up before dissolving into the regular FlowPilot session view. ~1.5-2 days. Must render gracefully when `ai_assessment` is `None` (assessment timed out — see `9bdd995`).
|
||||
2. **Owner-facing analytics page** at `/analytics/escalations` — period selector, conversion-rate, trend chart. ~0.5d. Optional for v1 demo.
|
||||
3. **Playwright e2e** for the magic-moment demo flow (junior escalates → senior receives → senior claims → opens session). Critical for the GTM Loom not to crash mid-recording.
|
||||
1. **Push + draft PR** — branch is unpushed. Open against `main`.
|
||||
2. **Suggested-step chips below the chat input** (Codex correction, design plan locks this) — surfaces `ai_assessment_data.suggested_steps[]` as clickable chips in `FlowPilotMessageBar` that prefill the input. Threading through `FlowPilotSession` → message bar.
|
||||
3. **Snapshot expansion in `HandoffManager._generate_snapshot`** — include the recent diagnostic steps / conversation tail so the magic-moment screen's "What's been tried" section can render the actual timeline pre-claim instead of "full timeline available after pickup".
|
||||
4. **Toolbar Context button on legacy-arrival sessions** — currently the button only appears when the senior arrived via the magic-moment flow this session. Lazy-fetching the handoff list on session-load (when status was-escalated) would make it work on revisits.
|
||||
5. **Owner-facing analytics page** at `/analytics/escalations` — period selector, conversion-rate, trend chart. ~0.5d. Optional for v1 demo.
|
||||
6. **Playwright e2e** for the magic-moment demo flow (junior escalates → senior receives via SSE → senior claims → opens session). Critical for the GTM Loom not to crash mid-recording.
|
||||
|
||||
## Two-metric framing — read this before quoting numbers to anyone
|
||||
|
||||
|
||||
@@ -2,51 +2,56 @@
|
||||
|
||||
# HANDOFF.md
|
||||
|
||||
**Last updated:** 2026-04-27 21:00 EDT
|
||||
**Last updated:** 2026-04-27 21:30 EDT
|
||||
|
||||
**Active task:** **Escalation Mode** wedge build. See [`CURRENT_TASK.md`](CURRENT_TASK.md) for the full status; this file holds the resume point only.
|
||||
|
||||
**Branch:** `feat/escalation-metric-endpoint` — frontend SSE live-arrival slice is shipped on top of the test-stabilized backend.
|
||||
**Branch:** `feat/escalation-metric-endpoint` — frontend live-arrival SSE slice + magic-moment handoff-context screen are both shipped on top of the test-stabilized backend. Branch is unpushed.
|
||||
|
||||
## Status
|
||||
|
||||
Previous session shipped the frontend SSE subscription that the next session was set up to do.
|
||||
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:
|
||||
What landed (commits added to the branch):
|
||||
|
||||
- `frontend/src/api/aiSessions.ts` — added `streamEscalations(handlers, signal)`. Fetch-based `ReadableStream` parser (native `EventSource` can't send auth headers). Handles SSE frames including `: keepalive` heartbeats. Dispatches `ready` and `handoff_created` events.
|
||||
- `frontend/src/types/ai-session.ts` — added `HandoffCreatedEvent` and `EscalationStreamHandlers` types mirroring the backend bus payload.
|
||||
- `frontend/src/components/flowpilot/EscalationQueue.tsx` — full data-layer rewrite. SSE subscription with `AbortController`, exponential-backoff reconnect (1s → 30s cap, attempt counter resets on `ready`). On `handoff_created` the component refetches the queue, diffs against the previous IDs via a `sessionsRef`, prepends new arrivals (newest-first) above established cards (oldest-first preserved). New IDs tagged for 800ms so the locked 200ms slide-in animation plays before cleanup. Tab-title flash captures `document.title` at mount, prefixes `(N)` while `document.hidden`, clears on `focus` / `visibilitychange`, restores on unmount. `prefers-reduced-motion: reduce` swaps `animate-slide-in-bottom` for `animate-fade-in`. ARIA: `role="region"` + `aria-live="polite"` on the list, `aria-label="N escalations awaiting pickup"` on the heading. Pick Up button bumped to `py-2.5` to clear the 44px touch floor.
|
||||
- `b8627f4` feat(escalations): subscribe EscalationQueue to live SSE arrivals — `streamEscalations` in `aiSessions.ts` (fetch-based `ReadableStream` parser; native `EventSource` can't send auth headers); `HandoffCreatedEvent` + `EscalationStreamHandlers` types; `EscalationQueue.tsx` rewrite with `AbortController`-managed subscription, exponential-backoff reconnect (1s → 30s cap, resets on `ready`), prepend-on-arrival with locked 200ms slide-in, tab-title `(N)` prefix while `document.hidden`, `prefers-reduced-motion` swap, ARIA live region.
|
||||
- `f65b657` docs(ai): handoff state after frontend SSE slice lands.
|
||||
- `8e9d22e` feat(escalations): magic-moment handoff-context screen on pickup — new `HandoffContextScreen.tsx` (4 sections; renders gracefully when `ai_assessment` is null per the 5s timeout from `9bdd995`; ARIA dialog + focus on primary CTA + Esc dismiss for re-open overlay; `prefers-reduced-motion` honored). `FlowPilotSessionPage.tsx` integration: 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 skip `loadSession` (senior would 404 pre-claim). "Start here" calls `claimHandoff`, drops the pickup query, and dismisses — `loadSession` then fires because senior is now `escalated_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).
|
||||
|
||||
Verified:
|
||||
|
||||
- Frontend `tsc -b` exit 0. Vite HMR'd the new file with no compile errors.
|
||||
- Backend regression: focused subset (`test_escalation_bus`, `test_handoff_manager`, `test_session_handoffs_api`, `test_flowpilot_analytics_escalations`) → `32 passed in 18.91s` with `-n auto`.
|
||||
- Live SSE handshake against the running dev stack returns 200 with `text/event-stream; charset=utf-8` and the locked headers (`cache-control: no-cache`, `x-accel-buffering: no`). Subscriber received the `ready` frame on connect; after posting a handoff via the API, the subscriber received the `handoff_created` frame with the full payload — wire format matches the new parser exactly.
|
||||
- `tsc -b` exit 0 after each commit.
|
||||
- Backend regression: focused subset still `32 passed in 18.91s` with `-n auto`. No backend changes in this session.
|
||||
- Live SSE handshake against the running dev stack: 200 + `text/event-stream`; `ready` frame on connect; `handoff_created` frame 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: `listHandoffs` returns the unclaimed handoff for a senior pre-claim; `claimHandoff` flips session status from `escalated` → `active` and sets `escalated_to_id`; subsequent `GET /ai-sessions/{id}` succeeds.
|
||||
|
||||
Not yet verified (would need a real browser session): the slide-in animation visually plays, the tab title actually updates, the reduced-motion media-query path, AbortController cancellation on unmount, backoff after a real network blip. Wire contract is confirmed; these are visual/timing-dependent and follow from correct parser + state machine.
|
||||
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…`) is sitting in the engineer's queue from the verification step. Harmless; useful as visual demo data.
|
||||
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
|
||||
|
||||
1. Build the **magic-moment handoff-context screen**: 4 sections (problem summary / what's been tried / AI assessment / Start here CTA), loads on Pick Up, then dissolves into the regular FlowPilot session view. Must render gracefully when `ai_assessment` is `None` (assessment timed out — see commit `9bdd995`). Surface `ai_assessment_data.suggested_steps[]` as chips below the chat input that prefill it on click — do NOT invent a "jump to most-likely-next-step" capability that doesn't exist in the session model.
|
||||
2. Push the branch and open a draft PR once the magic-moment screen is in.
|
||||
3. Optional v1: owner-facing `/analytics/escalations` page (period selector + conversion rate + trend chart).
|
||||
1. **Visual QA the two new frontend slices in a real browser.** Open `/escalations` as a senior, escalate from a separate session/tab, watch the slide-in + tab-title flash. Then click Pick Up and walk through the magic-moment screen → Start here → confirm the FlowPilot view loads cleanly. The `/qa` skill is the right tool.
|
||||
2. **Push the branch and open a draft PR** against `main`. Title: "Escalation Mode wedge". Body: link the design + test-plan artifacts in `docs/plans/`.
|
||||
3. **Pick up the deferred follow-ups** in `CURRENT_TASK.md` — the highest-leverage one is the suggested-step chips below the chat input (Codex correction, locked in design). The `HandoffManager._generate_snapshot` expansion to include recent steps/conversation is the next-highest leverage so the magic-moment screen can show the diagnostic timeline pre-claim.
|
||||
4. Optional v1: owner-facing `/analytics/escalations` page; Playwright e2e for the GTM Loom demo path.
|
||||
|
||||
## Useful breadcrumbs
|
||||
|
||||
- SSE endpoint: [`backend/app/api/endpoints/session_handoffs.py`](../backend/app/api/endpoints/session_handoffs.py) — `stream_escalations`.
|
||||
- Pub/sub bus: [`backend/app/core/escalation_bus.py`](../backend/app/core/escalation_bus.py). In-memory, account-scoped, non-durable, 64-event per-subscriber queue, drop-on-full.
|
||||
- Pub/sub bus: [`backend/app/core/escalation_bus.py`](../backend/app/core/escalation_bus.py).
|
||||
- Frontend SSE consumer: [`frontend/src/api/aiSessions.ts`](../frontend/src/api/aiSessions.ts) → `streamEscalations`.
|
||||
- Live-arrival queue UI: [`frontend/src/components/flowpilot/EscalationQueue.tsx`](../frontend/src/components/flowpilot/EscalationQueue.tsx).
|
||||
- Notification dispatch: [`backend/app/services/handoff_manager.py`](../backend/app/services/handoff_manager.py) — `dispatch_escalation_notifications`, called after `db.commit()` in the handoff endpoint.
|
||||
- Metric endpoint: [`backend/app/api/endpoints/flowpilot_analytics.py`](../backend/app/api/endpoints/flowpilot_analytics.py) — `get_escalation_metrics`.
|
||||
- Magic-moment screen: [`frontend/src/components/flowpilot/HandoffContextScreen.tsx`](../frontend/src/components/flowpilot/HandoffContextScreen.tsx).
|
||||
- Pickup integration: [`frontend/src/pages/FlowPilotSessionPage.tsx`](../frontend/src/pages/FlowPilotSessionPage.tsx) — `magicState`, `handleStartHere`, `openHandoffContextOverlay`.
|
||||
- Notification dispatch: [`backend/app/services/handoff_manager.py`](../backend/app/services/handoff_manager.py) — `dispatch_escalation_notifications`.
|
||||
- Metric endpoint: [`backend/app/api/endpoints/flowpilot_analytics.py`](../backend/app/api/endpoints/flowpilot_analytics.py).
|
||||
|
||||
## Watch-outs
|
||||
|
||||
- Do not reintroduce `client.stream()`/ASGITransport tests for infinite SSE responses; test the generator directly or use a real server-level test.
|
||||
- `DROP SCHEMA public CASCADE` per test is still the dominant cost: DB-backed tests spend ~1.7-2.8s in setup. Use `-n auto` for focused backend loops.
|
||||
- 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.
|
||||
- Escalation assessment can be missing when the 5s timeout fires. The handoff-context UI must render a graceful "assessment unavailable/in progress" state rather than treating it as required.
|
||||
- `streamEscalations` doesn't drive token refresh on a mid-stream 401 — the Axios interceptor only covers axios calls. Acceptable for v1 (queue page lifetime ≤ access-token lifetime in practice); revisit if pilots leave the page open for hours.
|
||||
- `streamEscalations` doesn'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_tier` plus 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.confidence` is typed `number` on the frontend but the backend currently emits `'low' | 'medium' | 'high'` strings. The `ConfidenceBadge` component handles both shapes at runtime; the type definition is stale and should be widened to `number | '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.
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-27 21:30 EDT — Claude Code — Escalation Mode: magic-moment handoff-context screen on pickup
|
||||
|
||||
- Continued the same session that shipped the live-arrival SSE subscription. Added the magic-moment screen on top.
|
||||
- New `frontend/src/components/flowpilot/HandoffContextScreen.tsx`: presentational 4-section view (header with problem summary + domain + step count + escalated-time + priority badge; "What's been tried" with engineer notes + step-count affordance; "AI assessment" with likely_cause / suggested_steps / confidence badge; "Start here" CTA). Confidence badge accepts both numeric (0..1) and string ("low"/"medium"/"high") shapes — backend emits the latter, the frontend type says `number`, runtime handles both. Renders an explicit "assessment unavailable — model didn't respond in time" branch when `ai_assessment_data` is null (the 5s timeout from `9bdd995` fired). `prefers-reduced-motion` swaps `animate-slide-up` for `animate-fade-in`. ARIA `role=dialog` + `aria-modal=true` + focus on primary CTA on mount + Esc dismiss when used as a re-openable overlay.
|
||||
- Integration in `frontend/src/pages/FlowPilotSessionPage.tsx`: on `/pilot/:id?pickup=true`, fetch the handoff list via `handoffsApi.listHandoffs` (account-scoped via RLS, no claim required) and find the latest unclaimed escalate handoff. If found, render the screen and skip `loadSession` (the senior would 404 pre-claim because they aren't yet `escalated_to_id`). "Start here" calls `handoffsApi.claimHandoff`, drops the `?pickup=true` query, and dismisses the screen — the existing `loadSession` effect then fires because the senior is now `escalated_to_id`. New "Context" toolbar button on active sessions (visible only when the senior arrived via the magic-moment flow this session — handoff lookup on demand) re-opens the screen as a dismissible overlay.
|
||||
- Verified end-to-end against the running dev stack: `listHandoffs` returns the unclaimed handoff with full payload (engineer_notes, snapshot keys); `claimHandoff` flips session status from `escalated` → `active` and sets `escalated_to_id`; subsequent `GET /ai-sessions/{id}` succeeds. `tsc -b` exit 0. No backend changes; backend tests still `32 passed in 18.91s`.
|
||||
- Deferred to TODOs in `CURRENT_TASK.md`: suggested-step chips below the chat input (Codex correction; threads through to `FlowPilotMessageBar`); `HandoffManager._generate_snapshot` expansion to include the recent diagnostic timeline pre-claim (today's snapshot is just `problem_summary, problem_domain, status, step_count, confidence_tier`); toolbar "Context" button visibility on revisited active sessions; owner-facing `/analytics/escalations` page; Playwright e2e for the GTM Loom demo path.
|
||||
- Branch state: 3 new commits (`b8627f4` SSE subscription, `f65b657` handoff doc bump, `8e9d22e` magic-moment screen). Branch is unpushed — next session pushes + opens draft PR.
|
||||
- Files touched this slice: `frontend/src/components/flowpilot/HandoffContextScreen.tsx` (new), `frontend/src/components/flowpilot/index.ts`, `frontend/src/pages/FlowPilotSessionPage.tsx`, `.ai/CURRENT_TASK.md`, `.ai/HANDOFF.md`, `.ai/SESSION_LOG.md`.
|
||||
|
||||
## 2026-04-27 21:00 EDT — Claude Code — Escalation Mode: frontend SSE subscription in EscalationQueue
|
||||
|
||||
- Picked up `feat/escalation-metric-endpoint` after the Codex test-stabilization pass. Confirmed green starting state: focused backend subset `32 passed in 18.78s` with `-n auto`.
|
||||
|
||||
Reference in New Issue
Block a user