docs(ai): handoff state after frontend SSE slice lands
Marks the SSE subscription as shipped, points the next-session resume target at the magic-moment handoff-context screen, and logs the live end-to-end verification. 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**. **Next:** frontend SSE subscription in `EscalationQueue.tsx`, then the 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 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.
|
||||
|
||||
**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,15 @@
|
||||
| `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 |
|
||||
|
||||
**Test status:** focused subset (`test_escalation_bus`, `test_handoff_manager`, `test_session_handoffs_api`, `test_flowpilot_analytics_escalations`) → `32 passed in 17.77s` with `-n auto`. Frontend `tsc -b` clean. 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. 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.
|
||||
|
||||
## Remaining work on this branch
|
||||
|
||||
1. **Frontend SSE subscription** in `EscalationQueue.tsx`. Use a fetch-based `ReadableStream` reader (matching [`frontend/src/api/aiSessions.ts`](../frontend/src/api/aiSessions.ts) `streamDocumentation` — native `EventSource` can't send auth headers). Prepend new cards with the locked 200ms slide-in. Reconnect with backoff. Tab-title flash when backgrounded. Respect `prefers-reduced-motion`.
|
||||
2. **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`).
|
||||
3. **Owner-facing analytics page** at `/analytics/escalations` — period selector, conversion-rate, trend chart. ~0.5d. Optional for v1 demo.
|
||||
4. **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. **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.
|
||||
|
||||
## Two-metric framing — read this before quoting numbers to anyone
|
||||
|
||||
|
||||
@@ -2,47 +2,45 @@
|
||||
|
||||
# HANDOFF.md
|
||||
|
||||
**Last updated:** 2026-04-27 EDT
|
||||
**Last updated:** 2026-04-27 21:00 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` — SSE backend WIP is now test-stabilized locally. Working tree should be clean after the handoff commit.
|
||||
**Branch:** `feat/escalation-metric-endpoint` — frontend SSE live-arrival slice is shipped on top of the test-stabilized backend.
|
||||
|
||||
## Status
|
||||
|
||||
Previous session diagnosed the slow-test issue and fixed the backend test loop.
|
||||
Previous session shipped the frontend SSE subscription that the next session was set up to do.
|
||||
|
||||
Root causes:
|
||||
- Multiple stale pytest processes were still alive inside `resolutionflow_backend`, despite the prior handoff saying they were dead. They held `resolutionflow_test` transactions open and caused later tests to block on `DROP SCHEMA public CASCADE`.
|
||||
- `test_escalations_stream_returns_sse_content_type` used HTTPX `ASGITransport` against an infinite SSE stream. That transport buffers the entire response body before returning, so the test waited forever and held the auth DB dependency transaction open.
|
||||
- Escalation handoff tests created `intent="escalate"` handoffs without stubbing `_generate_ai_assessment()`, so they waited on the real AI path instead of testing handoff behavior.
|
||||
- The bus keyed subscribers by raw `account_id`; string UUIDs and `UUID` objects for the same account did not match.
|
||||
What landed:
|
||||
|
||||
Fixes made:
|
||||
- `stream_escalations` now uses `Depends(require_engineer_or_admin, scope="function")` so auth DB dependencies are released before the long-lived stream body.
|
||||
- The SSE handshake test now calls `stream_escalations()` directly and consumes only the first generator yield, avoiding HTTPX's infinite-stream buffering behavior.
|
||||
- Handoff manager/API tests stub `_generate_ai_assessment()` with an `AsyncMock`.
|
||||
- `EscalationBus` normalizes string/UUID account IDs at subscribe/publish/unsubscribe/subscriber_count boundaries, with a regression test.
|
||||
- Follow-up fix: escalation AI assessment is now latency-bounded by `ESCALATION_AI_ASSESSMENT_TIMEOUT_SECONDS` (default 5s). If it times out, handoff creation proceeds with no assessment instead of blocking on the model/network path.
|
||||
- `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.
|
||||
|
||||
Verified:
|
||||
- `pytest tests/test_escalation_bus.py tests/test_handoff_manager.py tests/test_session_handoffs_api.py tests/test_flowpilot_analytics_escalations.py --override-ini=addopts= -q --durations=20` → `31 passed in 46.95s`
|
||||
- Same subset with `-n auto` → `31 passed in 17.80s`
|
||||
- After the assessment-timeout fix: same subset with `-n auto` → `32 passed in 17.77s`
|
||||
- No remaining pytest processes or `resolutionflow%test%` Postgres sessions after the run.
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Resume point
|
||||
|
||||
1. Continue the **Frontend SSE subscription** in `EscalationQueue.tsx`: fetch-based reader, prepend new cards with the locked 200ms slide-in, reconnect with backoff, tab-title flash when backgrounded, respect `prefers-reduced-motion`.
|
||||
2. Then ship 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 regular FlowPilot session view.
|
||||
3. Push the branch and open a draft PR when the frontend/live-arrival slice is ready.
|
||||
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).
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
- Frontend streaming reference: [`frontend/src/api/aiSessions.ts`](../frontend/src/api/aiSessions.ts) — `streamDocumentation` uses fetch + `ReadableStream`, which remains the right pattern because native `EventSource` cannot send auth headers.
|
||||
- Metric endpoint: [`backend/app/api/endpoints/flowpilot_analytics.py`](../backend/app/api/endpoints/flowpilot_analytics.py) — `get_escalation_metrics`.
|
||||
|
||||
## Watch-outs
|
||||
@@ -51,3 +49,4 @@ Verified:
|
||||
- `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.
|
||||
- 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.
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
- Implemented the live-arrival frontend slice. Added `streamEscalations(handlers, signal)` to `frontend/src/api/aiSessions.ts` — fetch-based `ReadableStream` reader (native `EventSource` can't send auth headers) that parses SSE frames (event/data/comment lines), buffers partial frames across chunks, ignores `: keepalive` heartbeats, dispatches `ready` and `handoff_created` events. Added `HandoffCreatedEvent` and `EscalationStreamHandlers` types in `frontend/src/types/ai-session.ts` mirroring the backend bus payload.
|
||||
- Rewrote `frontend/src/components/flowpilot/EscalationQueue.tsx`. 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 are 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.
|
||||
- Verified end-to-end against the running dev stack. `tsc -b` exit 0. Vite HMR'd the new component without errors. Raw SSE handshake against `/api/v1/ai-sessions/escalations/stream` returned 200 with `text/event-stream; charset=utf-8` plus 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 parser exactly. Backend regression: same focused subset still `32 passed in 18.91s`.
|
||||
- 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.
|
||||
- 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.
|
||||
- Left for next session: the magic-moment handoff-context screen — 4 sections (problem summary / what's been tried / AI assessment / Start here CTA), loads on Pick Up, dissolves into the regular FlowPilot session view. Must render gracefully when `ai_assessment` is `None` (per the 5s assessment timeout from Codex's earlier fix).
|
||||
- Files touched: `frontend/src/api/aiSessions.ts`, `frontend/src/types/ai-session.ts`, `frontend/src/components/flowpilot/EscalationQueue.tsx`, `.ai/CURRENT_TASK.md`, `.ai/HANDOFF.md`, `.ai/SESSION_LOG.md`.
|
||||
|
||||
## 2026-04-27 EDT — Claude Code — Escalation Mode wedge: design through SSE backend (8 commits)
|
||||
|
||||
- One long session that produced the entire planning artifact stack and most of the backend for the Escalation Mode wedge. Output of `/office-hours` (8 founder-signal session, top-tier YC archetype indicators), `/plan-eng-review` (scope reduced from "2-3 weeks greenfield" to "~6-9 days integration + metric + polish" once the existing handoff_manager surface was inventoried), `/plan-design-review` (6/10 → 9/10 with magic-moment screen, hero metric placement, and real-time arrival visual locked), and `/codex review` (12 findings, 6 applied — two-metric framing, notification routing, claim auth gate moved in-scope, unread-state fix, "Start here" CTA reframe, per-channel delivery model; 5 rejected including the full-scope reduction Codex pushed for).
|
||||
|
||||
Reference in New Issue
Block a user