From 4a5625f178ec472fcab08906259e03180f78e89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chuyusong=E2=80=9D?= <1350460443@qq.com> Date: Fri, 20 Mar 2026 03:17:01 +0800 Subject: [PATCH 1/2] fix: reject pending callbacks in cancel to prevent hanging When a session is cancelled (e.g., due to an error or abort), the callbacks array containing pending Promise resolve/reject handlers was not being processed. This caused callers waiting for the session to complete to hang indefinitely. This fix ensures all pending callbacks are rejected with an AbortError before the session state is cleaned up, both in the cancel() function and in the state dispose handler. --- packages/opencode/src/session/prompt.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index bac958ec1033..a20b6e2b5269 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -82,6 +82,15 @@ export namespace SessionPrompt { }, async (current) => { for (const item of Object.values(current)) { + // Reject all pending callbacks to prevent callers from hanging + const cancelError = new DOMException("Session cancelled", "AbortError") + for (const callback of item.callbacks) { + try { + callback.reject(cancelError) + } catch (e) { + log.error("failed to reject callback during dispose", { error: e }) + } + } item.abort.abort() } }, @@ -265,6 +274,15 @@ export namespace SessionPrompt { SessionStatus.set(sessionID, { type: "idle" }) return } + // Reject all pending callbacks to prevent callers from hanging + const cancelError = new DOMException("Session cancelled", "AbortError") + for (const callback of match.callbacks) { + try { + callback.reject(cancelError) + } catch (e) { + log.error("failed to reject callback during cancel", { error: e, sessionID }) + } + } match.abort.abort() delete s[sessionID] SessionStatus.set(sessionID, { type: "idle" }) From ac1a8cf46222299148594464f85a307a2bbbcffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chuyusong=E2=80=9D?= <1350460443@qq.com> Date: Fri, 20 Mar 2026 20:20:15 +0800 Subject: [PATCH 2/2] fix: improve subagent error handling to prevent hanging - Add try-catch in task.ts to catch and propagate subagent execution errors - Check session state existence in loop() before adding callbacks - Prevent race conditions where callbacks are added to deleted session state - Ensure parent agent receives proper error notification when subagent fails This fixes issues where subagents appear to be running but are actually crashed or cancelled, causing parent agents to wait indefinitely. --- packages/opencode/src/session/prompt.ts | 10 +++++-- packages/opencode/src/tool/task.ts | 38 ++++++++++++++----------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index a20b6e2b5269..efe635d28844 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -299,8 +299,14 @@ export namespace SessionPrompt { const abort = resume_existing ? resume(sessionID) : start(sessionID) if (!abort) { return new Promise((resolve, reject) => { - const callbacks = state()[sessionID].callbacks - callbacks.push({ resolve, reject }) + const current = state() + const sessionState = current[sessionID] + // Check if session state exists to prevent hanging on race conditions + if (!sessionState) { + reject(new DOMException("Session state not found", "AbortError")) + return + } + sessionState.callbacks.push({ resolve, reject }) }) } diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 9cabf47eb1dc..c2a4e3468805 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -127,22 +127,28 @@ export const TaskTool = Tool.define("task", async (ctx) => { using _ = defer(() => ctx.abort.removeEventListener("abort", cancel)) const promptParts = await SessionPrompt.resolvePromptParts(params.prompt) - const result = await SessionPrompt.prompt({ - messageID, - sessionID: session.id, - model: { - modelID: model.modelID, - providerID: model.providerID, - }, - agent: agent.name, - tools: { - todowrite: false, - todoread: false, - ...(hasTaskPermission ? {} : { task: false }), - ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), - }, - parts: promptParts, - }) + let result: MessageV2.WithParts + try { + result = await SessionPrompt.prompt({ + messageID, + sessionID: session.id, + model: { + modelID: model.modelID, + providerID: model.providerID, + }, + agent: agent.name, + tools: { + todowrite: false, + todoread: false, + ...(hasTaskPermission ? {} : { task: false }), + ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), + }, + parts: promptParts, + }) + } catch (error) { + // Ensure proper error propagation to parent agent + throw new Error(`Subagent execution failed: ${error instanceof Error ? error.message : String(error)}`) + } const text = result.parts.findLast((x) => x.type === "text")?.text ?? ""