-- 021_session_blocker_and_closed_at.up.sql -- Track why a session ended partial/failed and when it actually closed. -- See plans/2026-07-20-session-review-ten-sessions.md P1.5. -- -- `blocker` is a short structured reason: "approval_timeout", -- "classifier_overreach", "user_abandoned", "tool_error", "model_refusal", -- etc. Set by complete_task when outcome is partial/failed, derived from the -- last assistant message's text. Empty for success outcomes. -- -- `closed_at` is when the session reached its terminal state. Distinct from -- `last_active_at`, which is touched on any access (including the operator -- just opening the transcript) — `closed_at` is set ONCE at completion. -- Without it, "session duration" can only be computed as -- `last_active - created`, which lies for reopened sessions (a51e2086 -- reported 4-day duration because the operator reopened it to close it). ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS blocker TEXT NOT NULL DEFAULT ''; ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ; -- Backfill closed_at for already-terminal sessions so the new column isn't -- NULL forever on existing rows. Use last_active_at as the best proxy — it's -- the most recent touch, which is the closest we have to "when it ended" -- for historical sessions. New sessions set closed_at explicitly on -- complete_task. UPDATE agent_sessions SET closed_at = last_active_at WHERE closed_at IS NULL AND status IN ('done', 'failed'); -- Index for "show me partial sessions in the last N days" — the common -- audit query. Covers the blocker column too so the planner can answer -- "blocker breakdown over the last week" with an index-only scan. CREATE INDEX IF NOT EXISTS idx_agent_sessions_closed ON agent_sessions (closed_at DESC) WHERE status IN ('done', 'failed');