From e3d6b7c93f0e63b3e5417556366e71654601b077 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 24 Jul 2026 16:06:04 -0500 Subject: [PATCH] fix(opencode): keep concurrent task resumes foreground --- packages/opencode/src/tool/task.ts | 14 +++--- packages/opencode/test/tool/task.test.ts | 64 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 1384e5d19725..deb61a68a5f2 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -253,7 +253,9 @@ export const TaskTool = Tool.define( ) }) - if (yield* background.extend({ id: nextSession.id, run: runTask() })) { + const extended = yield* background.extend({ id: nextSession.id, run: runTask() }) + if (extended && runInBackground) { + yield* background.promote(nextSession.id) return { title: params.description, metadata: { @@ -320,11 +322,11 @@ export const TaskTool = Tool.define( }), () => Effect.gen(function* () { - const result = yield* Effect.raceFirst( - background.wait({ id: nextSession.id }).pipe(Effect.map((waited) => waited.info)), - background.waitForPromotion(nextSession.id), - ) - if (result?.metadata?.background === true) return backgroundResult() + const wait = background.wait({ id: nextSession.id }).pipe(Effect.map((waited) => waited.info)) + const result = yield* (extended + ? wait + : Effect.raceFirst(wait, background.waitForPromotion(nextSession.id))) + if (!extended && result?.metadata?.background === true) return backgroundResult() if (result?.status === "error") return yield* Effect.fail(new Error(result.error ?? "Task failed")) if (result?.status === "cancelled") return yield* Effect.fail(new Error("Task cancelled")) return { diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 2bcf05a2a49b..905ebdb63e9e 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -255,6 +255,69 @@ describe("tool.task", () => { }), ) + it.instance("concurrent foreground resumes remain foreground", () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" }) + const tool = yield* TaskTool + const def = yield* tool.init() + const firstStarted = yield* Deferred.make() + const firstDone = yield* Deferred.make() + const secondStarted = yield* Deferred.make() + const secondDone = yield* Deferred.make() + let prompts = 0 + const promptOps: TaskPromptOps = { + ...stubOps(), + prompt: (input) => { + prompts++ + if (prompts === 1) + return Deferred.succeed(firstStarted, undefined).pipe( + Effect.andThen(Deferred.await(firstDone)), + Effect.as(reply(input, "first done")), + ) + return Deferred.succeed(secondStarted, undefined).pipe( + Effect.andThen(Deferred.await(secondDone)), + Effect.as(reply(input, "second done")), + ) + }, + } + const context = { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, + } + const execute = (prompt: string) => + def.execute( + { + description: prompt, + prompt, + subagent_type: "general", + task_id: child.id, + }, + context, + ) + + const first = yield* execute("first follow-up").pipe(Effect.forkChild) + yield* Deferred.await(firstStarted) + const second = yield* execute("second follow-up").pipe(Effect.forkChild) + + yield* Deferred.succeed(firstDone, undefined) + yield* Deferred.await(secondStarted) + yield* Deferred.succeed(secondDone, undefined) + + const results = yield* Effect.all([Fiber.join(first), Fiber.join(second)]) + expect(prompts).toBe(2) + expect(results.every((result) => result.metadata.background !== true)).toBe(true) + expect(results.every((result) => result.output.includes('state="completed"'))).toBe(true) + }), + ) + it.instance("execute asks by default and skips checks when bypassed", () => Effect.gen(function* () { const { chat, assistant } = yield* seed() @@ -721,6 +784,7 @@ describe("tool.task", () => { prompt: "also inspect cancellation", subagent_type: "general", task_id: started.metadata.sessionId, + background: true, }, context, )