Files
platform/src/servers/api/pi
pastilhas 68c7973281 fix: Pi harness - dynamic models, correct RPC protocol, proper event handling
Backend:
- /api/pi/models now calls 'pi --list-models' with stored API keys
- pi-bridge.ts: callback-based event handling (matches pi-monorepo)
- pi-bridge.ts: correct RPC format (type: 'prompt' not jsonrpc)
- pi-bridge.ts: pass API keys to Pi process env
- websocket.ts: event handler runs in background, no blocking
- rest.ts: fix user home path (getHomeDir instead of hardcoded)

Frontend:
- Fix /api/ double prefix in useChatSessions, useChatGroups, useModels
- Add PROVIDER_DISPLAY mapping in SystemSettings.tsx
- Provider tabs show friendly names (e.g., 'OpenCode Zen')

UI (from previous session):
- Grouped session list with collapsible folders
- CreateGroupDialog, GroupContextMenu, SessionContextMenu components
2026-02-20 22:55:00 +00:00
..

Pi Harness — Architecture & Implementation Guide

Version: 1.0
Date: February 20, 2026
Status: Production


Overview

The Pi harness is a unified WebSocket + REST API that manages long-running chat sessions with AI models through the Pi CLI tool. It replaces three legacy harnesses (Claude, OpenCode, Pi-Mono) with a single, streamlined implementation.

Key Features

  • Session Management: One Pi process per chat session, spawned on demand
  • Resume from History: Load conversations from disk and continue seamlessly
  • Multi-Model Support: Switch models per session (GPT, Claude, etc.)
  • Concurrent Sessions: Users can run multiple sessions simultaneously
  • Idle Cleanup: Automatic Pi process termination after 1 hour of inactivity
  • Structured Logging: Comprehensive logging with context for debugging

Architecture

Components

┌─ Pi Harness
│
├─ websocket.ts         WebSocket handler (chat lifecycle)
├─ rest.ts              REST API (sessions, models, search)
├─ session-manager.ts   In-memory session tracking
├─ storage.ts           Disk persistence (messages, metadata)
├─ pi-bridge.ts         Pi process spawning & RPC communication
├─ types.ts             TypeScript type definitions
└─ logger.ts            Structured logging utility

Data Flow

Client
  ↓ WebSocket (chat message)
websocket.ts
  ↓ Spawn Pi process if needed
pi-bridge.ts → Pi CLI (RPC mode)
  ↓ Stream events back
session-manager.ts (track state)
  ↓ Save to disk on completion
storage.ts → {cwd}/.pi-sessions/{sessionId}/

Session Lifecycle

1. New Chat

Client  { type: "chat", prompt: "Hello", model: "gpt-4o", cwd: "/home/user" }
         
Server:  Spawn Pi process
         Send prompt via RPC
         Stream responses (text, tool calls, results)
         Save session to disk
         
Client  { type: "session:init", sessionId: "...", model: "...", cwd: "..." }
Client  { type: "assistant:delta", text: "..." }
Client  { type: "result", sessionId: "...", cost: {...} }

2. Resume Existing Chat

Client  { type: "resume", sessionId: "existing-session-id" }
         
Server:  Load session from disk
         Spawn fresh Pi process
         Inject conversation history as system context
         
Client  { type: "session:init", ... }
Client  { type: "sync:messages", messages: [...], isGenerating: false }

3. WebSocket Disconnect

Client disconnects
  
Server: Detach WebSocket from session
        Start 1-hour idle timer
        (Session and Pi process remain alive)

4. Idle Timeout

1 hour passes without reconnection
  
Server: Kill Pi process
        Save session to disk (if not already saved)
        Remove from memory

5. Stop Generation

Client  { type: "stop" }
         
Server:  Send abort RPC to Pi process
         Set isGenerating = false
         
Client  { type: "stopped" }

Wire Protocol

Client → Server Messages

Chat Message

{
  "type": "chat",
  "prompt": "Explain TypeScript generics",
  "sessionId": "uuid",           // optional: omit for new chat
  "model": "gpt-4o",             // optional: defaults to gpt-4o
  "cwd": "/home/user/project",   // optional: defaults to user home
  "attachmentIds": ["file-1"]    // optional: file references
}

Resume Session

{
  "type": "resume",
  "sessionId": "existing-uuid"
}

Stop Generation

{
  "type": "stop"
}

Server → Client Messages

Session Initialized

{
  "type": "session:init",
  "sessionId": "uuid",
  "model": "gpt-4o",
  "cwd": "/home/user/project"
}

Assistant Text (Complete Block)

{
  "type": "assistant:text",
  "text": "TypeScript generics allow..."
}

Assistant Text (Streaming Delta)

{
  "type": "assistant:delta",
  "text": "you to"
}

Tool Execution Started

{
  "type": "tool:start",
  "toolCallId": "call-uuid",
  "toolName": "bash",
  "toolInput": { "command": "ls -la" }
}

Tool Execution Result

{
  "type": "tool:result",
  "toolCallId": "call-uuid",
  "output": "file1.txt\nfile2.txt",
  "isError": false
}

Generation Completed

{
  "type": "result",
  "sessionId": "uuid",
  "cost": {
    "inputTokens": 500,
    "outputTokens": 300,
    "totalUSD": 0.012
  }
}

Full Sync (Resume)

{
  "type": "sync:messages",
  "sessionId": "uuid",
  "messages": [
    { "id": "msg-1", "role": "user", "text": "Hello", "timestamp": 1708396000000 },
    { "id": "msg-2", "role": "assistant", "text": "Hi!", "timestamp": 1708396001000, "model": "gpt-4o" }
  ],
  "isGenerating": false,
  "streamingText": ""
}

Error

{
  "type": "error",
  "message": "Failed to spawn Pi process",
  "errorCode": "SPAWN_ERROR"  // optional
}

Generation Stopped

{
  "type": "stopped"
}

Session Storage

Directory Structure

{userCwd}/.pi-sessions/
└── {sessionId}/
    ├── meta.json       (metadata: title, model, cwd, timestamps, cost)
    └── messages.json   (full conversation history)

meta.json

{
  "id": "session-uuid",
  "title": "First 100 characters of initial prompt",
  "model": "gpt-4o",
  "cwd": "/home/user/project",
  "createdAt": 1708396000000,
  "updatedAt": 1708396123000,
  "messageCount": 15,
  "cost": {
    "inputTokens": 5000,
    "outputTokens": 3000,
    "totalUSD": 0.15
  }
}

messages.json

{
  "messages": [
    {
      "id": "msg-uuid-1",
      "timestamp": 1708396000000,
      "role": "user",
      "text": "Hello, can you help me?"
    },
    {
      "id": "msg-uuid-2",
      "timestamp": 1708396001000,
      "role": "assistant",
      "text": "Of course! What do you need?",
      "model": "gpt-4o",
      "cost": {
        "inputTokens": 100,
        "outputTokens": 20,
        "totalUSD": 0.001
      }
    },
    {
      "id": "msg-uuid-3",
      "timestamp": 1708396002000,
      "role": "tool",
      "toolCallId": "call-uuid",
      "toolName": "bash",
      "output": "file1.txt\nfile2.txt",
      "isError": false
    }
  ]
}

REST API Endpoints

List Sessions

POST /api/pi/sessions
Authorization: Bearer {token}

Response: { sessions: SessionMeta[] }

Get Session Detail

GET /api/pi/sessions/{sessionId}
Authorization: Bearer {token}

Response: { session: { ...meta, messages: Message[] } }

Update Session (Rename)

PATCH /api/pi/sessions/{sessionId}
Authorization: Bearer {token}
Body: { title: "New Title" }

Response: { success: true, session: SessionMeta }

Delete Session

DELETE /api/pi/sessions/{sessionId}
Authorization: Bearer {token}

Response: { success: true }

Search Sessions

GET /api/pi/sessions/search?q={query}
Authorization: Bearer {token}

Response: { results: SessionMeta[] }

List Models

GET /api/pi/models
Authorization: Bearer {token}

Response: { models: ModelInfo[] }

Error Handling

Edge Cases & Solutions

1. Corrupted messages.json

Problem: JSON parse error when loading session
Solution: Catch error, return SESSION_NOT_FOUND to client, allow deletion

try {
  const { meta, messages } = await storage.loadSession(homeDir, sessionId);
  // ...
} catch (err) {
  logger.error('Failed to load session from disk', { sessionId, error: String(err) });
  ws.send({ type: 'error', message: 'Session not found', errorCode: 'SESSION_NOT_FOUND' });
}

2. Pi Process Crash During Streaming

Problem: Pi process exits unexpectedly while generating
Solution: readEvents generator exits naturally, session marked as isGenerating: false

for await (const event of piBridge.readEvents(session.piProcess)) {
  // Handle events...
}
// If process dies, loop exits and session.isGenerating remains false

3. WebSocket Disconnect During Generation

Problem: User closes browser while AI is generating
Solution: Session continues in background until Pi completes, then auto-saves

// In close handler:
sessionManager.detachWs(sessionId);
sessionManager.setIdleTimeout(sessionId, IDLE_TIMEOUT_MS);
// Pi process keeps running, will clean up after 1 hour

4. Concurrent WebSocket Connections to Same Session

Problem: User opens same session in two tabs
Solution: attachWs overwrites previous WebSocket reference, old tab receives no updates

// Only the most recent WebSocket receives updates
sessionManager.attachWs(sessionId, ws);

5. CWD Resolution When Not Provided

Problem: Client doesn't send cwd parameter
Solution: Default to user's home directory from email

const cwd = msg.cwd || getHomeDir(email);

6. Session Save Failure

Problem: Disk write fails (permissions, disk full)
Solution: Log error but continue, session remains in memory

try {
  await storage.saveSession(cwd, sessionId, session.meta, session.messages);
} catch (err) {
  logger.error('Failed to save session', { sessionId, error: String(err) });
  // Continue — session still in memory
}

Logging

Structured Logging Format

logger.info('Session saved to disk', { sessionId, messageCount: 15 });
// Output:
// [2026-02-20T19:49:00.000Z] [Pi] [INFO] Session saved to disk {"sessionId":"...","messageCount":15}

Log Levels

  • DEBUG: Detailed flow information (currently unused)
  • INFO: Normal operations (spawns, saves, connections)
  • WARN: Recoverable issues (currently unused)
  • ERROR: Failures requiring attention (parse errors, spawn failures)

Key Log Points

  • WebSocket open/close
  • Session creation/resumption
  • Pi process spawn/kill
  • Session save/load
  • Errors in message handling, streaming, RPC communication

Performance Considerations

Memory Usage

  • Each active session holds:
    • Full message history (in-memory)
    • Pi process (separate OS process)
    • WebSocket connection (if attached)

Mitigation: Idle timeout (1 hour) cleans up inactive sessions

Disk I/O

  • Sessions saved synchronously after each AI response
  • File writes are fast (JSON serialization)
  • No buffering — each message persisted immediately

Optimization: Could batch writes or use async queue (not currently needed)

Pi Process Overhead

  • Each session spawns one pi CLI process
  • Process runs in --mode rpc --no-extensions --no-skills
  • Minimal resource usage when idle

Limitation: Max concurrent sessions limited by system resources


Development Guide

Adding a New Message Type

  1. Add type to types.ts:

    export type ServerMessage =
      | { type: 'new-type'; data: string }
      | ...;
    
  2. Handle in websocket.ts:

    if (event.type === 'new-type') {
      const msg: ServerMessage = { type: 'new-type', data: event.data };
      ws.send(JSON.stringify(msg));
    }
    
  3. Update Pi bridge if needed (pi-bridge.ts)

Adding a New REST Endpoint

  1. Add route in rest.ts:

    piRestRouter.get('/pi/new-endpoint', async (ctx: Context) => {
      const email = ctx.get('email');
      // Implementation
      return ctx.json({ result: '...' });
    });
    
  2. Wire into hono.ts:

    protectedRouter.route('/', piRestRouter);
    

Debugging Tips

  • Check logs for session lifecycle events
  • Verify Pi process spawned: ps aux | grep "pi --mode rpc"
  • Inspect session files: cat ~/.pi-sessions/{sessionId}/meta.json
  • Test WebSocket manually: wscat -c ws://localhost:3000/api/pi/chat/ws

Migration from Legacy Harnesses

Differences from Claude/OpenCode/Pi-Mono

Feature Legacy Harnesses New Pi Harness
Processes 3 separate handlers Single unified handler
Session Storage Per-provider directories Unified .pi-sessions/
Resume Limited support Full history injection
Models Provider-specific Any model via Pi CLI
WebSocket Protocol Different per provider Unified wire protocol
REST API Scattered endpoints Centralized /api/pi/*

Migration Checklist

  • Delete old harness code (src/servers/api/claude, etc.)
  • Remove old routes from hono.ts and server.tsx
  • Update frontend to use new WebSocket protocol
  • Migrate old session storage to new format (manual or script)
  • Update user settings to reference only 'pi' provider

Testing

Manual Testing

# 1. Start server
bun dev

# 2. Test WebSocket (in separate terminal)
wscat -c "ws://localhost:3000/api/pi/chat/ws?token=your-jwt-token"

# Send chat message
> {"type":"chat","prompt":"Hello!","model":"gpt-4o"}

# 3. Test REST API
curl -H "Authorization: Bearer your-jwt-token" \
  http://localhost:3000/api/pi/models

curl -H "Authorization: Bearer your-jwt-token" \
  http://localhost:3000/api/pi/sessions

Integration Tests (TODO)

  • Session creation and resumption
  • Concurrent sessions per user
  • Idle timeout cleanup
  • WebSocket disconnect/reconnect
  • Error handling (corrupt files, Pi crashes)

Future Improvements

Potential Enhancements

  1. Streaming Buffer Optimization: Batch small deltas to reduce WebSocket overhead
  2. Session Compression: Gzip old messages to save disk space
  3. Model Auto-Detection: Dynamically discover available models from Pi CLI
  4. Cost Tracking Dashboard: Aggregate cost data across sessions
  5. Session Export: Export conversations to Markdown or JSON
  6. Real-time Collaboration: Multiple users in same session
  7. Attachment Support: Full implementation of file attachments

Troubleshooting

Common Issues

Issue: "Failed to spawn Pi process"
Solution: Verify pi CLI is installed and in PATH
Check: which pi → should return path

Issue: "Session not found" on resume
Solution: Check session files exist in {cwd}/.pi-sessions/{sessionId}/
Check: ls ~/.pi-sessions/

Issue: WebSocket disconnects immediately
Solution: Verify JWT token is valid and not expired
Check: Decode token, check exp claim

Issue: High memory usage
Solution: Check idle timeout is working, verify old sessions cleaned up
Check: ps aux | grep pi → should show minimal processes


Appendix

Type Definitions

See types.ts for complete type definitions:

  • ClientMessage
  • ServerMessage
  • Message
  • SessionMeta
  • UserSession
  • PiEvent
  • ModelInfo
  • MessageCost

Configuration

Setting Value Environment Variable
Idle Timeout 1 hour N/A (hardcoded)
Session Directory {cwd}/.pi-sessions/ N/A
Default Model gpt-4o N/A
Pi RPC Mode --mode rpc --no-extensions --no-skills N/A

Last Updated: February 20, 2026
Maintainer: Pi Harness Team
Questions? Check logs, read code, ask the team.