Root fix for orphaned background tasks: the platform drove Claude Code as a
one-shot `claude -p` per turn (stdin ignored, process exits at turn end), so
run_in_background/Monitor work — and its task_notification — had no live harness
to return to. Now each chat session runs ONE long-lived Agent SDK query() with
streaming input; turns are user messages pushed onto it, and the session stays
warm between turns.
- claude-manager: persistent `query({ prompt: AsyncIterable, options })` per
sessionKey (bypassPermissions, --resume, mcp via extraArgs, CLAUDECODE stripped).
Single consumer loop maps every SDK message → ChatEvent, incl. post-turn
task_started / task_notification. interrupt() = stop-turn; abort() = kill-session;
30-min idle GC.
- stream-parser: processMessage() (object-level, reused by the SDK loop) + task
message handling. ChatEvent/ServerMessage gain task:started / task:notification.
- API: the sidecar event subscription is now SESSION-scoped (no longer unsubscribes
on 'result'), so background events after turn-end still reach the client. First
turn opens the session; later turns push onto it. handleStop → interrupt (keeps
session warm); disconnect/deleteSession → kill.
- protocol/sidecar-registry/user-instance: claude:interrupt command + interruptClaude.
- client: render task:started / task:notification in the transcript.
Verified end-to-end through the real chat WS: a run_in_background task's completion
arrives ~6s AFTER the turn's result; multi-turn on one warm session works.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import type { MessageCost, ChatEvent } 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 }
|
|
| { type: 'claude:event'; sessionKey: string; event: ChatEvent }
|
|
| { 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-event stream, session id report
|
|
| { type: 'opencode:spawned'; id: string; sessionKey: string }
|
|
| { type: 'opencode:event'; sessionKey: string; event: ChatEvent }
|
|
| { 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 }
|
|
// 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)
|
|
};
|
|
|
|
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`)
|
|
};
|
|
|
|
// ── 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 };
|