@playwo/opencode-cursor-oauth 0.0.0-dev.2c48be2f48c9 → 0.0.0-dev.36e1216df6cf

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.
@@ -1,17 +1,22 @@
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
+ function sortedIds(values) {
13
+ return [...values].sort();
14
+ }
15
+ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
12
16
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
13
17
  const created = Math.floor(Date.now() / 1000);
14
18
  let keepaliveTimer;
19
+ const bridgeCloseController = createBridgeCloseController(bridge);
15
20
  const stopKeepalive = () => {
16
21
  if (!keepaliveTimer)
17
22
  return;
@@ -32,6 +37,8 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
32
37
  let assistantText = metadata.assistantSeedText ?? "";
33
38
  let mcpExecReceived = false;
34
39
  let endStreamError = null;
40
+ let toolCallsFlushed = false;
41
+ const streamedToolCalls = new Map();
35
42
  const sendSSE = (data) => {
36
43
  if (closed)
37
44
  return;
@@ -47,6 +54,19 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
47
54
  return;
48
55
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
49
56
  };
57
+ const failStream = (message, code) => {
58
+ if (closed)
59
+ return;
60
+ sendSSE({
61
+ error: {
62
+ message,
63
+ type: "server_error",
64
+ ...(code ? { code } : {}),
65
+ },
66
+ });
67
+ sendDone();
68
+ closeController();
69
+ };
50
70
  const closeController = () => {
51
71
  if (closed)
52
72
  return;
@@ -72,10 +92,145 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
72
92
  usage: { prompt_tokens, completion_tokens, total_tokens },
73
93
  };
74
94
  };
95
+ const ensureStreamedToolCall = (toolCallId, toolName) => {
96
+ const existing = streamedToolCalls.get(toolCallId);
97
+ if (existing) {
98
+ if (toolName && existing.toolName !== toolName) {
99
+ existing.toolName = toolName;
100
+ }
101
+ return existing;
102
+ }
103
+ const createdState = {
104
+ index: state.toolCallIndex++,
105
+ toolName,
106
+ started: false,
107
+ };
108
+ streamedToolCalls.set(toolCallId, createdState);
109
+ return createdState;
110
+ };
111
+ const emitPendingToolStart = (toolCallId, toolName, source) => {
112
+ if (!toolName)
113
+ return;
114
+ const toolCall = ensureStreamedToolCall(toolCallId, toolName);
115
+ if (toolCall.started)
116
+ return;
117
+ toolCall.started = true;
118
+ logPluginInfo("Streaming pending Cursor tool-call start", {
119
+ modelId,
120
+ bridgeKey,
121
+ convKey,
122
+ toolCallId,
123
+ toolName,
124
+ index: toolCall.index,
125
+ source,
126
+ });
127
+ sendSSE(makeChunk({
128
+ tool_calls: [
129
+ {
130
+ index: toolCall.index,
131
+ id: toolCallId,
132
+ type: "function",
133
+ function: {
134
+ name: toolCall.toolName,
135
+ arguments: "",
136
+ },
137
+ },
138
+ ],
139
+ }));
140
+ };
141
+ const publishPendingToolCalls = (source) => {
142
+ if (closed || toolCallsFlushed || state.pendingExecs.length === 0)
143
+ return;
144
+ logPluginInfo("Evaluating Cursor tool-call publish", {
145
+ modelId,
146
+ bridgeKey,
147
+ convKey,
148
+ source,
149
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
150
+ toolCallsFlushed,
151
+ mcpExecReceived,
152
+ nowMs: Date.now(),
153
+ });
154
+ toolCallsFlushed = true;
155
+ const flushed = tagFilter.flush();
156
+ if (flushed.reasoning)
157
+ sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
158
+ if (flushed.content) {
159
+ assistantText += flushed.content;
160
+ sendSSE(makeChunk({ content: flushed.content }));
161
+ }
162
+ const assistantSeedText = [
163
+ assistantText.trim(),
164
+ state.pendingExecs
165
+ .map((exec) => formatToolCallSummary({
166
+ id: exec.toolCallId,
167
+ type: "function",
168
+ function: {
169
+ name: exec.toolName,
170
+ arguments: exec.decodedArgs,
171
+ },
172
+ }))
173
+ .join("\n\n"),
174
+ ]
175
+ .filter(Boolean)
176
+ .join("\n\n");
177
+ activeBridges.set(bridgeKey, {
178
+ bridge,
179
+ heartbeatTimer,
180
+ blobStore,
181
+ cloudRule,
182
+ mcpTools,
183
+ pendingExecs: [...state.pendingExecs],
184
+ modelId,
185
+ metadata: {
186
+ ...metadata,
187
+ assistantSeedText,
188
+ },
189
+ diagnostics: {
190
+ announcedToolCallIds: [],
191
+ publishedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
192
+ lastMcpUpdate: "publish",
193
+ publishedAtMs: Date.now(),
194
+ },
195
+ });
196
+ logPluginInfo("Publishing Cursor tool-call envelope", {
197
+ modelId,
198
+ bridgeKey,
199
+ convKey,
200
+ source,
201
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
202
+ emittedToolCallIds: state.pendingExecs.map((exec) => exec.toolCallId),
203
+ assistantSeedTextLength: assistantSeedText.length,
204
+ });
205
+ for (const exec of state.pendingExecs) {
206
+ emitPendingToolStart(exec.toolCallId, exec.toolName, "publish");
207
+ const streamedToolCall = ensureStreamedToolCall(exec.toolCallId, exec.toolName);
208
+ sendSSE(makeChunk({
209
+ tool_calls: [
210
+ {
211
+ index: streamedToolCall.index,
212
+ function: {
213
+ arguments: exec.decodedArgs,
214
+ },
215
+ },
216
+ ],
217
+ }));
218
+ }
219
+ logPluginInfo("Stored active Cursor bridge for tool-result resume", {
220
+ modelId,
221
+ bridgeKey,
222
+ convKey,
223
+ diagnostics: activeBridges.get(bridgeKey)?.diagnostics,
224
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
225
+ });
226
+ sendSSE(makeChunk({}, "tool_calls"));
227
+ sendDone();
228
+ closeController();
229
+ };
75
230
  const processChunk = createConnectFrameParser((messageBytes) => {
76
231
  try {
77
232
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
78
- processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
233
+ processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
79
234
  if (isThinking) {
80
235
  sendSSE(makeChunk({ reasoning_content: text }));
81
236
  return;
@@ -88,57 +243,143 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
88
243
  sendSSE(makeChunk({ content }));
89
244
  }
90
245
  }, (exec) => {
91
- state.pendingExecs.push(exec);
246
+ if (toolCallsFlushed) {
247
+ logPluginWarn("Received Cursor MCP exec after tool-call envelope was already published", {
248
+ modelId,
249
+ bridgeKey,
250
+ convKey,
251
+ toolCallId: exec.toolCallId,
252
+ toolName: exec.toolName,
253
+ publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
254
+ [],
255
+ });
256
+ }
257
+ emitPendingToolStart(exec.toolCallId, exec.toolName, "exec");
258
+ const pendingBefore = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
259
+ const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
260
+ if (existingIndex >= 0) {
261
+ state.pendingExecs[existingIndex] = exec;
262
+ }
263
+ else {
264
+ state.pendingExecs.push(exec);
265
+ }
92
266
  mcpExecReceived = true;
93
- const flushed = tagFilter.flush();
94
- if (flushed.reasoning)
95
- sendSSE(makeChunk({ reasoning_content: flushed.reasoning }));
96
- if (flushed.content) {
97
- assistantText += flushed.content;
98
- sendSSE(makeChunk({ content: flushed.content }));
267
+ const pendingAfter = sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId));
268
+ const existingActiveBridge = activeBridges.get(bridgeKey);
269
+ if (existingActiveBridge) {
270
+ existingActiveBridge.diagnostics = {
271
+ announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
272
+ publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
273
+ lastMcpUpdate: `exec:${exec.toolCallId}`,
274
+ publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
275
+ lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
276
+ };
99
277
  }
100
- const assistantSeedText = [
101
- assistantText.trim(),
102
- formatToolCallSummary({
103
- id: exec.toolCallId,
104
- type: "function",
105
- function: {
106
- name: exec.toolName,
107
- arguments: exec.decodedArgs,
108
- },
109
- }),
110
- ]
111
- .filter(Boolean)
112
- .join("\n\n");
113
- sendSSE(makeChunk({
114
- tool_calls: [
115
- {
116
- index: state.toolCallIndex++,
117
- id: exec.toolCallId,
118
- type: "function",
119
- function: {
120
- name: exec.toolName,
121
- arguments: exec.decodedArgs,
122
- },
123
- },
124
- ],
125
- }));
126
- activeBridges.set(bridgeKey, {
127
- bridge,
128
- heartbeatTimer,
129
- blobStore,
130
- mcpTools,
131
- pendingExecs: state.pendingExecs,
278
+ logPluginInfo("Tracking Cursor MCP exec in streaming bridge", {
132
279
  modelId,
133
- metadata: {
134
- ...metadata,
135
- assistantSeedText,
136
- },
280
+ bridgeKey,
281
+ convKey,
282
+ toolCallId: exec.toolCallId,
283
+ toolName: exec.toolName,
284
+ pendingBefore,
285
+ pendingAfter,
286
+ hasStoredActiveBridge: Boolean(existingActiveBridge),
287
+ storedActiveBridgePendingExecToolCallIds: existingActiveBridge
288
+ ? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
289
+ : [],
290
+ });
291
+ }, (info) => {
292
+ if (toolCallsFlushed) {
293
+ logPluginWarn("Received Cursor MCP interaction update after tool-call envelope was already published", {
294
+ modelId,
295
+ bridgeKey,
296
+ convKey,
297
+ updateCase: info.updateCase,
298
+ toolCallId: info.toolCallId,
299
+ publishedToolCallIds: activeBridges.get(bridgeKey)?.diagnostics?.publishedToolCallIds ??
300
+ [],
301
+ });
302
+ }
303
+ if (info.updateCase !== "toolCallCompleted") {
304
+ emitPendingToolStart(info.toolCallId, info.toolName ?? "", "interaction");
305
+ }
306
+ const existingActiveBridge = activeBridges.get(bridgeKey);
307
+ if (existingActiveBridge) {
308
+ existingActiveBridge.diagnostics = {
309
+ announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
310
+ publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
311
+ lastMcpUpdate: `${info.updateCase}:${info.toolCallId}`,
312
+ publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
313
+ lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
314
+ };
315
+ }
316
+ }, (info) => {
317
+ const existingActiveBridge = activeBridges.get(bridgeKey);
318
+ if (existingActiveBridge) {
319
+ existingActiveBridge.diagnostics = {
320
+ announcedToolCallIds: existingActiveBridge.diagnostics?.announcedToolCallIds ?? [],
321
+ publishedToolCallIds: existingActiveBridge.diagnostics?.publishedToolCallIds ?? [],
322
+ lastMcpUpdate: info.updateCase === "stepCompleted"
323
+ ? `${info.updateCase}:${info.stepId}:${info.stepDurationMs ?? "unknown"}`
324
+ : `${info.updateCase}:${info.stepId}`,
325
+ publishedAtMs: existingActiveBridge.diagnostics?.publishedAtMs,
326
+ lastResumeAttemptAtMs: existingActiveBridge.diagnostics?.lastResumeAttemptAtMs,
327
+ };
328
+ }
329
+ logPluginInfo("Tracking Cursor step boundary in streaming bridge", {
330
+ modelId,
331
+ bridgeKey,
332
+ convKey,
333
+ updateCase: info.updateCase,
334
+ stepId: info.stepId,
335
+ stepDurationMs: info.stepDurationMs,
336
+ toolCallsFlushed,
337
+ mcpExecReceived,
338
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
339
+ streamedToolCallIds: sortedIds(streamedToolCalls.keys()),
340
+ hasStoredActiveBridge: Boolean(existingActiveBridge),
341
+ storedActiveBridgePendingExecToolCallIds: existingActiveBridge
342
+ ? sortedIds(existingActiveBridge.pendingExecs.map((candidate) => candidate.toolCallId))
343
+ : [],
344
+ storedActiveBridgeDiagnostics: existingActiveBridge?.diagnostics,
137
345
  });
138
- sendSSE(makeChunk({}, "tool_calls"));
139
- sendDone();
140
- closeController();
141
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), (info) => {
346
+ }, (checkpointBytes) => {
347
+ logPluginInfo("Received Cursor conversation checkpoint", {
348
+ modelId,
349
+ bridgeKey,
350
+ convKey,
351
+ toolCallsFlushed,
352
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
353
+ });
354
+ updateConversationCheckpoint(convKey, checkpointBytes);
355
+ bridgeCloseController.noteCheckpoint();
356
+ if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
357
+ publishPendingToolCalls("checkpoint");
358
+ }
359
+ }, () => {
360
+ logPluginInfo("Received Cursor turn-ended signal", {
361
+ modelId,
362
+ bridgeKey,
363
+ convKey,
364
+ toolCallsFlushed,
365
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((candidate) => candidate.toolCallId)),
366
+ });
367
+ bridgeCloseController.noteTurnEnded();
368
+ if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
369
+ publishPendingToolCalls("turnEnded");
370
+ }
371
+ }, (info) => {
372
+ endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
373
+ logPluginError("Closing Cursor bridge after unsupported message", {
374
+ modelId,
375
+ bridgeKey,
376
+ convKey,
377
+ category: info.category,
378
+ caseName: info.caseName,
379
+ detail: info.detail,
380
+ });
381
+ scheduleBridgeEnd(bridge);
382
+ }, (info) => {
142
383
  endStreamError = new Error(`Cursor requested unsupported exec type: ${info.execCase}`);
143
384
  logPluginError("Closing Cursor bridge after unsupported exec", {
144
385
  modelId,
@@ -155,6 +396,15 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
155
396
  // Skip unparseable messages.
156
397
  }
157
398
  }, (endStreamBytes) => {
399
+ logPluginInfo("Received Cursor end-of-stream signal", {
400
+ modelId,
401
+ bridgeKey,
402
+ convKey,
403
+ byteLength: endStreamBytes.length,
404
+ });
405
+ if (state.pendingExecs.length > 0 && !toolCallsFlushed) {
406
+ publishPendingToolCalls("endStream");
407
+ }
158
408
  endStreamError = parseConnectEndStream(endStreamBytes);
159
409
  if (endStreamError) {
160
410
  logPluginError("Cursor stream returned Connect end-stream error", {
@@ -180,17 +430,32 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
180
430
  stopKeepalive();
181
431
  }
182
432
  }, SSE_KEEPALIVE_INTERVAL_MS);
433
+ logPluginInfo("Opened Cursor streaming bridge", {
434
+ modelId,
435
+ bridgeKey,
436
+ convKey,
437
+ mcpToolCount: mcpTools.length,
438
+ hasCloudRule: Boolean(cloudRule),
439
+ });
183
440
  bridge.onData(processChunk);
184
441
  bridge.onClose((code) => {
442
+ logPluginInfo("Cursor streaming bridge closed", {
443
+ modelId,
444
+ bridgeKey,
445
+ convKey,
446
+ code,
447
+ mcpExecReceived,
448
+ hadEndStreamError: Boolean(endStreamError),
449
+ pendingExecToolCallIds: sortedIds(state.pendingExecs.map((exec) => exec.toolCallId)),
450
+ storedActiveBridgeDiagnostics: activeBridges.get(bridgeKey)?.diagnostics,
451
+ });
452
+ bridgeCloseController.dispose();
185
453
  clearInterval(heartbeatTimer);
186
454
  stopKeepalive();
187
455
  syncStoredBlobStore(convKey, blobStore);
188
456
  if (endStreamError) {
189
457
  activeBridges.delete(bridgeKey);
190
- if (!closed) {
191
- closed = true;
192
- controller.error(endStreamError);
193
- }
458
+ failStream(endStreamError.message, "cursor_bridge_closed");
194
459
  return;
195
460
  }
196
461
  if (!mcpExecReceived) {
@@ -210,15 +475,12 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
210
475
  }
211
476
  activeBridges.delete(bridgeKey);
212
477
  if (code !== 0 && !closed) {
213
- sendSSE(makeChunk({ content: "\n[Error: bridge connection lost]" }));
214
- sendSSE(makeChunk({}, "stop"));
215
- sendSSE(makeUsageChunk());
216
- sendDone();
217
- closeController();
478
+ failStream("Cursor bridge connection lost", "cursor_bridge_closed");
218
479
  }
219
480
  });
220
481
  },
221
482
  cancel(reason) {
483
+ bridgeCloseController.dispose();
222
484
  stopKeepalive();
223
485
  clearInterval(heartbeatTimer);
224
486
  syncStoredBlobStore(convKey, blobStore);
@@ -238,11 +500,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
238
500
  return new Response(stream, { headers: SSE_HEADERS });
239
501
  }
240
502
  export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
503
+ logPluginInfo("Starting Cursor streaming response", {
504
+ modelId,
505
+ bridgeKey,
506
+ convKey,
507
+ mcpToolCount: payload.mcpTools.length,
508
+ });
241
509
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
242
- return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
510
+ return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
243
511
  }
244
- export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
245
- const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
512
+ async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
513
+ const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
514
+ const deadline = Date.now() + timeoutMs;
515
+ while (Date.now() < deadline) {
516
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
517
+ if (unresolved.length === 0) {
518
+ return unresolved;
519
+ }
520
+ await new Promise((resolve) => setTimeout(resolve, 25));
521
+ }
522
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
523
+ if (unresolved.length > 0) {
524
+ logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
525
+ bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
526
+ modelId: active.modelId,
527
+ });
528
+ }
529
+ return unresolved;
530
+ }
531
+ export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
532
+ const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
246
533
  const resumeMetadata = {
247
534
  ...metadata,
248
535
  assistantSeedText: [
@@ -252,7 +539,73 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
252
539
  .filter(Boolean)
253
540
  .join("\n\n"),
254
541
  };
255
- for (const exec of pendingExecs) {
542
+ const pendingToolCallIds = new Set(pendingExecs.map((exec) => exec.toolCallId));
543
+ const toolResultIds = new Set(toolResults.map((result) => result.toolCallId));
544
+ const missingToolResultIds = pendingExecs
545
+ .map((exec) => exec.toolCallId)
546
+ .filter((toolCallId) => !toolResultIds.has(toolCallId));
547
+ const unexpectedToolResultIds = toolResults
548
+ .map((result) => result.toolCallId)
549
+ .filter((toolCallId) => !pendingToolCallIds.has(toolCallId));
550
+ const matchedPendingExecs = pendingExecs.filter((exec) => toolResultIds.has(exec.toolCallId));
551
+ logPluginInfo("Preparing Cursor tool-result resume", {
552
+ bridgeKey,
553
+ convKey,
554
+ modelId,
555
+ toolResults,
556
+ pendingExecs,
557
+ bridgeAlive: bridge.alive,
558
+ diagnostics: active.diagnostics,
559
+ });
560
+ if (active.diagnostics) {
561
+ active.diagnostics.lastResumeAttemptAtMs = Date.now();
562
+ }
563
+ const unresolved = await waitForResolvablePendingExecs(active, toolResults);
564
+ logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
565
+ bridgeKey,
566
+ convKey,
567
+ modelId,
568
+ toolResults,
569
+ pendingExecs,
570
+ unresolvedPendingExecs: unresolved,
571
+ diagnostics: active.diagnostics,
572
+ });
573
+ if (unresolved.length > 0) {
574
+ clearInterval(heartbeatTimer);
575
+ bridge.end();
576
+ return new Response(JSON.stringify({
577
+ error: {
578
+ message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
579
+ type: "invalid_request_error",
580
+ code: "cursor_missing_exec_metadata",
581
+ },
582
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
583
+ }
584
+ if (missingToolResultIds.length > 0 || unexpectedToolResultIds.length > 0) {
585
+ logPluginError("Aborting Cursor tool-result resume because tool-call ids did not match", {
586
+ bridgeKey,
587
+ convKey,
588
+ modelId,
589
+ pendingToolCallIds: [...pendingToolCallIds],
590
+ toolResultIds: [...toolResultIds],
591
+ missingToolResultIds,
592
+ unexpectedToolResultIds,
593
+ });
594
+ clearInterval(heartbeatTimer);
595
+ bridge.end();
596
+ return new Response(JSON.stringify({
597
+ error: {
598
+ message: "Tool-result ids did not match the active Cursor MCP tool calls. Aborting instead of resuming a potentially stuck session.",
599
+ type: "invalid_request_error",
600
+ code: "cursor_tool_result_mismatch",
601
+ details: {
602
+ missingToolResultIds,
603
+ unexpectedToolResultIds,
604
+ },
605
+ },
606
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
607
+ }
608
+ for (const exec of matchedPendingExecs) {
256
609
  const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
257
610
  const mcpResult = result
258
611
  ? create(McpResultSchema, {
@@ -292,7 +645,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
292
645
  const clientMessage = create(AgentClientMessageSchema, {
293
646
  message: { case: "execClientMessage", value: execClientMessage },
294
647
  });
648
+ logPluginInfo("Sending Cursor tool-result resume message", {
649
+ bridgeKey,
650
+ convKey,
651
+ modelId,
652
+ toolCallId: exec.toolCallId,
653
+ toolName: exec.toolName,
654
+ source: exec.source,
655
+ execId: exec.execId,
656
+ execMsgId: exec.execMsgId,
657
+ cursorCallId: exec.cursorCallId,
658
+ modelCallId: exec.modelCallId,
659
+ matchedToolResult: result,
660
+ });
295
661
  bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
296
662
  }
297
- return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
663
+ return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
298
664
  }
@@ -1,4 +1,4 @@
1
- import { logPluginWarn } from "../logger";
1
+ import { logPluginInfo, logPluginWarn } from "../logger";
2
2
  import { buildInitialHandoffPrompt, buildTitleSourceText, buildToolResumePrompt, detectTitleRequest, parseMessages, } from "../openai/messages";
3
3
  import { buildMcpToolDefinitions, selectToolsForChoice } from "../openai/tools";
4
4
  import { activeBridges, conversationStates, createStoredConversation, deriveBridgeKey, deriveConversationKey, evictStaleConversations, hashString, normalizeAgentKey, resetStoredConversation, } from "./conversation-state";
@@ -10,6 +10,19 @@ export function handleChatCompletion(body, accessToken, context = {}) {
10
10
  const { systemPrompt, userText, turns, toolResults, pendingAssistantSummary, completedTurnsFingerprint, } = parsed;
11
11
  const modelId = body.model;
12
12
  const normalizedAgentKey = normalizeAgentKey(context.agentKey);
13
+ logPluginInfo("Handling Cursor chat completion request", {
14
+ modelId,
15
+ stream: body.stream !== false,
16
+ messageCount: body.messages.length,
17
+ toolCount: body.tools?.length ?? 0,
18
+ toolChoice: body.tool_choice,
19
+ sessionId: context.sessionId,
20
+ agentKey: normalizedAgentKey,
21
+ parsedUserText: userText,
22
+ parsedToolResults: toolResults,
23
+ hasPendingAssistantSummary: pendingAssistantSummary.trim().length > 0,
24
+ turnCount: turns.length,
25
+ });
13
26
  const titleDetection = detectTitleRequest(body);
14
27
  const isTitleAgent = titleDetection.matched;
15
28
  if (isTitleAgent) {
@@ -38,7 +51,24 @@ export function handleChatCompletion(body, accessToken, context = {}) {
38
51
  const bridgeKey = deriveBridgeKey(modelId, body.messages, context.sessionId, context.agentKey);
39
52
  const convKey = deriveConversationKey(body.messages, context.sessionId, context.agentKey);
40
53
  const activeBridge = activeBridges.get(bridgeKey);
54
+ logPluginInfo("Resolved Cursor conversation keys", {
55
+ modelId,
56
+ bridgeKey,
57
+ convKey,
58
+ hasActiveBridge: Boolean(activeBridge),
59
+ sessionId: context.sessionId,
60
+ agentKey: normalizedAgentKey,
61
+ });
41
62
  if (activeBridge && toolResults.length > 0) {
63
+ logPluginInfo("Matched OpenAI tool results to active Cursor bridge", {
64
+ bridgeKey,
65
+ convKey,
66
+ requestedModelId: modelId,
67
+ activeBridgeModelId: activeBridge.modelId,
68
+ toolResults,
69
+ pendingExecs: activeBridge.pendingExecs,
70
+ diagnostics: activeBridge.diagnostics,
71
+ });
42
72
  activeBridges.delete(bridgeKey);
43
73
  if (activeBridge.bridge.alive) {
44
74
  if (activeBridge.modelId !== modelId) {
@@ -82,16 +112,27 @@ export function handleChatCompletion(body, accessToken, context = {}) {
82
112
  // Build the request. When tool results are present but the bridge died,
83
113
  // we must still include the last user text so Cursor has context.
84
114
  const mcpTools = buildMcpToolDefinitions(tools);
115
+ const hasPendingAssistantSummary = pendingAssistantSummary.trim().length > 0;
85
116
  const needsInitialHandoff = !stored.checkpoint &&
86
- (turns.length > 0 || pendingAssistantSummary || toolResults.length > 0);
117
+ (turns.length > 0 || hasPendingAssistantSummary || toolResults.length > 0);
87
118
  const replayTurns = needsInitialHandoff ? [] : turns;
88
119
  let effectiveUserText = needsInitialHandoff
89
120
  ? buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults)
90
- : toolResults.length > 0
121
+ : toolResults.length > 0 || hasPendingAssistantSummary
91
122
  ? buildToolResumePrompt(userText, pendingAssistantSummary, toolResults)
92
123
  : userText;
93
124
  const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
94
125
  payload.mcpTools = mcpTools;
126
+ logPluginInfo("Built Cursor run request payload", {
127
+ modelId,
128
+ bridgeKey,
129
+ convKey,
130
+ mcpToolCount: mcpTools.length,
131
+ conversationId: stored.conversationId,
132
+ hasCheckpoint: Boolean(stored.checkpoint),
133
+ replayTurnCount: replayTurns.length,
134
+ effectiveUserText,
135
+ });
95
136
  if (body.stream === false) {
96
137
  return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
97
138
  systemPrompt,