@playwo/opencode-cursor-oauth 0.0.0-dev.4258a6733133 → 0.0.0-dev.4696faa690e4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,8 @@ import { updateStoredConversationAfterCompletion } from "./conversation-state";
5
5
  import { startBridge } from "./bridge-session";
6
6
  import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
7
7
  import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
8
+ import { createBridgeCloseController } from "./bridge-close-controller";
9
+ const MCP_TOOL_BATCH_WINDOW_MS = 150;
8
10
  export async function handleNonStreamingResponse(payload, accessToken, modelId, convKey, metadata) {
9
11
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
10
12
  const created = Math.floor(Date.now() / 1000);
@@ -32,35 +34,55 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
32
34
  let fullText = "";
33
35
  let endStreamError = null;
34
36
  const pendingToolCalls = [];
37
+ let toolCallEndTimer;
35
38
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
39
+ const bridgeCloseController = createBridgeCloseController(bridge);
40
+ const stopToolCallEndTimer = () => {
41
+ if (!toolCallEndTimer)
42
+ return;
43
+ clearTimeout(toolCallEndTimer);
44
+ toolCallEndTimer = undefined;
45
+ };
46
+ const scheduleToolCallBridgeEnd = () => {
47
+ stopToolCallEndTimer();
48
+ toolCallEndTimer = setTimeout(() => scheduleBridgeEnd(bridge), MCP_TOOL_BATCH_WINDOW_MS);
49
+ };
36
50
  const state = {
37
51
  toolCallIndex: 0,
38
52
  pendingExecs: [],
39
53
  outputTokens: 0,
40
54
  totalTokens: 0,
41
- interactionToolArgsText: new Map(),
42
- emittedToolCallIds: new Set(),
43
55
  };
44
56
  const tagFilter = createThinkingTagFilter();
45
57
  bridge.onData(createConnectFrameParser((messageBytes) => {
46
58
  try {
47
59
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
48
- processServerMessage(serverMessage, payload.blobStore, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
60
+ processServerMessage(serverMessage, payload.blobStore, payload.cloudRule, payload.mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
49
61
  if (isThinking)
50
62
  return;
51
63
  const { content } = tagFilter.process(text);
52
64
  fullText += content;
53
65
  }, (exec) => {
54
- pendingToolCalls.push({
66
+ const toolCall = {
55
67
  id: exec.toolCallId,
56
68
  type: "function",
57
69
  function: {
58
70
  name: exec.toolName,
59
71
  arguments: exec.decodedArgs,
60
72
  },
61
- });
62
- scheduleBridgeEnd(bridge);
63
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
73
+ };
74
+ const existingIndex = pendingToolCalls.findIndex((call) => call.id === exec.toolCallId);
75
+ if (existingIndex >= 0) {
76
+ pendingToolCalls[existingIndex] = toolCall;
77
+ }
78
+ else {
79
+ pendingToolCalls.push(toolCall);
80
+ }
81
+ scheduleToolCallBridgeEnd();
82
+ }, (_info) => { }, (checkpointBytes) => {
83
+ updateConversationCheckpoint(convKey, checkpointBytes);
84
+ bridgeCloseController.noteCheckpoint();
85
+ }, () => bridgeCloseController.noteTurnEnded(), (info) => {
64
86
  endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
65
87
  logPluginError("Closing non-streaming Cursor bridge after unsupported message", {
66
88
  modelId,
@@ -97,6 +119,8 @@ async function collectFullResponse(payload, accessToken, modelId, convKey, metad
97
119
  scheduleBridgeEnd(bridge);
98
120
  }));
99
121
  bridge.onClose(() => {
122
+ bridgeCloseController.dispose();
123
+ stopToolCallEndTimer();
100
124
  clearInterval(heartbeatTimer);
101
125
  syncStoredBlobStore(convKey, payload.blobStore);
102
126
  const flushed = tagFilter.flush();
@@ -2,12 +2,10 @@ import { createCursorSession } from "../cursor/bidi-session";
2
2
  import { makeHeartbeatBytes } from "./stream-dispatch";
3
3
  const HEARTBEAT_INTERVAL_MS = 5_000;
4
4
  export async function startBridge(accessToken, requestBytes) {
5
- const requestId = crypto.randomUUID();
6
5
  const bridge = await createCursorSession({
7
6
  accessToken,
8
- requestId,
7
+ initialRequestBytes: requestBytes,
9
8
  });
10
- bridge.write(requestBytes);
11
9
  const heartbeatTimer = setInterval(() => bridge.write(makeHeartbeatBytes()), HEARTBEAT_INTERVAL_MS);
12
10
  return { bridge, heartbeatTimer };
13
11
  }
@@ -2,4 +2,4 @@ import { type ToolResultInfo } from "../openai/messages";
2
2
  import type { ConversationRequestMetadata } from "./conversation-meta";
3
3
  import type { ActiveBridge, CursorRequestPayload } from "./types";
4
4
  export declare function handleStreamingResponse(payload: CursorRequestPayload, accessToken: string, modelId: string, bridgeKey: string, convKey: string, metadata: ConversationRequestMetadata): Promise<Response>;
5
- export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Response;
5
+ export declare function handleToolResultResume(active: ActiveBridge, toolResults: ToolResultInfo[], bridgeKey: string, convKey: string): Promise<Response>;
@@ -1,23 +1,36 @@
1
1
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
2
2
  import { AgentClientMessageSchema, AgentServerMessageSchema, ExecClientMessageSchema, McpErrorSchema, McpResultSchema, McpSuccessSchema, McpTextContentSchema, McpToolResultContentItemSchema, } from "../proto/agent_pb";
3
- import { errorDetails, logPluginError, logPluginWarn } from "../logger";
3
+ import { errorDetails, logPluginError, logPluginInfo, logPluginWarn, } from "../logger";
4
4
  import { formatToolCallSummary, formatToolResultSummary, } from "../openai/messages";
5
5
  import { activeBridges, updateStoredConversationAfterCompletion, } from "./conversation-state";
6
6
  import { startBridge } from "./bridge-session";
7
7
  import { updateConversationCheckpoint, syncStoredBlobStore, } from "./state-sync";
8
8
  import { SSE_HEADERS } from "./sse";
9
9
  import { computeUsage, createConnectFrameParser, createThinkingTagFilter, parseConnectEndStream, processServerMessage, scheduleBridgeEnd, } from "./stream-dispatch";
10
+ import { createBridgeCloseController } from "./bridge-close-controller";
10
11
  const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
11
- function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, metadata) {
12
+ const MCP_TOOL_BATCH_WINDOW_MS = 150;
13
+ function sortedIds(values) {
14
+ return [...values].sort();
15
+ }
16
+ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
12
17
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
13
18
  const created = Math.floor(Date.now() / 1000);
14
19
  let keepaliveTimer;
20
+ let toolCallFlushTimer;
21
+ const bridgeCloseController = createBridgeCloseController(bridge);
15
22
  const stopKeepalive = () => {
16
23
  if (!keepaliveTimer)
17
24
  return;
18
25
  clearInterval(keepaliveTimer);
19
26
  keepaliveTimer = undefined;
20
27
  };
28
+ const stopToolCallFlushTimer = () => {
29
+ if (!toolCallFlushTimer)
30
+ return;
31
+ clearTimeout(toolCallFlushTimer);
32
+ toolCallFlushTimer = undefined;
33
+ };
21
34
  const stream = new ReadableStream({
22
35
  start(controller) {
23
36
  const encoder = new TextEncoder();
@@ -27,13 +40,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
27
40
  pendingExecs: [],
28
41
  outputTokens: 0,
29
42
  totalTokens: 0,
30
- interactionToolArgsText: new Map(),
31
- emittedToolCallIds: new Set(),
32
43
  };
33
44
  const tagFilter = createThinkingTagFilter();
34
45
  let assistantText = metadata.assistantSeedText ?? "";
35
46
  let mcpExecReceived = false;
36
47
  let endStreamError = null;
48
+ let toolCallsFlushed = false;
49
+ const streamedToolCalls = new Map();
37
50
  const sendSSE = (data) => {
38
51
  if (closed)
39
52
  return;
@@ -67,6 +80,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
67
80
  return;
68
81
  closed = true;
69
82
  stopKeepalive();
83
+ stopToolCallFlushTimer();
70
84
  controller.close();
71
85
  };
72
86
  const makeChunk = (delta, finishReason = null) => ({
@@ -87,10 +101,157 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
87
101
  usage: { prompt_tokens, completion_tokens, total_tokens },
88
102
  };
89
103
  };
104
+ const ensureStreamedToolCall = (toolCallId, toolName) => {
105
+ const existing = streamedToolCalls.get(toolCallId);
106
+ if (existing) {
107
+ if (toolName && existing.toolName !== toolName) {
108
+ existing.toolName = toolName;
109
+ }
110
+ return existing;
111
+ }
112
+ const createdState = {
113
+ index: state.toolCallIndex++,
114
+ toolName,
115
+ started: false,
116
+ };
117
+ streamedToolCalls.set(toolCallId, createdState);
118
+ return createdState;
119
+ };
120
+ const emitPendingToolStart = (toolCallId, toolName, source) => {
121
+ if (!toolName)
122
+ return;
123
+ const toolCall = ensureStreamedToolCall(toolCallId, toolName);
124
+ if (toolCall.started)
125
+ return;
126
+ toolCall.started = true;
127
+ logPluginInfo("Streaming pending Cursor tool-call start", {
128
+ modelId,
129
+ bridgeKey,
130
+ convKey,
131
+ toolCallId,
132
+ toolName,
133
+ index: toolCall.index,
134
+ source,
135
+ });
136
+ sendSSE(makeChunk({
137
+ tool_calls: [
138
+ {
139
+ index: toolCall.index,
140
+ id: toolCallId,
141
+ type: "function",
142
+ function: {
143
+ name: toolCall.toolName,
144
+ arguments: "",
145
+ },
146
+ },
147
+ ],
148
+ }));
149
+ };
150
+ const publishPendingToolCalls = () => {
151
+ if (closed || toolCallsFlushed || state.pendingExecs.length === 0)
152
+ return;
153
+ logPluginInfo("Evaluating Cursor tool-call publish", {
154
+ modelId,
155
+ bridgeKey,
156
+ convKey,
157
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
158
+ toolCallsFlushed,
159
+ mcpExecReceived,
160
+ nowMs: Date.now(),
161
+ });
162
+ toolCallsFlushed = true;
163
+ stopToolCallFlushTimer();
164
+ const flushed = tagFilter.flush();
165
+ if (flushed.reasoning)
166
+ sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
167
+ if (flushed.content) {
168
+ assistantText += flushed.content;
169
+ sendSSE(makeChunk({ content: flushed.content }));
170
+ }
171
+ const assistantSeedText = [
172
+ assistantText.trim(),
173
+ state.pendingExecs
174
+ .map((exec) => formatToolCallSummary({
175
+ id: exec.toolCallId,
176
+ type: "function",
177
+ function: {
178
+ name: exec.toolName,
179
+ arguments: exec.decodedArgs,
180
+ },
181
+ }))
182
+ .join("\n\n"),
183
+ ]
184
+ .filter(Boolean)
185
+ .join("\n\n");
186
+ activeBridges.set(bridgeKey, {
187
+ bridge,
188
+ heartbeatTimer,
189
+ blobStore,
190
+ cloudRule,
191
+ mcpTools,
192
+ pendingExecs: [...state.pendingExecs],
193
+ modelId,
194
+ metadata: {
195
+ ...metadata,
196
+ assistantSeedText,
197
+ },
198
+ diagnostics: {
199
+ announcedToolCallIds: [],
200
+ publishedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
201
+ lastMcpUpdate: "publish",
202
+ publishedAtMs: Date.now(),
203
+ },
204
+ });
205
+ logPluginInfo("Publishing Cursor tool-call envelope", {
206
+ modelId,
207
+ bridgeKey,
208
+ convKey,
209
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
210
+ emittedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
211
+ assistantSeedTextLength: assistantSeedText.length,
212
+ });
213
+ for (const exec of state.pendingExecs) {
214
+ emitPendingToolStart(exec.toolCallId, exec.toolName, "publish");
215
+ const streamedToolCall = ensureStreamedToolCall(exec.toolCallId, exec.toolName);
216
+ sendSSE(makeChunk({
217
+ tool_calls: [
218
+ {
219
+ index: streamedToolCall.index,
220
+ function: {
221
+ arguments: exec.decodedArgs,
222
+ },
223
+ },
224
+ ],
225
+ }));
226
+ }
227
+ logPluginInfo("Stored active Cursor bridge for tool-result resume", {
228
+ modelId,
229
+ bridgeKey,
230
+ convKey,
231
+ diagnostics: activeBridges.get(bridgeKey)?.diagnostics,
232
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
233
+ });
234
+ sendSSE(makeChunk({}, "tool_calls"));
235
+ sendDone();
236
+ closeController();
237
+ };
238
+ const schedulePendingToolCallPublish = () => {
239
+ if (toolCallsFlushed)
240
+ return;
241
+ stopToolCallFlushTimer();
242
+ logPluginInfo("Scheduling Cursor tool-call publish", {
243
+ modelId,
244
+ bridgeKey,
245
+ convKey,
246
+ delayMs: MCP_TOOL_BATCH_WINDOW_MS,
247
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
248
+ });
249
+ toolCallFlushTimer = setTimeout(publishPendingToolCalls, MCP_TOOL_BATCH_WINDOW_MS);
250
+ };
90
251
  const processChunk = createConnectFrameParser((messageBytes) => {
91
252
  try {
92
253
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
93
- processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
254
+ processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
94
255
  if (isThinking) {
95
256
  sendSSE(makeChunk({ reasoning_content: text }));
96
257
  return;
@@ -103,57 +264,91 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
103
264
  sendSSE(makeChunk({ content }));
104
265
  }
105
266
  }, (exec) => {
106
- state.pendingExecs.push(exec);
267
+ if (toolCallsFlushed) {
268
+ logPluginWarn("Received Cursor MCP exec after tool-call envelope was already published", {
269
+ modelId,
270
+ bridgeKey,
271
+ convKey,
272
+ toolCallId: exec.toolCallId,
273
+ toolName: exec.toolName,
274
+ publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
275
+ [],
276
+ });
277
+ }
278
+ emitPendingToolStart(exec.toolCallId, exec.toolName, "exec");
279
+ const pendingBefore = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
280
+ const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
281
+ if (existingIndex >= 0) {
282
+ state.pendingExecs[existingIndex] = exec;
283
+ }
284
+ else {
285
+ state.pendingExecs.push(exec);
286
+ }
107
287
  mcpExecReceived = true;
108
- const flushed = tagFilter.flush();
109
- if (flushed.reasoning)
110
- sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
111
- if (flushed.content) {
112
- assistantText += flushed.content;
113
- sendSSE(makeChunk({ content: flushed.content }));
288
+ const pendingAfter = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
289
+ const existingActiveBridge = activeBridges.get(bridgeKey);
290
+ if (existingActiveBridge) {
291
+ existingActiveBridge.diagnostics = {
292
+ announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
293
+ publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
294
+ lastMcpUpdate: `exec:${exec.toolCallId}`,
295
+ publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
296
+ lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
297
+ };
114
298
  }
115
- const assistantSeedText = [
116
- assistantText.trim(),
117
- formatToolCallSummary({
118
- id: exec.toolCallId,
119
- type: "function",
120
- function: {
121
- name: exec.toolName,
122
- arguments: exec.decodedArgs,
123
- },
124
- }),
125
- ]
126
- .filter(Boolean)
127
- .join("\n\n");
128
- sendSSE(makeChunk({
129
- tool_calls: [
130
- {
131
- index: state.toolCallIndex++,
132
- id: exec.toolCallId,
133
- type: "function",
134
- function: {
135
- name: exec.toolName,
136
- arguments: exec.decodedArgs,
137
- },
138
- },
139
- ],
140
- }));
141
- activeBridges.set(bridgeKey, {
142
- bridge,
143
- heartbeatTimer,
144
- blobStore,
145
- mcpTools,
146
- pendingExecs: state.pendingExecs,
299
+ logPluginInfo("Tracking Cursor MCP exec in streaming bridge", {
147
300
  modelId,
148
- metadata: {
149
- ...metadata,
150
- assistantSeedText,
151
- },
301
+ bridgeKey,
302
+ convKey,
303
+ toolCallId: exec.toolCallId,
304
+ toolName: exec.toolName,
305
+ pendingBefore,
306
+ pendingAfter,
307
+ hasStoredActiveBridge: Boolean(existingActiveBridge),
308
+ storedActiveBridgePendingExecToolCallIds: existingActiveBridge
309
+ ? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
310
+ : [],
311
+ });
312
+ schedulePendingToolCallPublish();
313
+ }, (info) => {
314
+ if (toolCallsFlushed) {
315
+ logPluginWarn("Received Cursor MCP interaction update after tool-call envelope was already published", {
316
+ modelId,
317
+ bridgeKey,
318
+ convKey,
319
+ updateCase: info.updateCase,
320
+ toolCallId: info.toolCallId,
321
+ publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
322
+ [],
323
+ });
324
+ }
325
+ if (info.updateCase !== "toolCallCompleted") {
326
+ emitPendingToolStart(info.toolCallId, info.toolName ?? "", "interaction");
327
+ }
328
+ const existingActiveBridge = activeBridges.get(bridgeKey);
329
+ if (existingActiveBridge) {
330
+ existingActiveBridge.diagnostics = {
331
+ announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
332
+ publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
333
+ lastMcpUpdate: `${info.updateCase}:${info.toolCallId}`,
334
+ publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
335
+ lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
336
+ };
337
+ }
338
+ logPluginInfo("Tracking Cursor MCP interaction state in streaming bridge", {
339
+ modelId,
340
+ bridgeKey,
341
+ convKey,
342
+ updateCase: info.updateCase,
343
+ toolCallId: info.toolCallId,
344
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
345
+ hasStoredActiveBridge: Boolean(existingActiveBridge),
346
+ storedActiveBridgeDiagnostics: existingActiveBridge?.diagnostics,
152
347
  });
153
- sendSSE(makeChunk({}, "tool_calls"));
154
- sendDone();
155
- closeController();
156
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
348
+ }, (checkpointBytes) => {
349
+ updateConversationCheckpoint(convKey, checkpointBytes);
350
+ bridgeCloseController.noteCheckpoint();
351
+ }, () => bridgeCloseController.noteTurnEnded(), (info) => {
157
352
  endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
158
353
  logPluginError("Closing Cursor bridge after unsupported message", {
159
354
  modelId,
@@ -206,10 +401,29 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
206
401
  stopKeepalive();
207
402
  }
208
403
  }, SSE_KEEPALIVE_INTERVAL_MS);
404
+ logPluginInfo("Opened Cursor streaming bridge", {
405
+ modelId,
406
+ bridgeKey,
407
+ convKey,
408
+ mcpToolCount: mcpTools.length,
409
+ hasCloudRule: Boolean(cloudRule),
410
+ });
209
411
  bridge.onData(processChunk);
210
412
  bridge.onClose((code) => {
413
+ logPluginInfo("Cursor streaming bridge closed", {
414
+ modelId,
415
+ bridgeKey,
416
+ convKey,
417
+ code,
418
+ mcpExecReceived,
419
+ hadEndStreamError: Boolean(endStreamError),
420
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
421
+ storedActiveBridgeDiagnostics: activeBridges.get(bridgeKey)?.diagnostics,
422
+ });
423
+ bridgeCloseController.dispose();
211
424
  clearInterval(heartbeatTimer);
212
425
  stopKeepalive();
426
+ stopToolCallFlushTimer();
213
427
  syncStoredBlobStore(convKey, blobStore);
214
428
  if (endStreamError) {
215
429
  activeBridges.delete(bridgeKey);
@@ -238,7 +452,9 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
238
452
  });
239
453
  },
240
454
  cancel(reason) {
455
+ bridgeCloseController.dispose();
241
456
  stopKeepalive();
457
+ stopToolCallFlushTimer();
242
458
  clearInterval(heartbeatTimer);
243
459
  syncStoredBlobStore(convKey, blobStore);
244
460
  const active = activeBridges.get(bridgeKey);
@@ -257,11 +473,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
257
473
  return new Response(stream, { headers: SSE_HEADERS });
258
474
  }
259
475
  export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
476
+ logPluginInfo("Starting Cursor streaming response", {
477
+ modelId,
478
+ bridgeKey,
479
+ convKey,
480
+ mcpToolCount: payload.mcpTools.length,
481
+ });
260
482
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
261
- return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
483
+ return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
262
484
  }
263
- export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
264
- const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
485
+ async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
486
+ const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
487
+ const deadline = Date.now() + timeoutMs;
488
+ while (Date.now() < deadline) {
489
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
490
+ if (unresolved.length === 0) {
491
+ return unresolved;
492
+ }
493
+ await new Promise((resolve) => setTimeout(resolve, 25));
494
+ }
495
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
496
+ if (unresolved.length > 0) {
497
+ logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
498
+ bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
499
+ modelId: active.modelId,
500
+ });
501
+ }
502
+ return unresolved;
503
+ }
504
+ export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
505
+ const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
265
506
  const resumeMetadata = {
266
507
  ...metadata,
267
508
  assistantSeedText: [
@@ -271,7 +512,73 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
271
512
  .filter(Boolean)
272
513
  .join("\n\n"),
273
514
  };
274
- for (const exec of pendingExecs) {
515
+ const pendingToolCallIds = new Set(pendingExecs.map((exec) => exec.toolCallId));
516
+ const toolResultIds = new Set(toolResults.map((result) => result.toolCallId));
517
+ const missingToolResultIds = pendingExecs
518
+ .map((exec) => exec.toolCallId)
519
+ .filter((toolCallId) => !toolResultIds.has(toolCallId));
520
+ const unexpectedToolResultIds = toolResults
521
+ .map((result) => result.toolCallId)
522
+ .filter((toolCallId) => !pendingToolCallIds.has(toolCallId));
523
+ const matchedPendingExecs = pendingExecs.filter((exec) => toolResultIds.has(exec.toolCallId));
524
+ logPluginInfo("Preparing Cursor tool-result resume", {
525
+ bridgeKey,
526
+ convKey,
527
+ modelId,
528
+ toolResults,
529
+ pendingExecs,
530
+ bridgeAlive: bridge.alive,
531
+ diagnostics: active.diagnostics,
532
+ });
533
+ if (active.diagnostics) {
534
+ active.diagnostics.lastResumeAttemptAtMs = Date.now();
535
+ }
536
+ const unresolved = await waitForResolvablePendingExecs(active, toolResults);
537
+ logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
538
+ bridgeKey,
539
+ convKey,
540
+ modelId,
541
+ toolResults,
542
+ pendingExecs,
543
+ unresolvedPendingExecs: unresolved,
544
+ diagnostics: active.diagnostics,
545
+ });
546
+ if (unresolved.length > 0) {
547
+ clearInterval(heartbeatTimer);
548
+ bridge.end();
549
+ return new Response(JSON.stringify({
550
+ error: {
551
+ message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
552
+ type: "invalid_request_error",
553
+ code: "cursor_missing_exec_metadata",
554
+ },
555
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
556
+ }
557
+ if (missingToolResultIds.length > 0 || unexpectedToolResultIds.length > 0) {
558
+ logPluginError("Aborting Cursor tool-result resume because tool-call ids did not match", {
559
+ bridgeKey,
560
+ convKey,
561
+ modelId,
562
+ pendingToolCallIds: [...pendingToolCallIds],
563
+ toolResultIds: [...toolResultIds],
564
+ missingToolResultIds,
565
+ unexpectedToolResultIds,
566
+ });
567
+ clearInterval(heartbeatTimer);
568
+ bridge.end();
569
+ return new Response(JSON.stringify({
570
+ error: {
571
+ message: "Tool-result ids did not match the active Cursor MCP tool calls. Aborting instead of resuming a potentially stuck session.",
572
+ type: "invalid_request_error",
573
+ code: "cursor_tool_result_mismatch",
574
+ details: {
575
+ missingToolResultIds,
576
+ unexpectedToolResultIds,
577
+ },
578
+ },
579
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
580
+ }
581
+ for (const exec of matchedPendingExecs) {
275
582
  const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
276
583
  const mcpResult = result
277
584
  ? create(McpResultSchema, {
@@ -311,7 +618,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
311
618
  const clientMessage = create(AgentClientMessageSchema, {
312
619
  message: { case: "execClientMessage", value: execClientMessage },
313
620
  });
621
+ logPluginInfo("Sending Cursor tool-result resume message", {
622
+ bridgeKey,
623
+ convKey,
624
+ modelId,
625
+ toolCallId: exec.toolCallId,
626
+ toolName: exec.toolName,
627
+ source: exec.source,
628
+ execId: exec.execId,
629
+ execMsgId: exec.execMsgId,
630
+ cursorCallId: exec.cursorCallId,
631
+ modelCallId: exec.modelCallId,
632
+ matchedToolResult: result,
633
+ });
314
634
  bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
315
635
  }
316
- return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
636
+ return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
317
637
  }