From 2f7f7941cd75a79b8a2aa381c7a45eb719a2feeb Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Wed, 7 Oct 2026 16:10:52 +0530 Subject: [PATCH 1/2] fix(server): OpenCode 2 threads keep T3's tools after an idle project is evicted OpenCode 2 keeps MCP servers added at runtime in the project Location and drops them when it evicts a Location after 60 idle minutes. The adapter added T3's MCP server once and cached that in `state.mcp`, so later turns ran without T3's tools while the instructions still said they were there. Add the server on every turn (OpenCode leaves an unchanged config connected) and tell the model the tools are available only when this turn's add worked. Fixes #16768 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Adapters/OpenCode2AdapterV2.test.ts | 52 +++++++++++++++++++ .../Adapters/OpenCode2AdapterV2.ts | 15 +++--- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index 7274de8c386f..f8dda849e1ed 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -2722,6 +2722,58 @@ describe("OpenCode2 adapter", () => { }).pipe(Effect.scoped), ); + it.effect("adds T3's MCP server again on every turn, in case OpenCode dropped it", () => + Effect.gen(function* () { + McpProviderSession.setMcpProviderSession({ + environmentId: EnvironmentId.make("environment:opencode2-adapter"), + threadId, + providerSessionId: "mcp:opencode2-adapter", + providerInstanceId: instanceId, + endpoint: "http://127.0.0.1:3773/mcp", + authorizationHeader: "Bearer thread-credential", + browserToolsAvailable: false, + }); + yield* Effect.addFinalizer(() => + Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId)), + ); + const server = "t3-code-thread_opencode2-adapter"; + const add = [ + out("mcp.add", { + server, + "location[directory]": WORK, + config: { + type: "remote", + url: "http://127.0.0.1:3773/mcp", + headers: { Authorization: "Bearer thread-credential" }, + oauth: false, + }, + }), + reply("mcp.add", null), + ]; + const turn = [ + out("session.prompt", { sessionID: SESSION, text: "" }), + promptAccepted, + event("session.execution.succeeded", { sessionID: SESSION }), + ]; + // OpenCode keeps runtime-added servers with the Location and forgets them + // when it evicts an idle one, without a reconnect T3 could notice. + const { runtime, thread } = yield* resumed([ + ...add, + ...turn, + ...add, + ...turn, + // Removed when the session closes. + out("mcp.remove", { server, "location[directory]": WORK }), + reply("mcp.remove", null), + ]); + for (let index = 0; index < 2; index++) { + const terminal = yield* terminalOf(runtime).pipe(Effect.forkScoped); + yield* runtime.startTurn(turnInput(thread)); + assert.equal((yield* Fiber.join(terminal))?.status, "completed"); + } + }).pipe(Effect.scoped), + ); + it.effect("registers a long thread id's MCP server under a name OpenCode accepts", () => Effect.gen(function* () { // Spelled out, this delegated thread's server name would be 120 characters. diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts index 102eb721bb1a..14d99d0fbcab 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts @@ -396,7 +396,7 @@ interface ThreadState { * inbox, where the next prompt would deliver them first, so it cancels them. */ readonly strandedSteers: Set; - /** T3's MCP server as registered for this thread, and the instructions entry sent with it. */ + /** T3's MCP server as last registered for this thread, and the instructions entry sent with it. */ mcp: | { readonly name: string; readonly directory: string; readonly credential: string } | undefined; @@ -3260,9 +3260,12 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr yield* removeMcp(state.mcp); state.mcp = undefined; } - // T3's tools are an addition: a server that cannot add them still runs the turn. - if (wanted !== undefined && state.mcp === undefined) { - const added = yield* client.mcp + // Added on every turn: OpenCode drops runtime-added servers when it evicts + // an idle Location, and leaves an unchanged one connected. T3's tools are + // an addition: a server that cannot add them still runs the turn. + let mcpAvailable = false; + if (wanted !== undefined) { + mcpAvailable = yield* client.mcp .add({ server: name, location: { directory }, @@ -3282,11 +3285,11 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr ), ), ); - if (added) state.mcp = wanted; + if (mcpAvailable) state.mcp = wanted; } const instructions = [ buildRuntimeInstructions({ harness: "OpenCode", model: turnInput.modelSelection.model }), - t3OrchestrationSystemPrompt(state.mcp !== undefined), + t3OrchestrationSystemPrompt(mcpAvailable), ] .filter((part) => part !== undefined && part.length > 0) .join("\n\n"); From 6a041390fd4f13a62ca8734a169b6311a1317fe6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 03:21:41 +0000 Subject: [PATCH 2/2] fix(opencode): retain T3 tools after idle location eviction --- .../Adapters/OpenCode2AdapterV2.test.ts | 49 ++++++++++--------- .../src/server/v2/adapter.ts | 5 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index fee6f8d0d8c8..6a56b55400af 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -35,6 +35,7 @@ import { describe } from "vite-plus/test"; import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; +import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; import { @@ -3243,9 +3244,10 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { }).pipe(Effect.scoped), ); - it.effect("adds T3's MCP server again on every turn, in case OpenCode dropped it", () => + it.effect("re-registers T3 MCP each turn and drops its instructions when adding fails", () => Effect.gen(function* () { - McpProviderSession.setMcpProviderSession({ + const mcpSessions = yield* McpProviderSessions.McpProviderSessions; + yield* mcpSessions.set({ environmentId: EnvironmentId.make("environment:opencode2-adapter"), threadId, providerSessionId: "mcp:opencode2-adapter", @@ -3254,36 +3256,37 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { authorizationHeader: "Bearer thread-credential", browserToolsAvailable: false, }); - yield* Effect.addFinalizer(() => - Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId)), - ); + yield* Effect.addFinalizer(() => mcpSessions.clear(threadId)); const server = "t3-code-thread_opencode2-adapter"; - const add = [ - out("mcp.add", { - server, - "location[directory]": WORK, - config: { - type: "remote", - url: "http://127.0.0.1:3773/mcp", - headers: { Authorization: "Bearer thread-credential" }, - oauth: false, - }, - }), - reply("mcp.add", null), - ]; + const add = out("mcp.add", { + server, + "location[directory]": WORK, + config: { + type: "remote", + url: "http://127.0.0.1:3773/mcp", + headers: { Authorization: "Bearer thread-credential" }, + oauth: false, + }, + }); const turn = [ out("session.prompt", { sessionID: SESSION, text: "" }), promptAccepted, event("session.execution.succeeded", { sessionID: SESSION }), ]; - // OpenCode keeps runtime-added servers with the Location and forgets them - // when it evicts an idle one, without a reconnect T3 could notice. + // Idle Location eviction drops OpenCode's runtime MCP servers without a reconnect. const { runtime, thread } = yield* resumed([ - ...add, + add, + reply("mcp.add", null), ...turn, - ...add, + 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, - // Removed when the session closes. out("mcp.remove", { server, "location[directory]": WORK }), reply("mcp.remove", null), ]); diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 0d7550c56507..17214ae2116d 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -2955,9 +2955,8 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr yield* removeMcp(state.mcp); state.mcp = undefined; } - // Added on every turn: OpenCode drops runtime-added servers when it evicts - // an idle Location, and leaves an unchanged one connected. T3's tools are - // an addition: a server that cannot add them still runs the turn. + // Idle Location eviction drops runtime MCP servers without clearing T3's cache. + // Re-adding an unchanged config keeps its existing connection. let mcpAvailable = false; if (wanted !== undefined) { mcpAvailable = yield* client.mcp