From b8221a883e1ad4de94a0d7af6204cd510e482748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:38:25 +0100 Subject: [PATCH 01/11] fix(shared): switch promptWithModelSuggestionRetry to use promptAsync --- src/shared/model-suggestion-retry.ts | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/shared/model-suggestion-retry.ts b/src/shared/model-suggestion-retry.ts index 4b2e6486..2564059f 100644 --- a/src/shared/model-suggestion-retry.ts +++ b/src/shared/model-suggestion-retry.ts @@ -84,28 +84,7 @@ export async function promptWithModelSuggestionRetry( client: Client, args: PromptArgs, ): Promise { - try { - await client.session.prompt(args as Parameters[0]) - } catch (error) { - const suggestion = parseModelSuggestion(error) - if (!suggestion || !args.body.model) { - throw error - } - - log("[model-suggestion-retry] Model not found, retrying with suggestion", { - original: `${suggestion.providerID}/${suggestion.modelID}`, - suggested: suggestion.suggestion, - }) - - await client.session.prompt({ - ...args, - body: { - ...args.body, - model: { - providerID: suggestion.providerID, - modelID: suggestion.suggestion, - }, - }, - } as Parameters[0]) - } + // NOTE: Model suggestion retry removed — promptAsync returns 204 immediately, + // model errors happen asynchronously server-side and cannot be caught here + await client.session.promptAsync(args as Parameters[0]) } From 108e860ddd28afb883ab16d0cd1fc4560699ddf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:42:19 +0100 Subject: [PATCH 02/11] fix(core): switch compatibility shim to promptAsync --- src/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index d97bd739..db49858c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -387,10 +387,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { } return []; }, - prompt: async (args) => { - await ctx.client.session.prompt(args); - }, - }, + prompt: async (args) => { + await ctx.client.session.promptAsync(args); + }, + promptAsync: async (args) => { + await ctx.client.session.promptAsync(args); + }, + }, }, }, { From 5f21ddf473ce6408e8e5430c11e7b601c2b27dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:42:20 +0100 Subject: [PATCH 03/11] fix(background-agent): switch session.prompt to promptAsync --- src/features/background-agent/manager.ts | 8 ++++---- src/features/background-agent/result-handler.ts | 2 +- src/features/background-agent/spawner.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index e58189a5..e631f30a 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -310,7 +310,7 @@ export class BackgroundManager { promptLength: input.prompt.length, }) - // Use prompt() instead of promptAsync() to properly initialize agent loop (fire-and-forget) + // Fire-and-forget prompt via promptAsync (no response body needed) // Include model if caller provided one (e.g., from Sisyphus category configs) // IMPORTANT: variant must be a top-level field in the body, NOT nested inside model // OpenCode's PromptInput schema expects: { model: { providerID, modelID }, variant: "max" } @@ -571,7 +571,7 @@ export class BackgroundManager { promptLength: input.prompt.length, }) - // Use prompt() instead of promptAsync() to properly initialize agent loop + // Fire-and-forget prompt via promptAsync (no response body needed) // Include model if task has one (preserved from original launch with category config) // variant must be top-level in body, not nested inside model (OpenCode PromptInput schema) const resumeModel = existingTask.model @@ -579,7 +579,7 @@ export class BackgroundManager { : undefined const resumeVariant = existingTask.model?.variant - this.client.session.prompt({ + this.client.session.promptAsync({ path: { id: existingTask.sessionID }, body: { agent: existingTask.agent, @@ -1198,7 +1198,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea }) try { - await this.client.session.prompt({ + await this.client.session.promptAsync({ path: { id: task.parentSessionID }, body: { noReply: !allComplete, diff --git a/src/features/background-agent/result-handler.ts b/src/features/background-agent/result-handler.ts index 3fca4091..84cb9084 100644 --- a/src/features/background-agent/result-handler.ts +++ b/src/features/background-agent/result-handler.ts @@ -240,7 +240,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea }) try { - await client.session.prompt({ + await client.session.promptAsync({ path: { id: task.parentSessionID }, body: { noReply: !allComplete, diff --git a/src/features/background-agent/spawner.ts b/src/features/background-agent/spawner.ts index f9c9cae5..477aafc1 100644 --- a/src/features/background-agent/spawner.ts +++ b/src/features/background-agent/spawner.ts @@ -221,7 +221,7 @@ export async function resumeTask( : undefined const resumeVariant = task.model?.variant - client.session.prompt({ + client.session.promptAsync({ path: { id: task.sessionID }, body: { agent: task.agent, From 46e02b94575b18f7c8a403bd538a83f05879488b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:42:24 +0100 Subject: [PATCH 04/11] fix(hooks): switch session.prompt to promptAsync in all hooks --- src/hooks/atlas/index.ts | 2 +- src/hooks/ralph-loop/index.ts | 2 +- src/hooks/session-recovery/index.ts | 4 +- src/hooks/todo-continuation-enforcer.ts | 2 +- src/hooks/unstable-agent-babysitter/index.ts | 11 +- src/shared/model-suggestion-retry.test.ts | 161 +++++++++++++++++-- src/tools/call-omo-agent/tools.ts | 24 +-- src/tools/delegate-task/executor.ts | 28 ++-- 8 files changed, 190 insertions(+), 44 deletions(-) diff --git a/src/hooks/atlas/index.ts b/src/hooks/atlas/index.ts index e3ab910a..ffad0459 100644 --- a/src/hooks/atlas/index.ts +++ b/src/hooks/atlas/index.ts @@ -484,7 +484,7 @@ export function createAtlasHook( : undefined } - await ctx.client.session.prompt({ + await ctx.client.session.promptAsync({ path: { id: sessionID }, body: { agent: agent ?? "atlas", diff --git a/src/hooks/ralph-loop/index.ts b/src/hooks/ralph-loop/index.ts index c961fe66..3cc77edd 100644 --- a/src/hooks/ralph-loop/index.ts +++ b/src/hooks/ralph-loop/index.ts @@ -364,7 +364,7 @@ export function createRalphLoopHook( : undefined } - await ctx.client.session.prompt({ + await ctx.client.session.promptAsync({ path: { id: sessionID }, body: { ...(agent !== undefined ? { agent } : {}), diff --git a/src/hooks/session-recovery/index.ts b/src/hooks/session-recovery/index.ts index ffd09077..2aecee15 100644 --- a/src/hooks/session-recovery/index.ts +++ b/src/hooks/session-recovery/index.ts @@ -75,7 +75,7 @@ function extractResumeConfig(userMessage: MessageData | undefined, sessionID: st async function resumeSession(client: Client, config: ResumeConfig): Promise { try { - await client.session.prompt({ + await client.session.promptAsync({ path: { id: config.sessionID }, body: { parts: [{ type: "text", text: RECOVERY_RESUME_TEXT }], @@ -185,7 +185,7 @@ async function recoverToolResultMissing( })) try { - await client.session.prompt({ + await client.session.promptAsync({ path: { id: sessionID }, // @ts-expect-error - SDK types may not include tool_result parts body: { parts: toolResultParts }, diff --git a/src/hooks/todo-continuation-enforcer.ts b/src/hooks/todo-continuation-enforcer.ts index 35e1df9d..3e3736be 100644 --- a/src/hooks/todo-continuation-enforcer.ts +++ b/src/hooks/todo-continuation-enforcer.ts @@ -245,7 +245,7 @@ ${todoList}` try { log(`[${HOOK_NAME}] Injecting continuation`, { sessionID, agent: agentName, model, incompleteCount: freshIncompleteCount }) - await ctx.client.session.prompt({ + await ctx.client.session.promptAsync({ path: { id: sessionID }, body: { agent: agentName, diff --git a/src/hooks/unstable-agent-babysitter/index.ts b/src/hooks/unstable-agent-babysitter/index.ts index 1a8b4057..e492281a 100644 --- a/src/hooks/unstable-agent-babysitter/index.ts +++ b/src/hooks/unstable-agent-babysitter/index.ts @@ -25,6 +25,15 @@ type BabysitterContext = { } query?: { directory?: string } }) => Promise + promptAsync: (args: { + path: { id: string } + body: { + parts: Array<{ type: "text"; text: string }> + agent?: string + model?: { providerID: string; modelID: string } + } + query?: { directory?: string } + }) => Promise } } } @@ -218,7 +227,7 @@ export function createUnstableAgentBabysitterHook(ctx: BabysitterContext, option const { agent, model } = await resolveMainSessionTarget(ctx, mainSessionID) try { - await ctx.client.session.prompt({ + await ctx.client.session.promptAsync({ path: { id: mainSessionID }, body: { ...(agent ? { agent } : {}), diff --git a/src/shared/model-suggestion-retry.test.ts b/src/shared/model-suggestion-retry.test.ts index 7c7d40cc..93338bd1 100644 --- a/src/shared/model-suggestion-retry.test.ts +++ b/src/shared/model-suggestion-retry.test.ts @@ -212,9 +212,9 @@ describe("parseModelSuggestion", () => { describe("promptWithModelSuggestionRetry", () => { it("should succeed on first try without retry", async () => { - // given a client where prompt succeeds + // given a client where promptAsync succeeds const promptMock = mock(() => Promise.resolve()) - const client = { session: { prompt: promptMock } } + const client = { session: { promptAsync: promptMock } } // when calling promptWithModelSuggestionRetry await promptWithModelSuggestionRetry(client as any, { @@ -225,21 +225,158 @@ describe("promptWithModelSuggestionRetry", () => { }, }) - // then should call prompt exactly once + // then should call promptAsync exactly once expect(promptMock).toHaveBeenCalledTimes(1) }) - it("should retry with suggestion on model-not-found error", async () => { - // given a client that fails first with model-not-found, then succeeds - const promptMock = mock() - .mockRejectedValueOnce({ - name: "ProviderModelNotFoundError", - data: { - providerID: "anthropic", - modelID: "claude-sonet-4", - suggestions: ["claude-sonnet-4"], + it("should throw error from promptAsync directly on model-not-found error", async () => { + // given a client that fails with model-not-found error + const promptMock = mock().mockRejectedValueOnce({ + name: "ProviderModelNotFoundError", + data: { + providerID: "anthropic", + modelID: "claude-sonet-4", + suggestions: ["claude-sonnet-4"], + }, + }) + const client = { session: { promptAsync: promptMock } } + + // when calling promptWithModelSuggestionRetry + // then should throw the error without retrying + await expect( + promptWithModelSuggestionRetry(client as any, { + path: { id: "session-1" }, + body: { + agent: "explore", + parts: [{ type: "text", text: "hello" }], + model: { providerID: "anthropic", modelID: "claude-sonet-4" }, }, }) + ).rejects.toThrow() + + // and should call promptAsync only once + expect(promptMock).toHaveBeenCalledTimes(1) + }) + + it("should throw original error when no suggestion available", async () => { + // given a client that fails with a non-model-not-found error + const originalError = new Error("Connection refused") + const promptMock = mock().mockRejectedValueOnce(originalError) + const client = { session: { promptAsync: promptMock } } + + // when calling promptWithModelSuggestionRetry + // then should throw the original error + await expect( + promptWithModelSuggestionRetry(client as any, { + path: { id: "session-1" }, + body: { + parts: [{ type: "text", text: "hello" }], + model: { providerID: "anthropic", modelID: "claude-sonnet-4" }, + }, + }) + ).rejects.toThrow("Connection refused") + + expect(promptMock).toHaveBeenCalledTimes(1) + }) + + it("should throw error from promptAsync directly", async () => { + // given a client that fails with an error + const error = new Error("Still not found") + const promptMock = mock().mockRejectedValueOnce(error) + const client = { session: { promptAsync: promptMock } } + + // when calling promptWithModelSuggestionRetry + // then should throw the error + await expect( + promptWithModelSuggestionRetry(client as any, { + path: { id: "session-1" }, + body: { + parts: [{ type: "text", text: "hello" }], + model: { providerID: "anthropic", modelID: "claude-sonnet-4" }, + }, + }) + ).rejects.toThrow("Still not found") + + // and should call promptAsync only once + expect(promptMock).toHaveBeenCalledTimes(1) + }) + + it("should pass all body fields through to promptAsync", async () => { + // given a client where promptAsync succeeds + const promptMock = mock().mockResolvedValueOnce(undefined) + const client = { session: { promptAsync: promptMock } } + + // when calling with additional body fields + await promptWithModelSuggestionRetry(client as any, { + path: { id: "session-1" }, + body: { + agent: "explore", + system: "You are a helpful agent", + tools: { task: false }, + parts: [{ type: "text", text: "hello" }], + model: { providerID: "anthropic", modelID: "claude-sonnet-4" }, + variant: "max", + }, + }) + + // then call should pass all fields through unchanged + const call = promptMock.mock.calls[0][0] + expect(call.body.agent).toBe("explore") + expect(call.body.system).toBe("You are a helpful agent") + expect(call.body.tools).toEqual({ task: false }) + expect(call.body.variant).toBe("max") + expect(call.body.model).toEqual({ + providerID: "anthropic", + modelID: "claude-sonnet-4", + }) + }) + + it("should throw string error message from promptAsync", async () => { + // given a client that fails with a string error + const promptMock = mock().mockRejectedValueOnce( + new Error("Model not found: anthropic/claude-sonet-4. Did you mean: claude-sonnet-4?") + ) + const client = { session: { promptAsync: promptMock } } + + // when calling promptWithModelSuggestionRetry + // then should throw the error + await expect( + promptWithModelSuggestionRetry(client as any, { + path: { id: "session-1" }, + body: { + parts: [{ type: "text", text: "hello" }], + model: { providerID: "anthropic", modelID: "claude-sonnet-4" }, + }, + }) + ).rejects.toThrow() + + // and should call promptAsync only once + expect(promptMock).toHaveBeenCalledTimes(1) + }) + + it("should throw error when no model in original request", async () => { + // given a client that fails with an error + const modelNotFoundError = new Error( + "Model not found: anthropic/claude-sonet-4. Did you mean: claude-sonnet-4?" + ) + const promptMock = mock().mockRejectedValueOnce(modelNotFoundError) + const client = { session: { promptAsync: promptMock } } + + // when calling without model in body + // then should throw the error + await expect( + promptWithModelSuggestionRetry(client as any, { + path: { id: "session-1" }, + body: { + parts: [{ type: "text", text: "hello" }], + }, + }) + ).rejects.toThrow() + + // and should call promptAsync only once + expect(promptMock).toHaveBeenCalledTimes(1) + }) +}) .mockResolvedValueOnce(undefined) const client = { session: { prompt: promptMock } } diff --git a/src/tools/call-omo-agent/tools.ts b/src/tools/call-omo-agent/tools.ts index f18a6eec..02c88052 100644 --- a/src/tools/call-omo-agent/tools.ts +++ b/src/tools/call-omo-agent/tools.ts @@ -219,18 +219,18 @@ Original error: ${createResult.error}` log(`[call_omo_agent] Sending prompt to session ${sessionID}`) log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100)) - try { - await ctx.client.session.prompt({ - path: { id: sessionID }, - body: { - agent: args.subagent_type, - tools: { - ...getAgentToolRestrictions(args.subagent_type), - task: false, - }, - parts: [{ type: "text", text: args.prompt }], - }, - }) + try { + await ctx.client.session.promptAsync({ + path: { id: sessionID }, + body: { + agent: args.subagent_type, + tools: { + ...getAgentToolRestrictions(args.subagent_type), + task: false, + }, + parts: [{ type: "text", text: args.prompt }], + }, + }) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) log(`[call_omo_agent] Prompt error:`, errorMessage) diff --git a/src/tools/delegate-task/executor.ts b/src/tools/delegate-task/executor.ts index bc6fd170..15d9983f 100644 --- a/src/tools/delegate-task/executor.ts +++ b/src/tools/delegate-task/executor.ts @@ -211,20 +211,20 @@ export async function executeSyncContinuation( : undefined } - await client.session.prompt({ - path: { id: args.session_id! }, - body: { - ...(resumeAgent !== undefined ? { agent: resumeAgent } : {}), - ...(resumeModel !== undefined ? { model: resumeModel } : {}), - tools: { - ...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}), - task: false, - call_omo_agent: true, - question: false, - }, - parts: [{ type: "text", text: args.prompt }], - }, - }) + await client.session.promptAsync({ + path: { id: args.session_id! }, + body: { + ...(resumeAgent !== undefined ? { agent: resumeAgent } : {}), + ...(resumeModel !== undefined ? { model: resumeModel } : {}), + tools: { + ...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}), + task: false, + call_omo_agent: true, + question: false, + }, + parts: [{ type: "text", text: args.prompt }], + }, + }) } catch (promptError) { if (toastManager) { toastManager.removeTask(taskId) From e984a5c639292024dfaab5128465815e66a057cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:42:49 +0100 Subject: [PATCH 05/11] test(shared): update model-suggestion-retry tests for promptAsync passthrough --- src/shared/model-suggestion-retry.test.ts | 159 ---------------------- 1 file changed, 159 deletions(-) diff --git a/src/shared/model-suggestion-retry.test.ts b/src/shared/model-suggestion-retry.test.ts index 93338bd1..10c7cde8 100644 --- a/src/shared/model-suggestion-retry.test.ts +++ b/src/shared/model-suggestion-retry.test.ts @@ -377,162 +377,3 @@ describe("promptWithModelSuggestionRetry", () => { expect(promptMock).toHaveBeenCalledTimes(1) }) }) - .mockResolvedValueOnce(undefined) - const client = { session: { prompt: promptMock } } - - // when calling promptWithModelSuggestionRetry - await promptWithModelSuggestionRetry(client as any, { - path: { id: "session-1" }, - body: { - agent: "explore", - parts: [{ type: "text", text: "hello" }], - model: { providerID: "anthropic", modelID: "claude-sonet-4" }, - }, - }) - - // then should call prompt twice - first with original, then with suggestion - expect(promptMock).toHaveBeenCalledTimes(2) - const retryCall = promptMock.mock.calls[1][0] - expect(retryCall.body.model).toEqual({ - providerID: "anthropic", - modelID: "claude-sonnet-4", - }) - }) - - it("should throw original error when no suggestion available", async () => { - // given a client that fails with a non-model-not-found error - const originalError = new Error("Connection refused") - const promptMock = mock().mockRejectedValueOnce(originalError) - const client = { session: { prompt: promptMock } } - - // when calling promptWithModelSuggestionRetry - // then should throw the original error - await expect( - promptWithModelSuggestionRetry(client as any, { - path: { id: "session-1" }, - body: { - parts: [{ type: "text", text: "hello" }], - model: { providerID: "anthropic", modelID: "claude-sonnet-4" }, - }, - }) - ).rejects.toThrow("Connection refused") - - expect(promptMock).toHaveBeenCalledTimes(1) - }) - - it("should throw original error when retry also fails", async () => { - // given a client that fails with model-not-found, retry also fails - const modelNotFoundError = { - name: "ProviderModelNotFoundError", - data: { - providerID: "anthropic", - modelID: "claude-sonet-4", - suggestions: ["claude-sonnet-4"], - }, - } - const retryError = new Error("Still not found") - const promptMock = mock() - .mockRejectedValueOnce(modelNotFoundError) - .mockRejectedValueOnce(retryError) - const client = { session: { prompt: promptMock } } - - // when calling promptWithModelSuggestionRetry - // then should throw the retry error (not the original) - await expect( - promptWithModelSuggestionRetry(client as any, { - path: { id: "session-1" }, - body: { - parts: [{ type: "text", text: "hello" }], - model: { providerID: "anthropic", modelID: "claude-sonet-4" }, - }, - }) - ).rejects.toThrow("Still not found") - - expect(promptMock).toHaveBeenCalledTimes(2) - }) - - it("should preserve other body fields during retry", async () => { - // given a client that fails first with model-not-found - const promptMock = mock() - .mockRejectedValueOnce({ - name: "ProviderModelNotFoundError", - data: { - providerID: "anthropic", - modelID: "claude-sonet-4", - suggestions: ["claude-sonnet-4"], - }, - }) - .mockResolvedValueOnce(undefined) - const client = { session: { prompt: promptMock } } - - // when calling with additional body fields - await promptWithModelSuggestionRetry(client as any, { - path: { id: "session-1" }, - body: { - agent: "explore", - system: "You are a helpful agent", - tools: { task: false }, - parts: [{ type: "text", text: "hello" }], - model: { providerID: "anthropic", modelID: "claude-sonet-4" }, - variant: "max", - }, - }) - - // then retry call should preserve all fields except corrected model - const retryCall = promptMock.mock.calls[1][0] - expect(retryCall.body.agent).toBe("explore") - expect(retryCall.body.system).toBe("You are a helpful agent") - expect(retryCall.body.tools).toEqual({ task: false }) - expect(retryCall.body.variant).toBe("max") - expect(retryCall.body.model).toEqual({ - providerID: "anthropic", - modelID: "claude-sonnet-4", - }) - }) - - it("should handle string error message with suggestion", async () => { - // given a client that fails with a string error containing suggestion - const promptMock = mock() - .mockRejectedValueOnce( - new Error("Model not found: anthropic/claude-sonet-4. Did you mean: claude-sonnet-4?") - ) - .mockResolvedValueOnce(undefined) - const client = { session: { prompt: promptMock } } - - // when calling promptWithModelSuggestionRetry - await promptWithModelSuggestionRetry(client as any, { - path: { id: "session-1" }, - body: { - parts: [{ type: "text", text: "hello" }], - model: { providerID: "anthropic", modelID: "claude-sonet-4" }, - }, - }) - - // then should retry with suggested model - expect(promptMock).toHaveBeenCalledTimes(2) - const retryCall = promptMock.mock.calls[1][0] - expect(retryCall.body.model.modelID).toBe("claude-sonnet-4") - }) - - it("should not retry when no model in original request", async () => { - // given a client that fails with model-not-found but original has no model param - const modelNotFoundError = new Error( - "Model not found: anthropic/claude-sonet-4. Did you mean: claude-sonnet-4?" - ) - const promptMock = mock().mockRejectedValueOnce(modelNotFoundError) - const client = { session: { prompt: promptMock } } - - // when calling without model in body - // then should throw without retrying - await expect( - promptWithModelSuggestionRetry(client as any, { - path: { id: "session-1" }, - body: { - parts: [{ type: "text", text: "hello" }], - }, - }) - ).rejects.toThrow() - - expect(promptMock).toHaveBeenCalledTimes(1) - }) -}) From 55dc64849f28dd866a00bfcb1709f9ee347a9f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:43:06 +0100 Subject: [PATCH 06/11] fix(tools): switch session.prompt to promptAsync in delegate-task and call-omo-agent --- src/tools/call-omo-agent/tools.ts | 2 +- src/tools/delegate-task/executor.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/call-omo-agent/tools.ts b/src/tools/call-omo-agent/tools.ts index 02c88052..64fa74a4 100644 --- a/src/tools/call-omo-agent/tools.ts +++ b/src/tools/call-omo-agent/tools.ts @@ -220,7 +220,7 @@ Original error: ${createResult.error}` log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100)) try { - await ctx.client.session.promptAsync({ + await (ctx.client.session as any).promptAsync({ path: { id: sessionID }, body: { agent: args.subagent_type, diff --git a/src/tools/delegate-task/executor.ts b/src/tools/delegate-task/executor.ts index 15d9983f..2a3be2b2 100644 --- a/src/tools/delegate-task/executor.ts +++ b/src/tools/delegate-task/executor.ts @@ -211,7 +211,7 @@ export async function executeSyncContinuation( : undefined } - await client.session.promptAsync({ + await (client.session as any).promptAsync({ path: { id: args.session_id! }, body: { ...(resumeAgent !== undefined ? { agent: resumeAgent } : {}), From fad7354b13af44ee087f9a8b8bf9cfee5e88bc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:46:03 +0100 Subject: [PATCH 07/11] fix(look-at): remove isJsonParseError band-aid (root cause fixed) --- src/tools/look-at/tools.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/tools/look-at/tools.ts b/src/tools/look-at/tools.ts index c9ad8973..2cf7ef69 100644 --- a/src/tools/look-at/tools.ts +++ b/src/tools/look-at/tools.ts @@ -245,27 +245,7 @@ Original error: ${createResult.error}` const errorMessage = promptError instanceof Error ? promptError.message : String(promptError) log(`[look_at] Prompt error:`, promptError) - const isJsonParseError = errorMessage.includes("JSON") && (errorMessage.includes("EOF") || errorMessage.includes("parse")) - if (isJsonParseError) { - return `Error: Failed to analyze ${isBase64Input ? "image" : "file"} - received malformed response from multimodal-looker agent. - -This typically occurs when: -1. The multimodal-looker model is not available or not connected -2. The model does not support this ${isBase64Input ? "image format" : `file type (${mimeType})`} -3. The API returned an empty or truncated response - -${isBase64Input ? "Source: clipboard/pasted image" : `File: ${args.file_path}`} -MIME type: ${mimeType} - -Try: -- Ensure a vision-capable model (e.g., gemini-3-flash, gpt-5.2) is available -- Check provider connections in opencode settings -${!isBase64Input ? "- For text files like .md, .txt, use the Read tool instead" : ""} - -Original error: ${errorMessage}` - } - - return `Error: Failed to send prompt to multimodal-looker agent: ${errorMessage}` + throw promptError } log(`[look_at] Prompt sent, fetching messages...`) From 6451b212f84fd5f43e3c281648b7119896c9c7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 13:51:28 +0100 Subject: [PATCH 08/11] test(todo-continuation): add promptAsync mocks for migrated hook --- src/hooks/todo-continuation-enforcer.test.ts | 280 +++++++++++-------- 1 file changed, 167 insertions(+), 113 deletions(-) diff --git a/src/hooks/todo-continuation-enforcer.test.ts b/src/hooks/todo-continuation-enforcer.test.ts index 23ae8a77..626d5c95 100644 --- a/src/hooks/todo-continuation-enforcer.test.ts +++ b/src/hooks/todo-continuation-enforcer.test.ts @@ -152,6 +152,15 @@ describe("todo-continuation-enforcer", () => { }) return {} }, + promptAsync: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, }, tui: { showToast: async (opts: any) => { @@ -977,32 +986,41 @@ describe("todo-continuation-enforcer", () => { data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }], }), messages: async () => ({ data: mockMessagesWithAssistant }), - prompt: async (opts: any) => { - promptCalls.push({ - sessionID: opts.path.id, - agent: opts.body.agent, - model: opts.body.model, - text: opts.body.parts[0].text, - }) - return {} - }, - }, - tui: { showToast: async () => ({}) }, - }, - directory: "/tmp/test", - } as any + prompt: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + promptAsync: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + }, + tui: { showToast: async () => ({}) }, + }, + directory: "/tmp/test", + } as any - const hook = createTodoContinuationEnforcer(mockInput, { - backgroundManager: createMockBackgroundManager(false), - }) + const hook = createTodoContinuationEnforcer(mockInput, { + backgroundManager: createMockBackgroundManager(false), + }) - // when - session goes idle - await hook.handler({ event: { type: "session.idle", properties: { sessionID } } }) - await fakeTimers.advanceBy(2500) + // when - session goes idle + await hook.handler({ event: { type: "session.idle", properties: { sessionID } } }) + await fakeTimers.advanceBy(2500) - // then - model should be extracted from assistant message's flat modelID/providerID - expect(promptCalls.length).toBe(1) - expect(promptCalls[0].model).toEqual({ providerID: "openai", modelID: "gpt-5.2" }) + // then - model should be extracted from assistant message's flat modelID/providerID + expect(promptCalls.length).toBe(1) + expect(promptCalls[0].model).toEqual({ providerID: "openai", modelID: "gpt-5.2" }) }) // ============================================================ @@ -1028,32 +1046,41 @@ describe("todo-continuation-enforcer", () => { todo: async () => ({ data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }], }), - messages: async () => ({ data: mockMessagesWithCompaction }), - prompt: async (opts: any) => { - promptCalls.push({ - sessionID: opts.path.id, - agent: opts.body.agent, - model: opts.body.model, - text: opts.body.parts[0].text, - }) - return {} - }, - }, - tui: { showToast: async () => ({}) }, - }, - directory: "/tmp/test", - } as any + messages: async () => ({ data: mockMessagesWithCompaction }), + prompt: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + promptAsync: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + }, + tui: { showToast: async () => ({}) }, + }, + directory: "/tmp/test", + } as any - const hook = createTodoContinuationEnforcer(mockInput, { - backgroundManager: createMockBackgroundManager(false), - }) + const hook = createTodoContinuationEnforcer(mockInput, { + backgroundManager: createMockBackgroundManager(false), + }) - // when - session goes idle - await hook.handler({ event: { type: "session.idle", properties: { sessionID } } }) - await fakeTimers.advanceBy(2500) + // when - session goes idle + await hook.handler({ event: { type: "session.idle", properties: { sessionID } } }) + await fakeTimers.advanceBy(2500) - // then - continuation uses Sisyphus (skipped compaction agent) - expect(promptCalls.length).toBe(1) + // then - continuation uses Sisyphus (skipped compaction agent) + expect(promptCalls.length).toBe(1) expect(promptCalls[0].agent).toBe("sisyphus") }) @@ -1072,32 +1099,41 @@ describe("todo-continuation-enforcer", () => { todo: async () => ({ data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }], }), - messages: async () => ({ data: mockMessagesOnlyCompaction }), - prompt: async (opts: any) => { - promptCalls.push({ - sessionID: opts.path.id, - agent: opts.body.agent, - model: opts.body.model, - text: opts.body.parts[0].text, - }) - return {} - }, - }, - tui: { showToast: async () => ({}) }, - }, - directory: "/tmp/test", - } as any + messages: async () => ({ data: mockMessagesOnlyCompaction }), + prompt: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + promptAsync: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + }, + tui: { showToast: async () => ({}) }, + }, + directory: "/tmp/test", + } as any - const hook = createTodoContinuationEnforcer(mockInput, {}) + const hook = createTodoContinuationEnforcer(mockInput, {}) - // when - session goes idle - await hook.handler({ - event: { type: "session.idle", properties: { sessionID } }, - }) + // when - session goes idle + await hook.handler({ + event: { type: "session.idle", properties: { sessionID } }, + }) - await fakeTimers.advanceBy(3000) + await fakeTimers.advanceBy(3000) - // then - no continuation (compaction is in default skipAgents) + // then - no continuation (compaction is in default skipAgents) expect(promptCalls).toHaveLength(0) }) @@ -1118,32 +1154,41 @@ describe("todo-continuation-enforcer", () => { todo: async () => ({ data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }], }), - messages: async () => ({ data: mockMessagesPrometheusCompacted }), - prompt: async (opts: any) => { - promptCalls.push({ - sessionID: opts.path.id, - agent: opts.body.agent, - model: opts.body.model, - text: opts.body.parts[0].text, - }) - return {} - }, - }, - tui: { showToast: async () => ({}) }, - }, - directory: "/tmp/test", - } as any + messages: async () => ({ data: mockMessagesPrometheusCompacted }), + prompt: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + promptAsync: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + }, + tui: { showToast: async () => ({}) }, + }, + directory: "/tmp/test", + } as any - const hook = createTodoContinuationEnforcer(mockInput, {}) + const hook = createTodoContinuationEnforcer(mockInput, {}) - // when - session goes idle - await hook.handler({ - event: { type: "session.idle", properties: { sessionID } }, - }) + // when - session goes idle + await hook.handler({ + event: { type: "session.idle", properties: { sessionID } }, + }) - await fakeTimers.advanceBy(3000) + await fakeTimers.advanceBy(3000) - // then - no continuation (prometheus found after filtering compaction, prometheus is in skipAgents) + // then - no continuation (prometheus found after filtering compaction, prometheus is in skipAgents) expect(promptCalls).toHaveLength(0) }) @@ -1164,32 +1209,41 @@ describe("todo-continuation-enforcer", () => { todo: async () => ({ data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }], }), - messages: async () => ({ data: mockMessagesNoAgent }), - prompt: async (opts: any) => { - promptCalls.push({ - sessionID: opts.path.id, - agent: opts.body.agent, - model: opts.body.model, - text: opts.body.parts[0].text, - }) - return {} - }, - }, - tui: { showToast: async () => ({}) }, - }, - directory: "/tmp/test", - } as any + messages: async () => ({ data: mockMessagesNoAgent }), + prompt: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + promptAsync: async (opts: any) => { + promptCalls.push({ + sessionID: opts.path.id, + agent: opts.body.agent, + model: opts.body.model, + text: opts.body.parts[0].text, + }) + return {} + }, + }, + tui: { showToast: async () => ({}) }, + }, + directory: "/tmp/test", + } as any - const hook = createTodoContinuationEnforcer(mockInput, { - skipAgents: [], - }) + const hook = createTodoContinuationEnforcer(mockInput, { + skipAgents: [], + }) - // when - session goes idle - await hook.handler({ - event: { type: "session.idle", properties: { sessionID } }, - }) + // when - session goes idle + await hook.handler({ + event: { type: "session.idle", properties: { sessionID } }, + }) - await wait(2500) + await wait(2500) // then - continuation injected (no agents to skip) expect(promptCalls.length).toBe(1) From 13da4ef4aafbb04ae7f17a3ada948b419262f51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 14:07:55 +0100 Subject: [PATCH 09/11] docs: add comprehensive local testing guide for acp-json-error branch --- LOCAL_TESTING_GUIDE.md | 400 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 LOCAL_TESTING_GUIDE.md diff --git a/LOCAL_TESTING_GUIDE.md b/LOCAL_TESTING_GUIDE.md new file mode 100644 index 00000000..439a048b --- /dev/null +++ b/LOCAL_TESTING_GUIDE.md @@ -0,0 +1,400 @@ +# Local Testing Guide: oh-my-opencode acp-json-error Branch + +## Overview + +This guide helps you test the `acp-json-error` branch locally to verify: +1. **TUI functionality** - OpenCode CLI works correctly +2. **ACP integration** - Claude Code Plugin loads and functions properly + +--- + +## Prerequisites + +- Bun installed (`bun --version`) +- OpenCode installed (`opencode --version`) +- Claude Code (if testing ACP integration) + +--- + +## Setup: Install Branch Locally + +### Option 1: Install from GitHub (Recommended) + +```bash +# Install directly from the branch +bun install --global github:potb/oh-my-opencode#acp-json-error + +# Verify installation +oh-my-opencode --version +``` + +### Option 2: Build from Source + +```bash +# Clone and checkout the branch +git clone https://github.com/potb/oh-my-opencode.git +cd oh-my-opencode +git checkout acp-json-error + +# Install dependencies +bun install + +# Build the project +bun run build + +# Link globally for testing +bun link + +# In another directory, link the package +bun link oh-my-opencode +``` + +--- + +## Test 1: TUI Functionality (OpenCode CLI) + +### 1.1 Basic Agent Operations + +Test that agents can be invoked and respond correctly: + +```bash +# Navigate to a test project +cd /path/to/test/project + +# Initialize oh-my-opencode (if not already done) +oh-my-opencode init + +# Test basic agent invocation +opencode "List all TypeScript files in src/" + +# Test background agent +opencode "Run a background task to analyze the codebase" + +# Test delegation +opencode "Delegate a task to the oracle agent" +``` + +**Expected**: Agents respond without JSON Parse errors, tasks complete successfully. + +### 1.2 Hook Behavior + +Test hooks that were modified in this PR: + +```bash +# Test Atlas hook (boulder continuation) +opencode "Create a work plan and start executing it" +# Let it run, then interrupt and resume - should continue from where it left off + +# Test Ralph loop +opencode "Start a ralph loop for continuous development" +# Should iterate without JSON Parse errors + +# Test todo continuation enforcer +opencode "Create a todo list and mark some items incomplete" +# Should inject continuation prompts without errors +``` + +**Expected**: Hooks fire correctly, no JSON Parse errors in logs. + +### 1.3 Background Tasks + +Test background task management: + +```bash +# Launch a background task +opencode "Launch a background task to search the codebase" + +# Check task status +opencode "Show background task status" + +# Cancel a task +opencode "Cancel the background task" +``` + +**Expected**: Background tasks launch, run, and can be cancelled without errors. + +### 1.4 Check Logs for Errors + +```bash +# Monitor OpenCode logs for JSON Parse errors +tail -f ~/.opencode/logs/opencode.log | grep -i "json parse" + +# Should see NO "JSON Parse error: Unexpected EOF" messages +``` + +--- + +## Test 2: ACP Integration (Claude Code Plugin) + +### 2.1 Install Plugin in Claude Code + +**Method 1: Via opencode.json** + +Edit your `~/.config/opencode/opencode.json`: + +```json +{ + "plugins": [ + { + "name": "oh-my-opencode", + "path": "/path/to/oh-my-opencode" // Path to your local build + } + ] +} +``` + +**Method 2: Via CLI** + +```bash +opencode plugin install /path/to/oh-my-opencode +``` + +### 2.2 Verify Plugin Loads + +```bash +# Start Claude Code +opencode + +# In Claude Code, check plugin status +/plugins list + +# Should see "oh-my-opencode" loaded without errors +``` + +### 2.3 Test Plugin Tools + +In Claude Code, test the tools that use `session.prompt()`: + +```bash +# Test delegate_task tool +/delegate "Test task delegation" --category quick + +# Test call_omo_agent tool +/agent oracle "What is the current project structure?" + +# Test background task tool +/background "Analyze codebase in background" + +# Test look_at tool (previously had isJsonParseError band-aid) +/look_at screenshot.png "Describe this image" +``` + +**Expected**: All tools execute without JSON Parse errors. + +### 2.4 Test Hook Integration + +Test hooks in the ACP environment: + +```bash +# Test Atlas hook (boulder continuation) +# Create a work plan, let it run, then check if continuation works + +# Test unstable-agent-babysitter hook +# Use an unstable model (gemini, minimax) and verify reminder fires + +# Test session recovery hook +# Simulate a session crash and verify recovery prompt fires +``` + +**Expected**: Hooks fire correctly in ACP environment, no JSON Parse errors. + +### 2.5 Monitor ACP Logs + +```bash +# Check Claude Code logs for errors +tail -f ~/.config/opencode/logs/plugin-oh-my-opencode.log | grep -i "json parse" + +# Should see NO "JSON Parse error: Unexpected EOF" messages +``` + +--- + +## Test 3: Regression Testing + +### 3.1 Verify No Breaking Changes + +Test existing functionality that should NOT be affected: + +```bash +# Test model suggestion retry (wrapper still works) +opencode "Use a non-existent model" --model "fake-model-123" +# Should get model suggestion error (not JSON Parse error) + +# Test parseModelSuggestion utility +# (Internal function, but verify via model suggestion errors) + +# Test fire-and-forget prompts +# (Background agent spawner, manager - should still catch HTTP errors) +``` + +**Expected**: Existing error handling still works, no regressions. + +### 3.2 Run Test Suite + +```bash +cd /path/to/oh-my-opencode +bun test + +# Focus on migration-specific tests +bun test src/shared/model-suggestion-retry.test.ts +bun test src/hooks/todo-continuation-enforcer.test.ts +``` + +**Expected**: All tests pass (migration tests: 57/57). + +--- + +## Test 4: Specific Scenarios + +### 4.1 Scenario: Background Agent Notification + +```bash +# Start a long-running background task +opencode "Analyze entire codebase and generate report" + +# Wait for completion +# Should receive notification via promptAsync (no JSON Parse error) +``` + +### 4.2 Scenario: Boulder Continuation + +```bash +# Create a work plan with multiple tasks +opencode "Create a plan to refactor the authentication module" + +# Let Atlas start executing +# Interrupt mid-execution (Ctrl+C) + +# Resume +opencode "Continue the work plan" + +# Should inject continuation prompt via promptAsync (no JSON Parse error) +``` + +### 4.3 Scenario: Look At Tool + +```bash +# Test the look_at tool (previously had isJsonParseError band-aid) +opencode "Analyze this diagram" --attach diagram.png + +# Should work without falling back to JSON Parse error handler +``` + +--- + +## Success Criteria + +### ✅ TUI Tests Pass If: +- Agents respond without JSON Parse errors +- Hooks fire correctly (Atlas, Ralph, todo-continuation) +- Background tasks launch and complete +- Logs show NO "JSON Parse error: Unexpected EOF" + +### ✅ ACP Tests Pass If: +- Plugin loads in Claude Code without errors +- All tools execute correctly (delegate_task, call_omo_agent, background, look_at) +- Hooks integrate properly in ACP environment +- Logs show NO "JSON Parse error: Unexpected EOF" + +### ✅ Regression Tests Pass If: +- Existing error handling still works +- Model suggestion errors still provide suggestions +- Fire-and-forget prompts still catch HTTP errors +- Test suite passes (57/57 migration tests) + +--- + +## Reporting Results + +After testing, update the PR with your findings: + +```markdown +## Testing Results + +### TUI Testing +- ✅/❌ Basic agent operations +- ✅/❌ Hook behavior (Atlas, Ralph, todo-continuation) +- ✅/❌ Background tasks +- ✅/❌ No JSON Parse errors in logs + +### ACP Testing +- ✅/❌ Plugin loads successfully +- ✅/❌ Tools execute correctly +- ✅/❌ Hooks integrate properly +- ✅/❌ No JSON Parse errors in logs + +### Regression Testing +- ✅/❌ No breaking changes +- ✅/❌ Test suite passes + +### Issues Found +[List any issues discovered during testing] + +### Recommendation +[Ready for merge / Needs fixes / etc.] +``` + +--- + +## Troubleshooting + +### Issue: "Module not found" after installation + +```bash +# Clear bun cache +rm -rf ~/.bun/install/cache + +# Reinstall +bun install --global github:potb/oh-my-opencode#acp-json-error +``` + +### Issue: Plugin not loading in Claude Code + +```bash +# Check plugin path in opencode.json +cat ~/.config/opencode/opencode.json + +# Verify plugin directory exists +ls -la /path/to/oh-my-opencode + +# Restart Claude Code +opencode restart +``` + +### Issue: Still seeing JSON Parse errors + +```bash +# Verify you're using the correct branch +cd /path/to/oh-my-opencode +git branch --show-current +# Should show: acp-json-error + +# Check for remaining session.prompt() calls +grep -rn 'session\.prompt(' src/ --include='*.ts' | grep -v 'promptAsync' | grep -v '\.test\.' +# Should return: 0 matches +``` + +--- + +## Next Steps After Testing + +1. **If tests pass**: Comment on PR with results, mark as ready for review +2. **If issues found**: Document issues, create follow-up tasks +3. **If ready**: Merge to dev branch (per AGENTS.md: ALL PRs → dev) + +--- + +## Quick Reference + +| Test Area | Command | Expected Result | +|-----------|---------|-----------------| +| Agent invocation | `opencode "List files"` | No JSON Parse error | +| Background task | `opencode "Background analysis"` | Task completes | +| Hook behavior | `opencode "Create work plan"` | Continuation works | +| Plugin load | `opencode plugin list` | oh-my-opencode loaded | +| Tool execution | `/delegate "Test task"` | Tool executes | +| Logs check | `tail -f ~/.opencode/logs/*.log` | No JSON Parse EOF | + +--- + +**Good luck with testing! 🚀** From fa77be0daf8f016ae2088c1b56a5937bd90299fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 14:14:06 +0100 Subject: [PATCH 10/11] chore: remove testing guide from branch --- LOCAL_TESTING_GUIDE.md | 400 ----------------------------------------- 1 file changed, 400 deletions(-) delete mode 100644 LOCAL_TESTING_GUIDE.md diff --git a/LOCAL_TESTING_GUIDE.md b/LOCAL_TESTING_GUIDE.md deleted file mode 100644 index 439a048b..00000000 --- a/LOCAL_TESTING_GUIDE.md +++ /dev/null @@ -1,400 +0,0 @@ -# Local Testing Guide: oh-my-opencode acp-json-error Branch - -## Overview - -This guide helps you test the `acp-json-error` branch locally to verify: -1. **TUI functionality** - OpenCode CLI works correctly -2. **ACP integration** - Claude Code Plugin loads and functions properly - ---- - -## Prerequisites - -- Bun installed (`bun --version`) -- OpenCode installed (`opencode --version`) -- Claude Code (if testing ACP integration) - ---- - -## Setup: Install Branch Locally - -### Option 1: Install from GitHub (Recommended) - -```bash -# Install directly from the branch -bun install --global github:potb/oh-my-opencode#acp-json-error - -# Verify installation -oh-my-opencode --version -``` - -### Option 2: Build from Source - -```bash -# Clone and checkout the branch -git clone https://github.com/potb/oh-my-opencode.git -cd oh-my-opencode -git checkout acp-json-error - -# Install dependencies -bun install - -# Build the project -bun run build - -# Link globally for testing -bun link - -# In another directory, link the package -bun link oh-my-opencode -``` - ---- - -## Test 1: TUI Functionality (OpenCode CLI) - -### 1.1 Basic Agent Operations - -Test that agents can be invoked and respond correctly: - -```bash -# Navigate to a test project -cd /path/to/test/project - -# Initialize oh-my-opencode (if not already done) -oh-my-opencode init - -# Test basic agent invocation -opencode "List all TypeScript files in src/" - -# Test background agent -opencode "Run a background task to analyze the codebase" - -# Test delegation -opencode "Delegate a task to the oracle agent" -``` - -**Expected**: Agents respond without JSON Parse errors, tasks complete successfully. - -### 1.2 Hook Behavior - -Test hooks that were modified in this PR: - -```bash -# Test Atlas hook (boulder continuation) -opencode "Create a work plan and start executing it" -# Let it run, then interrupt and resume - should continue from where it left off - -# Test Ralph loop -opencode "Start a ralph loop for continuous development" -# Should iterate without JSON Parse errors - -# Test todo continuation enforcer -opencode "Create a todo list and mark some items incomplete" -# Should inject continuation prompts without errors -``` - -**Expected**: Hooks fire correctly, no JSON Parse errors in logs. - -### 1.3 Background Tasks - -Test background task management: - -```bash -# Launch a background task -opencode "Launch a background task to search the codebase" - -# Check task status -opencode "Show background task status" - -# Cancel a task -opencode "Cancel the background task" -``` - -**Expected**: Background tasks launch, run, and can be cancelled without errors. - -### 1.4 Check Logs for Errors - -```bash -# Monitor OpenCode logs for JSON Parse errors -tail -f ~/.opencode/logs/opencode.log | grep -i "json parse" - -# Should see NO "JSON Parse error: Unexpected EOF" messages -``` - ---- - -## Test 2: ACP Integration (Claude Code Plugin) - -### 2.1 Install Plugin in Claude Code - -**Method 1: Via opencode.json** - -Edit your `~/.config/opencode/opencode.json`: - -```json -{ - "plugins": [ - { - "name": "oh-my-opencode", - "path": "/path/to/oh-my-opencode" // Path to your local build - } - ] -} -``` - -**Method 2: Via CLI** - -```bash -opencode plugin install /path/to/oh-my-opencode -``` - -### 2.2 Verify Plugin Loads - -```bash -# Start Claude Code -opencode - -# In Claude Code, check plugin status -/plugins list - -# Should see "oh-my-opencode" loaded without errors -``` - -### 2.3 Test Plugin Tools - -In Claude Code, test the tools that use `session.prompt()`: - -```bash -# Test delegate_task tool -/delegate "Test task delegation" --category quick - -# Test call_omo_agent tool -/agent oracle "What is the current project structure?" - -# Test background task tool -/background "Analyze codebase in background" - -# Test look_at tool (previously had isJsonParseError band-aid) -/look_at screenshot.png "Describe this image" -``` - -**Expected**: All tools execute without JSON Parse errors. - -### 2.4 Test Hook Integration - -Test hooks in the ACP environment: - -```bash -# Test Atlas hook (boulder continuation) -# Create a work plan, let it run, then check if continuation works - -# Test unstable-agent-babysitter hook -# Use an unstable model (gemini, minimax) and verify reminder fires - -# Test session recovery hook -# Simulate a session crash and verify recovery prompt fires -``` - -**Expected**: Hooks fire correctly in ACP environment, no JSON Parse errors. - -### 2.5 Monitor ACP Logs - -```bash -# Check Claude Code logs for errors -tail -f ~/.config/opencode/logs/plugin-oh-my-opencode.log | grep -i "json parse" - -# Should see NO "JSON Parse error: Unexpected EOF" messages -``` - ---- - -## Test 3: Regression Testing - -### 3.1 Verify No Breaking Changes - -Test existing functionality that should NOT be affected: - -```bash -# Test model suggestion retry (wrapper still works) -opencode "Use a non-existent model" --model "fake-model-123" -# Should get model suggestion error (not JSON Parse error) - -# Test parseModelSuggestion utility -# (Internal function, but verify via model suggestion errors) - -# Test fire-and-forget prompts -# (Background agent spawner, manager - should still catch HTTP errors) -``` - -**Expected**: Existing error handling still works, no regressions. - -### 3.2 Run Test Suite - -```bash -cd /path/to/oh-my-opencode -bun test - -# Focus on migration-specific tests -bun test src/shared/model-suggestion-retry.test.ts -bun test src/hooks/todo-continuation-enforcer.test.ts -``` - -**Expected**: All tests pass (migration tests: 57/57). - ---- - -## Test 4: Specific Scenarios - -### 4.1 Scenario: Background Agent Notification - -```bash -# Start a long-running background task -opencode "Analyze entire codebase and generate report" - -# Wait for completion -# Should receive notification via promptAsync (no JSON Parse error) -``` - -### 4.2 Scenario: Boulder Continuation - -```bash -# Create a work plan with multiple tasks -opencode "Create a plan to refactor the authentication module" - -# Let Atlas start executing -# Interrupt mid-execution (Ctrl+C) - -# Resume -opencode "Continue the work plan" - -# Should inject continuation prompt via promptAsync (no JSON Parse error) -``` - -### 4.3 Scenario: Look At Tool - -```bash -# Test the look_at tool (previously had isJsonParseError band-aid) -opencode "Analyze this diagram" --attach diagram.png - -# Should work without falling back to JSON Parse error handler -``` - ---- - -## Success Criteria - -### ✅ TUI Tests Pass If: -- Agents respond without JSON Parse errors -- Hooks fire correctly (Atlas, Ralph, todo-continuation) -- Background tasks launch and complete -- Logs show NO "JSON Parse error: Unexpected EOF" - -### ✅ ACP Tests Pass If: -- Plugin loads in Claude Code without errors -- All tools execute correctly (delegate_task, call_omo_agent, background, look_at) -- Hooks integrate properly in ACP environment -- Logs show NO "JSON Parse error: Unexpected EOF" - -### ✅ Regression Tests Pass If: -- Existing error handling still works -- Model suggestion errors still provide suggestions -- Fire-and-forget prompts still catch HTTP errors -- Test suite passes (57/57 migration tests) - ---- - -## Reporting Results - -After testing, update the PR with your findings: - -```markdown -## Testing Results - -### TUI Testing -- ✅/❌ Basic agent operations -- ✅/❌ Hook behavior (Atlas, Ralph, todo-continuation) -- ✅/❌ Background tasks -- ✅/❌ No JSON Parse errors in logs - -### ACP Testing -- ✅/❌ Plugin loads successfully -- ✅/❌ Tools execute correctly -- ✅/❌ Hooks integrate properly -- ✅/❌ No JSON Parse errors in logs - -### Regression Testing -- ✅/❌ No breaking changes -- ✅/❌ Test suite passes - -### Issues Found -[List any issues discovered during testing] - -### Recommendation -[Ready for merge / Needs fixes / etc.] -``` - ---- - -## Troubleshooting - -### Issue: "Module not found" after installation - -```bash -# Clear bun cache -rm -rf ~/.bun/install/cache - -# Reinstall -bun install --global github:potb/oh-my-opencode#acp-json-error -``` - -### Issue: Plugin not loading in Claude Code - -```bash -# Check plugin path in opencode.json -cat ~/.config/opencode/opencode.json - -# Verify plugin directory exists -ls -la /path/to/oh-my-opencode - -# Restart Claude Code -opencode restart -``` - -### Issue: Still seeing JSON Parse errors - -```bash -# Verify you're using the correct branch -cd /path/to/oh-my-opencode -git branch --show-current -# Should show: acp-json-error - -# Check for remaining session.prompt() calls -grep -rn 'session\.prompt(' src/ --include='*.ts' | grep -v 'promptAsync' | grep -v '\.test\.' -# Should return: 0 matches -``` - ---- - -## Next Steps After Testing - -1. **If tests pass**: Comment on PR with results, mark as ready for review -2. **If issues found**: Document issues, create follow-up tasks -3. **If ready**: Merge to dev branch (per AGENTS.md: ALL PRs → dev) - ---- - -## Quick Reference - -| Test Area | Command | Expected Result | -|-----------|---------|-----------------| -| Agent invocation | `opencode "List files"` | No JSON Parse error | -| Background task | `opencode "Background analysis"` | Task completes | -| Hook behavior | `opencode "Create work plan"` | Continuation works | -| Plugin load | `opencode plugin list` | oh-my-opencode loaded | -| Tool execution | `/delegate "Test task"` | Tool executes | -| Logs check | `tail -f ~/.opencode/logs/*.log` | No JSON Parse EOF | - ---- - -**Good luck with testing! 🚀** From 414cecd7df5b5e557b9e5cbd2e6d67148850d724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pe=C3=AFo=20Thibault?= Date: Sat, 7 Feb 2026 14:32:47 +0100 Subject: [PATCH 11/11] test: add promptAsync mocks to all test files for promptAsync migration --- src/features/background-agent/manager.test.ts | 385 ++-- src/hooks/atlas/index.test.ts | 1 + src/tools/delegate-task/tools.test.ts | 1787 +++++++++-------- src/tools/look-at/tools.test.ts | 62 +- 4 files changed, 1168 insertions(+), 1067 deletions(-) diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 014f9df6..d81698cb 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -171,6 +171,7 @@ function createBackgroundManager(): BackgroundManager { const client = { session: { prompt: async () => ({}), + promptAsync: async () => ({}), abort: async () => ({}), }, } @@ -880,12 +881,14 @@ describe("BackgroundManager.notifyParentSession - aborted parent", () => { test("should skip notification when parent session is aborted", async () => { //#given let promptCalled = false + const promptMock = async () => { + promptCalled = true + return {} + } const client = { session: { - prompt: async () => { - promptCalled = true - return {} - }, + prompt: promptMock, + promptAsync: promptMock, abort: async () => ({}), messages: async () => { const error = new Error("User aborted") @@ -922,14 +925,16 @@ describe("BackgroundManager.notifyParentSession - aborted parent", () => { test("should swallow aborted error from prompt", async () => { //#given let promptCalled = false + const promptMock = async () => { + promptCalled = true + const error = new Error("User aborted") + error.name = "MessageAbortedError" + throw error + } const client = { session: { - prompt: async () => { - promptCalled = true - const error = new Error("User aborted") - error.name = "MessageAbortedError" - throw error - }, + prompt: promptMock, + promptAsync: promptMock, abort: async () => ({}), messages: async () => ({ data: [] }), }, @@ -1054,19 +1059,20 @@ describe("BackgroundManager.tryCompleteTask", () => { expect(concurrencyManager.getCount(concurrencyKey)).toBe(0) }) - test("should abort session on completion", async () => { - // #given - const abortedSessionIDs: string[] = [] - const client = { - session: { - prompt: async () => ({}), - abort: async (args: { path: { id: string } }) => { - abortedSessionIDs.push(args.path.id) - return {} - }, - messages: async () => ({ data: [] }), - }, - } + test("should abort session on completion", async () => { + // #given + const abortedSessionIDs: string[] = [] + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async (args: { path: { id: string } }) => { + abortedSessionIDs.push(args.path.id) + return {} + }, + messages: async () => ({ data: [] }), + }, + } manager.shutdown() manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) stubNotifyParentSession(manager) @@ -1196,24 +1202,26 @@ describe("BackgroundManager.resume concurrency key", () => { }) describe("BackgroundManager.resume model persistence", () => { - let manager: BackgroundManager - let promptCalls: Array<{ path: { id: string }; body: Record }> + let manager: BackgroundManager + let promptCalls: Array<{ path: { id: string }; body: Record }> - beforeEach(() => { - // given - promptCalls = [] - const client = { - session: { - prompt: async (args: { path: { id: string }; body: Record }) => { - promptCalls.push(args) - return {} - }, - abort: async () => ({}), - }, - } - manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) - stubNotifyParentSession(manager) - }) + beforeEach(() => { + // given + promptCalls = [] + const promptMock = async (args: { path: { id: string }; body: Record }) => { + promptCalls.push(args) + return {} + } + const client = { + session: { + prompt: promptMock, + promptAsync: promptMock, + abort: async () => ({}), + }, + } + manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) + stubNotifyParentSession(manager) + }) afterEach(() => { manager.shutdown() @@ -1311,19 +1319,20 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { let manager: BackgroundManager let mockClient: ReturnType - function createMockClient() { - return { - session: { - create: async () => ({ data: { id: `ses_${crypto.randomUUID()}` } }), - get: async () => ({ data: { directory: "/test/dir" } }), - prompt: async () => ({}), - messages: async () => ({ data: [] }), - todo: async () => ({ data: [] }), - status: async () => ({ data: {} }), - abort: async () => ({}), - }, - } - } + function createMockClient() { + return { + session: { + create: async () => ({ data: { id: `ses_${crypto.randomUUID()}` } }), + get: async () => ({ data: { directory: "/test/dir" } }), + prompt: async () => ({}), + promptAsync: async () => ({}), + messages: async () => ({ data: [] }), + todo: async () => ({ data: [] }), + status: async () => ({ data: {} }), + abort: async () => ({}), + }, + } + } beforeEach(() => { // given @@ -1871,13 +1880,14 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { }) describe("BackgroundManager.checkAndInterruptStaleTasks", () => { - test("should NOT interrupt task running less than 30 seconds (min runtime guard)", async () => { - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - }, - } + test("should NOT interrupt task running less than 30 seconds (min runtime guard)", async () => { + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, + } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) const task: BackgroundTask = { @@ -1903,12 +1913,13 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { expect(task.status).toBe("running") }) - test("should NOT interrupt task with recent lastUpdate", async () => { - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - }, + test("should NOT interrupt task with recent lastUpdate", async () => { + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) @@ -1935,11 +1946,12 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { expect(task.status).toBe("running") }) - test("should interrupt task with stale lastUpdate (> 3min)", async () => { - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), + test("should interrupt task with stale lastUpdate (> 3min)", async () => { + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) @@ -1971,10 +1983,11 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { expect(task.completedAt).toBeDefined() }) - test("should respect custom staleTimeoutMs config", async () => { - const client = { - session: { - prompt: async () => ({}), + test("should respect custom staleTimeoutMs config", async () => { + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), abort: async () => ({}), }, } @@ -2005,13 +2018,14 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { expect(task.error).toContain("Stale timeout") }) - test("should release concurrency before abort", async () => { - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - }, - } + test("should release concurrency before abort", async () => { + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, + } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) stubNotifyParentSession(manager) @@ -2040,13 +2054,14 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { expect(task.status).toBe("cancelled") }) - test("should handle multiple stale tasks in same poll cycle", async () => { - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - }, - } + test("should handle multiple stale tasks in same poll cycle", async () => { + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, + } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, { staleTimeoutMs: 180_000 }) stubNotifyParentSession(manager) @@ -2091,13 +2106,14 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { expect(task2.status).toBe("cancelled") }) - test("should use default timeout when config not provided", async () => { - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - }, - } + test("should use default timeout when config not provided", async () => { + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, + } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) stubNotifyParentSession(manager) @@ -2126,18 +2142,19 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => { }) describe("BackgroundManager.shutdown session abort", () => { - test("should call session.abort for all running tasks during shutdown", () => { - // given - const abortedSessionIDs: string[] = [] - const client = { - session: { - prompt: async () => ({}), - abort: async (args: { path: { id: string } }) => { - abortedSessionIDs.push(args.path.id) - return {} - }, - }, - } + test("should call session.abort for all running tasks during shutdown", () => { + // given + const abortedSessionIDs: string[] = [] + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async (args: { path: { id: string } }) => { + abortedSessionIDs.push(args.path.id) + return {} + }, + }, + } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) const task1: BackgroundTask = { @@ -2175,18 +2192,19 @@ describe("BackgroundManager.shutdown session abort", () => { expect(abortedSessionIDs).toHaveLength(2) }) - test("should not call session.abort for completed or cancelled tasks", () => { - // given - const abortedSessionIDs: string[] = [] - const client = { - session: { - prompt: async () => ({}), - abort: async (args: { path: { id: string } }) => { - abortedSessionIDs.push(args.path.id) - return {} - }, - }, - } + test("should not call session.abort for completed or cancelled tasks", () => { + // given + const abortedSessionIDs: string[] = [] + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async (args: { path: { id: string } }) => { + abortedSessionIDs.push(args.path.id) + return {} + }, + }, + } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) const completedTask: BackgroundTask = { @@ -2235,15 +2253,16 @@ describe("BackgroundManager.shutdown session abort", () => { expect(abortedSessionIDs).toHaveLength(0) }) - test("should call onShutdown callback during shutdown", () => { - // given - let shutdownCalled = false - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - }, - } + test("should call onShutdown callback during shutdown", () => { + // given + let shutdownCalled = false + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, + } const manager = new BackgroundManager( { client, directory: tmpdir() } as unknown as PluginInput, undefined, @@ -2261,14 +2280,15 @@ describe("BackgroundManager.shutdown session abort", () => { expect(shutdownCalled).toBe(true) }) - test("should not throw when onShutdown callback throws", () => { - // given - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - }, - } + test("should not throw when onShutdown callback throws", () => { + // given + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + }, + } const manager = new BackgroundManager( { client, directory: tmpdir() } as unknown as PluginInput, undefined, @@ -2509,19 +2529,20 @@ describe("BackgroundManager.handleEvent - early session.idle deferral", () => { const realDateNow = Date.now const baseNow = realDateNow() - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - messages: async (args: { path: { id: string } }) => { - messagesCalls.push(args.path.id) - return { - data: [ - { - info: { role: "assistant" }, - parts: [{ type: "text", text: "ok" }], - }, - ], + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + messages: async (args: { path: { id: string } }) => { + messagesCalls.push(args.path.id) + return { + data: [ + { + info: { role: "assistant" }, + parts: [{ type: "text", text: "ok" }], + }, + ], } }, todo: async () => ({ data: [] }), @@ -2566,23 +2587,24 @@ describe("BackgroundManager.handleEvent - early session.idle deferral", () => { }) test("should not defer when session.idle fires after MIN_IDLE_TIME_MS", async () => { - //#given - a running task started more than MIN_IDLE_TIME_MS ago - const sessionID = "session-late-idle" - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - messages: async () => ({ - data: [ - { - info: { role: "assistant" }, - parts: [{ type: "text", text: "ok" }], - }, - ], - }), - todo: async () => ({ data: [] }), - }, - } + //#given - a running task started more than MIN_IDLE_TIME_MS ago + const sessionID = "session-late-idle" + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + messages: async () => ({ + data: [ + { + info: { role: "assistant" }, + parts: [{ type: "text", text: "ok" }], + }, + ], + }), + todo: async () => ({ data: [] }), + }, + } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) stubNotifyParentSession(manager) @@ -2618,20 +2640,21 @@ describe("BackgroundManager.handleEvent - early session.idle deferral", () => { const realDateNow = Date.now const baseNow = realDateNow() - const client = { - session: { - prompt: async () => ({}), - abort: async () => ({}), - messages: async () => { - messagesCallCount += 1 - return { - data: [ - { - info: { role: "assistant" }, - parts: [{ type: "text", text: "ok" }], - }, - ], - } + const client = { + session: { + prompt: async () => ({}), + promptAsync: async () => ({}), + abort: async () => ({}), + messages: async () => { + messagesCallCount += 1 + return { + data: [ + { + info: { role: "assistant" }, + parts: [{ type: "text", text: "ok" }], + }, + ], + } }, todo: async () => ({ data: [] }), }, diff --git a/src/hooks/atlas/index.test.ts b/src/hooks/atlas/index.test.ts index 502d8d41..88722682 100644 --- a/src/hooks/atlas/index.test.ts +++ b/src/hooks/atlas/index.test.ts @@ -34,6 +34,7 @@ describe("atlas hook", () => { client: { session: { prompt: promptMock, + promptAsync: promptMock, }, }, _promptMock: promptMock, diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index 01830352..63a42297 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -221,48 +221,49 @@ describe("sisyphus-task", () => { sessionID: "test-session", }), } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({}) }, - provider: { list: async () => ({ data: { connected: ["openai"] } }) }, - model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - status: async () => ({ data: {} }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({}) }, + provider: { list: async () => ({ data: { connected: ["openai"] } }) }, + model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", - agent: "sisyphus", - abort: new AbortController().signal, - } + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } - const args: { - description: string - prompt: string - category: string - run_in_background: boolean - load_skills: string[] - subagent_type?: string - } = { - description: "Quick category test", - prompt: "Do something", - category: "quick", - run_in_background: true, - load_skills: [], - } + const args: { + description: string + prompt: string + category: string + run_in_background: boolean + load_skills: string[] + subagent_type?: string + } = { + description: "Quick category test", + prompt: "Do something", + category: "quick", + run_in_background: true, + load_skills: [], + } - // when - await tool.execute(args, toolContext) + // when + await tool.execute(args, toolContext) // then expect(args.subagent_type).toBe("sisyphus-junior") @@ -272,72 +273,74 @@ describe("sisyphus-task", () => { // given a mock client with no model in config const { createDelegateTask } = require("./tools") - const mockManager = { launch: async () => ({ id: "task-123", status: "pending", description: "Test task", agent: "sisyphus-junior", sessionID: "test-session" }) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({}) }, // No model configured - provider: { list: async () => ({ data: { connected: ["openai"] } }) }, - model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - status: async () => ({ data: {} }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) - - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", - agent: "sisyphus", - abort: new AbortController().signal, - } - - // when delegating with a category - const result = await tool.execute( - { - description: "Test task", - prompt: "Do something", - category: "ultrabrain", - run_in_background: true, - load_skills: [], - }, - toolContext - ) - - // then proceeds without error - uses fallback chain - expect(result).not.toContain("oh-my-opencode requires a default model") + const mockManager = { launch: async () => ({ id: "task-123", status: "pending", description: "Test task", agent: "sisyphus-junior", sessionID: "test-session" }) } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({}) }, // No model configured + provider: { list: async () => ({ data: { connected: ["openai"] } }) }, + model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when delegating with a category + const result = await tool.execute( + { + description: "Test task", + prompt: "Do something", + category: "ultrabrain", + run_in_background: true, + load_skills: [], + }, + toolContext + ) + + // then proceeds without error - uses fallback chain + expect(result).not.toContain("oh-my-opencode requires a default model") }, { timeout: 10000 }) test("returns clear error when no model can be resolved", async () => { // given - custom category with no model, no systemDefaultModel, no available models const { createDelegateTask } = require("./tools") - const mockManager = { launch: async () => ({ id: "task-123" }) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({}) }, // No model configured - model: { list: async () => [] }, // No available models - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } - - // Custom category with no model defined - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - userCategories: { - "custom-no-model": { temperature: 0.5 }, // No model field - }, - }) + const mockManager = { launch: async () => ({ id: "task-123" }) } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({}) }, // No model configured + model: { list: async () => [] }, // No available models + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + // Custom category with no model defined + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + userCategories: { + "custom-no-model": { temperature: 0.5 }, // No model field + }, + }) const toolContext = { sessionID: "parent-session", @@ -383,28 +386,29 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [{ name: "explore", mode: "subagent" }] }) }, - config: { get: async () => ({}) }, - provider: { list: async () => ({ data: { connected: ["openai"] } }) }, - model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - status: async () => ({ data: {} }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [{ name: "explore", mode: "subagent" }] }) }, + config: { get: async () => ({}) }, + provider: { list: async () => ({ data: { connected: ["openai"] } }) }, + model: { list: async () => ({ data: [{ provider: "openai", id: "gpt-5.3-codex" }] }) }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) - const metadataCalls: Array<{ title?: string; metadata?: Record }> = [] - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", + const metadataCalls: Array<{ title?: string; metadata?: Record }> = [] + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", agent: "sisyphus", abort: new AbortController().signal, metadata: (input: { title?: string; metadata?: Record }) => { @@ -673,23 +677,24 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - userCategories: { - ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" }, - }, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + userCategories: { + ultrabrain: { model: "openai/gpt-5.2", variant: "xhigh" }, + }, + }) const toolContext = { sessionID: "parent-session", @@ -736,22 +741,23 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ provider: "anthropic", id: "claude-opus-4-6" }] }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ provider: "anthropic", id: "claude-opus-4-6" }] }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } - // NO userCategories - must use DEFAULT_CATEGORIES - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + // NO userCategories - must use DEFAULT_CATEGORIES + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -780,30 +786,33 @@ describe("sisyphus-task", () => { }) }) - test("DEFAULT_CATEGORIES variant passes to sync session.prompt WITHOUT userCategories", async () => { - // given - NO userCategories, testing DEFAULT_CATEGORIES for sync mode - const { createDelegateTask } = require("./tools") - let promptBody: any + test("DEFAULT_CATEGORIES variant passes to sync session.prompt WITHOUT userCategories", async () => { + // given - NO userCategories, testing DEFAULT_CATEGORIES for sync mode + const { createDelegateTask } = require("./tools") + let promptBody: any - const mockManager = { launch: async () => ({}) } + const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ provider: "anthropic", id: "claude-opus-4-6" }] }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_sync_default_variant" } }), - prompt: async (input: any) => { - promptBody = input.body - return { data: {} } - }, - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "done" }] }] - }), - status: async () => ({ data: { "ses_sync_default_variant": { type: "idle" } } }), - }, - } + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } + + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ provider: "anthropic", id: "claude-opus-4-6" }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_sync_default_variant" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "done" }] }] + }), + status: async () => ({ data: { "ses_sync_default_variant": { type: "idle" } } }), + }, + } // NO userCategories - must use DEFAULT_CATEGORIES const tool = createDelegateTask({ @@ -844,105 +853,111 @@ describe("sisyphus-task", () => { // given const { createDelegateTask } = require("./tools") - const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) - - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", - agent: "sisyphus", - abort: new AbortController().signal, - } - - // when - skills not provided (undefined) - // then - should throw error about missing skills - await expect(tool.execute( - { - description: "Test task", - prompt: "Do something", - category: "ultrabrain", - run_in_background: false, - }, - toolContext - )).rejects.toThrow("IT IS HIGHLY RECOMMENDED") + const mockManager = { launch: async () => ({}) } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when - skills not provided (undefined) + // then - should throw error about missing skills + await expect(tool.execute( + { + description: "Test task", + prompt: "Do something", + category: "ultrabrain", + run_in_background: false, + }, + toolContext + )).rejects.toThrow("IT IS HIGHLY RECOMMENDED") }) - test("null skills throws error", async () => { - // given - const { createDelegateTask } = require("./tools") - - const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) - - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", - agent: "sisyphus", - abort: new AbortController().signal, - } - - // when - null passed - // then - should throw error about null - await expect(tool.execute( - { - description: "Test task", - prompt: "Do something", - category: "ultrabrain", - run_in_background: false, - load_skills: null, - }, - toolContext - )).rejects.toThrow("IT IS HIGHLY RECOMMENDED") + test("null skills throws error", async () => { + // given + const { createDelegateTask } = require("./tools") + + const mockManager = { launch: async () => ({}) } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when - null passed + // then - should throw error about null + await expect(tool.execute( + { + description: "Test task", + prompt: "Do something", + category: "ultrabrain", + run_in_background: false, + load_skills: null, + }, + toolContext + )).rejects.toThrow("IT IS HIGHLY RECOMMENDED") }) - test("empty array [] is allowed and proceeds without skill content", async () => { - // given - const { createDelegateTask } = require("./tools") - let promptBody: any - - const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "test-session" } }), - prompt: async (input: any) => { - promptBody = input.body - return { data: {} } - }, - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] - }), - status: async () => ({ data: {} }), - }, - } + test("empty array [] is allowed and proceeds without skill content", async () => { + // given + const { createDelegateTask } = require("./tools") + let promptBody: any + + const mockManager = { launch: async () => ({}) } + + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } + + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "test-session" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] + }), + status: async () => ({ data: {} }), + }, + } const tool = createDelegateTask({ manager: mockManager, @@ -992,47 +1007,48 @@ describe("sisyphus-task", () => { launch: async () => mockTask, } - const mockClient = { - session: { - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [ - { - info: { role: "assistant", time: { created: Date.now() } }, - parts: [{ type: "text", text: "This is the continued task result" }], - }, - ], - }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - app: { - agents: async () => ({ data: [] }), - }, - } - - const tool = createDelegateTask({ - 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: "Continue test", - prompt: "Continue the task", - session_id: "ses_continue_test", - run_in_background: false, - load_skills: ["git-master"], - }, - toolContext - ) + const mockClient = { + session: { + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [ + { + info: { role: "assistant", time: { created: Date.now() } }, + parts: [{ type: "text", text: "This is the continued task result" }], + }, + ], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + app: { + agents: async () => ({ data: [] }), + }, + } + + const tool = createDelegateTask({ + 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: "Continue test", + prompt: "Continue the task", + session_id: "ses_continue_test", + run_in_background: false, + load_skills: ["git-master"], + }, + toolContext + ) // then - should contain actual result, not just "Background task continued" expect(result).toContain("This is the continued task result") @@ -1055,39 +1071,40 @@ describe("sisyphus-task", () => { resume: async () => mockTask, } - const mockClient = { - session: { - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [], - }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - } - - const tool = createDelegateTask({ - 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: "Continue bg test", - prompt: "Continue in background", - session_id: "ses_bg_continue", - run_in_background: true, - load_skills: ["git-master"], - }, - toolContext - ) + const mockClient = { + session: { + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + } + + const tool = createDelegateTask({ + 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: "Continue bg test", + prompt: "Continue in background", + session_id: "ses_bg_continue", + run_in_background: true, + load_skills: ["git-master"], + }, + toolContext + ) // then - should return background message expect(result).toContain("Background task continued") @@ -1104,26 +1121,29 @@ describe("sisyphus-task", () => { launch: async () => ({}), } - const mockClient = { - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_sync_error_test" } }), - prompt: async () => { - throw new Error("JSON Parse error: Unexpected EOF") - }, - messages: async () => ({ data: [] }), - status: async () => ({ data: {} }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - app: { - agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const promptMock = async () => { + throw new Error("JSON Parse error: Unexpected EOF") + } + + const mockClient = { + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_sync_error_test" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + app: { + agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -1159,31 +1179,32 @@ describe("sisyphus-task", () => { launch: async () => ({}), } - const mockClient = { - session: { - get: async () => ({ data: { directory: "/project" } }), - 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" } } }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - app: { - agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockClient = { + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_sync_success" } }), + prompt: async () => ({ data: {} }), + promptAsync: 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" } } }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + app: { + agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -1217,26 +1238,29 @@ describe("sisyphus-task", () => { launch: async () => ({}), } - const mockClient = { - session: { - get: async () => ({ data: { directory: "/project" } }), - 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: {} }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - app: { - agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const promptMock = async () => { + throw new Error("Cannot read property 'name' of undefined agent.name") + } + + const mockClient = { + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_agent_notfound" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + app: { + agents: async () => ({ data: [{ name: "ultrabrain", mode: "subagent" }] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -1262,28 +1286,32 @@ describe("sisyphus-task", () => { expect(result).toContain("registered") }) - test("sync mode passes category model to prompt", async () => { - // given - const { createDelegateTask } = require("./tools") - let promptBody: any + test("sync mode passes category model to prompt", async () => { + // given + const { createDelegateTask } = require("./tools") + let promptBody: any - const mockManager = { launch: async () => ({}) } - const mockClient = { - session: { - get: async () => ({ data: { directory: "/project" } }), - 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: {} }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - app: { agents: async () => ({ data: [] }) }, - } + const mockManager = { launch: async () => ({}) } + + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } + + const mockClient = { + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_sync_model" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] + }), + status: async () => ({ data: {} }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + app: { agents: async () => ({ data: [] }) }, + } const tool = createDelegateTask({ manager: mockManager, @@ -1336,27 +1364,28 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ provider: "google", id: "gemini-3-pro" }] }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_unstable_gemini" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [ - { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Gemini task completed successfully" }] } - ] - }), - status: async () => ({ data: { "ses_unstable_gemini": { type: "idle" } } }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ provider: "google", id: "gemini-3-pro" }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_unstable_gemini" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [ + { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Gemini task completed successfully" }] } + ] + }), + status: async () => ({ data: { "ses_unstable_gemini": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -1401,39 +1430,40 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) - - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", - agent: "sisyphus", - abort: new AbortController().signal, - } - - // when - using visual-engineering with run_in_background=true (normal background) - const result = await tool.execute( - { - description: "Test normal background", - prompt: "Do something visual", - category: "visual-engineering", - run_in_background: true, // User explicitly says true - normal background - load_skills: ["git-master"], - }, - toolContext - ) + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when - using visual-engineering with run_in_background=true (normal background) + const result = await tool.execute( + { + description: "Test normal background", + prompt: "Do something visual", + category: "visual-engineering", + run_in_background: true, // User explicitly says true - normal background + load_skills: ["git-master"], + }, + toolContext + ) // then - should NOT show unstable message (it's normal background flow) expect(launchCalled).toBe(true) @@ -1459,31 +1489,32 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_unstable_minimax" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [ - { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Minimax task completed successfully" }] } - ] - }), - status: async () => ({ data: { "ses_unstable_minimax": { type: "idle" } } }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_unstable_minimax" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [ + { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Minimax task completed successfully" }] } + ] + }), + status: async () => ({ data: { "ses_unstable_minimax": { type: "idle" } } }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - userCategories: { - "minimax-cat": { - model: "minimax/abab-5", - }, - }, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + userCategories: { + "minimax-cat": { + model: "minimax/abab-5", + }, + }, + }) const toolContext = { sessionID: "parent-session", @@ -1523,28 +1554,31 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_sync_non_gemini" } }), - prompt: async () => { - promptCalled = true - return { data: {} } - }, - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done sync" }] }] - }), - status: async () => ({ data: { "ses_sync_non_gemini": { type: "idle" } } }), - }, - } - - // Use ultrabrain which uses gpt-5.2 (non-gemini) - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const promptMock = async () => { + promptCalled = true + return { data: {} } + } + + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_sync_non_gemini" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done sync" }] }] + }), + status: async () => ({ data: { "ses_sync_non_gemini": { type: "idle" } } }), + }, + } + + // Use ultrabrain which uses gpt-5.2 (non-gemini) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -1589,27 +1623,28 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ provider: "google", id: "gemini-3-pro" }] }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_artistry_gemini" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [ - { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Artistry result here" }] } - ] - }), - status: async () => ({ data: { "ses_artistry_gemini": { type: "idle" } } }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ provider: "google", id: "gemini-3-pro" }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_artistry_gemini" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [ + { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Artistry result here" }] } + ] + }), + status: async () => ({ data: { "ses_artistry_gemini": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -1654,27 +1689,28 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ provider: "google", id: "gemini-3-flash" }] }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_writing_gemini" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [ - { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Writing result here" }] } - ] - }), - status: async () => ({ data: { "ses_writing_gemini": { type: "idle" } } }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ provider: "google", id: "gemini-3-flash" }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_writing_gemini" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [ + { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Writing result here" }] } + ] + }), + status: async () => ({ data: { "ses_writing_gemini": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -1726,6 +1762,7 @@ describe("sisyphus-task", () => { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "ses_custom_unstable" } }), prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), messages: async () => ({ data: [ { info: { role: "assistant", time: { created: Date.now() } }, parts: [{ type: "text", text: "Custom unstable result" }] } @@ -1856,21 +1893,25 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: "opencode/kimi-k2.5-free" } }) }, - model: { list: async () => [] }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [] }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + userCategories: { + "fallback-test": { model: "anthropic/claude-opus-4-6" }, + }, + }) const toolContext = { sessionID: "parent-session", @@ -1973,25 +2014,26 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [] }, - session: { - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [] }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - sisyphusJuniorModel: "anthropic/claude-sonnet-4-5", - userCategories: { - ultrabrain: { model: "openai/gpt-5.3-codex" }, - }, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + sisyphusJuniorModel: "anthropic/claude-sonnet-4-5", + userCategories: { + ultrabrain: { model: "openai/gpt-5.3-codex" }, + }, + }) const toolContext = { sessionID: "parent-session", @@ -2024,30 +2066,34 @@ describe("sisyphus-task", () => { const { createDelegateTask } = require("./tools") let promptBody: any - const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_browser_provider" } }), - prompt: async (input: any) => { - promptBody = input.body - return { data: {} } - }, - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] - }), - status: async () => ({ data: {} }), - }, - } + const mockManager = { launch: async () => ({}) } + + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } + + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_browser_provider" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] + }), + status: async () => ({ data: {} }), + }, + } - // Pass browserProvider to createDelegateTask - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - browserProvider: "agent-browser", - }) + // Pass browserProvider to createDelegateTask + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + browserProvider: "agent-browser", + }) const toolContext = { sessionID: "parent-session", @@ -2081,23 +2127,24 @@ describe("sisyphus-task", () => { const mockManager = { launch: async () => ({}) } const mockClient = { app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_no_browser_provider" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] - }), - status: async () => ({ data: {} }), - }, - } + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_no_browser_provider" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }] + }), + status: async () => ({ data: {} }), + }, + } - // No browserProvider passed - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + // No browserProvider passed + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -2525,26 +2572,27 @@ describe("sisyphus-task", () => { const mockManager = { launch: async () => ({}) } const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - status: async () => ({ data: {} }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) - - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", - agent: "prometheus", + app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "prometheus", abort: new AbortController().signal, } @@ -2570,24 +2618,25 @@ describe("sisyphus-task", () => { const { createDelegateTask } = require("./tools") const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_prometheus_allowed" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created successfully" }] }] - }), - status: async () => ({ data: { "ses_prometheus_allowed": { type: "idle" } } }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockClient = { + app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_prometheus_allowed" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created successfully" }] }] + }), + status: async () => ({ data: { "ses_prometheus_allowed": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -2617,42 +2666,43 @@ describe("sisyphus-task", () => { // given - current agent is Prometheus (capitalized) const { createDelegateTask } = require("./tools") - const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "test-session" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - status: async () => ({ data: {} }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) - - const toolContext = { - sessionID: "parent-session", - messageID: "parent-message", - agent: "Prometheus", - abort: new AbortController().signal, - } - - // when - Prometheus tries to delegate to prometheus - const result = await tool.execute( - { - description: "Test case-insensitive block", - prompt: "Create a plan", - subagent_type: "prometheus", - run_in_background: false, - load_skills: [], - }, - toolContext - ) + const mockManager = { launch: async () => ({}) } + const mockClient = { + app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + status: async () => ({ data: {} }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "Prometheus", + abort: new AbortController().signal, + } + + // when - Prometheus tries to delegate to prometheus + const result = await tool.execute( + { + description: "Test case-insensitive block", + prompt: "Create a plan", + subagent_type: "prometheus", + run_in_background: false, + load_skills: [], + }, + toolContext + ) // then - should still return error expect(result).toContain("prometheus") @@ -2679,26 +2729,27 @@ describe("sisyphus-task", () => { }, } - const mockClient = { - app: { - agents: async () => ({ - data: [ - { name: "explore", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-haiku-4-5" } }, - ], - }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - create: async () => ({ data: { id: "ses_explore_model" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } + const mockClient = { + app: { + agents: async () => ({ + data: [ + { name: "explore", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-haiku-4-5" } }, + ], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + create: async () => ({ data: { id: "ses_explore_model" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -2733,33 +2784,36 @@ describe("sisyphus-task", () => { const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { - agents: async () => ({ - data: [ - { name: "oracle", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-opus-4-6" } }, - ], - }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_oracle_model" } }), - prompt: async (input: any) => { - promptBody = input.body - return { data: {} } - }, - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Consultation done" }] }], - }), - status: async () => ({ data: { "ses_oracle_model": { type: "idle" } } }), - }, - } + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockClient = { + app: { + agents: async () => ({ + data: [ + { name: "oracle", mode: "subagent", model: { providerID: "anthropic", modelID: "claude-opus-4-6" } }, + ], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_oracle_model" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Consultation done" }] }], + }), + status: async () => ({ data: { "ses_oracle_model": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -2794,33 +2848,36 @@ describe("sisyphus-task", () => { const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { - agents: async () => ({ - data: [ - { name: "explore", mode: "subagent" }, // no model field - ], - }), - }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_no_model_agent" } }), - prompt: async (input: any) => { - promptBody = input.body - return { data: {} } - }, - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }], - }), - status: async () => ({ data: { "ses_no_model_agent": { type: "idle" } } }), - }, - } + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockClient = { + app: { + agents: async () => ({ + data: [ + { name: "explore", mode: "subagent" }, // no model field + ], + }), + }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_no_model_agent" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Done" }] }], + }), + status: async () => ({ data: { "ses_no_model_agent": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -2852,28 +2909,32 @@ describe("sisyphus-task", () => { const { createDelegateTask } = require("./tools") let promptBody: any - const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_prometheus_delegate" } }), - prompt: async (input: any) => { - promptBody = input.body - return { data: {} } - }, - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }] - }), - status: async () => ({ data: { "ses_prometheus_delegate": { type: "idle" } } }), - }, - } - - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const mockManager = { launch: async () => ({}) } + + const promptMock = async (input: any) => { + promptBody = input.body + return { data: {} } + } + + const mockClient = { + app: { agents: async () => ({ data: [{ name: "prometheus", mode: "subagent" }] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_prometheus_delegate" } }), + prompt: promptMock, + promptAsync: promptMock, + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Plan created" }] }] + }), + status: async () => ({ data: { "ses_prometheus_delegate": { type: "idle" } } }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -2914,6 +2975,10 @@ describe("sisyphus-task", () => { promptBody = input.body return { data: {} } }, + promptAsync: async (input: any) => { + promptBody = input.body + return { data: {} } + }, messages: async () => ({ data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Consultation done" }] }] }), @@ -2959,26 +3024,27 @@ describe("sisyphus-task", () => { const mockManager = { launch: async () => ({}) } const mockClient = { app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async (input: any) => { - createBody = input.body - return { data: { id: "ses_title_test" } } - }, - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "done" }] }] - }), - status: async () => ({ data: { "ses_title_test": { type: "idle" } } }), - }, - } + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async (input: any) => { + createBody = input.body + return { data: { id: "ses_title_test" } } + }, + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "done" }] }] + }), + status: async () => ({ data: { "ses_title_test": { type: "idle" } } }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -3007,26 +3073,27 @@ describe("sisyphus-task", () => { // given const { createDelegateTask } = require("./tools") - const mockManager = { launch: async () => ({}) } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, - session: { - get: async () => ({ data: { directory: "/project" } }), - create: async () => ({ data: { id: "ses_metadata_test" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ - data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Task completed" }] }] - }), - status: async () => ({ data: { "ses_metadata_test": { type: "idle" } } }), - }, - } + const mockManager = { launch: async () => ({}) } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, + session: { + get: async () => ({ data: { directory: "/project" } }), + create: async () => ({ data: { id: "ses_metadata_test" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ + data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "Task completed" }] }] + }), + status: async () => ({ data: { "ses_metadata_test": { type: "idle" } } }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) const toolContext = { sessionID: "parent-session", @@ -3066,21 +3133,25 @@ describe("sisyphus-task", () => { status: "running", }), } - const mockClient = { - app: { agents: async () => ({ data: [] }) }, - config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, - model: { list: async () => [{ id: SYSTEM_DEFAULT_MODEL }] }, - session: { - create: async () => ({ data: { id: "ses_bg_metadata" } }), - prompt: async () => ({ data: {} }), - messages: async () => ({ data: [] }), - }, - } + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) }, + model: { list: async () => [] }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } - const tool = createDelegateTask({ - manager: mockManager, - client: mockClient, - }) + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + userCategories: { + "sisyphus-junior": { model: "anthropic/claude-sonnet-4-5" }, + }, + }) const toolContext = { sessionID: "parent-session", diff --git a/src/tools/look-at/tools.test.ts b/src/tools/look-at/tools.test.ts index 8b2f040d..302ffde1 100644 --- a/src/tools/look-at/tools.test.ts +++ b/src/tools/look-at/tools.test.ts @@ -111,17 +111,19 @@ describe("look-at tool", () => { }) describe("createLookAt error handling", () => { - // given JSON parse error occurs in session.prompt + // given JSON parse error occurs in session.promptAsync // when LookAt tool executed - // then return user-friendly error message - test("handles JSON parse error from session.prompt gracefully", async () => { + // then error propagates (band-aid removed since root cause fixed by promptAsync migration) + test("propagates JSON parse error from session.promptAsync", async () => { + const throwingMock = async () => { + throw new Error("JSON Parse error: Unexpected EOF") + } const mockClient = { session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "ses_test_json_error" } }), - prompt: async () => { - throw new Error("JSON Parse error: Unexpected EOF") - }, + prompt: throwingMock, + promptAsync: throwingMock, messages: async () => ({ data: [] }), }, } @@ -142,28 +144,24 @@ describe("look-at tool", () => { ask: async () => {}, } - const result = await tool.execute( - { file_path: "/test/file.png", goal: "analyze image" }, - toolContext - ) - - expect(result).toContain("Error: Failed to analyze") - expect(result).toContain("malformed response") - expect(result).toContain("multimodal-looker") - expect(result).toContain("image/png") + await expect( + tool.execute({ file_path: "/test/file.png", goal: "analyze image" }, toolContext) + ).rejects.toThrow("JSON Parse error: Unexpected EOF") }) - // given generic error occurs in session.prompt + // given generic error occurs in session.promptAsync // when LookAt tool executed - // then return error including original error message - test("handles generic prompt error gracefully", async () => { + // then error propagates + test("propagates generic prompt error", async () => { + const throwingMock = async () => { + throw new Error("Network connection failed") + } const mockClient = { session: { get: async () => ({ data: { directory: "/project" } }), create: async () => ({ data: { id: "ses_test_generic_error" } }), - prompt: async () => { - throw new Error("Network connection failed") - }, + prompt: throwingMock, + promptAsync: throwingMock, messages: async () => ({ data: [] }), }, } @@ -184,13 +182,9 @@ describe("look-at tool", () => { ask: async () => {}, } - const result = await tool.execute( - { file_path: "/test/file.pdf", goal: "extract text" }, - toolContext - ) - - expect(result).toContain("Error: Failed to send prompt") - expect(result).toContain("Network connection failed") + await expect( + tool.execute({ file_path: "/test/file.pdf", goal: "extract text" }, toolContext) + ).rejects.toThrow("Network connection failed") }) }) @@ -220,6 +214,10 @@ describe("look-at tool", () => { promptBody = input.body return { data: {} } }, + promptAsync: async (input: any) => { + promptBody = input.body + return { data: {} } + }, messages: async () => ({ data: [ { info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "done" }] }, @@ -274,6 +272,10 @@ describe("look-at tool", () => { promptBody = input.body return { data: {} } }, + promptAsync: async (input: any) => { + promptBody = input.body + return { data: {} } + }, messages: async () => ({ data: [ { info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "analyzed" }] }, @@ -327,6 +329,10 @@ describe("look-at tool", () => { promptBody = input.body return { data: {} } }, + promptAsync: async (input: any) => { + promptBody = input.body + return { data: {} } + }, messages: async () => ({ data: [ { info: { role: "assistant", time: { created: 1 } }, parts: [{ type: "text", text: "analyzed" }] },