From 4b2bf9ccb5858639d9437dcdff0d04717cc9317b Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 7 Jan 2026 23:45:04 +0900 Subject: [PATCH] fix(sisyphus-task): add proper error handling for sync mode and implement BackgroundManager.resume() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add try-catch for session.prompt() in sync mode with detailed error messages - Sort assistant messages by time to get the most recent response - Add 'No assistant response found' error handling - Implement BackgroundManager.resume() method for task resumption - Fix ConcurrencyManager type mismatch (model → concurrencyKey) --- src/features/background-agent/manager.ts | 84 ++++++++++++++++++++---- src/tools/sisyphus-task/tools.ts | 48 +++++++++----- 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 282d239b..f697a96f 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -4,6 +4,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { BackgroundTask, LaunchInput, + ResumeInput, } from "./types" import { log } from "../../shared/logger" import { ConcurrencyManager } from "./concurrency" @@ -78,9 +79,9 @@ export class BackgroundManager { throw new Error("Agent parameter is required") } - const model = input.agent + const concurrencyKey = input.agent - await this.concurrencyManager.acquire(model) + await this.concurrencyManager.acquire(concurrencyKey) const createResult = await this.client.session.create({ body: { @@ -88,12 +89,12 @@ export class BackgroundManager { title: `Background: ${input.description}`, }, }).catch((error) => { - this.concurrencyManager.release(model) + this.concurrencyManager.release(concurrencyKey) throw error }) if (createResult.error) { - this.concurrencyManager.release(model) + this.concurrencyManager.release(concurrencyKey) throw new Error(`Failed to create background session: ${createResult.error}`) } @@ -115,7 +116,8 @@ export class BackgroundManager { lastUpdate: new Date(), }, parentModel: input.parentModel, - model, + model: input.model, + concurrencyKey, } this.tasks.set(task.id, task) @@ -155,8 +157,8 @@ export class BackgroundManager { existingTask.error = errorMessage } existingTask.completedAt = new Date() - if (existingTask.model) { - this.concurrencyManager.release(existingTask.model) + if (existingTask.concurrencyKey) { + this.concurrencyManager.release(existingTask.concurrencyKey) } this.markForNotification(existingTask) this.notifyParentSession(existingTask) @@ -238,6 +240,62 @@ export class BackgroundManager { return task } + async resume(input: ResumeInput): Promise { + const existingTask = this.findBySession(input.sessionId) + if (!existingTask) { + throw new Error(`Task not found for session: ${input.sessionId}`) + } + + existingTask.status = "running" + existingTask.completedAt = undefined + existingTask.error = undefined + existingTask.parentSessionID = input.parentSessionID + existingTask.parentMessageID = input.parentMessageID + existingTask.parentModel = input.parentModel + + existingTask.progress = { + toolCalls: existingTask.progress?.toolCalls ?? 0, + lastUpdate: new Date(), + } + + this.startPolling() + subagentSessions.add(existingTask.sessionID) + + const toastManager = getTaskToastManager() + if (toastManager) { + toastManager.addTask({ + id: existingTask.id, + description: existingTask.description, + agent: existingTask.agent, + isBackground: true, + }) + } + + log("[background-agent] Resuming task:", { taskId: existingTask.id, sessionID: existingTask.sessionID }) + + this.client.session.promptAsync({ + path: { id: existingTask.sessionID }, + body: { + agent: existingTask.agent, + tools: { + task: false, + call_omo_agent: false, + }, + parts: [{ type: "text", text: input.prompt }], + }, + }).catch((error) => { + log("[background-agent] resume promptAsync error:", error) + existingTask.status = "error" + const errorMessage = error instanceof Error ? error.message : String(error) + existingTask.error = errorMessage + existingTask.completedAt = new Date() + this.markForNotification(existingTask) + this.notifyParentSession(existingTask) + }) + + return existingTask + } + private async checkSessionTodos(sessionID: string): Promise { try { const response = await this.client.session.todo({ @@ -315,8 +373,8 @@ export class BackgroundManager { task.error = "Session deleted" } - if (task.model) { - this.concurrencyManager.release(task.model) + if (task.concurrencyKey) { + this.concurrencyManager.release(task.concurrencyKey) } this.tasks.delete(task.id) this.clearNotificationsForTask(task.id) @@ -391,8 +449,8 @@ export class BackgroundManager { const taskId = task.id setTimeout(async () => { - if (task.model) { - this.concurrencyManager.release(task.model) + if (task.concurrencyKey) { + this.concurrencyManager.release(task.concurrencyKey) } try { @@ -455,8 +513,8 @@ export class BackgroundManager { task.status = "error" task.error = "Task timed out after 30 minutes" task.completedAt = new Date() - if (task.model) { - this.concurrencyManager.release(task.model) + if (task.concurrencyKey) { + this.concurrencyManager.release(task.concurrencyKey) } this.clearNotificationsForTask(taskId) this.tasks.delete(taskId) diff --git a/src/tools/sisyphus-task/tools.ts b/src/tools/sisyphus-task/tools.ts index 7678aabf..36ddce33 100644 --- a/src/tools/sisyphus-task/tools.ts +++ b/src/tools/sisyphus-task/tools.ts @@ -276,34 +276,52 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id metadata: { sessionId: sessionID, category: args.category, sync: true }, }) - await client.session.prompt({ - path: { id: sessionID }, - body: { - agent: agentToUse, - model: categoryModel, - tools: { - task: false, - sisyphus_task: false, + try { + await client.session.prompt({ + path: { id: sessionID }, + body: { + agent: agentToUse, + model: categoryModel, + tools: { + task: false, + sisyphus_task: false, + }, + parts: [{ type: "text", text: args.prompt }], }, - parts: [{ type: "text", text: args.prompt }], - }, - }) + }) + } catch (promptError) { + if (toastManager && taskId !== undefined) { + toastManager.removeTask(taskId) + } + const errorMessage = promptError instanceof Error ? promptError.message : String(promptError) + if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) { + return `❌ Agent "${agentToUse}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\nSession ID: ${sessionID}` + } + return `❌ Failed to send prompt: ${errorMessage}\n\nSession ID: ${sessionID}` + } const messagesResult = await client.session.messages({ path: { id: sessionID }, }) if (messagesResult.error) { - return `❌ Error fetching result: ${messagesResult.error}` + return `❌ Error fetching result: ${messagesResult.error}\n\nSession ID: ${sessionID}` } const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as Array<{ - info?: { role?: string } + info?: { role?: string; time?: { created?: number } } parts?: Array<{ type?: string; text?: string }> }> - const assistantMessages = messages.filter((m) => m.info?.role === "assistant") - const lastMessage = assistantMessages[assistantMessages.length - 1] + const assistantMessages = messages + .filter((m) => m.info?.role === "assistant") + .sort((a, b) => (b.info?.time?.created ?? 0) - (a.info?.time?.created ?? 0)) + const lastMessage = assistantMessages[0] + + if (!lastMessage) { + return `❌ No assistant response found.\n\nSession ID: ${sessionID}` + } + const textParts = lastMessage?.parts?.filter((p) => p.type === "text") ?? [] const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n")