Skip to main content

Session Lifecycle

A HAPI session represents a single conversation with an AI coding agent. Sessions can be controlled locally from the terminal or remotely via web/PWA/Telegram, with seamless handoff between modes.
1

Session Creation

User runs hapi, hapi codex, or other agent command. CLI generates a unique session ID and connects to Hub.
2

Registration

CLI registers session with Hub via REST API (POST /cli/sessions). Hub creates entry in SQLite and broadcasts to web clients.
3

Active Phase

Session enters active state. Messages flow between user, agent, and Hub. Permission requests are handled in real-time.
4

Completion

Session ends when agent completes or user aborts. CLI sends session-end event. Session becomes inactive but remains in history.
5

Archival (Optional)

User can archive session via web UI or manually delete inactive sessions.

Local vs Remote Modes

HAPI’s defining feature is the ability to seamlessly switch between local and remote control without losing session state.

Local Mode

Direct Terminal Control

  • Full terminal UI with syntax highlighting
  • Direct keyboard input with instant response
  • Best for focused, uninterrupted coding sessions
  • All AI processing happens locally on your machine
When Local:
  • Session runs in foreground of terminal
  • User types directly into CLI
  • Agent responses stream to terminal
  • Terminal UI shows thinking indicators
# Example: Starting a Claude session in local mode
$ hapi
 Connected to hub
 Session started: ses_abc123

┌─ Claude Code Session ─────────────────────────┐
 Mode: Local
 Path: /home/user/project
 Permission: Default
└───────────────────────────────────────────────┘

You: Help me refactor this component

Remote Mode

Web/Phone Control

  • Control via PWA, browser, or Telegram
  • Approve permissions on the go
  • Monitor progress while away from desk
  • Session continues running on local machine
When Remote:
  • Session waits for web/Telegram input
  • Terminal shows “Remote mode - waiting for input”
  • User sends messages from phone/web
  • Responses stream to all connected clients via SSE

Mode Switching

Automatic switch when message arrives from web/phone:
1

Web Message Sent

User sends message from phone: “Add error handling”
2

Hub Routes to CLI

Hub receives REST request, emits Socket.IO event to CLI
3

CLI Switches Mode

CLI detects remote message, switches to remote mode
4

Terminal Updates

Terminal UI shows: Remote mode - waiting for input
5

Agent Processes

Agent receives message and starts processing
┌─────────────────────────────────────────────┐
│ Remote mode - waiting for input             │
│ Press space twice to regain control         │
└─────────────────────────────────────────────┘
Mode switching is instantaneous and preserves full session state. The agent doesn’t restart or lose context.

Message Flow

User Message → Agent

1

Web Client

User types message in web chat interface and clicks send.
2

REST Request

Web sends POST /api/sessions/:id/messages with message content.
3

Hub Validation

Hub validates message, stores in SQLite, assigns sequence number.
4

Socket.IO Emit

Hub emits message event to CLI via Socket.IO.
5

CLI Relay

CLI relays message to AI agent (Claude/Codex/etc).
6

Agent Processing

Agent begins processing and generates response.

Agent Response → User

1

Agent Output

Agent generates response (text, tool calls, etc).
2

CLI Capture

CLI captures agent output and wraps in message format.
3

Socket.IO Emit

CLI emits message to Hub via Socket.IO.
4

Hub Processing

Hub stores message, updates session state, increments version.
5

SSE Broadcast

Hub broadcasts message-received event to all SSE subscribers.
6

Web Update

Web receives SSE event, invalidates query cache, UI updates.
// Web SSE handler (from web/src/hooks/useSSE.ts pattern)
sse.addEventListener('message', (event) => {
  const syncEvent = JSON.parse(event.data)
  
  if (syncEvent.type === 'message-received') {
    // Invalidate TanStack Query cache
    queryClient.invalidateQueries({
      queryKey: ['sessions', syncEvent.sessionId, 'messages']
    })
  }
})

Permission System Flow

HAPI’s permission system allows remote approval of agent tool usage.

Permission Request

1

Agent Requests Permission

Agent wants to use a tool (e.g., edit_file).
2

CLI Creates Request

CLI generates permission request with unique ID.
3

Update Agent State

CLI calls update-state with request in agentState.requests.
4

Hub Stores & Notifies

Hub stores request, sends Telegram notification, broadcasts via SSE.
5

User Notified

User receives push notification or Telegram message with approve/deny buttons.
// From shared/src/schemas.ts
type AgentState = {
  controlledByUser?: boolean
  requests?: Record<string, AgentStateRequest>
  completedRequests?: Record<string, AgentStateCompletedRequest>
}

type AgentStateRequest = {
  tool: string
  arguments: unknown
  createdAt: number
}

Permission Approval

1

User Approves

User taps “Approve” in web UI or Telegram.
2

REST Request

Web sends POST /api/sessions/:id/permissions/:requestId/approve.
3

Hub RPC

Hub routes to CLI via RPC gateway.
4

CLI Completes Request

CLI moves request from requests to completedRequests with status approved.
5

Agent Continues

Agent receives approval signal and executes tool.
// Web API call
await fetch(`${hubUrl}/api/sessions/${sessionId}/permissions/${requestId}/approve`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` }
})

// Hub routes to CLI via RPC
socket.emit('rpc-request', {
  method: 'approve-permission',
  params: JSON.stringify({ requestId })
}, (response) => {
  // Success
})
Permission requests are persisted in SQLite. If the user is offline, they can approve/deny later without losing the request.

Real-Time Updates via SSE

Server-Sent Events provide a unidirectional stream from Hub to Web for live updates.

SSE Connection

// Web establishes SSE connection
const eventSource = new EventSource(`${hubUrl}/api/events?token=${token}`)

eventSource.addEventListener('message', (event) => {
  const syncEvent: SyncEvent = JSON.parse(event.data)
  
  switch (syncEvent.type) {
    case 'session-added':
      // New session appeared
      break
    case 'session-updated':
      // Session metadata or state changed
      break
    case 'message-received':
      // New message in session
      break
    case 'machine-updated':
      // Machine status changed
      break
    case 'toast':
      // Show notification
      break
  }
})

Event Types

session-added

New session created. Web adds to session list.

session-updated

Session metadata or agent state changed. Web invalidates cache.

message-received

New message in session. Web fetches latest messages.

machine-updated

Machine came online/offline. Web updates machine list.

toast

Notification to display. Web shows toast message.

heartbeat

Keep-alive ping. Web verifies connection is active.
SSE automatically reconnects on disconnect. The Hub sends a heartbeat event every 30 seconds to detect broken connections.

Message Synchronization

Sequence Numbers

Every message gets a unique, monotonically increasing sequence number per session:
// From shared/src/schemas.ts
type DecryptedMessage = {
  id: string          // Unique message ID
  seq: number         // Sequence number (1, 2, 3, ...)
  localId: string | null  // Optional local ID for optimistic updates
  content: unknown    // Message content (tool calls, text, etc)
  createdAt: number   // Unix timestamp (ms)
}
Sequence numbers enable efficient pagination and gap detection. Web can request “messages after seq 50” to fetch only new messages.

Optimistic Updates

Web uses localId for optimistic UI updates:
1

User Sends Message

Web generates localId, immediately adds message to UI with “sending” state.
2

REST Request

Web sends POST /api/sessions/:id/messages with localId.
3

Hub Assigns Seq

Hub stores message, assigns sequence number, includes localId in response.
4

SSE Broadcast

Hub broadcasts message-received with both seq and localId.
5

Web Reconciles

Web matches localId, replaces optimistic message with confirmed one.

Session State Management

Metadata vs Agent State

Sessions have two separately versioned state objects:
// From shared/src/schemas.ts
type Metadata = {
  path: string              // Working directory
  host: string              // Hostname
  version?: string          // CLI version
  name?: string             // Custom session name
  os?: string               // Operating system
  summary?: {               // AI-generated summary
    text: string
    updatedAt: number
  }
  machineId?: string        // Machine ID
  claudeSessionId?: string  // Claude session ID
  codexSessionId?: string   // Codex session ID
  geminiSessionId?: string  // Gemini session ID
  opencodeSessionId?: string // OpenCode session ID
  cursorSessionId?: string  // Cursor session ID
  tools?: string[]          // Available tools
  slashCommands?: string[]  // Available slash commands
  flavor?: string           // Agent flavor
  worktree?: {              // Git worktree info
    basePath: string
    branch: string
    name: string
    worktreePath?: string
    createdAt?: number
  }
}
Metadata describes the session environment and configuration. It’s read-only from the web perspective.

Versioning Pattern

// From shared/src/schemas.ts
type Session = {
  id: string
  namespace: string
  seq: number
  createdAt: number
  updatedAt: number
  active: boolean
  activeAt: number
  metadata: Metadata | null
  metadataVersion: number      // Incremented on metadata update
  agentState: AgentState | null
  agentStateVersion: number    // Incremented on agent state update
  thinking: boolean
  thinkingAt: number
  todos?: TodoItem[]
  permissionMode?: PermissionMode
  modelMode?: ModelMode
}
Always include expectedVersion when updating metadata or agent state. The Hub rejects stale updates to prevent race conditions.

Session Control Operations

Abort Session

// Web: POST /api/sessions/:id/abort
// Hub routes to CLI via RPC
// CLI terminates agent process
// Session becomes inactive

Switch to Remote

// Web: POST /api/sessions/:id/switch
// Hub routes to CLI via RPC
// CLI switches to remote mode
// Terminal shows "Remote mode - waiting for input"

Resume Inactive Session

// Web: POST /api/sessions/:id/resume
// Hub routes to machine's runner via RPC
// Runner spawns new CLI process with session ID
// Session becomes active again
Resume only works if the original machine is online and has a runner daemon. Use hapi runner start to enable remote resume.

Use Cases

Remote Control While Away

Start session at desk, continue from phone during commute.

Permission Approval

Agent requests file access, approve with one tap on phone.

Multi-Device Monitoring

View session progress on phone while desktop does heavy lifting.

Team Collaboration

Multiple team members monitor and approve agent actions.

Architecture

Technical architecture and components

Sessions

Session metadata and state management

Agents

Multi-agent support and flavors

Quick Start

Get started with HAPI

Build docs developers (and LLMs) love