diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index bac958ec1033..efe635d28844 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" }) @@ -281,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 ?? ""