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:
@@ -1,19 +1,17 @@
|
||||
import type { ChatEvent, MessageCost } from '../../api/chat/types';
|
||||
import type { OpenCodeRunParams } from '../protocol';
|
||||
import type { RunnerMessage } from './runner';
|
||||
import { mapServeEvent } from './serve-events';
|
||||
|
||||
// Phase B: drive a turn through the serve instead of spawning `opencode run`.
|
||||
// How an OpenCode turn runs. The only way, since 2026-08-10.
|
||||
//
|
||||
// OFF BY DEFAULT. `index.ts` picks between this and `runner.ts` on `OPENCODE_TURNS`, and the subprocess
|
||||
// stays the default until this has run for a while — a bad day should be one restart from the path that
|
||||
// has worked all along, not a rollback.
|
||||
// It used to be `opencode run --format json`, a subprocess per turn with `stdin: 'ignore'`. Everything
|
||||
// that path could not do followed from that one closed pipe: no token streaming, no mid-turn injection,
|
||||
// no queue, and a stop that could only kill the session rather than interrupt it. The serve offers all
|
||||
// four as primitives, each verified end to end before the subprocess was deleted
|
||||
// (docs/opencode-fork-decision.md, docs/opencode-serve-migration-plan.md).
|
||||
//
|
||||
// ── Why bother, given the subprocess works ──
|
||||
//
|
||||
// Everything the subprocess cannot do is a consequence of `stdin: 'ignore'`: no token streaming, no
|
||||
// mid-turn injection, no queue, no interrupt that leaves the session alive. The serve offers all four as
|
||||
// primitives, verified against 1.18.16 (docs/opencode-fork-decision.md).
|
||||
// There is no fallback engine any more. If this path breaks, the recovery is git, not a config flag —
|
||||
// a deliberate choice made while nothing depended on OpenCode.
|
||||
//
|
||||
// ── One global stream, demultiplexed ──
|
||||
//
|
||||
@@ -39,6 +37,18 @@ type ServeConfig = {
|
||||
fallbackCwd: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a turn reports to the sidecar it runs in.
|
||||
*
|
||||
* Lived in `runner.ts` until the subprocess path was deleted (Phase D). `opencode:event` is not a wire
|
||||
* event: the sidecar translates each one into a TurnMessage and commits it to `chat_session_events`
|
||||
* before officer sees anything, so the durable record does not depend on officer being up.
|
||||
* `opencode:session` is the routing fact — which `ses_…` to resume — and goes over the wire live.
|
||||
*/
|
||||
export type RunnerMessage =
|
||||
| { type: 'opencode:event'; sessionKey: string; event: ChatEvent }
|
||||
| { type: 'opencode:session'; sessionKey: string; sessionId: string };
|
||||
|
||||
type Emit = (msg: RunnerMessage) => void;
|
||||
|
||||
type ServeTurn = {
|
||||
@@ -301,7 +311,7 @@ function promptFiles(images: OpenCodeRunParams['images']): { uri: string; name:
|
||||
}));
|
||||
}
|
||||
|
||||
/** The serve analog of `listRunningOpenCodeTurns`. Same shape, so the Live panel needs no changes. */
|
||||
/** The turns running right now, for `opencode:list` and the Live panel. */
|
||||
export function listRunningServeTurns(): { sessionKey: string }[] {
|
||||
return [...bySessionKey.keys()].map((sessionKey) => ({ sessionKey }));
|
||||
}
|
||||
@@ -326,7 +336,14 @@ export async function killServeTurn(sessionKey: string, config: ServeConfig): Pr
|
||||
retire(turn, { type: 'stopped' });
|
||||
}
|
||||
|
||||
/** Retire every live turn, for shutdown. Mirrors `stopAllOpenCodeTurns` on the subprocess path. */
|
||||
/**
|
||||
* Retire every live turn, for shutdown.
|
||||
*
|
||||
* Note what this does NOT do, and why it is right: the turns keep running inside the serve, which is a
|
||||
* separate process and survives us. Officer stops routing them and says so in the transcript. When the
|
||||
* subprocess ran turns, shutdown had to kill children or they were orphaned; here the work is somebody
|
||||
* else's and killing it would be the wrong call.
|
||||
*/
|
||||
export function stopAllServeTurns(message: string): number {
|
||||
const turns = [...bySessionKey.values()];
|
||||
for (const turn of turns) retire(turn, { type: 'error', message });
|
||||
|
||||
Reference in New Issue
Block a user