diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 75bd3c9dfaca..7bace09ea37b 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -78,6 +78,7 @@ export namespace SessionPrompt { }, async (current) => { for (const item of Object.values(current)) { + item.callbacks = [] item.abort.abort() } }, @@ -261,6 +262,14 @@ export namespace SessionPrompt { SessionStatus.set(sessionID, { type: "idle" }) return } + + const error = new Error("Session cancelled") + const queued = match.callbacks + match.callbacks = [] + for (const q of queued) { + q.reject(error) + } + match.abort.abort() delete s[sessionID] SessionStatus.set(sessionID, { type: "idle" }) @@ -714,7 +723,9 @@ export namespace SessionPrompt { SessionCompaction.prune({ sessionID }) for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user") continue - const queued = state()[sessionID]?.callbacks ?? [] + const match = state()[sessionID] + const queued = match?.callbacks ?? [] + if (match) match.callbacks = [] for (const q of queued) { q.resolve(item) } diff --git a/packages/opencode/test/session/prompt-cancel.test.ts b/packages/opencode/test/session/prompt-cancel.test.ts new file mode 100644 index 000000000000..796ddc01caa4 --- /dev/null +++ b/packages/opencode/test/session/prompt-cancel.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "bun:test" +import { Instance } from "../../src/project/instance" +import { Session } from "../../src/session" +import { SessionPrompt } from "../../src/session/prompt" +import { Log } from "../../src/util/log" +import { tmpdir } from "../fixture/fixture" + +Log.init({ print: false }) + +describe("session.prompt cancel", () => { + test("rejects queued loop callbacks", async () => { + await using tmp = await tmpdir({ + git: true, + config: { + agent: { + build: { + model: "openai/gpt-5.2", + }, + }, + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}) + + const command = process.platform === "win32" ? "ping -n 6 127.0.0.1 >nul" : "sleep 5" + + const running = SessionPrompt.shell({ + sessionID: session.id, + agent: "build", + command, + }) + void running.catch(() => {}) + + const queued = SessionPrompt.loop({ sessionID: session.id }) + SessionPrompt.cancel(session.id) + + const result = await Promise.race([ + queued.then( + () => ({ type: "resolved" as const }), + (error) => ({ type: "rejected" as const, error }), + ), + Bun.sleep(200).then(() => ({ type: "timeout" as const })), + ]) + + expect(result.type).toBe("rejected") + if (result.type === "rejected") { + expect(result.error).toBeInstanceOf(Error) + expect((result.error as Error).message).toBe("Session cancelled") + } + + await Session.remove(session.id) + }, + }) + }) +})