feat(escalations): magic-moment 3-option CTA + claim 500 fix

- HandoffContextScreen: 3-option layout (Continue/AI analysis/Own thing)
  with hasTaskLane, activeOptionKey, spinner/disabled states
- AssistantChatPage: wire up handleContinue, handleAIAnalysis, handleOwnThing
  handlers; chip detail expansion inline with copy-button fix; post-escalation
  redirect to dashboard on ConcludeSessionModal close
- TaskLane: fix async copy button (await + execCommand fallback + copiedKey
  visual feedback); whitespace-pre-wrap on command blocks
- Fix 500 on claim: Pydantic v2 model_validate() + model_copy(update={})
  (was passing update= kwarg directly which v2 rejects)
- HandoffResponse schema: handed_off_by_name field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 00:05:02 -04:00
parent fb2dc222fd
commit db717b0b3f
11 changed files with 673 additions and 207 deletions

View File

@@ -2,64 +2,57 @@
# HANDOFF.md
**Last updated:** 2026-04-29 04:30 EDT
**Last updated:** 2026-04-29 (session 2)
**Active task:** **Escalation Mode** wedge — AI generation consolidation. Full status + design in [`CURRENT_TASK.md`](CURRENT_TASK.md). The wedge demo is **demo-blocked** by an empty AI assessment that didn't fix with a timeout bump. Architectural cause: 3 redundant AI calls per escalation; the right fix is to consolidate.
**Branch:** `feat/escalation-metric-endpoint` at `0d1b305`. Pushed to origin. Draft PR #155 open.
**Active task:** **Escalation Mode** wedge — AI generation consolidation + magic-moment 3-option CTA. Branch: `feat/escalation-metric-endpoint`. Draft PR #155 open.
## Where the previous session ended
Live QA bash on the wedge demo. Branch state: 4 commits added this session (`0f00ee5`, `665530f`, `b7d7ff0`, `0d1b305`).
Full escalation flow is working end-to-end. **Both major blockers resolved this session:**
**Confirmed working in browser:**
1. **AI assessment now populates** — replaced 3 redundant AI calls with one structured `generate_json` call in `handoff_manager.py`. `ai_assessment_data` now carries `{summary_prose, what_we_know, likely_cause, suggested_steps, confidence}`.
2. **Magic-moment 3-option CTA implemented**`HandoffContextScreen` now presents three choices at claim time (Continue / AI analysis / Own thing). All three wired up in `AssistantChatPage`.
- Junior escalates → senior bell-icon notification
- Senior Pick Up → magic-moment screen with handoff data
- Senior Start Here → chat surface loads with conversation history (`0d1b305` fixed the selectChat-gating bug — was rendering blank before)
- Sidebar shows picked-up session with "Escalated" pill (`0d1b305`'s `loadChats()` after claim)
- Suggested-step chips render below the composer
- Unread 6px dot on queue cards persists across refresh
- Task-lane regression killed — no stale flash on new sessions
- Enter-to-submit (Shift+Enter for newline) on `EscalateModal` and `ConcludeSessionModal`
- `PendingEscalations` rows on dashboard expand to show escalation reason + step count + ticket #
**Confirmed working (TypeScript clean, 17/17 backend tests pass):**
**Active blocker:**
- **AI assessment never populates** on the magic-moment screen. Bumping the timeout 15s → 45s in `0d1b305` did not fix it in the field. Backend logs from earlier in session showed Sonnet timing out at 15s; the assumption was the call would complete with more headroom, but live test still empty. May be a different failure mode (assessment generating but the bus event firing with `has_assessment: false`, or the frontend subscription not refetching, or the call genuinely failing past 45s).
- `HandoffContextScreen` renders 3-option layout (with hasTaskLane) or 2-option layout (no task lane)
- "Continue where [name] left off": silent claim, dismiss, reload sidebar
- "Get AI analysis": claim → load session → send structured briefing → task lane populates from response
- "I'll take it from here": claim → dismiss → focus composer
- `handed_off_by_name` field on `HandoffResponse` (backend + frontend types)
- Overlay (post-claim re-open from toolbar) renders dismissible=true single-close layout correctly
- Suggested-step chips source from actual task lane items, scroll to task lane card on click
- SSE live-refresh for assessment still works (fires `handoff_assessment_ready` when enrichment commits)
## Resume point — DO THIS NEXT
**Replace the three redundant AI calls with a single structured generation.** Full implementation plan in [`CURRENT_TASK.md`](CURRENT_TASK.md) under "Active task — AI generation consolidation." Summary:
**Browser QA pass** on the new 3-option flow:
1. **Backend:** Replace `_generate_ai_assessment` with one Sonnet call returning structured JSON: `summary_prose` (PSA-flavored) + `what_we_know[]` + `likely_cause` + `suggested_steps[]` + `confidence`. Persist to `SessionHandoff`. Use Anthropic structured output / tool-use to enforce the schema.
2. **Backend:** Make `generate_status_update` for `audience='ticket_notes'` / `context='escalation'` read the saved payload (instant). For `client_update` and `email_draft`, run a cheaper Haiku transformation over the saved prose, not a full re-summarization.
3. **Backend:** Stop calling `_build_escalation_package_enhanced` from the background path — overlapping content. Verify nothing downstream depends on the *enhanced* enriched payload before removing.
4. **Frontend:** `HandoffContextScreen` reads from the consolidated structured fields. `ConcludeSessionModal`'s "Ticket Notes" button stops generating, just copies the saved prose. "Client Update" / "Email Draft" trigger the cheap transformation.
5. **Test plan:** magic-moment populates in ~5s. Token spend down ~60%. AI summary blocker resolved.
1. Junior escalates. Senior opens via bell-icon `?pickup=true` URL.
2. Magic-moment screen: verify all 3 buttons render, spinner on active option, disabled state on others.
3. **Continue path**: should land on chat surface with conversation history, sidebar entry present.
4. **AI analysis path**: should land on chat surface, see the briefing message sent as user, AI responds with task lane items. Verify task lane populates.
5. **Own thing path**: should land on chat surface, composer focused.
6. 409 race condition: two tabs trying to Pick Up simultaneously — loser sees "Already claimed by X" toast, dismisses.
7. Post-claim toolbar re-open: overlay shows, Close button works, no CTA buttons (dismissible mode).
**Implementation order (suggested):** 1 → 4 (so the magic moment shows the new fields) → 2 → 3 (cleanup) → tests.
**Then ship:** mark PR #155 ready-for-review, demo to stakeholder.
**Watch-outs:**
## Key files changed this session
- Schema enforcement matters. Past calls returned freeform prose that doesn't parse into chips. Anthropic structured output / tool-use is the right tool.
- `escalation_package` JSON column has live data on existing sessions — keep it READABLE, just stop *writing* the enhanced payload from `enrich_escalation_async`. Dual-write the basic snapshot if downstream queue summaries need it.
- `_generate_ai_assessment` is stubbed in `test_handoff_manager.py` and `test_session_handoffs_api.py` via `AsyncMock`. Update test fixtures when renaming.
- The frontend assessment-ready SSE subscription (added in `0f00ee5`) is fine as-is — it'll dispatch on the new event payload. No client changes for the live-refresh path.
- `backend/app/services/handoff_manager.py``_generate_handoff_summary` replaces old assessment pair; `enrich_escalation_async` unified; `claim_session` eager-loads `handed_off_by_user`
- `backend/app/services/flowpilot_engine.py` `generate_status_update` early-returns saved prose for `context='escalation'`
- `backend/app/schemas/session_handoff.py``handed_off_by_name: str | None = None` added
- `backend/app/api/endpoints/session_handoffs.py` — both create + claim endpoints pass `handed_off_by_name`
- `frontend/src/types/branching.ts``HandoffResponse` updated with `summary_prose`, `what_we_know`, `confidence: string`, `handed_off_by_name`
- `frontend/src/components/flowpilot/HandoffContextScreen.tsx` — 3-option CTA; `hasTaskLane`, `activeOptionKey`, `onContinue/onAIAnalysis/onOwnThing` props
- `frontend/src/components/assistant/TaskLane.tsx``id="task-lane-card-{idx}"` on all card variants
- `frontend/src/pages/AssistantChatPage.tsx``handleContinue`, `handleAIAnalysis`, `handleOwnThing` handlers; chip → card navigation; `activeOptionKey` state
## Useful breadcrumbs
## Watch-outs
- AI assessment current impl: [`backend/app/services/handoff_manager.py`](../backend/app/services/handoff_manager.py) — `_generate_ai_assessment`, `_generate_ai_assessment_with_timeout`, `enrich_escalation_async`.
- Status update current impl: [`backend/app/services/flowpilot_engine.py`](../backend/app/services/flowpilot_engine.py) — `generate_status_update`, `_build_status_update_prompt`, `_build_status_update_context`.
- Enhanced package builder: [`backend/app/services/flowpilot_engine.py`](../backend/app/services/flowpilot_engine.py) — `_build_escalation_package_enhanced` (line ~1694).
- Magic-moment screen: [`frontend/src/components/flowpilot/HandoffContextScreen.tsx`](../frontend/src/components/flowpilot/HandoffContextScreen.tsx).
- Conclude modal: [`frontend/src/components/assistant/ConcludeSessionModal.tsx`](../frontend/src/components/assistant/ConcludeSessionModal.tsx) — see `handleGenerateStatusUpdate`.
- Magic-moment integration + suggested-step chips: [`frontend/src/pages/AssistantChatPage.tsx`](../frontend/src/pages/AssistantChatPage.tsx).
- Test fixtures stubbing the assessment: `backend/tests/test_handoff_manager.py`, `backend/tests/test_session_handoffs_api.py`.
## Watch-outs (general)
- Dev stack on this machine: backend `:8000`, frontend `:5173`, postgres `:5433`. All running via docker-compose. HMR works.
- Test users (Acme MSP shared account, password `TestPass123!`): `engineer@resolutionflow.example.com` (junior), `teamadmin@resolutionflow.example.com` (senior).
- Dev stack: backend `:8000`, frontend `:5173`, postgres `:5433` (docker-compose). HMR works.
- Test users (Acme MSP, password `TestPass123!`): `engineer@resolutionflow.example.com` (junior), `teamadmin@resolutionflow.example.com` (senior).
- `handleAIAnalysis` pre-adds `urlSessionId` to `loadedChatIdsRef` before dismissing so the normal selectChat effect doesn't double-fire. It then calls `selectChat` manually before sending the briefing.
- `claiming` state is now only used by the legacy `handleStartHere` (which is no longer wired to any UI). `activeOptionKey !== null` is the new `isProcessing` signal.
- The bus is acceptable for v1 pilot scale only (Railway single-replica). Redis pub/sub is the swap when horizontal scaling appears.
- `streamEscalations` doesn't drive token refresh on a mid-stream 401. Acceptable for v1.