pi-provider-cursor-ask 0.1.0
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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/README.zh-CN.md +87 -0
- package/UPSTREAM_CHANGELOG.md +368 -0
- package/UPSTREAM_SOURCE.md +23 -0
- package/dist/index.js +54 -0
- package/package.json +97 -0
- package/src/auth/cli-credentials.ts +275 -0
- package/src/auth/consent.ts +25 -0
- package/src/auth/index.ts +23 -0
- package/src/auth/oauth.ts +282 -0
- package/src/auth/refresh-guard.ts +93 -0
- package/src/client/bridge.ts +673 -0
- package/src/client/cursor-wire.ts +213 -0
- package/src/client/h2-unary.ts +142 -0
- package/src/client/index.ts +18 -0
- package/src/config/index.ts +69 -0
- package/src/diagnostics/diagnostics.ts +116 -0
- package/src/diagnostics/index.ts +1 -0
- package/src/extension/auth.ts +99 -0
- package/src/extension/commands.ts +163 -0
- package/src/extension/compaction-guard.ts +86 -0
- package/src/extension/debug-hooks.ts +359 -0
- package/src/extension/index.ts +8 -0
- package/src/extension/provider.ts +277 -0
- package/src/extension/quota-adapter.ts +175 -0
- package/src/extension/report-dashboard.ts +133 -0
- package/src/identity.ts +16 -0
- package/src/index.ts +186 -0
- package/src/models/ask-catalog.ts +384 -0
- package/src/models/catalog.json +1163 -0
- package/src/models/cost.ts +126 -0
- package/src/models/index.ts +6 -0
- package/src/models/limits.ts +36 -0
- package/src/models/parameterized.ts +416 -0
- package/src/models/processing.ts +313 -0
- package/src/proto/agent_pb.ts +14577 -0
- package/src/stream/bridge-session.ts +215 -0
- package/src/stream/client-transcript.ts +51 -0
- package/src/stream/config.ts +5 -0
- package/src/stream/context-normalize.ts +308 -0
- package/src/stream/context-usage.ts +168 -0
- package/src/stream/debug-log.ts +316 -0
- package/src/stream/drift.ts +122 -0
- package/src/stream/images.ts +201 -0
- package/src/stream/index.ts +68 -0
- package/src/stream/interaction-query.ts +369 -0
- package/src/stream/message-parsing.ts +402 -0
- package/src/stream/model-cache.ts +100 -0
- package/src/stream/model-discovery.ts +242 -0
- package/src/stream/model-routing.ts +100 -0
- package/src/stream/native-core.ts +2121 -0
- package/src/stream/pi-adapter.ts +414 -0
- package/src/stream/protocol.ts +63 -0
- package/src/stream/recovery.ts +494 -0
- package/src/stream/request-build.ts +668 -0
- package/src/stream/root-prompt.ts +184 -0
- package/src/stream/run-journal.ts +474 -0
- package/src/stream/run-usage.ts +107 -0
- package/src/stream/server-messages.ts +777 -0
- package/src/stream/session-state.ts +499 -0
- package/src/stream/stream-writer.ts +211 -0
- package/src/stream/thinking-filter.ts +63 -0
- package/src/stream/tool-schema.ts +185 -0
- package/src/stream/transport-errors.ts +150 -0
- package/src/stream/tuning.ts +250 -0
- package/src/stream/types.ts +330 -0
- package/src/types/enums.ts +103 -0
- package/src/types/index.ts +4 -0
- package/src/usage.ts +262 -0
- package/src/utils/cache-dir.ts +39 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/security.ts +68 -0
- package/src/utils/util.ts +43 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch for everything Cursor sends back on the bidirectional stream.
|
|
3
|
+
*
|
|
4
|
+
* Three families arrive interleaved with assistant output and each needs a reply
|
|
5
|
+
* on the same stream or the server parks waiting:
|
|
6
|
+
* - `kvServerMessage` blob get/set against the local blob store
|
|
7
|
+
* - `execServerMessage` tool execution — MCP calls are handed to the caller,
|
|
8
|
+
* Cursor's own native tools (shell/read/write/...) get an explicit reject so
|
|
9
|
+
* the model re-plans instead of stalling
|
|
10
|
+
* - `interactionQuery` permission prompts, answered by ./interaction-query.ts
|
|
11
|
+
*
|
|
12
|
+
* Every handler returns whether it made forward progress, which is what feeds
|
|
13
|
+
* the idle watchdog — see `processServerMessage` for the exact contract.
|
|
14
|
+
*/
|
|
15
|
+
import { create, toBinary } from "@bufbuild/protobuf";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
AgentClientMessageSchema,
|
|
19
|
+
BackgroundShellSpawnResultSchema,
|
|
20
|
+
ComputerUseErrorSchema,
|
|
21
|
+
ComputerUseResultSchema,
|
|
22
|
+
ConversationStateStructureSchema,
|
|
23
|
+
DeleteRejectedSchema,
|
|
24
|
+
DeleteResultSchema,
|
|
25
|
+
DiagnosticsResultSchema,
|
|
26
|
+
ExecClientControlMessageSchema,
|
|
27
|
+
ExecClientMessageSchema,
|
|
28
|
+
ExecClientThrowSchema,
|
|
29
|
+
FetchErrorSchema,
|
|
30
|
+
FetchResultSchema,
|
|
31
|
+
GetBlobResultSchema,
|
|
32
|
+
GrepErrorSchema,
|
|
33
|
+
GrepResultSchema,
|
|
34
|
+
KvClientMessageSchema,
|
|
35
|
+
ListMcpResourcesExecResultSchema,
|
|
36
|
+
ListMcpResourcesRejectedSchema,
|
|
37
|
+
LsRejectedSchema,
|
|
38
|
+
LsResultSchema,
|
|
39
|
+
McpResultSchema,
|
|
40
|
+
McpToolNotFoundSchema,
|
|
41
|
+
ReadMcpResourceExecResultSchema,
|
|
42
|
+
ReadMcpResourceRejectedSchema,
|
|
43
|
+
ReadRejectedSchema,
|
|
44
|
+
ReadResultSchema,
|
|
45
|
+
RecordScreenFailureSchema,
|
|
46
|
+
RecordScreenResultSchema,
|
|
47
|
+
RequestContextResultSchema,
|
|
48
|
+
RequestContextSchema,
|
|
49
|
+
RequestContextSuccessSchema,
|
|
50
|
+
SetBlobResultSchema,
|
|
51
|
+
ShellRejectedSchema,
|
|
52
|
+
ShellResultSchema,
|
|
53
|
+
ShellStreamSchema,
|
|
54
|
+
WriteRejectedSchema,
|
|
55
|
+
WriteResultSchema,
|
|
56
|
+
WriteShellStdinErrorSchema,
|
|
57
|
+
WriteShellStdinResultSchema,
|
|
58
|
+
type AgentServerMessage,
|
|
59
|
+
type ConversationStateStructure,
|
|
60
|
+
type ExecServerMessage,
|
|
61
|
+
type InteractionQuery,
|
|
62
|
+
type KvServerMessage,
|
|
63
|
+
type McpToolDefinition,
|
|
64
|
+
} from "../proto/agent_pb.js";
|
|
65
|
+
import { frameConnectMessage } from "../client/bridge.js";
|
|
66
|
+
import { debugLog, lifecycleLog, reportCursorAnomaly } from "./debug-log.js";
|
|
67
|
+
import { recordDriftSignal, recordUnknownFields } from "./drift.js";
|
|
68
|
+
import { handleInteractionQuery } from "./interaction-query.js";
|
|
69
|
+
import { decodeMcpArgsMap } from "./request-build.js";
|
|
70
|
+
import {
|
|
71
|
+
interactionUpdateProgress,
|
|
72
|
+
MAX_ACTIVE_BLOB_BYTES,
|
|
73
|
+
MAX_ACTIVE_BLOB_ENTRIES,
|
|
74
|
+
MAX_INDIVIDUAL_BLOB_BYTES,
|
|
75
|
+
type StreamProgress,
|
|
76
|
+
} from "./tuning.js";
|
|
77
|
+
import { markBlobMiss, trimBlobStore } from "./session-state.js";
|
|
78
|
+
import { billedUsageFromTurnEnded } from "./pi-adapter.js";
|
|
79
|
+
import { recordRunReceipt } from "./run-usage.js";
|
|
80
|
+
import type { PendingExec, StreamState } from "./types.js";
|
|
81
|
+
import { setLastStreamEvent } from "../diagnostics/diagnostics.js";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Classifies a server message for the stream idle watchdog.
|
|
85
|
+
*
|
|
86
|
+
* `work` — the run is moving: non-empty `textDelta` / `thinkingDelta`,
|
|
87
|
+
* `tokenDelta` (long reasoning often emits only these for minutes), tool-call
|
|
88
|
+
* events, any answered `execServerMessage` (MCP exec **or** native-tool reject),
|
|
89
|
+
* answered interaction queries, KV blob round-trips, checkpoints.
|
|
90
|
+
*
|
|
91
|
+
* `liveness` — a heartbeat. The socket is healthy; the turn may still be parked.
|
|
92
|
+
*
|
|
93
|
+
* `none` — empty deltas, unanswered exec/KV/interaction cases, other noise.
|
|
94
|
+
*/
|
|
95
|
+
export function processServerMessage(
|
|
96
|
+
msg: AgentServerMessage,
|
|
97
|
+
blobStore: Map<string, Uint8Array>,
|
|
98
|
+
mcpTools: McpToolDefinition[],
|
|
99
|
+
sendFrame: (data: Uint8Array) => void,
|
|
100
|
+
state: StreamState,
|
|
101
|
+
onText: (text: string, isThinking?: boolean) => void,
|
|
102
|
+
onMcpExec: (exec: PendingExec) => void,
|
|
103
|
+
onCheckpoint?: (checkpointBytes: Uint8Array, contextTokens?: number) => void,
|
|
104
|
+
onExecUnanswerable?: (execCase: string | undefined) => void,
|
|
105
|
+
convKey?: string,
|
|
106
|
+
): StreamProgress {
|
|
107
|
+
const msgCase = msg.message.case;
|
|
108
|
+
debugLog("server_message", { msgCase, msg });
|
|
109
|
+
recordUnknownFields(`AgentServerMessage.${msgCase ?? "none"}`, msg);
|
|
110
|
+
recordUnknownFields(`${msgCase ?? "none"}.payload`, msg.message.value);
|
|
111
|
+
|
|
112
|
+
if (msgCase === "interactionUpdate") {
|
|
113
|
+
const update = msg.message.value as any;
|
|
114
|
+
const updateCase = update.message?.case;
|
|
115
|
+
if (updateCase === "textDelta") {
|
|
116
|
+
const delta = update.message.value.text || "";
|
|
117
|
+
if (delta) {
|
|
118
|
+
onText(delta, false);
|
|
119
|
+
return interactionUpdateProgress(updateCase, true);
|
|
120
|
+
}
|
|
121
|
+
return "none";
|
|
122
|
+
}
|
|
123
|
+
if (updateCase === "thinkingDelta") {
|
|
124
|
+
const delta = update.message.value.text || "";
|
|
125
|
+
if (delta) {
|
|
126
|
+
onText(delta, true);
|
|
127
|
+
return interactionUpdateProgress(updateCase, true);
|
|
128
|
+
}
|
|
129
|
+
return "none";
|
|
130
|
+
}
|
|
131
|
+
if (updateCase === "tokenDelta") {
|
|
132
|
+
state.outputTokens += update.message.value.tokens ?? 0;
|
|
133
|
+
return interactionUpdateProgress(updateCase);
|
|
134
|
+
}
|
|
135
|
+
if (updateCase === "toolCallCompleted") {
|
|
136
|
+
const completed = update.message.value as any;
|
|
137
|
+
const mcpToolCall =
|
|
138
|
+
completed.toolCall?.tool?.case === "mcpToolCall"
|
|
139
|
+
? completed.toolCall.tool.value
|
|
140
|
+
: undefined;
|
|
141
|
+
const result = mcpToolCall?.result?.result;
|
|
142
|
+
if (result?.case && result.case !== "success") {
|
|
143
|
+
const args = mcpToolCall.args;
|
|
144
|
+
const value = result.value as any;
|
|
145
|
+
debugLog("native.stream.mcp_tool_error", {
|
|
146
|
+
callId: completed.callId,
|
|
147
|
+
modelCallId: completed.modelCallId,
|
|
148
|
+
resultCase: result.case,
|
|
149
|
+
toolName: args?.toolName,
|
|
150
|
+
mcpName: args?.name,
|
|
151
|
+
providerIdentifier: args?.providerIdentifier,
|
|
152
|
+
error: value?.error ?? value?.reason,
|
|
153
|
+
errorUnknown: value?.$unknown,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return interactionUpdateProgress(updateCase);
|
|
157
|
+
}
|
|
158
|
+
if (updateCase === "turnEnded") {
|
|
159
|
+
// Cursor closes the HTTP/2 connection right after this. Recorded so the close is
|
|
160
|
+
// finalized as a completed turn instead of retried as a failure (upstream #3).
|
|
161
|
+
state.turnEnded = true;
|
|
162
|
+
const billed = billedUsageFromTurnEnded(update.message.value ?? {});
|
|
163
|
+
if (billed) {
|
|
164
|
+
if (billed.cacheRead + billed.cacheWrite > billed.input) {
|
|
165
|
+
debugLog("native.stream.turn_ended_cache_exceeds_input", {
|
|
166
|
+
input: billed.input,
|
|
167
|
+
cacheRead: billed.cacheRead,
|
|
168
|
+
cacheWrite: billed.cacheWrite,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
recordRunReceipt(state, billed, update.message.value ?? {});
|
|
172
|
+
}
|
|
173
|
+
return "work";
|
|
174
|
+
}
|
|
175
|
+
// Remaining cases (heartbeat, toolCallStarted, partialToolCall, ...) are already
|
|
176
|
+
// classified by interactionUpdateProgress; reuse it rather than keeping a second list.
|
|
177
|
+
const progress = interactionUpdateProgress(updateCase);
|
|
178
|
+
if (progress !== "none") return progress;
|
|
179
|
+
// Unrecognized update cases are informational rather than stranding — the
|
|
180
|
+
// stream keeps flowing — but they are the first sign our schema is behind.
|
|
181
|
+
recordDriftSignal("interaction_update", updateCase);
|
|
182
|
+
return "none";
|
|
183
|
+
}
|
|
184
|
+
if (msgCase === "kvServerMessage") {
|
|
185
|
+
return handleKvMessage(msg.message.value as KvServerMessage, blobStore, sendFrame, convKey)
|
|
186
|
+
? "work"
|
|
187
|
+
: "none";
|
|
188
|
+
}
|
|
189
|
+
if (msgCase === "execServerMessage") {
|
|
190
|
+
const execMsg = msg.message.value as ExecServerMessage;
|
|
191
|
+
const execCase = (execMsg as { message?: { case?: string } }).message?.case;
|
|
192
|
+
const handled = handleExecMessage(execMsg, mcpTools, sendFrame, onMcpExec);
|
|
193
|
+
// execServerMessage was previously invisible in the lifecycle log — the exact
|
|
194
|
+
// blind spot behind unexplained mid-run stalls. Record the exec case and whether
|
|
195
|
+
// we answered it, so a parked stream can be diagnosed from the sanitized log
|
|
196
|
+
// alone. mcpArgs is the normal tool-call path; anything unhandled here means the
|
|
197
|
+
// upstream run may park waiting for a result we never sent.
|
|
198
|
+
if (execCase !== "mcpArgs") {
|
|
199
|
+
lifecycleLog("exec_server", { execCase: execCase ?? "unknown", handled });
|
|
200
|
+
}
|
|
201
|
+
if (!handled) {
|
|
202
|
+
setLastStreamEvent(`exec_unanswered:${String(execCase ?? "unknown")}`);
|
|
203
|
+
recordDriftSignal("exec_message", execCase);
|
|
204
|
+
onExecUnanswerable?.(execCase);
|
|
205
|
+
}
|
|
206
|
+
return handled ? "work" : "none";
|
|
207
|
+
}
|
|
208
|
+
if (msgCase === "interactionQuery") {
|
|
209
|
+
const query = msg.message.value as InteractionQuery;
|
|
210
|
+
const result = handleInteractionQuery(query, sendFrame);
|
|
211
|
+
lifecycleLog("interaction_query", {
|
|
212
|
+
id: query.id,
|
|
213
|
+
queryCase: result.queryCase,
|
|
214
|
+
action: result.action,
|
|
215
|
+
handled: result.handled,
|
|
216
|
+
});
|
|
217
|
+
debugLog(
|
|
218
|
+
result.handled ? "native.interaction_query.handled" : "native.interaction_query.unhandled",
|
|
219
|
+
{
|
|
220
|
+
id: query.id,
|
|
221
|
+
queryCase: result.queryCase,
|
|
222
|
+
action: result.action,
|
|
223
|
+
clientVersion: process.env.PI_CURSOR_CLIENT_VERSION || "default",
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
setLastStreamEvent(
|
|
227
|
+
result.handled
|
|
228
|
+
? `interaction_query:${result.action}`
|
|
229
|
+
: `interaction_query_unhandled:${result.queryCase ?? "unknown"}`,
|
|
230
|
+
);
|
|
231
|
+
if (!result.handled) {
|
|
232
|
+
recordDriftSignal("interaction_query", result.queryCase);
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Unsupported Cursor interaction query ${result.queryCase ?? "unknown"} was rejected`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return "work";
|
|
238
|
+
}
|
|
239
|
+
if (msgCase === "execServerControlMessage") {
|
|
240
|
+
const control = msg.message.value as { message?: { case?: string } };
|
|
241
|
+
const controlCase = control.message?.case;
|
|
242
|
+
debugLog("native.exec_server_control", { controlCase });
|
|
243
|
+
lifecycleLog("exec_server_control", { controlCase });
|
|
244
|
+
// Abort notices are informational; the stream may continue or end separately.
|
|
245
|
+
return controlCase === "abort" ? "work" : "none";
|
|
246
|
+
}
|
|
247
|
+
if (msgCase === "conversationCheckpointUpdate") {
|
|
248
|
+
const stateStructure = msg.message.value as ConversationStateStructure;
|
|
249
|
+
const usedTokens = stateStructure.tokenDetails?.usedTokens;
|
|
250
|
+
const contextTokens =
|
|
251
|
+
typeof usedTokens === "number" && Number.isFinite(usedTokens) && usedTokens > 0
|
|
252
|
+
? usedTokens
|
|
253
|
+
: undefined;
|
|
254
|
+
// Pending tool checkpoints can carry 0/0 placeholders. Preserve the last positive observation,
|
|
255
|
+
// but accept genuine smaller positive values after an upstream context reduction.
|
|
256
|
+
if (contextTokens !== undefined) state.totalTokens = contextTokens;
|
|
257
|
+
if (onCheckpoint) {
|
|
258
|
+
onCheckpoint(toBinary(ConversationStateStructureSchema, stateStructure), contextTokens);
|
|
259
|
+
return "work";
|
|
260
|
+
}
|
|
261
|
+
return "none";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Nothing matched: Cursor sent a server message this build has no branch for.
|
|
265
|
+
// Nobody answers it, so if the run was waiting on it the turn will park until
|
|
266
|
+
// the idle watchdog fires — record it so the timeout is explainable.
|
|
267
|
+
recordDriftSignal("server_message", msgCase);
|
|
268
|
+
return "none";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function sendKvResponse(
|
|
272
|
+
kvMsg: KvServerMessage,
|
|
273
|
+
messageCase: string,
|
|
274
|
+
value: unknown,
|
|
275
|
+
sendFrame: (data: Uint8Array) => void,
|
|
276
|
+
): void {
|
|
277
|
+
const response = create(KvClientMessageSchema, {
|
|
278
|
+
id: (kvMsg as any).id,
|
|
279
|
+
message: { case: messageCase as any, value: value as any },
|
|
280
|
+
});
|
|
281
|
+
const clientMsg = create(AgentClientMessageSchema, {
|
|
282
|
+
message: { case: "kvClientMessage", value: response },
|
|
283
|
+
});
|
|
284
|
+
sendFrame(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMsg)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Returns true when a recognized KV branch fired (real round-trip with cursor). */
|
|
288
|
+
function handleKvMessage(
|
|
289
|
+
kvMsg: KvServerMessage,
|
|
290
|
+
blobStore: Map<string, Uint8Array>,
|
|
291
|
+
sendFrame: (data: Uint8Array) => void,
|
|
292
|
+
convKey?: string,
|
|
293
|
+
): boolean {
|
|
294
|
+
const kvCase = (kvMsg as any).message.case;
|
|
295
|
+
if (kvCase === "getBlobArgs") {
|
|
296
|
+
const blobId = (kvMsg as any).message.value.blobId;
|
|
297
|
+
const blobIdKey = Buffer.from(blobId).toString("hex");
|
|
298
|
+
const blobData = blobStore.get(blobIdKey);
|
|
299
|
+
if (!blobData) {
|
|
300
|
+
// An empty result is valid history to Cursor, not a missing-blob error.
|
|
301
|
+
// Refuse the round-trip and fail this generation so the next turn rebuilds
|
|
302
|
+
// from Pi. Invalidate by conversation key: the live blob store is a clone.
|
|
303
|
+
lifecycleLog("kv_blob_miss", { blobId: blobIdKey.slice(0, 16), storeSize: blobStore.size });
|
|
304
|
+
setLastStreamEvent("kv_blob_miss");
|
|
305
|
+
if (convKey) markBlobMiss(convKey);
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Cursor asked for blob ${blobIdKey.slice(0, 16)} that is not in the local store (${blobStore.size} entries). Refusing to answer empty. Retry to rebuild from Pi history.`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
sendKvResponse(kvMsg, "getBlobResult", create(GetBlobResultSchema, { blobData }), sendFrame);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
if (kvCase === "setBlobArgs") {
|
|
314
|
+
const { blobId, blobData } = (kvMsg as any).message.value;
|
|
315
|
+
const blobIdKey = Buffer.from(blobId).toString("hex");
|
|
316
|
+
if (!(blobData instanceof Uint8Array)) throw new Error("Cursor sent invalid blob data");
|
|
317
|
+
if (blobData.byteLength > MAX_INDIVIDUAL_BLOB_BYTES) {
|
|
318
|
+
throw new Error(`Cursor blob exceeds the ${MAX_INDIVIDUAL_BLOB_BYTES} byte per-blob limit`);
|
|
319
|
+
}
|
|
320
|
+
// Reject blobs that cannot fit even in an empty store before any eviction, so a
|
|
321
|
+
// failed write cannot punch holes in history Cursor still references.
|
|
322
|
+
if (blobData.byteLength > MAX_ACTIVE_BLOB_BYTES) {
|
|
323
|
+
throw new Error(`Cursor blob store exceeds the ${MAX_ACTIVE_BLOB_BYTES} byte limit`);
|
|
324
|
+
}
|
|
325
|
+
if (!blobStore.has(blobIdKey)) {
|
|
326
|
+
const evicted = trimBlobStore(
|
|
327
|
+
blobStore,
|
|
328
|
+
MAX_ACTIVE_BLOB_BYTES - blobData.byteLength,
|
|
329
|
+
MAX_ACTIVE_BLOB_ENTRIES - 1,
|
|
330
|
+
);
|
|
331
|
+
if (evicted.removed > 0) {
|
|
332
|
+
debugLog("kv.blob_store_evicted", {
|
|
333
|
+
removed: evicted.removed,
|
|
334
|
+
totalBytes: evicted.totalBytes,
|
|
335
|
+
entries: blobStore.size,
|
|
336
|
+
maxEntries: MAX_ACTIVE_BLOB_ENTRIES,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
blobStore.set(blobIdKey, blobData);
|
|
341
|
+
sendKvResponse(kvMsg, "setBlobResult", create(SetBlobResultSchema, {}), sendFrame);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
recordDriftSignal("kv_message", kvCase);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Returns true when this `execServerMessage` was handled (MCP exec **or** a
|
|
350
|
+
* native-tool reject/response). Handled round-trips count as idle-watchdog
|
|
351
|
+
* progress so Cursor-native tool reject loops cannot stall for minutes and
|
|
352
|
+
* then trip the idle timer.
|
|
353
|
+
*/
|
|
354
|
+
function handleExecMessage(
|
|
355
|
+
execMsg: ExecServerMessage,
|
|
356
|
+
mcpTools: McpToolDefinition[],
|
|
357
|
+
sendFrame: (data: Uint8Array) => void,
|
|
358
|
+
onMcpExec: (exec: PendingExec) => void,
|
|
359
|
+
): boolean {
|
|
360
|
+
return handleExecMessageInner(execMsg, mcpTools, sendFrame, onMcpExec);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// mcpTools is fixed for the life of a stream but `mcpArgs` exec messages can arrive many times
|
|
364
|
+
// per turn; cache the derived name list by array identity instead of rebuilding it every call.
|
|
365
|
+
const availableToolNamesCache = new WeakMap<McpToolDefinition[], string[]>();
|
|
366
|
+
|
|
367
|
+
function availableToolNamesFor(mcpTools: McpToolDefinition[]): string[] {
|
|
368
|
+
const cached = availableToolNamesCache.get(mcpTools);
|
|
369
|
+
if (cached) return cached;
|
|
370
|
+
const names = [...new Set(mcpTools.flatMap((tool) => [tool.toolName, tool.name]))].filter(
|
|
371
|
+
Boolean,
|
|
372
|
+
) as string[];
|
|
373
|
+
availableToolNamesCache.set(mcpTools, names);
|
|
374
|
+
return names;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const NATIVE_EXEC_MCP_HINTS: Record<string, string[]> = {
|
|
378
|
+
readArgs: ["read", "Read"],
|
|
379
|
+
lsArgs: ["ls", "LS"],
|
|
380
|
+
grepArgs: ["grep", "Grep"],
|
|
381
|
+
writeArgs: ["write", "edit", "Edit"],
|
|
382
|
+
deleteArgs: ["bash", "edit", "Edit"],
|
|
383
|
+
shellArgs: ["bash"],
|
|
384
|
+
shellStreamArgs: ["bash"],
|
|
385
|
+
backgroundShellSpawnArgs: ["bash"],
|
|
386
|
+
writeShellStdinArgs: ["bash"],
|
|
387
|
+
fetchArgs: ["web_search", "fetch"],
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
function nativeToolRejectReason(execCase: string, mcpTools: McpToolDefinition[]): string {
|
|
391
|
+
const available = availableToolNamesFor(mcpTools);
|
|
392
|
+
const candidates = (NATIVE_EXEC_MCP_HINTS[execCase] ?? []).filter((name) =>
|
|
393
|
+
available.includes(name),
|
|
394
|
+
);
|
|
395
|
+
if (candidates.length > 0) {
|
|
396
|
+
return (
|
|
397
|
+
`This native Cursor tool is not available in Pi. ` +
|
|
398
|
+
`Call the MCP tool "${candidates[0]}" with the same arguments instead.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return "This native Cursor tool is not available in Pi. Use the MCP tools provided instead.";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function handleExecMessageInner(
|
|
405
|
+
execMsg: ExecServerMessage,
|
|
406
|
+
mcpTools: McpToolDefinition[],
|
|
407
|
+
sendFrame: (data: Uint8Array) => void,
|
|
408
|
+
onMcpExec: (exec: PendingExec) => void,
|
|
409
|
+
): boolean {
|
|
410
|
+
const execCase = (execMsg as any).message.case;
|
|
411
|
+
const REJECT_REASON = nativeToolRejectReason(execCase ?? "", mcpTools);
|
|
412
|
+
|
|
413
|
+
if (execCase === "requestContextArgs") {
|
|
414
|
+
const requestContext = create(RequestContextSchema, {
|
|
415
|
+
rules: [],
|
|
416
|
+
repositoryInfo: [],
|
|
417
|
+
tools: mcpTools,
|
|
418
|
+
gitRepos: [],
|
|
419
|
+
projectLayouts: [],
|
|
420
|
+
mcpInstructions: [],
|
|
421
|
+
fileContents: {},
|
|
422
|
+
customSubagents: [],
|
|
423
|
+
});
|
|
424
|
+
const result = create(RequestContextResultSchema, {
|
|
425
|
+
result: { case: "success", value: create(RequestContextSuccessSchema, { requestContext }) },
|
|
426
|
+
});
|
|
427
|
+
sendExecResult(execMsg, "requestContextResult", result, sendFrame);
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (execCase === "mcpArgs") {
|
|
432
|
+
const mcpArgs = (execMsg as any).message.value;
|
|
433
|
+
const toolName =
|
|
434
|
+
typeof mcpArgs.toolName === "string" && mcpArgs.toolName
|
|
435
|
+
? mcpArgs.toolName
|
|
436
|
+
: typeof mcpArgs.name === "string"
|
|
437
|
+
? mcpArgs.name
|
|
438
|
+
: "";
|
|
439
|
+
const availableTools = availableToolNamesFor(mcpTools);
|
|
440
|
+
if (!toolName || !availableTools.includes(toolName)) {
|
|
441
|
+
const notFound = create(McpResultSchema, {
|
|
442
|
+
result: {
|
|
443
|
+
case: "toolNotFound",
|
|
444
|
+
value: create(McpToolNotFoundSchema, { name: toolName, availableTools }),
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
sendExecResult(execMsg, "mcpResult", notFound, sendFrame);
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
const decoded = decodeMcpArgsMap(mcpArgs.args ?? {});
|
|
451
|
+
onMcpExec({
|
|
452
|
+
execId: (execMsg as any).execId,
|
|
453
|
+
execMsgId: (execMsg as any).id,
|
|
454
|
+
toolCallId: mcpArgs.toolCallId || crypto.randomUUID(),
|
|
455
|
+
toolName,
|
|
456
|
+
decodedArgs: JSON.stringify(decoded),
|
|
457
|
+
});
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Reject native Cursor tools so model falls back to MCP tools
|
|
462
|
+
if (execCase === "readArgs") {
|
|
463
|
+
const args = (execMsg as any).message.value;
|
|
464
|
+
sendExecResult(
|
|
465
|
+
execMsg,
|
|
466
|
+
"readResult",
|
|
467
|
+
create(ReadResultSchema, {
|
|
468
|
+
result: {
|
|
469
|
+
case: "rejected",
|
|
470
|
+
value: create(ReadRejectedSchema, { path: args.path, reason: REJECT_REASON }),
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
sendFrame,
|
|
474
|
+
);
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
if (execCase === "lsArgs") {
|
|
478
|
+
const args = (execMsg as any).message.value;
|
|
479
|
+
sendExecResult(
|
|
480
|
+
execMsg,
|
|
481
|
+
"lsResult",
|
|
482
|
+
create(LsResultSchema, {
|
|
483
|
+
result: {
|
|
484
|
+
case: "rejected",
|
|
485
|
+
value: create(LsRejectedSchema, { path: args.path, reason: REJECT_REASON }),
|
|
486
|
+
},
|
|
487
|
+
}),
|
|
488
|
+
sendFrame,
|
|
489
|
+
);
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
if (execCase === "grepArgs") {
|
|
493
|
+
sendExecResult(
|
|
494
|
+
execMsg,
|
|
495
|
+
"grepResult",
|
|
496
|
+
create(GrepResultSchema, {
|
|
497
|
+
result: { case: "error", value: create(GrepErrorSchema, { error: REJECT_REASON }) },
|
|
498
|
+
}),
|
|
499
|
+
sendFrame,
|
|
500
|
+
);
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (execCase === "writeArgs") {
|
|
504
|
+
const args = (execMsg as any).message.value;
|
|
505
|
+
sendExecResult(
|
|
506
|
+
execMsg,
|
|
507
|
+
"writeResult",
|
|
508
|
+
create(WriteResultSchema, {
|
|
509
|
+
result: {
|
|
510
|
+
case: "rejected",
|
|
511
|
+
value: create(WriteRejectedSchema, { path: args.path, reason: REJECT_REASON }),
|
|
512
|
+
},
|
|
513
|
+
}),
|
|
514
|
+
sendFrame,
|
|
515
|
+
);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
if (execCase === "deleteArgs") {
|
|
519
|
+
const args = (execMsg as any).message.value;
|
|
520
|
+
sendExecResult(
|
|
521
|
+
execMsg,
|
|
522
|
+
"deleteResult",
|
|
523
|
+
create(DeleteResultSchema, {
|
|
524
|
+
result: {
|
|
525
|
+
case: "rejected",
|
|
526
|
+
value: create(DeleteRejectedSchema, { path: args.path, reason: REJECT_REASON }),
|
|
527
|
+
},
|
|
528
|
+
}),
|
|
529
|
+
sendFrame,
|
|
530
|
+
);
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
if (execCase === "shellArgs") {
|
|
534
|
+
const args = (execMsg as any).message.value;
|
|
535
|
+
sendExecResult(
|
|
536
|
+
execMsg,
|
|
537
|
+
"shellResult",
|
|
538
|
+
create(ShellResultSchema, {
|
|
539
|
+
result: {
|
|
540
|
+
case: "rejected",
|
|
541
|
+
value: create(ShellRejectedSchema, {
|
|
542
|
+
command: args.command ?? "",
|
|
543
|
+
workingDirectory: args.workingDirectory ?? "",
|
|
544
|
+
reason: REJECT_REASON,
|
|
545
|
+
isReadonly: false,
|
|
546
|
+
}),
|
|
547
|
+
},
|
|
548
|
+
}),
|
|
549
|
+
sendFrame,
|
|
550
|
+
);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
if (execCase === "shellStreamArgs") {
|
|
554
|
+
const args = (execMsg as any).message.value;
|
|
555
|
+
sendExecResult(
|
|
556
|
+
execMsg,
|
|
557
|
+
"shellStream",
|
|
558
|
+
create(ShellStreamSchema, {
|
|
559
|
+
event: {
|
|
560
|
+
case: "rejected",
|
|
561
|
+
value: create(ShellRejectedSchema, {
|
|
562
|
+
command: args.command ?? "",
|
|
563
|
+
workingDirectory: args.workingDirectory ?? "",
|
|
564
|
+
reason: REJECT_REASON,
|
|
565
|
+
isReadonly: false,
|
|
566
|
+
}),
|
|
567
|
+
},
|
|
568
|
+
}),
|
|
569
|
+
sendFrame,
|
|
570
|
+
);
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
if (execCase === "backgroundShellSpawnArgs") {
|
|
574
|
+
const args = (execMsg as any).message.value;
|
|
575
|
+
sendExecResult(
|
|
576
|
+
execMsg,
|
|
577
|
+
"backgroundShellSpawnResult",
|
|
578
|
+
create(BackgroundShellSpawnResultSchema, {
|
|
579
|
+
result: {
|
|
580
|
+
case: "rejected",
|
|
581
|
+
value: create(ShellRejectedSchema, {
|
|
582
|
+
command: args.command ?? "",
|
|
583
|
+
workingDirectory: args.workingDirectory ?? "",
|
|
584
|
+
reason: REJECT_REASON,
|
|
585
|
+
isReadonly: false,
|
|
586
|
+
}),
|
|
587
|
+
},
|
|
588
|
+
}),
|
|
589
|
+
sendFrame,
|
|
590
|
+
);
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
if (execCase === "writeShellStdinArgs") {
|
|
594
|
+
sendExecResult(
|
|
595
|
+
execMsg,
|
|
596
|
+
"writeShellStdinResult",
|
|
597
|
+
create(WriteShellStdinResultSchema, {
|
|
598
|
+
result: {
|
|
599
|
+
case: "error",
|
|
600
|
+
value: create(WriteShellStdinErrorSchema, { error: REJECT_REASON }),
|
|
601
|
+
},
|
|
602
|
+
}),
|
|
603
|
+
sendFrame,
|
|
604
|
+
);
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
if (execCase === "fetchArgs") {
|
|
608
|
+
const args = (execMsg as any).message.value;
|
|
609
|
+
sendExecResult(
|
|
610
|
+
execMsg,
|
|
611
|
+
"fetchResult",
|
|
612
|
+
create(FetchResultSchema, {
|
|
613
|
+
result: {
|
|
614
|
+
case: "error",
|
|
615
|
+
value: create(FetchErrorSchema, { url: args.url ?? "", error: REJECT_REASON }),
|
|
616
|
+
},
|
|
617
|
+
}),
|
|
618
|
+
sendFrame,
|
|
619
|
+
);
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
if (execCase === "diagnosticsArgs") {
|
|
623
|
+
sendExecResult(execMsg, "diagnosticsResult", create(DiagnosticsResultSchema, {}), sendFrame);
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (execCase === "listMcpResourcesExecArgs") {
|
|
628
|
+
sendExecResult(
|
|
629
|
+
execMsg,
|
|
630
|
+
"listMcpResourcesExecResult",
|
|
631
|
+
create(ListMcpResourcesExecResultSchema, {
|
|
632
|
+
result: {
|
|
633
|
+
case: "rejected",
|
|
634
|
+
value: create(ListMcpResourcesRejectedSchema, { reason: REJECT_REASON }),
|
|
635
|
+
},
|
|
636
|
+
}),
|
|
637
|
+
sendFrame,
|
|
638
|
+
);
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
if (execCase === "readMcpResourceExecArgs") {
|
|
642
|
+
const args = (execMsg as any).message.value;
|
|
643
|
+
sendExecResult(
|
|
644
|
+
execMsg,
|
|
645
|
+
"readMcpResourceExecResult",
|
|
646
|
+
create(ReadMcpResourceExecResultSchema, {
|
|
647
|
+
result: {
|
|
648
|
+
case: "rejected",
|
|
649
|
+
value: create(ReadMcpResourceRejectedSchema, {
|
|
650
|
+
uri: args.uri ?? "",
|
|
651
|
+
reason: REJECT_REASON,
|
|
652
|
+
}),
|
|
653
|
+
},
|
|
654
|
+
}),
|
|
655
|
+
sendFrame,
|
|
656
|
+
);
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
if (execCase === "recordScreenArgs") {
|
|
660
|
+
sendExecResult(
|
|
661
|
+
execMsg,
|
|
662
|
+
"recordScreenResult",
|
|
663
|
+
create(RecordScreenResultSchema, {
|
|
664
|
+
result: {
|
|
665
|
+
case: "failure",
|
|
666
|
+
value: create(RecordScreenFailureSchema, { error: REJECT_REASON }),
|
|
667
|
+
},
|
|
668
|
+
}),
|
|
669
|
+
sendFrame,
|
|
670
|
+
);
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
if (execCase === "computerUseArgs") {
|
|
674
|
+
const args = (execMsg as any).message.value;
|
|
675
|
+
sendExecResult(
|
|
676
|
+
execMsg,
|
|
677
|
+
"computerUseResult",
|
|
678
|
+
create(ComputerUseResultSchema, {
|
|
679
|
+
result: {
|
|
680
|
+
case: "error",
|
|
681
|
+
value: create(ComputerUseErrorSchema, {
|
|
682
|
+
error: REJECT_REASON,
|
|
683
|
+
actionCount: Array.isArray(args.actions) ? args.actions.length : 0,
|
|
684
|
+
durationMs: 0,
|
|
685
|
+
}),
|
|
686
|
+
},
|
|
687
|
+
}),
|
|
688
|
+
sendFrame,
|
|
689
|
+
);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// No result shape is known for an exec case this build has no branch for, and
|
|
694
|
+
// guessing one is unsafe: a fabricated success is indistinguishable from having
|
|
695
|
+
// actually performed a destructive operation. But silence is not the alternative
|
|
696
|
+
// — Cursor parks the run on the unanswered exec id and heartbeats forever.
|
|
697
|
+
// ExecClientThrow answers any exec by id without claiming a result, so the model
|
|
698
|
+
// sees a failed tool instead of a dead stream.
|
|
699
|
+
const unhandledCase = String(execCase ?? "unknown");
|
|
700
|
+
reportCursorAnomaly(
|
|
701
|
+
"unhandled_exec",
|
|
702
|
+
`Cursor unhandled exec answered with a throw (${unhandledCase})`,
|
|
703
|
+
{ execCase: unhandledCase },
|
|
704
|
+
{ level: "warning", stderrIfNoSink: true },
|
|
705
|
+
);
|
|
706
|
+
lifecycleLog("exec_unknown_shape", {
|
|
707
|
+
execCase: unhandledCase,
|
|
708
|
+
unknownFields: describeUnknownFields(execMsg),
|
|
709
|
+
});
|
|
710
|
+
setLastStreamEvent(`unhandled_exec:${unhandledCase}`);
|
|
711
|
+
sendExecThrow(
|
|
712
|
+
execMsg,
|
|
713
|
+
`Pi's Cursor provider has no handler for exec case "${unhandledCase}" ` +
|
|
714
|
+
`(wire drift: this build's agent.proto is behind Cursor). ${REJECT_REASON}`,
|
|
715
|
+
sendFrame,
|
|
716
|
+
);
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Field numbers, wire types and sizes of protobuf fields our schema lacks — the
|
|
722
|
+
* only evidence available for identifying a new Cursor exec case, and safe to
|
|
723
|
+
* keep in the always-on lifecycle log because it carries no payload content.
|
|
724
|
+
*/
|
|
725
|
+
function describeUnknownFields(message: unknown): string {
|
|
726
|
+
const unknown = (
|
|
727
|
+
message as { $unknown?: readonly { no: number; wireType: number; data: Uint8Array }[] }
|
|
728
|
+
).$unknown;
|
|
729
|
+
if (!unknown || unknown.length === 0) return "";
|
|
730
|
+
return unknown
|
|
731
|
+
.map((field) => `${field.no}:wt${field.wireType}:${field.data?.byteLength ?? 0}b`)
|
|
732
|
+
.join(",");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* The one reply that fits every exec: `ExecClientThrow` is keyed by exec id, not
|
|
737
|
+
* by exec case, so it releases a parked run whose request we cannot understand.
|
|
738
|
+
*/
|
|
739
|
+
function sendExecThrow(
|
|
740
|
+
execMsg: ExecServerMessage,
|
|
741
|
+
error: string,
|
|
742
|
+
sendFrame: (data: Uint8Array) => void,
|
|
743
|
+
): void {
|
|
744
|
+
const control = create(ExecClientControlMessageSchema, {
|
|
745
|
+
message: {
|
|
746
|
+
case: "throw",
|
|
747
|
+
value: create(ExecClientThrowSchema, { id: (execMsg as any).id, error }),
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
751
|
+
message: { case: "execClientControlMessage", value: control },
|
|
752
|
+
});
|
|
753
|
+
sendFrame(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function sendExecResult(
|
|
757
|
+
execMsg: ExecServerMessage,
|
|
758
|
+
messageCase: string,
|
|
759
|
+
value: unknown,
|
|
760
|
+
sendFrame: (data: Uint8Array) => void,
|
|
761
|
+
): void {
|
|
762
|
+
const execClientMessage = create(ExecClientMessageSchema, {
|
|
763
|
+
id: (execMsg as any).id,
|
|
764
|
+
execId: (execMsg as any).execId,
|
|
765
|
+
message: { case: messageCase as any, value: value as any },
|
|
766
|
+
});
|
|
767
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
768
|
+
message: { case: "execClientMessage", value: execClientMessage },
|
|
769
|
+
});
|
|
770
|
+
sendFrame(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export const __testInternals = {
|
|
774
|
+
nativeToolRejectReason,
|
|
775
|
+
handleExecMessageInner,
|
|
776
|
+
describeUnknownFields,
|
|
777
|
+
};
|