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>
This commit is contained in:
@@ -168,11 +168,39 @@ export async function runOpenCodeTurnOnServe(
|
||||
|
||||
ensureEventStream(config);
|
||||
|
||||
// Supersede any turn still registered under this key. Unlike the subprocess path there is no process
|
||||
// to kill — the serve owns execution — so this is bookkeeping only, and the old turn is retired
|
||||
// silently rather than reporting an error against a key that now belongs to its replacement.
|
||||
const stale = bySessionKey.get(sessionKey);
|
||||
if (stale) retire(stale, null);
|
||||
// ── A message arriving while a turn is running is an INJECTION, not a new turn ──
|
||||
//
|
||||
// This is where the subprocess and the serve genuinely part company. `opencode run` had no input
|
||||
// channel, so a second message could only supersede: kill the process, start again, lose the turn.
|
||||
// The serve takes another prompt into the RUNNING turn, so the right move is to hand it over and keep
|
||||
// the existing turn exactly as it is.
|
||||
//
|
||||
// Keeping the same turn object is the load-bearing part. Retiring it and registering a replacement —
|
||||
// which is what this did at first — stops officer routing the events the serve is still producing,
|
||||
// while the serve carries on regardless. The output goes nowhere and the turn looks hung.
|
||||
//
|
||||
// `steer` because the user typed it during the turn and means it now; officer's composer already
|
||||
// treats a send-while-generating as "add this to what you are doing". A prompt sent when nothing is
|
||||
// running takes `queue`, which is a no-op with an empty queue but never accidentally merges two
|
||||
// messages into one turn.
|
||||
const live = bySessionKey.get(sessionKey);
|
||||
if (live && !live.done) {
|
||||
try {
|
||||
await serveJson(config, `/api/session/${live.openCodeSessionId}/prompt`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt: { text: params.prompt }, delivery: 'steer' }),
|
||||
cwd,
|
||||
});
|
||||
} catch (err) {
|
||||
// The turn itself is unharmed — only the injection failed — so say so and leave it running.
|
||||
emit({
|
||||
type: 'opencode:event',
|
||||
sessionKey,
|
||||
event: { type: 'error', message: `OpenCode would not take that mid-turn: ${errText(err)}` },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let openCodeSessionId = params.resumeSessionId ?? '';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user