The second copy of the same problem. The opencode sidecar reported raw ChatEvents and officer translated them, buffered the assistant text and wrote every durable message to chat_session_events — so an officer restart mid-turn lost whatever the model had produced since the last write, and `connect.ts` dropped the events that arrived while it was down without a word. Both harnesses speak ChatEvents, so the sidecar reuses the agent's session log verbatim: translate, commit, then deliver the finished message with its cursor id as `opencode:message`. Officer folds it into the in-memory transcript and relays it, exactly as it now does for claude — `createEventHandler` (166 lines, a duplicate of turn-stream.ts) and `emitToSession` are gone, and nothing in officer writes to chat_session_events any more. `opencode:event` stops being a wire event; it is the runner's internal report to the sidecar it runs in, typed as such so it cannot leak back onto the socket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
168 lines
6.5 KiB
TypeScript
168 lines
6.5 KiB
TypeScript
import type { MessageCost, TurnMessage } from '../api/chat/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:interrupt'; id: string; sessionKey: string }
|
|
| { type: 'claude:clear-session'; id: string; sessionKey: string }
|
|
// OpenCode — drive a turn via `opencode run … --format json` (tools re-anchored to cwd via --dir)
|
|
| { type: 'opencode:run-streaming'; id: string; params: OpenCodeRunParams }
|
|
| { type: 'opencode:kill'; id: string; sessionKey: string }
|
|
// VNC
|
|
| { type: 'vnc:start'; id: string; params: VncStartParams }
|
|
// Provision the VNC password without starting a server — the UI needs it before it can connect
|
|
| { type: 'vnc:ensure-password'; id: string; email: string }
|
|
| { type: 'vnc:stop'; id: string; email: string }
|
|
| { type: 'vnc:status'; id: string; email: string };
|
|
|
|
// ── Responses/Events (sidecar → API server) ──
|
|
|
|
export type SidecarEvent =
|
|
| { type: 'pong'; id: string }
|
|
| { type: 'state:sync'; id: string; state: ClaudeState }
|
|
| { type: 'proxy:secret'; id: string; secret: string }
|
|
// Claude Code
|
|
| { type: 'claude:spawned'; id: string; sessionKey: string }
|
|
// A finished, browser-facing turn message. The agent has already committed it to chat_session_events
|
|
// and `seq` is its cursor id there; officer relays it verbatim. No `seq` means it is not durable —
|
|
// an `assistant:delta` (superseded by the text that follows) or a message whose write failed.
|
|
| { type: 'claude:message'; sessionKey: string; msg: TurnMessage; seq?: number }
|
|
| { type: 'claude:result'; id: string; result: ClaudeCodeResult }
|
|
| { type: 'claude:error'; id: string; error: string }
|
|
| { type: 'claude:killed'; id: string }
|
|
| { type: 'claude:interrupted'; id: string }
|
|
| { type: 'claude:session-cleared'; id: string }
|
|
// VNC
|
|
| { type: 'vnc:started'; id: string; port: number; display: number }
|
|
| { type: 'vnc:password'; id: string; password: string }
|
|
| { type: 'vnc:stopped'; id: string }
|
|
| { type: 'vnc:status'; id: string; session: VncSessionInfo | null }
|
|
| { type: 'vnc:error'; id: string; error: string }
|
|
// Email
|
|
| { type: 'email:new'; userEmail: string }
|
|
// OpenCode — the sidecar reports where its `opencode serve` is listening (random port) on connect
|
|
| { type: 'opencode:server'; port: number }
|
|
// OpenCode turn streaming (analog of claude:*): spawned ack, per-message stream, session id report
|
|
| { type: 'opencode:spawned'; id: string; sessionKey: string }
|
|
// Same contract as `claude:message`: a finished turn message the sidecar has already committed to
|
|
// chat_session_events, plus the cursor id it landed on. Officer relays it and folds it into its
|
|
// in-memory transcript; it does not translate or persist.
|
|
| { type: 'opencode:message'; sessionKey: string; msg: TurnMessage; seq?: number }
|
|
| { type: 'opencode:session'; sessionKey: string; sessionId: string }
|
|
| { type: 'opencode:error'; id: string; error: string }
|
|
// Music — the sidecar reports where its audio-streaming HTTP server is listening (random port) on connect
|
|
| { type: 'music:server'; port: number }
|
|
// Vault — the sidecar reports where its Vaultwarden reverse-proxy HTTP/WS server is listening on connect
|
|
| { type: 'vault:server'; port: number }
|
|
// slskd — the sidecar reports where its slskd reverse-proxy HTTP server is listening (random port) on connect
|
|
| { type: 'slskd:server'; port: number }
|
|
// Generic
|
|
| { type: 'error'; id?: string; error: string };
|
|
|
|
// ── Claude sidecar state ──
|
|
|
|
export type ClaudeState = {
|
|
proxySecret: string;
|
|
claudeSessions: Record<string, string>; // sessionKey → Claude Code session_id
|
|
};
|
|
|
|
// ── Param types ──
|
|
|
|
export type ClaudeSpawnParams = {
|
|
userId: number;
|
|
email: string;
|
|
username: string;
|
|
prompt: string;
|
|
sessionKey: string;
|
|
model?: string;
|
|
cwd?: string;
|
|
};
|
|
|
|
export type ClaudeSpawnStreamingParams = {
|
|
userId: number;
|
|
email: string;
|
|
username: string;
|
|
prompt: string;
|
|
sessionKey: string;
|
|
cwd?: string;
|
|
model?: string;
|
|
resumeSessionId?: string; // resume this Claude session uuid (from the /chat session list)
|
|
// Whether turn output should be committed to chat_session_events (default true). A chat session wants
|
|
// it — that is what survives an officer restart. A pipeline step does not: its sessionKey is a throwaway
|
|
// uuid no browser will ever replay, and the job's own event log is its record.
|
|
durable?: boolean;
|
|
};
|
|
|
|
export type ClaudeCodeResult = {
|
|
text: string;
|
|
sessionId: string;
|
|
model: string;
|
|
cost: MessageCost;
|
|
};
|
|
|
|
// ── OpenCode turn params ──
|
|
|
|
export type OpenCodeRunParams = {
|
|
sessionKey: string;
|
|
prompt: string;
|
|
cwd?: string; // passed to `opencode run --dir` — hard-re-anchors tools to this directory
|
|
model?: string; // `providerID/modelID` (e.g. opencode/claude-haiku-4-5); passed to --model verbatim
|
|
resumeSessionId?: string; // OpenCode `ses_…` id to continue (`--session`)
|
|
durable?: boolean; // commit turn output to chat_session_events (default true) — see ClaudeSpawnStreamingParams
|
|
};
|
|
|
|
// ── VNC types ──
|
|
|
|
export type VncStartParams = {
|
|
email: string;
|
|
username: string;
|
|
resolution?: string;
|
|
};
|
|
|
|
export type VncSessionInfo = {
|
|
email: string;
|
|
display: number;
|
|
port: number;
|
|
pid: number;
|
|
alive: boolean;
|
|
};
|
|
|
|
// ── PTY types ──
|
|
|
|
export type PtyInitConfig = {
|
|
sessionId: string;
|
|
shell?: { command: string; args?: string[] };
|
|
cwd?: string;
|
|
homeDir?: string;
|
|
userLabel?: string;
|
|
host?: boolean;
|
|
cols?: number;
|
|
rows?: number;
|
|
env?: Record<string, string>;
|
|
};
|
|
|
|
// PTY commands (API → PTY sidecar)
|
|
export type PtyCommand =
|
|
| { type: 'pty:init'; id: string; sessionId: string; config: PtyInitConfig }
|
|
| { type: 'pty:input'; id: string; sessionId: string; data: string }
|
|
| { type: 'pty:resize'; id: string; sessionId: string; cols: number; rows: number }
|
|
| { type: 'pty:close'; id: string; sessionId: string };
|
|
|
|
// PTY events (PTY sidecar → API)
|
|
export type PtyEvent =
|
|
| { type: 'pty:ready'; id: string; sessionId: string }
|
|
| { type: 'pty:output'; sessionId: string; data: string }
|
|
| { type: 'pty:exit'; sessionId: string; exitCode: number; signal?: number };
|