diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index 4f2a838152d7c..1efbbfad09d81 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -4150,26 +4150,37 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { promptAccepted, event("session.execution.succeeded", { sessionID: SESSION }), ]; + const prompts: Array = []; // Idle Location eviction drops OpenCode's runtime MCP servers without a reconnect. - const { runtime, thread } = yield* resumed([ - add, - reply("mcp.add", null), - ...turn, - add, - reply("mcp.add", { status: 500, body: { message: "MCP unavailable" } }), - out("session.instructions.entry.put", { - sessionID: SESSION, - key: "t3-code", - value: buildRuntimeInstructions({ harness: "OpenCode", model: bigPickle.model }), - }), - reply("session.instructions.entry.put", null), - ...turn, - out("mcp.remove", { server, "location[directory]": WORK }), - reply("mcp.remove", null), - ]); + const { runtime, thread } = yield* resumed( + [ + add, + reply("mcp.add", null), + ...turn, + add, + reply("mcp.add", { status: 500, body: { message: "MCP unavailable" } }), + out("session.instructions.entry.put", { + sessionID: SESSION, + key: "t3-code", + value: buildRuntimeInstructions({ harness: "OpenCode", model: bigPickle.model }), + }), + reply("session.instructions.entry.put", null), + ...turn, + out("mcp.remove", { server, "location[directory]": WORK }), + reply("mcp.remove", null), + ], + { onPrompt: (input) => prompts.push(input) }, + ); for (let index = 0; index < 2; index++) { const terminal = yield* terminalOf(runtime).pipe(Effect.forkScoped); - yield* runtime.startTurn(turnInput(thread)); + const started = yield* runtime.startTurn(turnInput(thread)).pipe(Effect.forkScoped); + // OpenCode before 2.2.6 loads a newly added server's tools after a debounce. + if (index === 0) { + yield* TestClock.adjust("249 millis"); + assert.lengthOf(prompts, 0, "prompted before OpenCode loaded the new MCP server's tools"); + yield* TestClock.adjust("1 millis"); + } + yield* Fiber.join(started); assert.equal((yield* Fiber.join(terminal))?.status, "completed"); } }).pipe(Effect.scoped), @@ -4230,7 +4241,11 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { runtimePolicy: policy(), }); const terminal = yield* terminalOf(runtime).pipe(Effect.forkScoped); - yield* runtime.startTurn({ ...turnInput(thread), threadId: child }); + const started = yield* runtime + .startTurn({ ...turnInput(thread), threadId: child }) + .pipe(Effect.forkScoped); + yield* TestClock.adjust("250 millis"); + yield* Fiber.join(started); assert.equal((yield* Fiber.join(terminal))?.status, "completed"); yield* runtime.unloadThread!({ providerThread: thread }); }).pipe(Effect.scoped), diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 33d54172335f8..b13f6c361bacf 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -592,6 +592,7 @@ const RECONNECT_WAIT = "30 seconds"; const LOST_BACKGROUND = "T3 Code lost its connection to OpenCode while this subagent ran, so its result is not shown."; const INVENTORY_TIMEOUT = "5 seconds"; +const MCP_TOOL_RELOAD_SETTLE = "250 millis"; const ACTIVE_CHECK_TIMEOUT = "5 seconds"; // Other failures may still have admitted the prompt. const CLEAR_PROMPT_REJECTIONS: ReadonlySet = new Set([ @@ -4060,6 +4061,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr // Re-adding an unchanged config keeps its existing connection. let mcpAvailable = false; if (wanted !== undefined) { + const fresh = state.mcp === undefined; mcpAvailable = yield* client.mcp .add({ server: name, @@ -4081,6 +4083,10 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr ), ); if (mcpAvailable) state.mcp = wanted; + // Before 2.2.6, OpenCode loads a new server's tools 100 ms after it connects, and a prompt sent sooner lacks them. + if (mcpAvailable && fresh && compareSemverVersions(connection.version, "2.2.6") < 0) { + yield* Effect.sleep(MCP_TOOL_RELOAD_SETTLE); + } } const instructions = [ buildRuntimeInstructions({ harness: "OpenCode", model: turnInput.modelSelection.model }),