fix(sisyphus-task): add proper error handling for sync mode and implement BackgroundManager.resume()

- 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)
This commit is contained in:
YeonGyu-Kim 2026-01-07 23:45:04 +09:00
parent b442b1c857
commit 4b2bf9ccb5
2 changed files with 104 additions and 28 deletions

View File

@ -4,6 +4,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import type { import type {
BackgroundTask, BackgroundTask,
LaunchInput, LaunchInput,
ResumeInput,
} from "./types" } from "./types"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { ConcurrencyManager } from "./concurrency" import { ConcurrencyManager } from "./concurrency"
@ -78,9 +79,9 @@ export class BackgroundManager {
throw new Error("Agent parameter is required") 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({ const createResult = await this.client.session.create({
body: { body: {
@ -88,12 +89,12 @@ export class BackgroundManager {
title: `Background: ${input.description}`, title: `Background: ${input.description}`,
}, },
}).catch((error) => { }).catch((error) => {
this.concurrencyManager.release(model) this.concurrencyManager.release(concurrencyKey)
throw error throw error
}) })
if (createResult.error) { if (createResult.error) {
this.concurrencyManager.release(model) this.concurrencyManager.release(concurrencyKey)
throw new Error(`Failed to create background session: ${createResult.error}`) throw new Error(`Failed to create background session: ${createResult.error}`)
} }
@ -115,7 +116,8 @@ export class BackgroundManager {
lastUpdate: new Date(), lastUpdate: new Date(),
}, },
parentModel: input.parentModel, parentModel: input.parentModel,
model, model: input.model,
concurrencyKey,
} }
this.tasks.set(task.id, task) this.tasks.set(task.id, task)
@ -155,8 +157,8 @@ export class BackgroundManager {
existingTask.error = errorMessage existingTask.error = errorMessage
} }
existingTask.completedAt = new Date() existingTask.completedAt = new Date()
if (existingTask.model) { if (existingTask.concurrencyKey) {
this.concurrencyManager.release(existingTask.model) this.concurrencyManager.release(existingTask.concurrencyKey)
} }
this.markForNotification(existingTask) this.markForNotification(existingTask)
this.notifyParentSession(existingTask) this.notifyParentSession(existingTask)
@ -238,6 +240,62 @@ export class BackgroundManager {
return task return task
} }
async resume(input: ResumeInput): Promise<BackgroundTask> {
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<boolean> { private async checkSessionTodos(sessionID: string): Promise<boolean> {
try { try {
const response = await this.client.session.todo({ const response = await this.client.session.todo({
@ -315,8 +373,8 @@ export class BackgroundManager {
task.error = "Session deleted" task.error = "Session deleted"
} }
if (task.model) { if (task.concurrencyKey) {
this.concurrencyManager.release(task.model) this.concurrencyManager.release(task.concurrencyKey)
} }
this.tasks.delete(task.id) this.tasks.delete(task.id)
this.clearNotificationsForTask(task.id) this.clearNotificationsForTask(task.id)
@ -391,8 +449,8 @@ export class BackgroundManager {
const taskId = task.id const taskId = task.id
setTimeout(async () => { setTimeout(async () => {
if (task.model) { if (task.concurrencyKey) {
this.concurrencyManager.release(task.model) this.concurrencyManager.release(task.concurrencyKey)
} }
try { try {
@ -455,8 +513,8 @@ export class BackgroundManager {
task.status = "error" task.status = "error"
task.error = "Task timed out after 30 minutes" task.error = "Task timed out after 30 minutes"
task.completedAt = new Date() task.completedAt = new Date()
if (task.model) { if (task.concurrencyKey) {
this.concurrencyManager.release(task.model) this.concurrencyManager.release(task.concurrencyKey)
} }
this.clearNotificationsForTask(taskId) this.clearNotificationsForTask(taskId)
this.tasks.delete(taskId) this.tasks.delete(taskId)

View File

@ -276,6 +276,7 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id
metadata: { sessionId: sessionID, category: args.category, sync: true }, metadata: { sessionId: sessionID, category: args.category, sync: true },
}) })
try {
await client.session.prompt({ await client.session.prompt({
path: { id: sessionID }, path: { id: sessionID },
body: { body: {
@ -288,22 +289,39 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id
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({ const messagesResult = await client.session.messages({
path: { id: sessionID }, path: { id: sessionID },
}) })
if (messagesResult.error) { 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<{ 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 }> parts?: Array<{ type?: string; text?: string }>
}> }>
const assistantMessages = messages.filter((m) => m.info?.role === "assistant") const assistantMessages = messages
const lastMessage = assistantMessages[assistantMessages.length - 1] .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 textParts = lastMessage?.parts?.filter((p) => p.type === "text") ?? []
const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n") const textContent = textParts.map((p) => p.text ?? "").filter(Boolean).join("\n")