@@ -39,14 +39,16 @@ import {
3939 computeUsage ,
4040 createConnectFrameParser ,
4141 createThinkingTagFilter ,
42+ type McpToolCallUpdateInfo ,
4243 parseConnectEndStream ,
4344 processServerMessage ,
4445 scheduleBridgeEnd ,
4546} from "./stream-dispatch" ;
4647import { createBridgeCloseController } from "./bridge-close-controller" ;
4748
4849const SSE_KEEPALIVE_INTERVAL_MS = 15_000 ;
49- const MCP_TOOL_BATCH_WINDOW_MS = 75 ;
50+ const MCP_TOOL_BATCH_WINDOW_MS = 150 ;
51+ const MCP_TOOL_METADATA_GRACE_MS = 1_500 ;
5052
5153function createBridgeStreamResponse (
5254 bridge : CursorSession ,
@@ -91,6 +93,8 @@ function createBridgeStreamResponse(
9193 let mcpExecReceived = false ;
9294 let endStreamError : Error | null = null ;
9395 let toolCallsFlushed = false ;
96+ const announcedToolCallIds = new Set < string > ( ) ;
97+ let toolCallMetadataDeadlineMs = 0 ;
9498
9599 const sendSSE = ( data : object ) => {
96100 if ( closed ) return ;
@@ -145,12 +149,51 @@ function createBridgeStreamResponse(
145149 usage : { prompt_tokens, completion_tokens, total_tokens } ,
146150 } ;
147151 } ;
152+ const getMissingAnnouncedToolCallIds = ( ) => {
153+ const pendingExecIds = new Set (
154+ state . pendingExecs . map ( ( exec ) => exec . toolCallId ) ,
155+ ) ;
156+ return [ ...announcedToolCallIds ] . filter (
157+ ( toolCallId ) => ! pendingExecIds . has ( toolCallId ) ,
158+ ) ;
159+ } ;
148160 const publishPendingToolCalls = ( ) => {
149161 if ( closed || toolCallsFlushed || state . pendingExecs . length === 0 ) return ;
150162
163+ const missingAnnouncedToolCallIds = getMissingAnnouncedToolCallIds ( ) ;
164+ if (
165+ missingAnnouncedToolCallIds . length > 0 &&
166+ Date . now ( ) < toolCallMetadataDeadlineMs
167+ ) {
168+ schedulePendingToolCallPublish ( ) ;
169+ return ;
170+ }
171+
151172 toolCallsFlushed = true ;
152173 stopToolCallFlushTimer ( ) ;
153174
175+ if ( missingAnnouncedToolCallIds . length > 0 ) {
176+ logPluginError (
177+ "Aborting Cursor stream because announced MCP tool calls never received exec metadata" ,
178+ {
179+ modelId,
180+ bridgeKey,
181+ convKey,
182+ pendingExecToolCallIds : state . pendingExecs . map (
183+ ( exec ) => exec . toolCallId ,
184+ ) ,
185+ missingAnnouncedToolCallIds,
186+ } ,
187+ ) ;
188+ clearInterval ( heartbeatTimer ) ;
189+ bridge . end ( ) ;
190+ failStream (
191+ `Cursor announced MCP tool calls without matching exec metadata: ${ missingAnnouncedToolCallIds . join ( ", " ) } ` ,
192+ "cursor_mcp_tool_mismatch" ,
193+ ) ;
194+ return ;
195+ }
196+
154197 const flushed = tagFilter . flush ( ) ;
155198 if ( flushed . reasoning )
156199 sendSSE ( makeChunk ( { reasoning_content : flushed . reasoning } ) ) ;
@@ -183,7 +226,7 @@ function createBridgeStreamResponse(
183226 blobStore,
184227 cloudRule,
185228 mcpTools,
186- pendingExecs : [ ... state . pendingExecs ] ,
229+ pendingExecs : state . pendingExecs ,
187230 modelId,
188231 metadata : {
189232 ...metadata ,
@@ -233,6 +276,7 @@ function createBridgeStreamResponse(
233276 }
234277 } ,
235278 ( exec ) => {
279+ announcedToolCallIds . add ( exec . toolCallId ) ;
236280 const existingIndex = state . pendingExecs . findIndex (
237281 ( candidate ) => candidate . toolCallId === exec . toolCallId ,
238282 ) ;
@@ -242,9 +286,22 @@ function createBridgeStreamResponse(
242286 state . pendingExecs . push ( exec ) ;
243287 }
244288 mcpExecReceived = true ;
289+ toolCallMetadataDeadlineMs =
290+ Date . now ( ) + MCP_TOOL_METADATA_GRACE_MS ;
245291
246292 schedulePendingToolCallPublish ( ) ;
247293 } ,
294+ ( info : McpToolCallUpdateInfo ) => {
295+ if ( info . updateCase === "toolCallCompleted" ) {
296+ announcedToolCallIds . delete ( info . toolCallId ) ;
297+ } else {
298+ announcedToolCallIds . add ( info . toolCallId ) ;
299+ }
300+
301+ if ( mcpExecReceived ) {
302+ schedulePendingToolCallPublish ( ) ;
303+ }
304+ } ,
248305 ( checkpointBytes ) => {
249306 updateConversationCheckpoint ( convKey , checkpointBytes ) ;
250307 bridgeCloseController . noteCheckpoint ( ) ;
@@ -479,7 +536,14 @@ export async function handleToolResultResume(
479536 . filter ( Boolean )
480537 . join ( "\n\n" ) ,
481538 } ;
539+ const pendingToolCallIds = new Set ( pendingExecs . map ( ( exec ) => exec . toolCallId ) ) ;
482540 const toolResultIds = new Set ( toolResults . map ( ( result ) => result . toolCallId ) ) ;
541+ const missingToolResultIds = pendingExecs
542+ . map ( ( exec ) => exec . toolCallId )
543+ . filter ( ( toolCallId ) => ! toolResultIds . has ( toolCallId ) ) ;
544+ const unexpectedToolResultIds = toolResults
545+ . map ( ( result ) => result . toolCallId )
546+ . filter ( ( toolCallId ) => ! pendingToolCallIds . has ( toolCallId ) ) ;
483547 const matchedPendingExecs = pendingExecs . filter ( ( exec ) =>
484548 toolResultIds . has ( exec . toolCallId ) ,
485549 ) ;
@@ -519,6 +583,35 @@ export async function handleToolResultResume(
519583 ) ;
520584 }
521585
586+ if ( missingToolResultIds . length > 0 || unexpectedToolResultIds . length > 0 ) {
587+ logPluginError ( "Aborting Cursor tool-result resume because tool-call ids did not match" , {
588+ bridgeKey,
589+ convKey,
590+ modelId,
591+ pendingToolCallIds : [ ...pendingToolCallIds ] ,
592+ toolResultIds : [ ...toolResultIds ] ,
593+ missingToolResultIds,
594+ unexpectedToolResultIds,
595+ } ) ;
596+ clearInterval ( heartbeatTimer ) ;
597+ bridge . end ( ) ;
598+ return new Response (
599+ JSON . stringify ( {
600+ error : {
601+ message :
602+ "Tool-result ids did not match the active Cursor MCP tool calls. Aborting instead of resuming a potentially stuck session." ,
603+ type : "invalid_request_error" ,
604+ code : "cursor_tool_result_mismatch" ,
605+ details : {
606+ missingToolResultIds,
607+ unexpectedToolResultIds,
608+ } ,
609+ } ,
610+ } ) ,
611+ { status : 400 , headers : { "Content-Type" : "application/json" } } ,
612+ ) ;
613+ }
614+
522615 for ( const exec of matchedPendingExecs ) {
523616 const result = toolResults . find (
524617 ( toolResult ) => toolResult . toolCallId === exec . toolCallId ,
0 commit comments