Compare commits

...
1 Commits
Author SHA1 Message Date
Aiden Cline e3d6b7c93f fix(opencode): keep concurrent task resumes foreground 2026-07-24 16:06:04 -05:00
2 changed files with 72 additions and 6 deletions
+8 -6
View File
@@ -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 {
+64
View File
@@ -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<void>()
const firstDone = yield* Deferred.make<void>()
const secondStarted = yield* Deferred.make<void>()
const secondDone = yield* Deferred.make<void>()
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,
)