You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
V2: MCP discovery can starve chat reads in the client request queue #53049
The app's request queue reserves slots for session reads when git/worktree requests are slow, but MCP and command catalogs are not classified as slow. Four catalog requests for different locations can occupy all four slots. A subsequent chat message request stays in the client queue even if the server could answer it immediately.
Command catalogs can wait for MCP startup/discovery. A slow or unavailable MCP server therefore delays unrelated chat reads until a catalog request completes or hits its header deadline.
Start four /api/command requests for different locations through createRequestQueue, holding their responses open.
Request /api/session/<id>/message through that queue.
Observe that all four catalogs are dispatched and the message request is not.
The same queue failure occurs for /api/mcp and /api/mcp/resource. A regression test demonstrates dispatch order; a loopback HTTP replay verifies that chat can complete before the held catalogs are released after the proposed fix.
Expected: discovery requests use the existing slow-request quota, preserving capacity for session reads. This is a reproduced starvation mechanism, not a claim that it explains every observed timeout.
Operating System
Debian GNU/Linux forky/sid, Linux x86-64.
Client and reproduction environment
OpenCode Desktop 2.0.22 on the workstation where intermittent chat loading was reported. The scheduler failure itself was reproduced with Bun 1.4.2 in the app queue test and a loopback HTTP fixture without plugins; it is not a CLI session-execution defect. A full Desktop trace correlating an original timeout to this mechanism has not been captured.
Added the operating system, Desktop version and exact reproduction environment to the report.
I checked #52852 and #48386. They concern repeated requests on one MCP connection and a toggle awaiting resource refresh, respectively. This issue concerns the shared client request queue: four pending catalog requests consume every slot, so a chat message read is not dispatched. Reducing discovery traffic or making the toggle asynchronous does not by itself reserve request capacity for chat reads; #53050 addresses that scheduler behavior.
Thanks for the detailed report. I can confirm the queue behaviour you describe.
Using the app's createRequestQueue with a fake fetch that holds catalog responses open, I started four requests to /api/command for different locations and then requested /api/session/<id>/message. The queue ended up with 4 requests in flight and the message request waiting; it never reached the server. /api/mcp and /api/mcp/resource behave the same way. This happens on v2.0.22 and still on the latest dev (c250c272c): slowRequestPaths there is still only /api/vcs and /api/worktree. The recent priority change for the first transcript page only changes the read's place in the waiting line; it doesn't free a slot. The script below reproduces it.
In a quick check, counting /api/command as slow let the message request through. Related: #53458 is an open PR that references this issue but takes a different approach (more slots and priority for writes).
Reproduction script (queue-starvation.ts)
// Reproduces #53049: catalog requests (/api/command, /api/mcp, /api/mcp/resource) fill every slot// in the app's request queue, so a chat transcript read is never dispatched.// Usage: bun /workspace/repro/queue-starvation.ts [path/to/request-queue.ts]// Default module: /workspace/repo/packages/app/src/runtime/server/request-queue.tsconstmodulePath=process.argv[2]??"/workspace/repo/packages/app/src/runtime/server/request-queue.ts"const{ createRequestQueue }=awaitimport(modulePath)letfailed=falsefor(constcatalogof["/api/command","/api/mcp","/api/mcp/resource"]){constdispatched: string[]=[]constheld: Array<()=>void>=[]constfakeFetch=((req: Request)=>{constu=newURL(req.url)dispatched.push(u.pathname+u.search)// Catalog responses are held open (e.g. waiting on a slow MCP server); everything else answers at once.if(u.pathname===catalog)returnnewPromise<Response>((r)=>held.push(()=>r(newResponse("[]"))))returnPromise.resolve(newResponse("[]"))})astypeoffetchconstq=createRequestQueue({fetch: fakeFetch,log: ()=>{}})constcatalogs=[1,2,3,4].map((i)=>q.fetch(`http://127.0.0.1:4096${catalog}?location=/proj${i}`))letchatDone=falseconstchat=q.fetch("http://127.0.0.1:4096/api/session/ses_1/message").then(()=>(chatDone=true))awaitnewPromise((r)=>setTimeout(r,200))constchatDispatched=dispatched.some((d)=>d.startsWith("/api/session/ses_1/message"))console.log(`${catalog}: inflight=${q.inflight()} queued=${q.queued()} chatDispatched=${chatDispatched} chatDone=${chatDone}`,)if(!chatDispatched)failed=trueheld.forEach((r)=>r())awaitPromise.all([...catalogs,chat])}console.log(failed ? "BUG: chat read starved behind catalog requests" : "OK: chat read dispatched while catalogs held")process.exit(failed ? 1 : 0)
Thanks for independently reproducing this on current dev. The priority distinction is exactly the issue: priority orders requests that are still waiting, but cannot free a slot occupied by an in-flight catalog request.
I checked all three paths against #53050 at e08a92fa69af846ca3466eca4ef41be6798bc229 using a controlled fetch that holds the four catalog responses. For each of /api/command, /api/mcp and /api/mcp/resource, only two catalog requests were dispatched, and the chat transcript read completed before any catalog response was released. All requests drained afterward. The resource path is covered by the same prefix classification, not a separate special case.
#53050 retains the upstream write/navigation priorities and adds those catalogs to the existing slow-request quota. #53458 takes a broader approach, including raising the total budget from 4 to 12; its changed queue still classifies only VCS/worktree requests as slow. Increasing the budget can accommodate the four-request example, but sufficiently many held catalogs can fill the larger budget too. The catalog quota is the part that prevents that class of saturation.
This verifies the queue scheduling mechanism, not every Desktop timeout or a server-side performance cause. #53050 is still open and unmerged.
One small detail if reusing the reproduction against the fixed queue: the cleanup needs to release catalogs dispatched after the first batch is released as well. With a two-catalog cap, a one-time held.forEach(...) can otherwise leave the remaining queued catalogs pending.
Activity
mlimarenko commentedon Oct 4, 2026
Added the operating system, Desktop version and exact reproduction environment to the report.
I checked #52852 and #48386. They concern repeated requests on one MCP connection and a toggle awaiting resource refresh, respectively. This issue concerns the shared client request queue: four pending catalog requests consume every slot, so a chat message read is not dispatched. Reducing discovery traffic or making the toggle asynchronous does not by itself reserve request capacity for chat reads; #53050 addresses that scheduler behavior.
github-actions commentedon Oct 4, 2026
Thanks for updating the issue with the system details and reproduction context!
opencodejr commentedon Oct 27, 2026
Thanks for the detailed report. I can confirm the queue behaviour you describe.
Using the app's
createRequestQueuewith a fake fetch that holds catalog responses open, I started four requests to/api/commandfor different locations and then requested/api/session/<id>/message. The queue ended up with 4 requests in flight and the message request waiting; it never reached the server./api/mcpand/api/mcp/resourcebehave the same way. This happens on v2.0.22 and still on the latestdev(c250c272c):slowRequestPathsthere is still only/api/vcsand/api/worktree. The recent priority change for the first transcript page only changes the read's place in the waiting line; it doesn't free a slot. The script below reproduces it.In a quick check, counting
/api/commandas slow let the message request through. Related: #53458 is an open PR that references this issue but takes a different approach (more slots and priority for writes).Reproduction script (queue-starvation.ts)
Run it:
bun queue-starvation.tsmlimarenko commentedon Oct 27, 2026
Thanks for independently reproducing this on current dev. The priority distinction is exactly the issue: priority orders requests that are still waiting, but cannot free a slot occupied by an in-flight catalog request.
I checked all three paths against #53050 at
e08a92fa69af846ca3466eca4ef41be6798bc229using a controlled fetch that holds the four catalog responses. For each of/api/command,/api/mcpand/api/mcp/resource, only two catalog requests were dispatched, and the chat transcript read completed before any catalog response was released. All requests drained afterward. The resource path is covered by the same prefix classification, not a separate special case.#53050 retains the upstream write/navigation priorities and adds those catalogs to the existing slow-request quota. #53458 takes a broader approach, including raising the total budget from 4 to 12; its changed queue still classifies only VCS/worktree requests as slow. Increasing the budget can accommodate the four-request example, but sufficiently many held catalogs can fill the larger budget too. The catalog quota is the part that prevents that class of saturation.
This verifies the queue scheduling mechanism, not every Desktop timeout or a server-side performance cause. #53050 is still open and unmerged.
One small detail if reusing the reproduction against the fixed queue: the cleanup needs to release catalogs dispatched after the first batch is released as well. With a two-catalog cap, a one-time
held.forEach(...)can otherwise leave the remaining queued catalogs pending.