Commit Graph
4 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 a3dbda7d3b 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>
2026-08-10 19:33:58 +01:00
pastilhasandClaude Opus 5 b79eca45fb send images on the serve path too, before anyone switches to it
runOpenCodeTurnOnServe ignored params.images entirely, which is defect B4 rebuilt on the new
path: the image renders in your own bubble and the model never receives it, with nothing
reporting a loss. Fixed before the path is switched on for anyone rather than after.

data: URIs, not file://, and that is measured — the wrong choice is accepted with a 200 and
then dies inside the turn with "Anthropic Messages media must contain valid base64". The
data URI round-trips and the model describes the image.

Strictly better than the subprocess path here: no temp file to spill and nothing to clean up,
because the bytes travel in the request.

Both prompt paths carry them — an ordinary send and a mid-turn injection. Verified end to end
through the chat socket with the serve engine on: a red png came back "**Red**", with deltas
streaming.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:25:48 +01:00
pastilhasandClaude Opus 5 a058cbb3fd inject a mid-turn message instead of replacing the turn
Phase C server half, and a real flaw in phase B.

On the subprocess path a second message could only supersede — kill the process, start again,
lose the turn — because opencode run has no input channel. The serve takes another prompt
into the running turn, so a message arriving mid-turn is handed over with delivery steer and
the existing turn is left exactly as it is.

Keeping the same turn object is the load-bearing part. Phase B retired it and registered a
replacement, which stops officer routing events the serve is still producing while the serve
carries on regardless: output goes nowhere and the turn looks hung.

Verified end to end through the chat socket — sent a count to 50, injected a change of plan
eight seconds in, and BANANA INJECTED came back inside the same turn with deltas streaming
throughout.

No client change was needed. Officer composer already sends while generating; the difference
is only what the sidecar does with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:17:26 +01:00
pastilhasandClaude Opus 5 a06422bd4c phase B: run a turn through the serve, behind a switch
OPENCODE_TURNS=serve picks the new engine; unset keeps the subprocess, which is the default
and stays the default until this has been lived with. A bad evening should cost one restart,
not a revert. Claude is a different sidecar and is untouched.

Verified end to end through the real chat socket:

  session:init -> tool:start(bash) -> tool:result -> assistant:delta x3 -> assistant:text
  -> result, cost in=304 out=73

Those deltas are the first token streaming an opencode turn has ever produced in officer.
Stop is now an INTERRUPT: the turn ends and the session survives — verified by sending a
second prompt to the same session afterwards and getting an answer, which killing a
subprocess could never do.

Reads the LIVE global stream rather than the durable per-session one, because it is a strict
superset — same tool.called, tool.success, step.ended, text.ended, plus the deltas that are
the whole point. Global means one socket carries every session, so everything filters on
sessionID; one subscription is shared for the process rather than one per turn.

A turn ends on step.ended with finish != tool-calls. tool-calls is a step boundary MID-turn,
and treating it as terminal would cut every tool-using conversation in half.

delivery is stated explicitly as queue because it DEFAULTS to steer, which injects into a
running turn — wrong for an ordinary send, where two quick messages would merge into one.
Wiring steer to the button that means it is phase C.

What phase B does not do: read the durable stream. The sidecar still commits every event to
chat_session_events as it arrives, so durability is unchanged, but recovering a turn this
process never saw needs the ?after= cursor and is its own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:12:49 +01:00