From 3b41191980d0d7909f611325daa184be25c07d2a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 24 Mar 2026 20:28:01 +0900 Subject: [PATCH] fix(background-agent): honor explicit model override in manager Keep BackgroundManager launch and resume from sending both agent and model so OpenCode does not override configured subagent models. Add launch and resume regressions for the live production path. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/background-agent/manager.test.ts | 79 +++++++++++++++++-- src/features/background-agent/manager.ts | 8 +- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 4553cf1cb..11025c730 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -1668,7 +1668,7 @@ describe("BackgroundManager.resume model persistence", () => { // then - model should be passed in prompt body expect(promptCalls).toHaveLength(1) expect(promptCalls[0].body.model).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-20250514" }) - expect(promptCalls[0].body.agent).toBe("explore") + expect("agent" in promptCalls[0].body).toBe(false) }) test("should NOT pass model when task has no model (backward compatibility)", async () => { @@ -1806,9 +1806,9 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { expect(task.sessionID).toBeUndefined() }) - test("should return immediately even with concurrency limit", async () => { - // given - const config = { defaultConcurrency: 1 } + test("should return immediately even with concurrency limit", async () => { + // given + const config = { defaultConcurrency: 1 } manager.shutdown() manager = new BackgroundManager({ client: mockClient, directory: tmpdir() } as unknown as PluginInput, config) @@ -1828,9 +1828,76 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { // then expect(endTime - startTime).toBeLessThan(100) // Should be instant - expect(task1.status).toBe("pending") - expect(task2.status).toBe("pending") + expect(task1.status).toBe("pending") + expect(task2.status).toBe("pending") + }) + + test("should omit agent when launch has model and keep agent without model", async () => { + // given + const promptBodies: Array> = [] + let resolveFirstPromptStarted: (() => void) | undefined + let resolveSecondPromptStarted: (() => void) | undefined + const firstPromptStarted = new Promise((resolve) => { + resolveFirstPromptStarted = resolve }) + const secondPromptStarted = new Promise((resolve) => { + resolveSecondPromptStarted = resolve + }) + const customClient = { + session: { + create: async (_args?: unknown) => ({ data: { id: `ses_${crypto.randomUUID()}` } }), + get: async () => ({ data: { directory: "/test/dir" } }), + prompt: async () => ({}), + promptAsync: async (args: { path: { id: string }; body: Record }) => { + promptBodies.push(args.body) + if (promptBodies.length === 1) { + resolveFirstPromptStarted?.() + } + if (promptBodies.length === 2) { + resolveSecondPromptStarted?.() + } + return {} + }, + messages: async () => ({ data: [] }), + todo: async () => ({ data: [] }), + status: async () => ({ data: {} }), + abort: async () => ({}), + }, + } + manager.shutdown() + manager = new BackgroundManager({ client: customClient, directory: tmpdir() } as unknown as PluginInput) + + const launchInputWithModel = { + description: "Test task with model", + prompt: "Do something", + agent: "test-agent", + parentSessionID: "parent-session", + parentMessageID: "parent-message", + model: { providerID: "anthropic", modelID: "claude-opus-4-6" }, + } + const launchInputWithoutModel = { + description: "Test task without model", + prompt: "Do something else", + agent: "test-agent", + parentSessionID: "parent-session", + parentMessageID: "parent-message", + } + + // when + const taskWithModel = await manager.launch(launchInputWithModel) + await firstPromptStarted + const taskWithoutModel = await manager.launch(launchInputWithoutModel) + await secondPromptStarted + + // then + expect(taskWithModel.status).toBe("pending") + expect(taskWithoutModel.status).toBe("pending") + expect(promptBodies).toHaveLength(2) + expect(promptBodies[0].model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" }) + expect("agent" in promptBodies[0]).toBe(false) + expect(promptBodies[1].agent).toBe("test-agent") + expect("model" in promptBodies[1]).toBe(false) + }) test("should queue multiple tasks without blocking", async () => { // given diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index c4ea7528b..c858667a8 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -515,7 +515,9 @@ export class BackgroundManager { promptWithModelSuggestionRetry(this.client, { path: { id: sessionID }, body: { - agent: input.agent, + // When a model is explicitly provided, omit the agent name so opencode's + // built-in agent fallback chain does not override the user-specified model. + ...(launchModel ? {} : { agent: input.agent }), ...(launchModel ? { model: launchModel } : {}), ...(launchVariant ? { variant: launchVariant } : {}), system: input.skillContent, @@ -792,7 +794,9 @@ export class BackgroundManager { this.client.session.promptAsync({ path: { id: existingTask.sessionID }, body: { - agent: existingTask.agent, + // When a model is explicitly provided, omit the agent name so opencode's + // built-in agent fallback chain does not override the user-specified model. + ...(resumeModel ? {} : { agent: existingTask.agent }), ...(resumeModel ? { model: resumeModel } : {}), ...(resumeVariant ? { variant: resumeVariant } : {}), tools: (() => {