Skip to content

Commit 81efd48

Browse files
committed
fix(opencode): fork a stopped session before its end event arrives
Stop marks the active turn interrupted and leaves it in place until OpenCode's execution-end event arrives. A fork or rollback in that window refused with a protocol error even though OpenCode had already ended the run. Ask the server for the authoritative active set when the local turn was stopped, then proceed when the session is idle.
1 parent 3afb9b3 commit 81efd48

2 files changed

Lines changed: 42 additions & 9 deletions

File tree

‎apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3778,6 +3778,36 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => {
37783778
}).pipe(Effect.scoped),
37793779
);
37803780

3781+
it.effect("forks after a Stop before the interrupted execution's end event arrives", () =>
3782+
Effect.gen(function* () {
3783+
const FORK = "ses_f1484db83ffeLGtrRCFimo1H0e";
3784+
const { runtime, thread } = yield* resumed([
3785+
out("session.prompt", { sessionID: SESSION, text: "<any>" }),
3786+
promptAccepted,
3787+
event("session.execution.started", { sessionID: SESSION }),
3788+
out("session.interrupt", { sessionID: SESSION }),
3789+
reply("session.interrupt", { interrupted: true }),
3790+
// OpenCode has ended the run; its event has not reached T3 yet.
3791+
out("session.active"),
3792+
replyData("session.active", {}),
3793+
out("session.fork", { sessionID: SESSION }),
3794+
replyData("session.fork", sessionInfo({ id: FORK })),
3795+
out("session.update", { sessionID: FORK, permissions: "<any>" }),
3796+
reply("session.update", null),
3797+
]);
3798+
yield* runtime.startTurn(turnInput(thread));
3799+
yield* runtime.interruptTurn({
3800+
providerThread: thread,
3801+
providerTurnId: yield* providerTurnId,
3802+
});
3803+
const forked = yield* runtime.forkThread({
3804+
sourceProviderThread: thread,
3805+
targetThreadId: ThreadId.make("thread:opencode2-adapter:fork-after-stop"),
3806+
});
3807+
assert.equal(forked.nativeThreadRef?.nativeId, FORK);
3808+
}).pipe(Effect.scoped),
3809+
);
3810+
37813811
it.effect("moves a fork into its target thread's worktree before the first prompt", () =>
37823812
Effect.gen(function* () {
37833813
const FORK = "ses_f1484db83ffeLGtrRCFimo1H0e";

‎packages/provider-opencode/src/server/v2/adapter.ts‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2899,16 +2899,19 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr
28992899
verb: "roll back" | "fork",
29002900
) {
29012901
const state = threads.get(sessionId);
2902+
const background = state !== undefined && verb === "roll back" && hasBackground(state);
29022903
const busyHere =
2903-
state !== undefined &&
2904-
(state.active !== undefined ||
2905-
busy.has(sessionId) ||
2906-
(verb === "roll back" && hasBackground(state)));
2907-
const busyThere =
2908-
!busyHere &&
2909-
(state === undefined || state.unsettled) &&
2910-
sessionId in (yield* client.session.active().pipe(Effect.timeout(ACTIVE_CHECK_TIMEOUT)));
2911-
if (busyHere || busyThere) {
2904+
state !== undefined && (state.active !== undefined || busy.has(sessionId) || background);
2905+
// A Stop replies before OpenCode's execution-end event reaches T3. The
2906+
// run is already over there, so ask the server instead of refusing.
2907+
const stopping = state?.active?.interrupted === true;
2908+
const active =
2909+
(busyHere && stopping) || state === undefined || state.unsettled
2910+
? yield* client.session.active().pipe(Effect.timeout(ACTIVE_CHECK_TIMEOUT))
2911+
: undefined;
2912+
const busyThere = active !== undefined && sessionId in active;
2913+
const stoppedBeforeEvent = stopping && !background && active !== undefined && !busyThere;
2914+
if ((busyHere && !stoppedBeforeEvent) || busyThere) {
29122915
return yield* new ProviderAdapter.ProviderAdapterProtocolError({
29132916
driver: OPENCODE_PROVIDER,
29142917
detail: `Cannot ${verb} OpenCode session ${sessionId} while it is still working`,

0 commit comments

Comments
 (0)