Merge branch 'sidecars-opencode' into sidecars
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import type { ChatEvent, TurnMessage } from '../../api/chat/types';
|
||||
|
||||
// Translation from the parser's ChatEvent stream to the browser-facing turn messages, moved here from
|
||||
// the main server (`chat/websocket.ts:createEventHandler`). It lives with the process that produces the
|
||||
// Translation from a harness's ChatEvent stream to the browser-facing turn messages, moved here from the
|
||||
// main server (it was `createEventHandler` in `chat/websocket.ts`, once per harness). Both the agent and
|
||||
// the opencode sidecar use this one copy. It lives with the process that produces the
|
||||
// stream because it is stateful: `delta` events accumulate into a buffer that has to be flushed as one
|
||||
// `assistant:text` at the next boundary (a tool call, or the end of the turn). A consumer downstream of
|
||||
// a socket that can drop cannot hold that state correctly.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { DATA_PATH } from '../../data-path';
|
||||
import { createSidecarConnector } from '../connect';
|
||||
import { createSessionLogStore } from '../claude/session-log';
|
||||
import type { SidecarCommand, SidecarEvent } from '../protocol';
|
||||
import { runOpenCodeTurn, killOpenCodeTurn } from './runner';
|
||||
|
||||
@@ -131,22 +132,33 @@ console.log(`[opencode] serve healthy on port ${port}`);
|
||||
// ── Command handlers ──
|
||||
|
||||
type ReplyFn = (msg: SidecarEvent) => void;
|
||||
type SendFn = (msg: SidecarEvent) => void;
|
||||
|
||||
const RUNNER_CONFIG = { bin: OPENCODE_BIN, fallbackCwd: SERVE_CWD };
|
||||
|
||||
function handleCommand(cmd: SidecarCommand, reply: ReplyFn, send: SendFn) {
|
||||
function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
|
||||
switch (cmd.type) {
|
||||
case 'ping':
|
||||
reply({ type: 'pong', id: cmd.id });
|
||||
break;
|
||||
case 'opencode:run-streaming':
|
||||
// Fire the turn; events stream back via `send` (opencode:event / opencode:session / terminal).
|
||||
runOpenCodeTurn(cmd.params, RUNNER_CONFIG, send);
|
||||
reply({ type: 'opencode:spawned', id: cmd.id, sessionKey: cmd.params.sessionKey });
|
||||
case 'opencode:run-streaming': {
|
||||
const { sessionKey, durable = true } = cmd.params;
|
||||
// Turn output goes through the session log: translated to TurnMessages and committed to
|
||||
// chat_session_events here, in the process that produced it. Officer being down during a turn
|
||||
// no longer costs the transcript — the browser replays it from its cursor.
|
||||
runOpenCodeTurn(cmd.params, RUNNER_CONFIG, (msg) => {
|
||||
if (msg.type === 'opencode:event') {
|
||||
sessionLog.push(sessionKey, msg.event, durable);
|
||||
return;
|
||||
}
|
||||
// opencode:session is a routing fact (which `ses_…` to resume), not transcript — send it live.
|
||||
connection.send(msg);
|
||||
});
|
||||
reply({ type: 'opencode:spawned', id: cmd.id, sessionKey });
|
||||
break;
|
||||
}
|
||||
case 'opencode:kill':
|
||||
killOpenCodeTurn(cmd.sessionKey);
|
||||
sessionLog.drop(cmd.sessionKey);
|
||||
break;
|
||||
default:
|
||||
reply({
|
||||
@@ -164,8 +176,7 @@ const connection = createSidecarConnector({
|
||||
name: 'opencode',
|
||||
capabilities: ['opencode'],
|
||||
onCommand(cmd, reply) {
|
||||
// Streaming turn events use a stable send (always the current ws), not the per-command reply.
|
||||
handleCommand(cmd as SidecarCommand, reply as ReplyFn, (msg) => connection.send(msg));
|
||||
handleCommand(cmd as SidecarCommand, reply as ReplyFn);
|
||||
},
|
||||
onConnected() {
|
||||
// Tell the API where our OpenCode HTTP server is listening, so it can route requests there.
|
||||
@@ -174,6 +185,13 @@ const connection = createSidecarConnector({
|
||||
},
|
||||
});
|
||||
|
||||
// Translate → commit → deliver, in that order and one at a time per session. Shared with the agent
|
||||
// sidecar (`claude/session-log.ts`): both harnesses speak ChatEvents, so the translation and the write
|
||||
// are the same code, and only the wire event type differs.
|
||||
const sessionLog = createSessionLogStore((d) =>
|
||||
connection.send({ type: 'opencode:message', sessionKey: d.sessionId, msg: d.msg, seq: d.seq }),
|
||||
);
|
||||
|
||||
// ── Graceful shutdown ──
|
||||
|
||||
function shutdown(signal: string) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import type { Subprocess } from 'bun';
|
||||
import type { ChatEvent, MessageCost } from '../../api/chat/types';
|
||||
import type { OpenCodeRunParams, SidecarEvent } from '../protocol';
|
||||
import type { OpenCodeRunParams } from '../protocol';
|
||||
|
||||
// Drives one chat turn by spawning `opencode run … --format json` and mapping its newline-delimited
|
||||
// JSON events to the shared ChatEvent contract. This is the reliable path: `--dir <cwd>` hard-anchors
|
||||
@@ -21,7 +21,14 @@ export type RunnerConfig = {
|
||||
fallbackCwd: string; // used when params.cwd is missing/nonexistent
|
||||
};
|
||||
|
||||
type Emit = (event: SidecarEvent) => void;
|
||||
// What a turn reports to the sidecar it runs in. `opencode:event` is deliberately not a wire event any
|
||||
// more: the sidecar translates each one into a TurnMessage and commits it before officer sees anything,
|
||||
// so the durable record does not depend on officer being up (see index.ts).
|
||||
export type RunnerMessage =
|
||||
| { type: 'opencode:event'; sessionKey: string; event: ChatEvent }
|
||||
| { type: 'opencode:session'; sessionKey: string; sessionId: string };
|
||||
|
||||
type Emit = (msg: RunnerMessage) => void;
|
||||
|
||||
type RunHandle = { proc: Subprocess; killedByUser: boolean };
|
||||
|
||||
@@ -108,7 +115,10 @@ export function runOpenCodeTurn(params: OpenCodeRunParams, config: RunnerConfig,
|
||||
} catch {
|
||||
/* already gone */
|
||||
}
|
||||
finish({ type: 'error', message: `OpenCode turn stalled (no output for ${INACTIVITY_MS / 1000}s) and was stopped` });
|
||||
finish({
|
||||
type: 'error',
|
||||
message: `OpenCode turn stalled (no output for ${INACTIVITY_MS / 1000}s) and was stopped`,
|
||||
});
|
||||
}, INACTIVITY_MS);
|
||||
};
|
||||
bumpInactivity();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MessageCost, ChatEvent, TurnMessage } from '../api/chat/types';
|
||||
import type { MessageCost, TurnMessage } from '../api/chat/types';
|
||||
|
||||
// ── Envelope ──
|
||||
|
||||
@@ -54,9 +54,12 @@ export type SidecarEvent =
|
||||
| { 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
|
||||
// OpenCode turn streaming (analog of claude:*): spawned ack, per-message stream, session id report
|
||||
| { type: 'opencode:spawned'; id: string; sessionKey: string }
|
||||
| { type: 'opencode:event'; sessionKey: string; event: ChatEvent }
|
||||
// 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
|
||||
@@ -117,6 +120,7 @@ export type OpenCodeRunParams = {
|
||||
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 ──
|
||||
|
||||
Reference in New Issue
Block a user