From e381117b1c705fc5c1875b97fbde9491cccd8e3d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 4 Jun 2026 13:32:51 +0700 Subject: [PATCH] Limit OpenCode metadata refresh concurrency --- .../agent/providers/opencode-agent.test.ts | 44 +++++++++++++++++++ .../server/agent/providers/opencode-agent.ts | 33 +++++++++----- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index 32813ef0a..cca4a04e2 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -226,6 +226,50 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { expect(openCodeClient.calls.providerList).toEqual([{ directory: cwd }]); }, 60_000); + test("limits concurrent OpenCode metadata requests across clients", async () => { + const runtime = new TestOpenCodeRuntime(); + let activeProviderListCalls = 0; + let maxActiveProviderListCalls = 0; + const response = { + data: { + connected: ["opencode"], + all: [ + { + id: "opencode", + name: "OpenCode", + source: "api", + models: { + "big-pickle": { + name: "Big Pickle", + }, + }, + }, + ], + }, + }; + + for (let index = 0; index < 12; index += 1) { + const openCodeClient = new TestOpenCodeClient(); + openCodeClient.providerListImplementation = async () => { + activeProviderListCalls += 1; + maxActiveProviderListCalls = Math.max(maxActiveProviderListCalls, activeProviderListCalls); + await new Promise((resolve) => setTimeout(resolve, 20)); + activeProviderListCalls -= 1; + return response; + }; + runtime.enqueueClient(openCodeClient); + } + + const client = new OpenCodeAgentClient(logger, undefined, { runtime }); + await Promise.all( + Array.from({ length: 12 }, (_, index) => + client.listModels({ cwd: path.join(os.tmpdir(), `opencode-cwd-${index}`), force: false }), + ), + ); + + expect(maxActiveProviderListCalls).toBeLessThanOrEqual(4); + }); + test("available modes include build and plan", async () => { const cwd = tmpCwd(); const runtime = new TestOpenCodeRuntime(); diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 49cce78bc..fd301e991 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -11,6 +11,7 @@ import { type TextPartInput as OpenCodeTextPartInput, } from "@opencode-ai/sdk/v2/client"; import { createPathEquivalenceMatcher } from "../../../utils/path.js"; +import pLimit from "p-limit"; import type { Logger } from "pino"; import { z } from "zod"; @@ -248,6 +249,8 @@ type OpenCodeMcpConfig = const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const; const OPENCODE_PROVIDER_LIST_TIMEOUT_MS = 30_000; +const OPENCODE_METADATA_CONCURRENCY = 4; +const openCodeMetadataLimit = pLimit(OPENCODE_METADATA_CONCURRENCY); const OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS: AgentSlashCommand[] = [ { name: "compact", description: "Compact the current session", argumentHint: "" }, { name: "summarize", description: "Compact the current session", argumentHint: "" }, @@ -1336,10 +1339,12 @@ export class OpenCodeAgentClient implements AgentClient { try { // Background model discovery can be legitimately slow while OpenCode refreshes // provider state, so allow longer than turn execution paths. - const response = await withTimeout( - client.provider.list({ directory: options.cwd }), - OPENCODE_PROVIDER_LIST_TIMEOUT_MS, - `OpenCode provider.list timed out after ${OPENCODE_PROVIDER_LIST_TIMEOUT_MS / 1000}s - server may not be authenticated or connected to any providers`, + const response = await openCodeMetadataLimit(() => + withTimeout( + client.provider.list({ directory: options.cwd }), + OPENCODE_PROVIDER_LIST_TIMEOUT_MS, + `OpenCode provider.list timed out after ${OPENCODE_PROVIDER_LIST_TIMEOUT_MS / 1000}s - server may not be authenticated or connected to any providers`, + ), ); if (response.error) { @@ -1401,10 +1406,12 @@ export class OpenCodeAgentClient implements AgentClient { const client = this.runtime.createClient({ baseUrl: url, directory }); try { - const response = await withTimeout( - client.app.agents({ directory }), - 10_000, - "OpenCode app.agents timed out after 10s", + const response = await openCodeMetadataLimit(() => + withTimeout( + client.app.agents({ directory }), + 10_000, + "OpenCode app.agents timed out after 10s", + ), ); if (response.error || !response.data) { @@ -1561,7 +1568,7 @@ export class OpenCodeAgentClient implements AgentClient { client: OpencodeClient, cwd: string, ): Promise { - const response = await client.provider.list({ directory: cwd }); + const response = await openCodeMetadataLimit(() => client.provider.list({ directory: cwd })); if (response.error || !response.data) { return; } @@ -3350,9 +3357,11 @@ class OpenCodeAgentSession implements AgentSession { return this.availableModesCache; } - const response = await this.client.app.agents({ - directory: this.config.cwd, - }); + const response = await openCodeMetadataLimit(() => + this.client.app.agents({ + directory: this.config.cwd, + }), + ); const agents = response.error || !response.data ? [] : response.data; const discoveredModes = agents.filter(isSelectableOpenCodeAgent).map(mapOpenCodeAgentToMode);