docs(ai): handoff state after /escalate unification through HandoffManager
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
CI / backend (pull_request) Successful in 10m3s
CI / frontend (pull_request) Successful in 5m34s
CI / e2e (pull_request) Successful in 9m26s

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>
This commit is contained in:
2026-04-27 22:29:40 -04:00
parent 029680ab2d
commit 5085bb47c2
3 changed files with 31 additions and 9 deletions

View File

@@ -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`. 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).
**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**, **magic-moment handoff-context screen**, and **bell-icon notification fix** all shipped. **`/escalate` and `/handoff` are now unified** through `HandoffManager` — every escalation creates a SessionHandoff, persists an AppNotification, fans out on the SSE bus, dispatches Slack/Teams via `notify()`, and emails per-user, regardless of which URL it entered through. **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.
@@ -27,8 +27,10 @@
| `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 |
| `2a2329a` | Handoff state docs after bell-icon fix; record draft PR #155 |
| `029680a` | Unify `/escalate` through `HandoffManager` — single canonical path for every escalation. `HandoffCreateRequest.target_user_id`, `create_handoff` does the legacy enriched-package work + sets `escalation_reason`, `finalize_escalation` runs documentation + PSA push + `notify()` pre-commit, `dispatch_escalation_notifications` keeps only fire-and-forget IO post-commit. `pickup_session` accepts either status for in-flight migration. `flowpilot_engine.escalate_session` no longer called from any endpoint |
**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.
**Test status:** full backend suite`1103 passed in 259.63s` with `-n auto` after the unification. 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 `GET` an in-transit session detail; **a single legacy `/escalate` call now produces status='escalated', SessionDocumentation, SessionHandoff row, AppNotification with link `/pilot/{id}?pickup=true` for the team admin, and a PSA push attempt** — all from one funneled HandoffManager call. Branch pushed; draft PR #155 open.
## Remaining work on this branch

View File

@@ -2,11 +2,11 @@
# HANDOFF.md
**Last updated:** 2026-04-27 21:50 EDT
**Last updated:** 2026-04-27 22: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` — 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`.
**Branch:** `feat/escalation-metric-endpoint` — pushed (latest: `029680a`). **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`. **/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
@@ -19,14 +19,17 @@ What landed (commits added to the branch):
- `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.
- `2a2329a` docs(ai): handoff state after bell-icon fix; record draft PR #155.
- `029680a` feat(escalations): unify `/escalate` through `HandoffManager` — single canonical path for every escalation. `HandoffCreateRequest.target_user_id` added (rejects self-targeting). `HandoffManager.create_handoff` for intent='escalate' now sets `session.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-loads `session.steps` + `session.user` to dodge async lazy-load greenlet errors. New `HandoffManager.finalize_escalation` runs `_generate_documentation` + `_push_to_psa` + `notify()` pre-commit so the AppNotification rows and PSA writes land atomically with the handoff. `dispatch_escalation_notifications` keeps only fire-and-forget IO (bus publish + per-user emails) post-commit. The `/escalate` endpoint is a thin shim: owner-only session lookup → `create_handoff(intent='escalate')``finalize_escalation` → commit → `dispatch_escalation_notifications` → return `SessionCloseResponse`. `flowpilot_engine.escalate_session` is no longer called by any endpoint. `pickup_session` accepts both `requesting_escalation` and `escalated` for in-flight migration. Escalation queue list + sidebar count match either status.
Verified:
- `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`.
- Full backend test suite after unification: `1103 passed in 259.63s` 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.
- Live unification verification: a single legacy `/escalate` call from a junior produced status='escalated', a `SessionDocumentation`, a `SessionHandoff` row, an attempted PSA push (`no_psa` since no ticket linked), AND an `AppNotification` row 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.
@@ -34,10 +37,10 @@ Smoke-test artifact: a single test handoff (`0f6149db…` on session `50ea20d4
## Resume point
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.
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 with the rich handoff data → 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 — 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 to `steps_tried` / `remaining_hypotheses` / `suggested_next_steps` once it's wired to read them.
3. Optional v1: owner-facing `/analytics/escalations` page; Playwright e2e for the GTM Loom demo path.
4. Eventual cleanup: `flowpilot_engine.escalate_session` is no longer called by any endpoint and could be deleted; the legacy `SessionBriefing` render branch in `FlowPilotSessionPage.tsx` is effectively dead code for any new escalation (magic-moment takes over) but still useful for in-flight legacy `requesting_escalation` sessions during the transition window. Both can come out after pilots have run a couple of weeks on the unified path.
## Useful breadcrumbs

View File

@@ -12,6 +12,23 @@
---
## 2026-04-27 22:30 EDT — Claude Code — Escalation Mode: unify /escalate through HandoffManager
- User pushed back on the dual-path proposal: "why would we want two different escalation methods? Should the new one just be the way we escalate regardless if we're using a PSA or not using a PSA?" Right answer. Unified everything through `HandoffManager`.
- Backend changes (commit `029680a`):
- `HandoffCreateRequest` gains optional `target_user_id`; rejects self-targeting.
- `HandoffManager.create_handoff` for intent='escalate' now does what the legacy `flowpilot_engine.escalate_session` used to: sets `session.escalation_reason` and `escalated_to_id`, builds the legacy AI-enhanced `escalation_package` via Sonnet (`_build_escalation_package_enhanced` lazy-imported with graceful fallback), and merges handoff metadata (`intent`, `handoff_id`, `snapshot`, `engineer_notes`) into it. Eager-loads `session.steps` + `session.user` via `selectinload` to dodge async lazy-load `MissingGreenlet` errors.
- New `HandoffManager.finalize_escalation`: generates `SessionDocumentation`, pushes to PSA, and runs `notify()` (bell-icon AppNotification + Slack/Teams external channels) — all pre-commit so persistent state lands atomically with the handoff. Pulls engineer name via a separate User query rather than relying on `session.user` lazy access.
- `dispatch_escalation_notifications` keeps only the fire-and-forget IO (bus publish + per-user emails) post-commit. Found and fixed an in-flight bug: had originally put `notify()` inside dispatch (post-commit), which left `Notification` rows uncommitted — moved into `finalize_escalation` (pre-commit).
- `/handoff` endpoint passes `target_user_id` through and calls `finalize_escalation` pre-commit.
- `/escalate` is now a thin shim: owner-only session lookup → `create_handoff(intent='escalate')``finalize_escalation` → commit → `dispatch_escalation_notifications` → return `SessionCloseResponse`. `flowpilot_engine.escalate_session` is no longer called by any endpoint.
- `pickup_session` accepts both `requesting_escalation` (legacy in-flight) and `escalated` (new canonical) so existing queue items migrate seamlessly.
- Escalation queue list (`/escalation-queue`) and sidebar count match either status.
- Frontend: `useFlowPilotSession` optimistic update flips status to `escalated` instead of `requesting_escalation` so the page state matches the unified backend response.
- Verified end-to-end live against the running dev stack: a single legacy `/escalate` call from `engineer@` produced status=`escalated`, a `SessionHandoff` row (`ea9b375a…`, intent='escalate'), a `SessionDocumentation`, a PSA push attempt (`no_psa` since no ticket), AND an `AppNotification` for `teamadmin@` with title "Session escalated by Jordan Tech" and link `/pilot/{session_id}?pickup=true`. Backend test suite: `1103 passed in 259.63s` with `-n auto`. Frontend `tsc -b` clean.
- The legacy `SessionBriefing` render branch in `FlowPilotSessionPage.tsx` is now effectively dead for any new escalation (magic-moment takes over via the handoff record), but stays in place during the transition for legacy in-flight `requesting_escalation` sessions. Slated for cleanup after pilots run a couple of weeks on the unified path. `flowpilot_engine.escalate_session` is similarly orphaned and can be deleted at the same time.
- Files touched: `backend/app/api/endpoints/ai_sessions.py`, `backend/app/api/endpoints/session_handoffs.py`, `backend/app/api/endpoints/sidebar.py`, `backend/app/schemas/session_handoff.py`, `backend/app/services/flowpilot_engine.py`, `backend/app/services/handoff_manager.py`, `frontend/src/hooks/useFlowPilotSession.ts`.
## 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.