process sidecar: independent process manager for long-running work

Introduces a separate Bun process (port 5100) that owns all spawned
processes and long-running work, so the API server can restart freely
without disrupting active sessions.

The sidecar owns:
- Anthropic proxy (port 5051) with persisted secret across restarts
- Claude Code process spawning and session tracking (--resume support)
- Pi agent spawning and RPC lifecycle (prompt/abort/thinking)
- Job queue engine (lane processing, retries, notifications)

The API server becomes a thin client that forwards commands over a
single WebSocket connection with auto-reconnect. send-claude-code.ts
goes from 550 lines of spawn logic to 73 lines of sidecar delegation.

State persisted to data/sidecar/state.json every 30s and on shutdown.
Lockfile prevents duplicate instances. See SIDECAR.md for full docs
and manual testing procedures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 07:36:47 +00:00
co-authored by Claude Opus 4.6
parent 726c77e346
commit 86c2d6333a
21 changed files with 2415 additions and 610 deletions
+115
View File
@@ -0,0 +1,115 @@
import type { MessageCost, PiEvent } from '../api/pi/types';
import type { Job, EnqueueParams, JobProgress } from '../queue/types';
// ── Envelope ──
export type SidecarMessage = SidecarCommand | SidecarEvent;
// ── Commands (API server → sidecar) ──
export type SidecarCommand =
| { type: 'ping'; id: string }
| { type: 'state:sync'; id: string }
// Proxy
| { type: 'proxy:secret'; id: string }
// Claude Code
| { type: 'claude:spawn'; id: string; params: ClaudeSpawnParams }
| { type: 'claude:spawn-streaming'; id: string; params: ClaudeSpawnStreamingParams }
| { type: 'claude:kill'; id: string; sessionKey: string }
| { type: 'claude:clear-session'; id: string; sessionKey: string }
// Pi
| { type: 'pi:spawn'; id: string; params: PiSpawnParams }
| { type: 'pi:prompt'; id: string; sessionId: string; prompt: string; requestId: string }
| { type: 'pi:abort'; id: string; sessionId: string; requestId: string }
| { type: 'pi:kill'; id: string; sessionId: string }
| { type: 'pi:set-thinking'; id: string; sessionId: string; level: string }
// Queue
| { type: 'queue:enqueue'; id: string; params: EnqueueParams }
| { type: 'queue:cancel'; id: string; jobId: string }
| { type: 'queue:list'; id: string }
| { type: 'queue:get'; id: string; jobId: string };
// ── Responses/Events (sidecar → API server) ──
export type SidecarEvent =
| { type: 'pong'; id: string }
| { type: 'state:sync'; id: string; state: SidecarState }
| { type: 'proxy:secret'; id: string; secret: string }
// Claude Code
| { type: 'claude:spawned'; id: string; sessionKey: string }
| { type: 'claude:event'; sessionKey: string; event: PiEvent }
| { type: 'claude:result'; id: string; result: ClaudeCodeResult }
| { type: 'claude:error'; id: string; error: string }
| { type: 'claude:killed'; id: string }
| { type: 'claude:session-cleared'; id: string }
// Pi
| { type: 'pi:spawned'; id: string; sessionId: string }
| { type: 'pi:event'; sessionId: string; event: PiEvent }
| { type: 'pi:error'; id: string; error: string }
| { type: 'pi:killed'; id: string }
// Queue
| { type: 'queue:enqueued'; id: string; job: Job }
| { type: 'queue:cancelled'; id: string; job: Job | null }
| { type: 'queue:list'; id: string; jobs: Job[] }
| { type: 'queue:get'; id: string; job: Job | null }
| { type: 'queue:error'; id: string; error: string }
// Generic
| { type: 'error'; id?: string; error: string };
// ── Shared state snapshot ──
export type SidecarState = {
proxySecret: string;
claudeSessions: Record<string, string>; // sessionKey → Claude Code session_id
piSessions: PiSessionInfo[];
uptime: number;
};
export type PiSessionInfo = {
sessionId: string;
email: string;
userId: number;
model: string;
cwd: string;
pid: number;
alive: boolean;
};
// ── Param types ──
export type ClaudeSpawnParams = {
userId: number;
email: string;
username: string;
prompt: string;
sessionKey: string;
model?: string;
};
export type ClaudeSpawnStreamingParams = {
userId: number;
email: string;
username: string;
prompt: string;
sessionKey: string;
cwd?: string;
model?: string;
};
export type ClaudeCodeResult = {
text: string;
sessionId: string;
model: string;
cost: MessageCost;
};
export type PiSpawnParams = {
sessionId: string;
email: string;
userId: number;
username: string;
role: string;
cwd: string;
model: string;
sessionFile?: string;
};