# 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

```typescript
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

```typescript
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

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

### 4. Idle Timeout

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

### 5. Stop Generation

```typescript
Client → { type: "stop" }
         ↓
Server:  Send abort RPC to Pi process
         Set isGenerating = false
         ↓
Client ← { type: "stopped" }
```

---

## Wire Protocol

### Client → Server Messages

#### Chat Message
```json
{
  "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
```json
{
  "type": "resume",
  "sessionId": "existing-uuid"
}
```

#### Stop Generation
```json
{
  "type": "stop"
}
```

### Server → Client Messages

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

#### Assistant Text (Complete Block)
```json
{
  "type": "assistant:text",
  "text": "TypeScript generics allow..."
}
```

#### Assistant Text (Streaming Delta)
```json
{
  "type": "assistant:delta",
  "text": "you to"
}
```

#### Tool Execution Started
```json
{
  "type": "tool:start",
  "toolCallId": "call-uuid",
  "toolName": "bash",
  "toolInput": { "command": "ls -la" }
}
```

#### Tool Execution Result
```json
{
  "type": "tool:result",
  "toolCallId": "call-uuid",
  "output": "file1.txt\nfile2.txt",
  "isError": false
}
```

#### Generation Completed
```json
{
  "type": "result",
  "sessionId": "uuid",
  "cost": {
    "inputTokens": 500,
    "outputTokens": 300,
    "totalUSD": 0.012
  }
}
```

#### Full Sync (Resume)
```json
{
  "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
```json
{
  "type": "error",
  "message": "Failed to spawn Pi process",
  "errorCode": "SPAWN_ERROR"  // optional
}
```

#### Generation Stopped
```json
{
  "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

```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

```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
```http
POST /api/pi/sessions
Authorization: Bearer {token}

Response: { sessions: SessionMeta[] }
```

### Get Session Detail
```http
GET /api/pi/sessions/{sessionId}
Authorization: Bearer {token}

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

### Update Session (Rename)
```http
PATCH /api/pi/sessions/{sessionId}
Authorization: Bearer {token}
Body: { title: "New Title" }

Response: { success: true, session: SessionMeta }
```

### Delete Session
```http
DELETE /api/pi/sessions/{sessionId}
Authorization: Bearer {token}

Response: { success: true }
```

### Search Sessions
```http
GET /api/pi/sessions/search?q={query}
Authorization: Bearer {token}

Response: { results: SessionMeta[] }
```

### List Models
```http
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

```typescript
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`

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
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`:
   ```typescript
   export type ServerMessage =
     | { type: 'new-type'; data: string }
     | ...;
   ```

2. Handle in `websocket.ts`:
   ```typescript
   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`:
   ```typescript
   piRestRouter.get('/pi/new-endpoint', async (ctx: Context) => {
     const email = ctx.get('email');
     // Implementation
     return ctx.json({ result: '...' });
   });
   ```

2. Wire into `hono.ts`:
   ```typescript
   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

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

---

## Testing

### Manual Testing

```bash
# 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.
