phase D: delete the subprocess path
The serve is now the only way an opencode turn runs. runner.ts and its tests are gone, and so is the OPENCODE_TURNS switch — there is no fallback engine any more, and the recovery for a bad day is git rather than a config flag. Deliberate, and cheap right now precisely because nothing depends on opencode yet. What goes with it: mapRunLine and its NDJSON fixtures, the temp-file spill for --file image attachments, the supersede-and-kill dance, the process watchdogs, the pidfile-adjacent child tracking, and stopAllOpenCodeTurns. All of it existed to work around stdin being /dev/null. Verified after deletion, with no env var set at all: tool call, tool result, 5 streaming deltas, text and cost, through the real chat socket. Also corrected the comments the deletion falsified rather than leaving them to mislead — the module header, the wire contract description of opencode:run-streaming, and serve-runner own header, which still announced itself as off by default. One difference worth stating: shutdown no longer kills anything. Turns run inside the serve, which is a separate process that survives us, so officer stops routing them and says so in the transcript. When a subprocess ran the turn, failing to kill it orphaned it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,17 +7,16 @@ import { sweepRecordedServe } from './serve-sweep';
|
||||
import { connectProviderCredential } from './connect-credential';
|
||||
import { createSessionLogStore } from '../claude/session-log';
|
||||
import type { SidecarCommand, SidecarEvent } from '../protocol';
|
||||
import type { RunnerMessage } from './runner';
|
||||
import { runOpenCodeTurn, killOpenCodeTurn, listRunningOpenCodeTurns, stopAllOpenCodeTurns } from './runner';
|
||||
import type { RunnerMessage } from './serve-runner';
|
||||
import { runOpenCodeTurnOnServe, killServeTurn, listRunningServeTurns, stopAllServeTurns } from './serve-runner';
|
||||
|
||||
// The OpenCode sidecar (officer-opencode). Same philosophy as officer-claude: a singleton process that
|
||||
// OWNS its runtime — here, an `opencode serve` — registers with the API server, and answers commands. It
|
||||
// listens on a random port, reported to the API on connect so it can route there.
|
||||
//
|
||||
// The serve's working directory is DATA_PATH/opencode_server, and that is now ALL it is: turns do not go
|
||||
// through the serve, they are `opencode run --dir <cwd>` subprocesses (runner.ts). The serve is used for
|
||||
// session CRUD and model enumeration only.
|
||||
// The serve runs EVERYTHING: turns (serve-runner.ts), session CRUD and model enumeration. It used to be
|
||||
// CRUD only, with turns spawned as `opencode run --dir <cwd>` subprocesses — that path was deleted on
|
||||
// 2026-08-10 once the serve had streaming, mid-turn injection and interrupt working end to end.
|
||||
|
||||
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
|
||||
const OPENCODE_BIN = process.env.OPENCODE_BIN || join(homedir(), '.opencode', 'bin', 'opencode');
|
||||
@@ -144,19 +143,8 @@ void connectProviderCredential(baseUrl);
|
||||
|
||||
type ReplyFn = (msg: SidecarEvent) => void;
|
||||
|
||||
const RUNNER_CONFIG = { bin: OPENCODE_BIN, fallbackCwd: SERVE_CWD };
|
||||
const SERVE_RUNNER_CONFIG = { baseUrl, fallbackCwd: SERVE_CWD };
|
||||
|
||||
// Which engine runs a turn. `subprocess` (the default) spawns `opencode run`; `serve` drives the
|
||||
// serve's /api/session surface, which is the only way to get streaming, steer, queue and a stop that
|
||||
// leaves the session alive.
|
||||
//
|
||||
// A switch rather than a replacement, and defaulted to the old path on purpose: the subprocess has
|
||||
// worked all day and the serve path has not been lived with yet. A bad evening should cost one restart
|
||||
// with OPENCODE_TURNS unset, not a revert.
|
||||
const USE_SERVE_TURNS = (process.env.OPENCODE_TURNS ?? 'subprocess').toLowerCase() === 'serve';
|
||||
console.log(`[opencode] turn engine: ${USE_SERVE_TURNS ? 'serve (/api/session)' : 'subprocess (opencode run)'}`);
|
||||
|
||||
function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
|
||||
switch (cmd.type) {
|
||||
case 'ping':
|
||||
@@ -178,24 +166,18 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
|
||||
connection.send(msg);
|
||||
};
|
||||
|
||||
if (USE_SERVE_TURNS) void runOpenCodeTurnOnServe(cmd.params, SERVE_RUNNER_CONFIG, onMessage);
|
||||
else runOpenCodeTurn(cmd.params, RUNNER_CONFIG, onMessage);
|
||||
void runOpenCodeTurnOnServe(cmd.params, SERVE_RUNNER_CONFIG, onMessage);
|
||||
reply({ type: 'opencode:spawned', id: cmd.id, sessionKey });
|
||||
break;
|
||||
}
|
||||
case 'opencode:list':
|
||||
reply({
|
||||
type: 'opencode:sessions',
|
||||
id: cmd.id,
|
||||
sessions: USE_SERVE_TURNS ? listRunningServeTurns() : listRunningOpenCodeTurns(),
|
||||
});
|
||||
reply({ type: 'opencode:sessions', id: cmd.id, sessions: listRunningServeTurns() });
|
||||
break;
|
||||
|
||||
case 'opencode:kill':
|
||||
// On the serve this is an INTERRUPT: the turn stops and the session survives, so the conversation
|
||||
// can be continued rather than only re-opened.
|
||||
if (USE_SERVE_TURNS) void killServeTurn(cmd.sessionKey, SERVE_RUNNER_CONFIG);
|
||||
else killOpenCodeTurn(cmd.sessionKey);
|
||||
// An INTERRUPT, not a kill: the turn stops and the session survives, so the conversation can be
|
||||
// continued rather than only re-opened.
|
||||
void killServeTurn(cmd.sessionKey, SERVE_RUNNER_CONFIG);
|
||||
sessionLog.drop(cmd.sessionKey);
|
||||
break;
|
||||
default:
|
||||
@@ -247,7 +229,7 @@ async function shutdown(signal: string) {
|
||||
// over this socket. Tearing it down first would stop every turn silently — the exact outcome this is
|
||||
// here to prevent.
|
||||
const message = `The OpenCode sidecar restarted (${signal}), so this turn stopped.`;
|
||||
const stopped = USE_SERVE_TURNS ? stopAllServeTurns(message) : stopAllOpenCodeTurns(message);
|
||||
const stopped = stopAllServeTurns(message);
|
||||
if (stopped > 0) {
|
||||
console.log(`[opencode] ${signal} received, stopping ${stopped} in-flight turn(s)...`);
|
||||
await Promise.race([sessionLog.flush(), Bun.sleep(SHUTDOWN_FLUSH_MS)]);
|
||||
|
||||
Reference in New Issue
Block a user