Files
resolutionflow/.ai/HANDOFF.md
Michael Chihlas 5085bb47c2
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
docs(ai): handoff state after /escalate unification through HandoffManager
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>
2026-04-27 22:29:40 -04:00

10 KiB

HANDOFF.md

Last updated: 2026-04-27 22:30 EDT

Active task: Escalation Mode wedge build. See CURRENT_TASK.md for the full status; this file holds the resume point only.

Branch: feat/escalation-metric-endpoint — pushed (latest: 029680a). Draft PR #155 open against main (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

Previous session shipped the two remaining frontend slices: live-arrival SSE subscription in EscalationQueue.tsx, and the magic-moment HandoffContextScreen for senior pickup.

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.
  • 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.
  • 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 escalatedactive 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.

Smoke-test artifact: a single test handoff (0f6149db… on session 50ea20d4…) was claimed during verification and is now an active session owned by the engineer test user. Harmless; useful as visual demo data.

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 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 FlowPilotSessionFlowPilotMessageBar). 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

Watch-outs

  • Do not reintroduce client.stream()/ASGITransport tests for infinite SSE responses; test the generator directly.
  • 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.
  • streamEscalations doesn't drive token refresh on a mid-stream 401 — the Axios interceptor only covers axios calls. Acceptable for v1.
  • The handoff snapshot today is sparse (problem_summary, problem_domain, status, step_count, confidence_tier plus optional branch info). The magic-moment screen's "What's been tried" section currently shows engineer notes + step-count affordance, not the actual step timeline. Snapshot expansion is the right fix.
  • HandoffResponse.ai_assessment_data.confidence is typed number on the frontend but the backend currently emits 'low' | 'medium' | 'high' strings. The ConfidenceBadge component handles both shapes at runtime; the type definition is stale and should be widened to number | 'low' | 'medium' | 'high'.
  • The toolbar "Context" button is hidden on revisited active sessions where the senior didn't arrive via magic-moment this session — known scope cut. Lazy-fetching handoff list on session-load (when status was previously escalated) is the cleanup.