From f9a203da4b4a89123e22609b88e25708d8d50ad8 Mon Sep 17 00:00:00 2001 From: CreatorGhost Date: Sun, 12 Jul 2026 00:55:35 +0530 Subject: [PATCH] fix(session): settle busy status after stream close Ported from upstream anomalyco/opencode#36002. --- .../instance/httpapi/handlers/session.ts | 5 +++- packages/opencode/src/session/run-state.ts | 13 ++++++++- .../opencode/test/server/httpapi-sdk.test.ts | 28 +++++++++++++++++++ packages/opencode/test/session/prompt.test.ts | 3 ++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts index 662585020a64..c76d1d5b8ec0 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts @@ -303,7 +303,10 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", sessionID: ctx.params.sessionID, }) .pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) - return HttpServerResponse.stream(Stream.make(JSON.stringify(message)).pipe(Stream.encodeText), { + const body = Stream.make(JSON.stringify(message)).pipe(Stream.encodeText) + const settledBody = + ctx.payload.noReply === true ? body : body.pipe(Stream.ensuring(runState.settle(ctx.params.sessionID))) + return HttpServerResponse.stream(settledBody, { contentType: "application/json", }) }) diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts index 5cefdd04a3f3..5a339147dee7 100644 --- a/packages/opencode/src/session/run-state.ts +++ b/packages/opencode/src/session/run-state.ts @@ -11,6 +11,7 @@ import { SessionStatus } from "./status" export interface Interface { readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect readonly cancel: (sessionID: SessionID) => Effect.Effect + readonly settle: (sessionID: SessionID) => Effect.Effect readonly ensureRunning: ( sessionID: SessionID, onInterrupt: Effect.Effect, @@ -85,6 +86,16 @@ const layer = Layer.effect( yield* existing.cancel }) + const settle = Effect.fn("SessionRunState.settle")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + const existing = data.runners.get(sessionID) + if (existing?.busy) return + data.runners.delete(sessionID) + const current = yield* status.get(sessionID) + if (current.type === "idle") return + yield* status.set(sessionID, { type: "idle" }) + }) + const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* ( sessionID: SessionID, onInterrupt: Effect.Effect, @@ -104,7 +115,7 @@ const layer = Layer.effect( .pipe(Effect.catchTag("RunnerBusy", () => Effect.fail(busyError(sessionID)))) }) - return Service.of({ assertNotBusy, cancel, ensureRunning, startShell }) + return Service.of({ assertNotBusy, cancel, settle, ensureRunning, startShell }) }), ) diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index c81b3b771b24..7935cd5513bc 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -805,6 +805,34 @@ describe("HttpApi SDK", () => { ), ) + serverPathParity("settles session status immediately after prompt stream completes", (serverPath) => + withFakeLlm(serverPath, ({ sdk, llm }) => + Effect.gen(function* () { + yield* llm.text("status settled", { usage: { input: 5, output: 3 } }) + const session = yield* capture(() => + sdk.session.create({ + title: "status settles", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }), + ) + const sessionID = String(record(session.data).id) + const prompt = yield* capture(() => + sdk.session.prompt({ + sessionID, + agent: "build", + model: { providerID: "test", modelID: "test-model" }, + parts: [{ type: "text", text: "hello status" }], + }), + ) + const status = yield* capture(() => sdk.session.status()) + + expect(prompt.status).toBe(200) + expect(status.status).toBe(200) + expect(record(status.data)[sessionID]).toBeUndefined() + }), + ), + ) + httpapi( "includes project skills in REST API prompt context", withFakeLlmProject("default", { setup: writeProjectSkill }, ({ sdk, llm }) => diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 491ad06aaf47..e27002f5f556 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1062,6 +1062,7 @@ it.instance( const prompt = yield* SessionPrompt.Service const sessions = yield* Session.Service const status = yield* SessionStatus.Service + const run = yield* SessionRunState.Service yield* llm.hang @@ -1071,6 +1072,8 @@ it.instance( const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) yield* llm.wait(1) expect((yield* status.get(chat.id)).type).toBe("busy") + yield* run.settle(chat.id) + expect((yield* status.get(chat.id)).type).toBe("busy") yield* prompt.cancel(chat.id) yield* Fiber.await(fiber) expect((yield* status.get(chat.id)).type).toBe("idle")