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 {
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<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> {
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)

View File

@@ -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")