@playwo/opencode-cursor-oauth 0.0.0-dev.4258a6733133 → 0.0.0-dev.494d4e1cfa84

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,19 @@
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 createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, metadata) {
12
13
  const completionId = `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 28)}`;
13
14
  const created = Math.floor(Date.now() / 1000);
14
15
  let keepaliveTimer;
16
+ const bridgeCloseController = createBridgeCloseController(bridge);
15
17
  const stopKeepalive = () => {
16
18
  if (!keepaliveTimer)
17
19
  return;
@@ -27,8 +29,6 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
27
29
  pendingExecs: [],
28
30
  outputTokens: 0,
29
31
  totalTokens: 0,
30
- interactionToolArgsText: new Map(),
31
- emittedToolCallIds: new Set(),
32
32
  };
33
33
  const tagFilter = createThinkingTagFilter();
34
34
  let assistantText = metadata.assistantSeedText ?? "";
@@ -90,7 +90,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
90
90
  const processChunk = createConnectFrameParser((messageBytes) => {
91
91
  try {
92
92
  const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
93
- processServerMessage(serverMessage, blobStore, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
93
+ processServerMessage(serverMessage, blobStore, cloudRule, mcpTools, (data) => bridge.write(data), state, (text, isThinking) => {
94
94
  if (isThinking) {
95
95
  sendSSE(makeChunk({ reasoning_content: text }));
96
96
  return;
@@ -103,7 +103,13 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
103
103
  sendSSE(makeChunk({ content }));
104
104
  }
105
105
  }, (exec) => {
106
- state.pendingExecs.push(exec);
106
+ const existingIndex = state.pendingExecs.findIndex((candidate) => candidate.toolCallId === exec.toolCallId);
107
+ if (existingIndex >= 0) {
108
+ state.pendingExecs[existingIndex] = exec;
109
+ }
110
+ else {
111
+ state.pendingExecs.push(exec);
112
+ }
107
113
  mcpExecReceived = true;
108
114
  const flushed = tagFilter.flush();
109
115
  if (flushed.reasoning)
@@ -142,6 +148,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
142
148
  bridge,
143
149
  heartbeatTimer,
144
150
  blobStore,
151
+ cloudRule,
145
152
  mcpTools,
146
153
  pendingExecs: state.pendingExecs,
147
154
  modelId,
@@ -153,7 +160,10 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
153
160
  sendSSE(makeChunk({}, "tool_calls"));
154
161
  sendDone();
155
162
  closeController();
156
- }, (checkpointBytes) => updateConversationCheckpoint(convKey, checkpointBytes), () => scheduleBridgeEnd(bridge), (info) => {
163
+ }, (checkpointBytes) => {
164
+ updateConversationCheckpoint(convKey, checkpointBytes);
165
+ bridgeCloseController.noteCheckpoint();
166
+ }, () => bridgeCloseController.noteTurnEnded(), (info) => {
157
167
  endStreamError = new Error(`Cursor returned unsupported ${info.category}: ${info.caseName}${info.detail ? ` (${info.detail})` : ""}`);
158
168
  logPluginError("Closing Cursor bridge after unsupported message", {
159
169
  modelId,
@@ -206,8 +216,24 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
206
216
  stopKeepalive();
207
217
  }
208
218
  }, SSE_KEEPALIVE_INTERVAL_MS);
219
+ logPluginInfo("Opened Cursor streaming bridge", {
220
+ modelId,
221
+ bridgeKey,
222
+ convKey,
223
+ mcpToolCount: mcpTools.length,
224
+ hasCloudRule: Boolean(cloudRule),
225
+ });
209
226
  bridge.onData(processChunk);
210
227
  bridge.onClose((code) => {
228
+ logPluginInfo("Cursor streaming bridge closed", {
229
+ modelId,
230
+ bridgeKey,
231
+ convKey,
232
+ code,
233
+ mcpExecReceived,
234
+ hadEndStreamError: Boolean(endStreamError),
235
+ });
236
+ bridgeCloseController.dispose();
211
237
  clearInterval(heartbeatTimer);
212
238
  stopKeepalive();
213
239
  syncStoredBlobStore(convKey, blobStore);
@@ -238,6 +264,7 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
238
264
  });
239
265
  },
240
266
  cancel(reason) {
267
+ bridgeCloseController.dispose();
241
268
  stopKeepalive();
242
269
  clearInterval(heartbeatTimer);
243
270
  syncStoredBlobStore(convKey, blobStore);
@@ -257,11 +284,36 @@ function createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools,
257
284
  return new Response(stream, { headers: SSE_HEADERS });
258
285
  }
259
286
  export async function handleStreamingResponse(payload, accessToken, modelId, bridgeKey, convKey, metadata) {
287
+ logPluginInfo("Starting Cursor streaming response", {
288
+ modelId,
289
+ bridgeKey,
290
+ convKey,
291
+ mcpToolCount: payload.mcpTools.length,
292
+ });
260
293
  const { bridge, heartbeatTimer } = await startBridge(accessToken, payload.requestBytes);
261
- return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
294
+ return createBridgeStreamResponse(bridge, heartbeatTimer, payload.blobStore, payload.cloudRule, payload.mcpTools, modelId, bridgeKey, convKey, metadata);
295
+ }
296
+ async function waitForResolvablePendingExecs(active, toolResults, timeoutMs = 2_000) {
297
+ const pendingToolCallIds = new Set(toolResults.map((result) => result.toolCallId));
298
+ const deadline = Date.now() + timeoutMs;
299
+ while (Date.now() < deadline) {
300
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
301
+ if (unresolved.length === 0) {
302
+ return unresolved;
303
+ }
304
+ await new Promise((resolve) => setTimeout(resolve, 25));
305
+ }
306
+ const unresolved = active.pendingExecs.filter((exec) => pendingToolCallIds.has(exec.toolCallId) && exec.execMsgId === 0);
307
+ if (unresolved.length > 0) {
308
+ logPluginWarn("Cursor exec metadata did not arrive before tool-result resume", {
309
+ bridgeToolCallIds: unresolved.map((exec) => exec.toolCallId),
310
+ modelId: active.modelId,
311
+ });
312
+ }
313
+ return unresolved;
262
314
  }
263
- export function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
264
- const { bridge, heartbeatTimer, blobStore, mcpTools, pendingExecs, modelId, metadata, } = active;
315
+ export async function handleToolResultResume(active, toolResults, bridgeKey, convKey) {
316
+ const { bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, pendingExecs, modelId, metadata, } = active;
265
317
  const resumeMetadata = {
266
318
  ...metadata,
267
319
  assistantSeedText: [
@@ -271,6 +323,33 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
271
323
  .filter(Boolean)
272
324
  .join("\n\n"),
273
325
  };
326
+ logPluginInfo("Preparing Cursor tool-result resume", {
327
+ bridgeKey,
328
+ convKey,
329
+ modelId,
330
+ toolResults,
331
+ pendingExecs,
332
+ });
333
+ const unresolved = await waitForResolvablePendingExecs(active, toolResults);
334
+ logPluginInfo("Resolved pending exec state before Cursor tool-result resume", {
335
+ bridgeKey,
336
+ convKey,
337
+ modelId,
338
+ toolResults,
339
+ pendingExecs,
340
+ unresolvedPendingExecs: unresolved,
341
+ });
342
+ if (unresolved.length > 0) {
343
+ clearInterval(heartbeatTimer);
344
+ bridge.end();
345
+ return new Response(JSON.stringify({
346
+ error: {
347
+ message: "Cursor requested a tool call but never provided resumable exec metadata. Aborting instead of retrying with synthetic ids.",
348
+ type: "invalid_request_error",
349
+ code: "cursor_missing_exec_metadata",
350
+ },
351
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
352
+ }
274
353
  for (const exec of pendingExecs) {
275
354
  const result = toolResults.find((toolResult) => toolResult.toolCallId === exec.toolCallId);
276
355
  const mcpResult = result
@@ -311,7 +390,20 @@ export function handleToolResultResume(active, toolResults, bridgeKey, convKey)
311
390
  const clientMessage = create(AgentClientMessageSchema, {
312
391
  message: { case: "execClientMessage", value: execClientMessage },
313
392
  });
393
+ logPluginInfo("Sending Cursor tool-result resume message", {
394
+ bridgeKey,
395
+ convKey,
396
+ modelId,
397
+ toolCallId: exec.toolCallId,
398
+ toolName: exec.toolName,
399
+ source: exec.source,
400
+ execId: exec.execId,
401
+ execMsgId: exec.execMsgId,
402
+ cursorCallId: exec.cursorCallId,
403
+ modelCallId: exec.modelCallId,
404
+ matchedToolResult: result,
405
+ });
314
406
  bridge.write(toBinary(AgentClientMessageSchema, clientMessage));
315
407
  }
316
- return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
408
+ return createBridgeStreamResponse(bridge, heartbeatTimer, blobStore, cloudRule, mcpTools, modelId, bridgeKey, convKey, resumeMetadata);
317
409
  }
@@ -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,23 @@ 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
+ });
42
71
  activeBridges.delete(bridgeKey);
43
72
  if (activeBridge.bridge.alive) {
44
73
  if (activeBridge.modelId !== modelId) {
@@ -93,6 +122,16 @@ export function handleChatCompletion(body, accessToken, context = {}) {
93
122
  : userText;
94
123
  const payload = buildCursorRequest(modelId, systemPrompt, effectiveUserText, replayTurns, stored.conversationId, stored.checkpoint, stored.blobStore);
95
124
  payload.mcpTools = mcpTools;
125
+ logPluginInfo("Built Cursor run request payload", {
126
+ modelId,
127
+ bridgeKey,
128
+ convKey,
129
+ mcpToolCount: mcpTools.length,
130
+ conversationId: stored.conversationId,
131
+ hasCheckpoint: Boolean(stored.checkpoint),
132
+ replayTurnCount: replayTurns.length,
133
+ effectiveUserText,
134
+ });
96
135
  if (body.stream === false) {
97
136
  return handleNonStreamingResponse(payload, accessToken, modelId, convKey, {
98
137
  systemPrompt,
@@ -1,13 +1,8 @@
1
1
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
2
- import { createHash } from "node:crypto";
3
- import { AgentClientMessageSchema, AgentRunRequestSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationStepSchema, AgentConversationTurnStructureSchema, ConversationTurnStructureSchema, AssistantMessageSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
2
+ import { AgentClientMessageSchema, AgentRunRequestSchema, AgentConversationTurnStructureSchema, AssistantMessageSchema, ConversationActionSchema, ConversationStateStructureSchema, ConversationTurnStructureSchema, ConversationStepSchema, ModelDetailsSchema, ResumeActionSchema, UserMessageActionSchema, UserMessageSchema, } from "../proto/agent_pb";
4
3
  export function buildCursorRequest(modelId, systemPrompt, userText, turns, conversationId, checkpoint, existingBlobStore) {
5
4
  const blobStore = new Map(existingBlobStore ?? []);
6
- // System prompt → blob store (Cursor requests it back via KV handshake)
7
- const systemJson = JSON.stringify({ role: "system", content: systemPrompt });
8
- const systemBytes = new TextEncoder().encode(systemJson);
9
- const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest());
10
- blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes);
5
+ const cloudRule = buildCloudRule(systemPrompt);
11
6
  let conversationState;
12
7
  if (checkpoint) {
13
8
  conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
@@ -40,7 +35,7 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
40
35
  turnBytes.push(toBinary(ConversationTurnStructureSchema, turnStructure));
41
36
  }
42
37
  conversationState = create(ConversationStateStructureSchema, {
43
- rootPromptMessagesJson: [systemBlobId],
38
+ rootPromptMessagesJson: [],
44
39
  turns: turnBytes,
45
40
  todos: [],
46
41
  pendingToolCalls: [],
@@ -64,14 +59,11 @@ export function buildCursorRequest(modelId, systemPrompt, userText, turns, conve
64
59
  value: create(UserMessageActionSchema, { userMessage }),
65
60
  },
66
61
  });
67
- return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
62
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
68
63
  }
69
64
  export function buildCursorResumeRequest(modelId, systemPrompt, conversationId, checkpoint, existingBlobStore) {
70
65
  const blobStore = new Map(existingBlobStore ?? []);
71
- const systemJson = JSON.stringify({ role: "system", content: systemPrompt });
72
- const systemBytes = new TextEncoder().encode(systemJson);
73
- const systemBlobId = new Uint8Array(createHash("sha256").update(systemBytes).digest());
74
- blobStore.set(Buffer.from(systemBlobId).toString("hex"), systemBytes);
66
+ const cloudRule = buildCloudRule(systemPrompt);
75
67
  const conversationState = fromBinary(ConversationStateStructureSchema, checkpoint);
76
68
  const action = create(ConversationActionSchema, {
77
69
  action: {
@@ -79,9 +71,9 @@ export function buildCursorResumeRequest(modelId, systemPrompt, conversationId,
79
71
  value: create(ResumeActionSchema, {}),
80
72
  },
81
73
  });
82
- return buildRunRequest(modelId, conversationId, conversationState, action, blobStore);
74
+ return buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule);
83
75
  }
84
- function buildRunRequest(modelId, conversationId, conversationState, action, blobStore) {
76
+ function buildRunRequest(modelId, conversationId, conversationState, action, blobStore, cloudRule) {
85
77
  const modelDetails = create(ModelDetailsSchema, {
86
78
  modelId,
87
79
  displayModelId: modelId,
@@ -99,6 +91,11 @@ function buildRunRequest(modelId, conversationId, conversationState, action, blo
99
91
  return {
100
92
  requestBytes: toBinary(AgentClientMessageSchema, clientMessage),
101
93
  blobStore,
94
+ cloudRule,
102
95
  mcpTools: [],
103
96
  };
104
97
  }
98
+ function buildCloudRule(systemPrompt) {
99
+ const content = systemPrompt.trim();
100
+ return content || undefined;
101
+ }
@@ -39,4 +39,4 @@ export declare function computeUsage(state: StreamState): {
39
39
  completion_tokens: number;
40
40
  total_tokens: number;
41
41
  };
42
- export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;
42
+ export declare function processServerMessage(msg: AgentServerMessage, blobStore: Map<string, Uint8Array>, cloudRule: string | undefined, mcpTools: McpToolDefinition[], sendFrame: (data: Uint8Array) => void, state: StreamState, onText: (text: string, isThinking?: boolean) => void, onMcpExec: (exec: PendingExec) => void, onCheckpoint?: (checkpointBytes: Uint8Array) => void, onTurnEnded?: () => void, onUnsupportedMessage?: (info: UnsupportedServerMessageInfo) => void, onUnhandledExec?: (info: UnhandledExecInfo) => void): void;