From 931bf6c31b9c42388ed30c2165898e0955428ef1 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 17 Feb 2026 01:29:17 +0900 Subject: [PATCH] fix: resolve JSON parse error in Oracle after promptAsync refactor (#1681) --- .../delegate-task/sync-prompt-sender.test.ts | 129 +++++++++++++++--- src/tools/delegate-task/sync-prompt-sender.ts | 80 ++++++++--- 2 files changed, 166 insertions(+), 43 deletions(-) diff --git a/src/tools/delegate-task/sync-prompt-sender.test.ts b/src/tools/delegate-task/sync-prompt-sender.test.ts index 365d796de..d7e0eb0e3 100644 --- a/src/tools/delegate-task/sync-prompt-sender.test.ts +++ b/src/tools/delegate-task/sync-prompt-sender.test.ts @@ -1,12 +1,17 @@ -const { describe, test, expect, mock } = require("bun:test") +const { + describe: bunDescribe, + test: bunTest, + expect: bunExpect, + mock: bunMock, +} = require("bun:test") -describe("sendSyncPrompt", () => { - test("passes question=false via tools parameter", async () => { +bunDescribe("sendSyncPrompt", () => { + bunTest("passes question=false via tools parameter", async () => { //#given const { sendSyncPrompt } = require("./sync-prompt-sender") let promptArgs: any - const promptAsync = mock(async (input: any) => { + const promptAsync = bunMock(async (input: any) => { promptArgs = input return { data: {} } }) @@ -33,19 +38,19 @@ describe("sendSyncPrompt", () => { } //#when - await sendSyncPrompt(mockClient as any, input) + await sendSyncPrompt(mockClient, input) //#then - expect(promptAsync).toHaveBeenCalled() - expect(promptArgs.body.tools.question).toBe(false) + bunExpect(promptAsync).toHaveBeenCalled() + bunExpect(promptArgs.body.tools.question).toBe(false) }) - test("applies agent tool restrictions for explore agent", async () => { + bunTest("applies agent tool restrictions for explore agent", async () => { //#given const { sendSyncPrompt } = require("./sync-prompt-sender") let promptArgs: any - const promptAsync = mock(async (input: any) => { + const promptAsync = bunMock(async (input: any) => { promptArgs = input return { data: {} } }) @@ -73,19 +78,19 @@ describe("sendSyncPrompt", () => { } //#when - await sendSyncPrompt(mockClient as any, input) + await sendSyncPrompt(mockClient, input) //#then - expect(promptAsync).toHaveBeenCalled() - expect(promptArgs.body.tools.call_omo_agent).toBe(false) + bunExpect(promptAsync).toHaveBeenCalled() + bunExpect(promptArgs.body.tools.call_omo_agent).toBe(false) }) - test("applies agent tool restrictions for librarian agent", async () => { + bunTest("applies agent tool restrictions for librarian agent", async () => { //#given const { sendSyncPrompt } = require("./sync-prompt-sender") let promptArgs: any - const promptAsync = mock(async (input: any) => { + const promptAsync = bunMock(async (input: any) => { promptArgs = input return { data: {} } }) @@ -113,19 +118,19 @@ describe("sendSyncPrompt", () => { } //#when - await sendSyncPrompt(mockClient as any, input) + await sendSyncPrompt(mockClient, input) //#then - expect(promptAsync).toHaveBeenCalled() - expect(promptArgs.body.tools.call_omo_agent).toBe(false) + bunExpect(promptAsync).toHaveBeenCalled() + bunExpect(promptArgs.body.tools.call_omo_agent).toBe(false) }) - test("does not restrict call_omo_agent for sisyphus agent", async () => { + bunTest("does not restrict call_omo_agent for sisyphus agent", async () => { //#given const { sendSyncPrompt } = require("./sync-prompt-sender") let promptArgs: any - const promptAsync = mock(async (input: any) => { + const promptAsync = bunMock(async (input: any) => { promptArgs = input return { data: {} } }) @@ -153,10 +158,90 @@ describe("sendSyncPrompt", () => { } //#when - await sendSyncPrompt(mockClient as any, input) + await sendSyncPrompt(mockClient, input) //#then - expect(promptAsync).toHaveBeenCalled() - expect(promptArgs.body.tools.call_omo_agent).toBe(true) + bunExpect(promptAsync).toHaveBeenCalled() + bunExpect(promptArgs.body.tools.call_omo_agent).toBe(true) + }) + + bunTest("retries with promptSync for oracle when promptAsync fails with unexpected EOF", async () => { + //#given + const { sendSyncPrompt } = require("./sync-prompt-sender") + + const promptWithModelSuggestionRetry = bunMock(async () => { + throw new Error("JSON Parse error: Unexpected EOF") + }) + const promptSyncWithModelSuggestionRetry = bunMock(async () => {}) + + const input = { + sessionID: "test-session", + agentToUse: "oracle", + args: { + description: "test task", + prompt: "test prompt", + run_in_background: false, + load_skills: [], + }, + systemContent: undefined, + categoryModel: undefined, + toastManager: null, + taskId: undefined, + } + + //#when + const result = await sendSyncPrompt( + { session: { promptAsync: bunMock(async () => ({ data: {} })) } }, + input, + { + promptWithModelSuggestionRetry, + promptSyncWithModelSuggestionRetry, + }, + ) + + //#then + bunExpect(result).toBeNull() + bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1) + bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(1) + }) + + bunTest("does not retry with promptSync for non-oracle on unexpected EOF", async () => { + //#given + const { sendSyncPrompt } = require("./sync-prompt-sender") + + const promptWithModelSuggestionRetry = bunMock(async () => { + throw new Error("JSON Parse error: Unexpected EOF") + }) + const promptSyncWithModelSuggestionRetry = bunMock(async () => {}) + + const input = { + sessionID: "test-session", + agentToUse: "metis", + args: { + description: "test task", + prompt: "test prompt", + run_in_background: false, + load_skills: [], + }, + systemContent: undefined, + categoryModel: undefined, + toastManager: null, + taskId: undefined, + } + + //#when + const result = await sendSyncPrompt( + { session: { promptAsync: bunMock(async () => ({ data: {} })) } }, + input, + { + promptWithModelSuggestionRetry, + promptSyncWithModelSuggestionRetry, + }, + ) + + //#then + bunExpect(result).toContain("JSON Parse error: Unexpected EOF") + bunExpect(promptWithModelSuggestionRetry).toHaveBeenCalledTimes(1) + bunExpect(promptSyncWithModelSuggestionRetry).toHaveBeenCalledTimes(0) }) }) diff --git a/src/tools/delegate-task/sync-prompt-sender.ts b/src/tools/delegate-task/sync-prompt-sender.ts index 34659b812..e7aa20dc0 100644 --- a/src/tools/delegate-task/sync-prompt-sender.ts +++ b/src/tools/delegate-task/sync-prompt-sender.ts @@ -1,10 +1,33 @@ import type { DelegateTaskArgs, OpencodeClient } from "./types" import { isPlanFamily } from "./constants" -import { promptWithModelSuggestionRetry } from "../../shared/model-suggestion-retry" +import { + promptSyncWithModelSuggestionRetry, + promptWithModelSuggestionRetry, +} from "../../shared/model-suggestion-retry" import { formatDetailedError } from "./error-formatting" import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions" import { setSessionTools } from "../../shared/session-tools-store" +type SendSyncPromptDeps = { + promptWithModelSuggestionRetry: typeof promptWithModelSuggestionRetry + promptSyncWithModelSuggestionRetry: typeof promptSyncWithModelSuggestionRetry +} + +const sendSyncPromptDeps: SendSyncPromptDeps = { + promptWithModelSuggestionRetry, + promptSyncWithModelSuggestionRetry, +} + +function isOracleAgent(agentToUse: string): boolean { + return agentToUse.toLowerCase() === "oracle" +} + +function isUnexpectedEofError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error) + const lowered = message.toLowerCase() + return lowered.includes("unexpected eof") || lowered.includes("json parse error") +} + export async function sendSyncPrompt( client: OpencodeClient, input: { @@ -15,29 +38,44 @@ export async function sendSyncPrompt( categoryModel: { providerID: string; modelID: string; variant?: string } | undefined toastManager: { removeTask: (id: string) => void } | null | undefined taskId: string | undefined - } + }, + deps: SendSyncPromptDeps = sendSyncPromptDeps ): Promise { + const allowTask = isPlanFamily(input.agentToUse) + const tools = { + task: allowTask, + call_omo_agent: true, + question: false, + ...getAgentToolRestrictions(input.agentToUse), + } + setSessionTools(input.sessionID, tools) + + const promptArgs = { + path: { id: input.sessionID }, + body: { + agent: input.agentToUse, + system: input.systemContent, + tools, + parts: [{ type: "text", text: input.args.prompt }], + ...(input.categoryModel + ? { model: { providerID: input.categoryModel.providerID, modelID: input.categoryModel.modelID } } + : {}), + ...(input.categoryModel?.variant ? { variant: input.categoryModel.variant } : {}), + }, + } + try { - const allowTask = isPlanFamily(input.agentToUse) - const tools = { - task: allowTask, - call_omo_agent: true, - question: false, - ...getAgentToolRestrictions(input.agentToUse), - } - setSessionTools(input.sessionID, tools) - await promptWithModelSuggestionRetry(client, { - path: { id: input.sessionID }, - body: { - agent: input.agentToUse, - system: input.systemContent, - tools, - parts: [{ type: "text", text: input.args.prompt }], - ...(input.categoryModel ? { model: { providerID: input.categoryModel.providerID, modelID: input.categoryModel.modelID } } : {}), - ...(input.categoryModel?.variant ? { variant: input.categoryModel.variant } : {}), - }, - }) + await deps.promptWithModelSuggestionRetry(client, promptArgs) } catch (promptError) { + if (isOracleAgent(input.agentToUse) && isUnexpectedEofError(promptError)) { + try { + await deps.promptSyncWithModelSuggestionRetry(client, promptArgs) + return null + } catch (oracleRetryError) { + promptError = oracleRetryError + } + } + if (input.toastManager && input.taskId !== undefined) { input.toastManager.removeTask(input.taskId) }