From 179f57fa96b7ef05906c55e068d2a0750472e96d Mon Sep 17 00:00:00 2001 From: Ivan Marshall Widjaja <60992624+imarshallwidjaja@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:26:32 +1100 Subject: [PATCH] fix(sisyphus_task): resolve sync mode JSON parse error (#708) --- src/tools/sisyphus-task/tools.test.ts | 216 +++++++++++++++++++++++++- src/tools/sisyphus-task/tools.ts | 51 ++---- 2 files changed, 230 insertions(+), 37 deletions(-) diff --git a/src/tools/sisyphus-task/tools.test.ts b/src/tools/sisyphus-task/tools.test.ts index d26db75d1..fcf0b278d 100644 --- a/src/tools/sisyphus-task/tools.test.ts +++ b/src/tools/sisyphus-task/tools.test.ts @@ -377,7 +377,221 @@ describe("sisyphus-task", () => { }) }) -describe("buildSystemContent", () => { + describe("sync mode new task (run_in_background=false)", () => { + test("sync mode prompt error returns error message immediately", async () => { + // #given + const { createSisyphusTask } = require("./tools") + + const mockManager = { + launch: async () => ({}), + } + + const mockClient = { + session: { + create: async () => ({ data: { id: "ses_sync_error_test" } }), + prompt: async () => { + throw new Error("JSON Parse error: Unexpected EOF") + }, + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + app: { + agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), + }, + } + + const tool = createSisyphusTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "Sisyphus", + abort: new AbortController().signal, + } + + // #when + const result = await tool.execute( + { + description: "Sync error test", + prompt: "Do something", + category: "ultrabrain", + run_in_background: false, + skills: [], + }, + toolContext + ) + + // #then - should return error message with the prompt error + expect(result).toContain("❌") + expect(result).toContain("Failed to send prompt") + expect(result).toContain("JSON Parse error") + }) + + test("sync mode success returns task result with content", async () => { + // #given + const { createSisyphusTask } = require("./tools") + + const mockManager = { + launch: async () => ({}), + } + + const mockClient = { + session: { + create: async () => ({ data: { id: "ses_sync_success" } }), + prompt: async () => ({ data: {} }), + messages: async () => ({ + data: [ + { + info: { role: "assistant", time: { created: Date.now() } }, + parts: [{ type: "text", text: "Sync task completed successfully" }], + }, + ], + }), + status: async () => ({ data: { "ses_sync_success": { type: "idle" } } }), + }, + app: { + agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), + }, + } + + const tool = createSisyphusTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "Sisyphus", + abort: new AbortController().signal, + } + + // #when + const result = await tool.execute( + { + description: "Sync success test", + prompt: "Do something", + category: "ultrabrain", + run_in_background: false, + skills: [], + }, + toolContext + ) + + // #then - should return the task result content + expect(result).toContain("Sync task completed successfully") + expect(result).toContain("Task completed") + }, { timeout: 20000 }) + + test("sync mode agent not found returns helpful error", async () => { + // #given + const { createSisyphusTask } = require("./tools") + + const mockManager = { + launch: async () => ({}), + } + + const mockClient = { + session: { + create: async () => ({ data: { id: "ses_agent_notfound" } }), + prompt: async () => { + throw new Error("Cannot read property 'name' of undefined agent.name") + }, + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + app: { + agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), + }, + } + + const tool = createSisyphusTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "Sisyphus", + abort: new AbortController().signal, + } + + // #when + const result = await tool.execute( + { + description: "Agent not found test", + prompt: "Do something", + category: "ultrabrain", + run_in_background: false, + skills: [], + }, + toolContext + ) + + // #then - should return agent not found error + expect(result).toContain("❌") + expect(result).toContain("not found") + expect(result).toContain("registered") + }) + + test("sync mode passes category model to prompt", async () => { + // #given + const { createSisyphusTask } = require("./tools") + let promptBody: any + + const mockManager = { launch: async () => ({}) } + const mockClient = { + session: { + create: async () => ({ data: { id: "ses_sync_model" } }), + prompt: async (input: any) => { + promptBody = input.body + return { data: {} } + }, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] + }), + status: async () => ({ data: {} }), + }, + app: { agents: async () => ({ data: [] }) }, + } + + const tool = createSisyphusTask({ + manager: mockManager, + client: mockClient, + userCategories: { + "custom-cat": { model: "provider/custom-model" } + } + }) + + const toolContext = { + sessionID: "parent", + messageID: "msg", + agent: "Sisyphus", + abort: new AbortController().signal + } + + // #when + await tool.execute({ + description: "Sync model test", + prompt: "test", + category: "custom-cat", + run_in_background: false, + skills: [] + }, toolContext) + + // #then + expect(promptBody.model).toEqual({ + providerID: "provider", + modelID: "custom-model" + }) + }, { timeout: 20000 }) + }) + + describe("buildSystemContent", () => { test("returns undefined when no skills and no category promptAppend", () => { // #given const { buildSystemContent } = require("./tools") diff --git a/src/tools/sisyphus-task/tools.ts b/src/tools/sisyphus-task/tools.ts index 42113ca52..ca4534fe9 100644 --- a/src/tools/sisyphus-task/tools.ts +++ b/src/tools/sisyphus-task/tools.ts @@ -419,32 +419,25 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id metadata: { sessionId: sessionID, category: args.category, sync: true }, }) - // Use fire-and-forget prompt() - awaiting causes JSON parse errors with thinking models - // Note: Don't pass model in body - use agent's configured model instead - let promptError: Error | undefined - client.session.prompt({ - path: { id: sessionID }, - body: { - agent: agentToUse, - system: systemContent, - tools: { - task: false, - sisyphus_task: false, + try { + await client.session.prompt({ + path: { id: sessionID }, + body: { + agent: agentToUse, + system: systemContent, + tools: { + task: false, + sisyphus_task: false, + }, + parts: [{ type: "text", text: args.prompt }], + ...(categoryModel ? { model: categoryModel } : {}), }, - parts: [{ type: "text", text: args.prompt }], - }, - }).catch((error) => { - promptError = error instanceof Error ? error : new Error(String(error)) - }) - - // Small delay to let the prompt start - await new Promise(resolve => setTimeout(resolve, 100)) - - if (promptError) { + }) + } catch (promptError) { if (toastManager && taskId !== undefined) { toastManager.removeTask(taskId) } - const errorMessage = promptError.message + 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}` } @@ -464,20 +457,6 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id while (Date.now() - pollStart < MAX_POLL_TIME_MS) { await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)) - // Check for async errors that may have occurred after the initial 100ms delay - // TypeScript doesn't understand async mutation, so we cast to check - const asyncError = promptError as Error | undefined - if (asyncError) { - if (toastManager && taskId !== undefined) { - toastManager.removeTask(taskId) - } - const errorMessage = asyncError.message - 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 statusResult = await client.session.status() const allStatuses = (statusResult.data ?? {}) as Record const sessionStatus = allStatuses[sessionID]