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,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.
|
||||
|
||||
Reference in New Issue
Block a user