Files
claude-hud/docs/adr/004-session-handling.md
Jarrod Watts e39933fac2 docs: Add architecture decision records for v2
ADR 001: State Management - Custom hooks + useReducer
- Extract state logic from app.tsx into domain-specific hooks
- useReducer for predictable state transitions
- Each hook independently testable

ADR 002: Data Flow - Event-driven with minimal polling
- Primary: Hook events via FIFO (real-time)
- Secondary: Single consolidated poll (git/mcp only)
- Eliminate redundant polling sources that cause flickering

ADR 003: Shell vs TypeScript - Minimal shell, logic in TS
- Hooks must be shell scripts (Claude Code requirement)
- Keep shell scripts minimal (extract data, write FIFO)
- Complex logic moves to testable TypeScript

ADR 004: Session Handling - Track session ID, graceful reset
- Session ID in all events for change detection
- Reset state on session change (/new, /exit, /resume)
- Exponential backoff reconnection strategy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 23:10:26 +11:00

6.1 KiB

ADR 004: Session Handling

Status

Accepted

Context

Current problems with session handling:

  1. HUD doesn't attach correctly when user runs /new
  2. /exit and /resume leave HUD in stale state
  3. Session ID not tracked consistently
  4. No graceful handling of session switches

Users expect the HUD to "just work" regardless of how they navigate Claude Code sessions.

Decision

Track session ID in all events, detect changes, and reset state gracefully.

Session Lifecycle

┌─────────────────────────────────────────────────────────────┐
│                    Session States                            │
│                                                              │
│    ┌──────────┐                                             │
│    │  INIT    │ ─────────────────────────┐                  │
│    └────┬─────┘                          │                  │
│         │ SessionStart                   │                  │
│         ▼                                │                  │
│    ┌──────────┐     /new or /exit        │                  │
│    │  ACTIVE  │ ──────────────────►  ┌───┴───┐              │
│    └────┬─────┘                      │ RESET │              │
│         │                            └───┬───┘              │
│         │ Stop event                     │                  │
│         ▼                                │                  │
│    ┌──────────┐                          │                  │
│    │  IDLE    │ ◄────────────────────────┘                  │
│    └──────────┘                                             │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Session ID Tracking

Every event includes session_id:

interface HudEvent {
  session_id: string;  // Always present
  hook_event_name: string;
  timestamp: number;
  // ... other fields
}

Session Change Detection

// hooks/useSession.ts
function useSession() {
  const [currentSession, setCurrentSession] = useState<string | null>(null);

  const handleEvent = useCallback((event: HudEvent) => {
    if (currentSession && event.session_id !== currentSession) {
      // SESSION CHANGED - reset all state
      resetAllState();
    }
    setCurrentSession(event.session_id);
  }, [currentSession]);

  return { currentSession, handleEvent };
}

State Reset on Session Change

When session changes, reset:

  • Tool history (clear)
  • Agent list (clear)
  • Context tracking (reset to zero)
  • Cost tracking (reset)
  • Todos (clear)
  • Modified files (clear)

Do NOT reset:

  • Git status (session-independent)
  • MCP status (session-independent)
  • UI preferences (if any)

FIFO Reconnection Strategy

// lib/event-reader.ts
class EventReader {
  private sessionId: string;
  private reconnectAttempts = 0;
  private maxReconnectDelay = 30_000; // 30 seconds max

  private getReconnectDelay(): number {
    // Exponential backoff: 100ms, 200ms, 400ms, ... up to 30s
    const delay = Math.min(
      100 * Math.pow(2, this.reconnectAttempts),
      this.maxReconnectDelay
    );
    this.reconnectAttempts++;
    return delay;
  }

  private async reconnect(): Promise<void> {
    const delay = this.getReconnectDelay();
    await sleep(delay);

    // Check if FIFO still exists
    if (await this.fifoExists()) {
      await this.connect();
      this.reconnectAttempts = 0; // Reset on success
    } else {
      // Session ended, clean up
      this.emit('session_ended');
    }
  }
}

Handling /new, /exit, /resume

Command Expected Behavior
/new New session starts, HUD resets and attaches to new session
/exit Session ends, HUD shows idle/disconnected state
/resume Resume session, HUD reconnects to existing FIFO

Implementation: SessionStart Hook

# session-start.sh
# Check if HUD is already running for this session
existing_pid=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$existing_pid" 2>/dev/null; then
  # Send signal to reconnect instead of spawning new
  kill -USR1 "$existing_pid"
else
  # Spawn new HUD instance
  spawn_hud
fi

UI Feedback for Session States

State UI Indication
INIT "Connecting..."
ACTIVE Normal display
IDLE "Idle" badge, dimmed metrics
DISCONNECTED "Reconnecting..." with spinner
SESSION_CHANGED Brief "New Session" indicator, then reset

Consequences

Positive

  • Seamless transitions: /new /exit /resume just work
  • No stale data: Session changes trigger clean reset
  • User confidence: HUD always shows current session state
  • Debuggable: Session ID in all events for tracing

Negative

  • State loss on /new: Previous session data gone (acceptable)
  • Brief disruption: Reset causes momentary blank UI
  • Complexity: More state to manage

Edge Cases

  1. Rapid /new /new /new: Each creates new FIFO, HUD follows last one
  2. FIFO deleted externally: Reconnect logic handles gracefully
  3. Claude Code crashes: SessionEnd may not fire, rely on FIFO absence
  4. Multiple terminals: Each Claude instance has its own FIFO

Implementation Notes

  1. Add session_id to all events in capture-event.sh
  2. Create useSession hook for session state management
  3. Emit 'session_changed' event when ID changes
  4. All state hooks listen for session_changed to reset
  5. Add reconnection logic to EventReader
  6. Add session state indicator to StatusBar component