docs(ai): handoff state after bell-icon fix; record draft PR #155
Updates the handoff trio after the legacy notification flow fix and the branch push. PR #155 is open against main as draft. Resume point is now visual QA via /qa, then deferred follow-ups (chat-input suggested-step chips, snapshot expansion). Logs the open question about whether EscalateModal should switch to /handoff. 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 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.
|
||||
**Status:** in-flight on `feat/escalation-metric-endpoint`. Branch is pushed; **draft PR #155** is open against `main` ([gitea.resolutionflow.com/chihlasm/resolutionflow/pulls/155](https://gitea.resolutionflow.com/chihlasm/resolutionflow/pulls/155)). Backend is **feature-complete and test-stabilized**. **Frontend live-arrival SSE subscription** is shipped. **Magic-moment handoff-context screen** is shipped. **Bell-icon notification fix** is shipped (notification link now includes `?pickup=true`; GET access policy relaxed for in-transit sessions). **Next:** visual QA via `/qa`, then optional follow-ups (suggested-step chips, snapshot expansion, analytics page, Playwright e2e).
|
||||
|
||||
**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.
|
||||
|
||||
@@ -25,12 +25,14 @@
|
||||
| `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) |
|
||||
| `c194ba4` | Handoff state docs after magic-moment screen lands |
|
||||
| `641853a` | Bell-icon notification opens the pickup flow — notification link template adds `?pickup=true`; GET `/ai-sessions/{id}` allows account-scoped read for `requesting_escalation` / `escalated` states |
|
||||
|
||||
**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.
|
||||
**Test status:** broader subset (focused 4 + `test_sessions` + `test_session_sharing`) → `94 passed in 43.26s` with `-n auto` after the access-policy change. Frontend `tsc -b` clean. End-to-end smoke test against the running dev stack confirmed: SSE handshake delivers `ready` + `handoff_created` frames; `listHandoffs` returns the unclaimed handoff for a senior pre-claim; `claimHandoff` flips session status `escalated` → `active`; senior (non-owner, non-target) can now `GET` an escalated session detail post-policy-change. Branch pushed; draft PR #155 open.
|
||||
|
||||
## Remaining work on this branch
|
||||
|
||||
1. **Push + draft PR** — branch is unpushed. Open against `main`.
|
||||
1. **Visual QA in a real browser** via `/qa` — slide-in animation, tab-title flash, magic-moment layout, dissolve, full junior-escalates → senior-receives → senior-claims demo path.
|
||||
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.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
# HANDOFF.md
|
||||
|
||||
**Last updated:** 2026-04-27 21:30 EDT
|
||||
**Last updated:** 2026-04-27 21:50 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 live-arrival SSE slice + magic-moment handoff-context screen are both shipped on top of the test-stabilized backend. Branch is unpushed.
|
||||
**Branch:** `feat/escalation-metric-endpoint` — pushed. **Draft PR #155** open against `main` ([gitea.resolutionflow.com/chihlasm/resolutionflow/pulls/155](https://gitea.resolutionflow.com/chihlasm/resolutionflow/pulls/155)). Wedge is feature-complete pending visual QA + the deferred follow-ups in `CURRENT_TASK.md`.
|
||||
|
||||
## Status
|
||||
|
||||
@@ -17,13 +17,16 @@ What landed (commits added to the branch):
|
||||
- `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).
|
||||
- `c194ba4` docs(ai): handoff state after magic-moment screen lands.
|
||||
- `641853a` fix(escalations): bell-icon notification opens the pickup flow — `_build_notification_link` for `session.escalated` now ends with `?pickup=true` so notification clicks route through the senior-pickup flow. `GET /ai-sessions/{id}` now allows account-scoped read for `requesting_escalation` / `escalated` status (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.
|
||||
|
||||
Verified:
|
||||
|
||||
- `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.
|
||||
- `tsc -b` exit 0 after each frontend commit.
|
||||
- Backend regression with the access-policy change: focused subset + `test_sessions` + `test_session_sharing` → `94 passed in 43.26s` with `-n auto`.
|
||||
- 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.
|
||||
- Live access-policy verification: senior (non-owner, non-target) can now `GET` an in-transit escalated session detail.
|
||||
|
||||
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.
|
||||
|
||||
@@ -31,9 +34,9 @@ Smoke-test artifact: a single test handoff (`0f6149db…` on session `50ea20d4
|
||||
|
||||
## Resume point
|
||||
|
||||
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.
|
||||
1. **Visual QA via `/qa` against 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 → Start here → FlowPilot session view loads. Also: open `/escalations` as senior with a second session escalating in the background, watch the slide-in + tab-title flash. The PR description has a checklist mirroring this.
|
||||
2. **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 through `FlowPilotSession` → `FlowPilotMessageBar`). Next: `HandoffManager._generate_snapshot` expansion to include the recent diagnostic timeline pre-claim so the "What's been tried" section shows the actual conversation tail instead of a step-count affordance.
|
||||
3. **EscalateModal currently uses the legacy `/escalate` endpoint, not `/handoff`.** That means the user's recent test went through the legacy notification path (which now works post `641853a`) rather than the new handoff/SSE flow. Wedge demo recording will be cleaner if EscalateModal is switched over — open question whether to do it as a parallel path (legacy escalate also creates a handoff) or a full migration (replace `/escalate` with `/handoff` + intent=escalate). Legacy path produces full PSA documentation push that the handoff path doesn't, so a parallel path is probably the right call for v1.
|
||||
4. Optional v1: owner-facing `/analytics/escalations` page; Playwright e2e for the GTM Loom demo path.
|
||||
|
||||
## Useful breadcrumbs
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-27 21:50 EDT — Claude Code — Escalation Mode: bell-icon notification fix; push + draft PR
|
||||
|
||||
- User ran a live escalation test via the EscalateModal (legacy `/escalate` path) and reported that clicking the bell-icon notification "just clears the notification instead of taking me to the session". Diagnosed: navigation IS happening, but the notification link template was `/pilot/{session_id}` without `?pickup=true`, so the senior landed on `FlowPilotSessionPage` with no pickup mode. `loadSession` then hit `GET /ai-sessions/{id}` which 404'd because the senior wasn't owner / `escalated_to_id` / picked-up handler. The user perceived the resulting error state as the action having done nothing.
|
||||
- Two-part backend fix shipped in `641853a`. (1) `_build_notification_link` for `session.escalated` now ends with `?pickup=true` so notification clicks route through the senior-pickup flow (handoff-based or legacy SessionBriefing). (2) `GET /ai-sessions/{id}` access policy: any account member can now read a session's detail when status is `requesting_escalation` or `escalated`. Tenant boundary enforced by RLS — the owner-only guard was overly restrictive for explicitly-shared in-transit states. After-pickup access (handler / `escalated_to_id`) checks still apply for active/resolved sessions.
|
||||
- Verified end-to-end live: re-login as senior engineer (non-owner, non-target) and `GET /ai-sessions/{escalated-session-id}` returns 200 with full detail. Backend regression with broader subset (`test_escalation_bus`, `test_handoff_manager`, `test_session_handoffs_api`, `test_flowpilot_analytics_escalations`, `test_sessions`, `test_session_sharing`) → 94 passed in 43.26s.
|
||||
- Pushed `feat/escalation-metric-endpoint` to Gitea. Opened **draft PR #155** against `main` via Gitea API ([gitea.resolutionflow.com/chihlasm/resolutionflow/pulls/155](https://gitea.resolutionflow.com/chihlasm/resolutionflow/pulls/155)). Title prefixed `WIP:` so Gitea marks it `draft: true`. PR body links the design + test-plan artifacts and mirrors the test plan as a checklist with visual QA + e2e demo flow as the unchecked items.
|
||||
- Open question for next session: EscalateModal still calls the legacy `/escalate` endpoint, not the new `/handoff` path. The wedge demo flow (junior escalates → magic-moment renders) is cleaner if EscalateModal goes through `/handoff`. Legacy path does PSA documentation push that the handoff path doesn't, so a parallel path (legacy escalate also creates a handoff record) is probably the right call rather than full migration.
|
||||
- Files touched: `backend/app/api/endpoints/ai_sessions.py`, `backend/app/services/notification_service.py`, `.ai/CURRENT_TASK.md`, `.ai/HANDOFF.md`, `.ai/SESSION_LOG.md`.
|
||||
|
||||
## 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.
|
||||
|
||||
Reference in New Issue
Block a user