Claude Session Lifecycle Tracking

Issue: #2426 Status: implemented (PR to dev)

Track every local Claude Code session (dev-laptop, and any workstation) as a row in the claude_sessions Supabase table via the meta-dispatcher CF Worker /sessions endpoint. A PreToolUse hook registers + heartbeats the session; a Stop hook closes it and appends a JSONL audit record to ~/.claude/claude-sessions.log (7-day retention). Zombie sessions (no heartbeat

5 min) are marked orphaned and reaped after 7 days.

Scope: local interactive sessions only. vps-i1 / bms-4 autonomous worker sessions are tracked separately via agent_sessions / dev_r_agent_sessions — do not point these hooks at those hosts.


Architecture

PreToolUse hook (debounced 60s)          Stop hook
  -> session-heartbeat.ps1                 -> session-stop.ps1
      no lock  -> POST   /sessions             DELETE /sessions/:id
      stale    -> PATCH  /sessions/:id         append ~/.claude/claude-sessions.log (JSONL)
      fresh    -> no-op                         delete lock file

Nightly Task Scheduler (register-cleanup-task.ps1)
  -> cleanup-sessions.ps1  -> drop log entries where delete_at < now()
                              on error: Discord embed + gh issue (radieu/p24-infra, label=bug)

Storage: claude_sessions (Supabase). No D1/KV — the worker has no D1 binding.

ComponentPath (repo source of truth)Deploy target
Table migrationsupabase/migrations/20260701162903_claude_sessions.sqlSupabase
CF Worker endpointinfra-src/meta-dispatcher/src/sessions.ts + index.ts routewrangler deploy
PreToolUse hook.claude/hooks/session-heartbeat.ps1~/.claude/hooks/
Stop hook.claude/hooks/session-stop.ps1~/.claude/hooks/
Nightly cleanupscripts/cleanup-sessions.ps1~/.claude/scripts/
Task registrationscripts/register-cleanup-task.ps1run once
Key setupscripts/setup-session-key.ps1run once
Zombie migrationscripts/migrate-zombie-sessions.ps1run once

Deployment order (MANDATORY)

The migration must be live before the CF Worker is deployed, or insertSession 404s with PostgREST “relation does not exist”.

  1. Apply the migrationsupabase/migrations/20260701162903_claude_sessions.sql (via CI, supabase db push, or the Supabase MCP apply_migration).
  2. Deploy the CF Workercd infra-src/meta-dispatcher && npm run deploy (wrangler deploy).
  3. Populate the keyscripts/setup-session-key.ps1 writes QUEUE_API_URL + QUEUE_API_KEY from secrets/monitoring.env.sops into ~/.claude/.env.
  4. Install the hooks — copy .claude/hooks/session-heartbeat.ps1 and session-stop.ps1 into ~/.claude/hooks/, then add the hook wiring to ~/.claude/settings.json (see below).
  5. Register the cleanup task — copy scripts/cleanup-sessions.ps1 to ~/.claude/scripts/, then run scripts/register-cleanup-task.ps1.
  6. Clear the backlog — run scripts/migrate-zombie-sessions.ps1 once (use -DryRun first).

settings.json wiring (MERGE — never overwrite)

~/.claude/settings.json already has live PostToolUse (redact-credentials.py — security critical) and SessionStart hooks. Add the two keys below into the existing hooks object; do not replace the file. Losing redact-credentials.py is a security regression.

{
  "hooks": {
    // ... keep existing PostToolUse + SessionStart entries ...
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "pwsh -NoProfile -File \"$HOME/.claude/hooks/session-heartbeat.ps1\"" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "pwsh -NoProfile -File \"$HOME/.claude/hooks/session-stop.ps1\"" }
        ]
      }
    ]
  }
}

The PreToolUse hook is non-blocking (debounced 60s, ~<200ms, fire-and-forget) — a down API or missing key is swallowed and logged to ~/.claude/session-hook.log; tool calls are never delayed.


Data formats

claude_sessions row

session_id (PK, UUID from the lock file) · machine_id · branch · started_at · last_heartbeat · ended_at · status (active | orphaned | completed) · delete_at (= started_at + 7d).

~/.claude/claude-sessions.log (JSONL, one object per line)

{"session_id":"uuid","machine_id":"dev-laptop","branch":"fix/...","started_at":"...","ended_at":"...","status":"completed","delete_at":"2026-07-08T..."}

status = completed (clean Stop hook) or orphaned (reaped by zombie migration).


Session identity (lock files)

Claude Code does not expose a stable per-session UUID env var in PreToolUse hooks. If the hook stdin JSON carries session_id, the lock is keyed on it; otherwise the key is derived from the working directory and a fresh UUID is generated and persisted in the lock file:

~/.claude/sessions/active/<key>.lock   # {session_id, machine_id, branch, started_at, last_heartbeat}

Lock-file presence = session active. Two VSCode windows on the same worktree produce two locks (two real sessions) — benign; the zombie migration handles both.


Troubleshooting

SymptomCauseFix
Session never appears in claude_sessionsQUEUE_API_KEY not in ~/.claude/.envrun setup-session-key.ps1; check ~/.claude/session-hook.log
Session stuck active after closing VSCodeStop hook didn’t fire (crash/kill)zombie migration marks it orphaned after 5 min; run migrate-zombie-sessions.ps1
insertSession 404 relation does not existmigration not applied before deployapply migration (step 1), redeploy worker
Hooks fail silentlyQUEUE_API_KEY rotatedre-run setup-session-key.ps1
Cleanup task never runsTask Scheduler entry missingre-run register-cleanup-task.ps1; verify Get-ScheduledTask -TaskName P24-ClaudeSessionCleanup

Manually close a stuck session

# via the endpoint (preferred)
Invoke-RestMethod -Method Delete -Uri "$env:QUEUE_API_URL/sessions/<session_id>" `
  -Headers @{ Authorization = "Bearer $env:QUEUE_API_KEY" }
 
# or directly in Supabase (service role): PATCH status='orphaned' / DELETE the row

Rollback

Session hooks are non-blocking — even a fully broken /sessions endpoint cannot affect existing queue operations (/queue-issue, /trigger) or delay tool calls. To roll back the endpoint:

  1. Remove the /sessions route block + handleSessionRequest import from index.ts.
  2. Delete sessions.ts.
  3. Revert the insertSession / updateSession / deleteSession additions in supabase.ts and the SessionRow type in types.ts.
  4. Redeploy the CF Worker (wrangler deploy).
  5. The claude_sessions table may remain (harmless). Local hooks fail silently until re-installed.

To disable client-side only: remove the PreToolUse + Stop entries from ~/.claude/settings.json.


Error notification

cleanup-sessions.ps1 follows the CLAUDE.md Error Notification Standard — on any failure it sends a Discord embed via P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL and creates a GitHub issue in radieu/p24-infra (label bug).