cursor-opencode-provider 0.4.2 → 0.5.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/README.md +17 -12
- package/dist/context/build.d.ts +8 -0
- package/dist/context/build.js +72 -23
- package/dist/context/frozen.d.ts +29 -0
- package/dist/context/frozen.js +168 -0
- package/dist/context/index.d.ts +2 -1
- package/dist/context/index.js +2 -1
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +1 -1
- package/dist/language-model.d.ts +10 -9
- package/dist/language-model.js +390 -112
- package/dist/pricing-data.d.ts +6 -0
- package/dist/pricing-data.js +6 -0
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +1 -1
- package/dist/protocol/blob-reachability.d.ts +15 -0
- package/dist/protocol/blob-reachability.js +140 -0
- package/dist/protocol/blob-store.d.ts +37 -0
- package/dist/protocol/blob-store.js +115 -0
- package/dist/protocol/conversation-bind.d.ts +13 -0
- package/dist/protocol/conversation-bind.js +27 -1
- package/dist/protocol/conversation-persistence.d.ts +32 -0
- package/dist/protocol/conversation-persistence.js +504 -0
- package/dist/protocol/conversation-state.d.ts +16 -0
- package/dist/protocol/conversation-state.js +62 -0
- package/dist/protocol/kv.d.ts +1 -0
- package/dist/protocol/kv.js +2 -0
- package/dist/protocol/messages.d.ts +2 -0
- package/dist/protocol/messages.js +30 -2
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +2 -0
- package/dist/protocol/token-details.d.ts +33 -0
- package/dist/protocol/token-details.js +155 -0
- package/dist/protocol/tools.d.ts +17 -0
- package/dist/protocol/tools.js +76 -0
- package/dist/session.d.ts +39 -11
- package/dist/shared.d.ts +3 -0
- package/dist/shared.js +3 -0
- package/dist/usage.d.ts +42 -22
- package/dist/usage.js +200 -45
- package/package.json +2 -1
package/dist/language-model.js
CHANGED
|
@@ -5,19 +5,22 @@ import { bidiRunStream, CursorRunInterruptedError, normalizeAgentRunOrigin, } fr
|
|
|
5
5
|
import { trace, traceRequestContextPaths } from "./debug.js";
|
|
6
6
|
import { buildRunRequest, buildHeartbeat } from "./protocol/request.js";
|
|
7
7
|
import { decodeFramePayload } from "./protocol/framing.js";
|
|
8
|
-
import { debugWalkTurnEnded, decodeMessage } from "./protocol/messages.js";
|
|
9
|
-
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, isUriReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildListMcpResourcesFallback, buildReadMcpResourceFallback, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, CUSTOM_LIST_MCP_RESOURCES_TOOL, CUSTOM_READ_MCP_RESOURCE_TOOL, } from "./protocol/tools.js";
|
|
8
|
+
import { debugWalkTurnEnded, decodeMessage, encodeMessage } from "./protocol/messages.js";
|
|
9
|
+
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, isUriReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildListMcpResourcesFallback, buildReadMcpResourceFallback, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, buildCompleteEditReadMessages, rejectPartialReadMutation, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, CUSTOM_LIST_MCP_RESOURCES_TOOL, CUSTOM_READ_MCP_RESOURCE_TOOL, } from "./protocol/tools.js";
|
|
10
10
|
import { describeCursorExecVariant } from "./protocol/exec-variants.js";
|
|
11
11
|
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, } from "./protocol/tool-call-bridge.js";
|
|
12
12
|
import { handleKvServerMessage } from "./protocol/kv.js";
|
|
13
13
|
import { handleInteractionQuery } from "./protocol/interactions.js";
|
|
14
|
-
import { getCheckpoint, setCheckpoint } from "./protocol/checkpoint.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { clearCheckpoint, getCheckpoint, setCheckpoint } from "./protocol/checkpoint.js";
|
|
15
|
+
import { cursorContextUsageMetadata, decodeConversationTokenDetails, } from "./protocol/token-details.js";
|
|
16
|
+
import { clearConversationBlobs, conversationBlobCount, inspectConversationBlobGraph, } from "./protocol/blob-store.js";
|
|
17
|
+
import { bindConversationId, resolveConversationGroupId, } from "./protocol/conversation-bind.js";
|
|
18
|
+
import { clearPersistedConversationState, hydrateConversationState, persistConversationState, } from "./protocol/conversation-state.js";
|
|
19
|
+
import { initializeConversationPersistence } from "./protocol/conversation-persistence.js";
|
|
17
20
|
import { resolveContinuationPolicy, sessionManager, } from "./session.js";
|
|
18
21
|
import { CursorAuthError, CursorLocalCancellationError, CursorProtocolError, CursorProviderError, CursorRetryExhaustedError, CursorServerError, CursorTransportError, isTransientGrpcStatus, retrySuppressedError, toCursorProviderError, } from "./errors.js";
|
|
19
22
|
import { readCache, cacheFilePath, resolveVariantParameters, paramsImplyMaxMode, extractCursorVariantParameters, resolveCursorWireModelId } from "./models.js";
|
|
20
|
-
import {
|
|
23
|
+
import { getOrBuildRequestContext } from "./context/frozen.js";
|
|
21
24
|
import { workspaceRootFromRequestContext } from "./context/env.js";
|
|
22
25
|
import { opencodeGlobalCacheDir, setHostCacheDirOverride } from "./context/paths.js";
|
|
23
26
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
@@ -29,7 +32,7 @@ import { resolveCursorModelSupportsImages } from "./model-metadata.js";
|
|
|
29
32
|
import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
|
|
30
33
|
import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
|
|
31
34
|
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
32
|
-
import {
|
|
35
|
+
import { buildLanguageModelV3UsageFromCounters, cursorUsageCountersFromTurnEnded, emptyLanguageModelV3Usage, formatCursorCacheDiagnostics, formatCursorTokenCategories, formatTurnUsageValidation, turnEndedCounter, } from "./usage.js";
|
|
33
36
|
let _availableModels;
|
|
34
37
|
// mtime of the cache file the last time we loaded it. Compared on each call
|
|
35
38
|
// so discoverModels' background refresh is picked up without a process restart.
|
|
@@ -56,6 +59,11 @@ const MAX_RETRY_DELAY_MS = 30_000;
|
|
|
56
59
|
const RUN_REQUEST_DECODE_FAILED = "CURSOR_RUN_REQUEST_DECODE_FAILED";
|
|
57
60
|
const RUN_REQUEST_UNSUPPORTED = "CURSOR_RUN_REQUEST_UNSUPPORTED";
|
|
58
61
|
const RUN_REPLY_FAILED = "CURSOR_RUN_REPLY_FAILED";
|
|
62
|
+
/** Cursor's own conversation export path treats state above 100 MiB as large. */
|
|
63
|
+
export const MAX_CHECKPOINT_BLOB_GRAPH_BYTES = 100 * 1024 * 1024;
|
|
64
|
+
export function checkpointBlobGraphRequiresRebase(stats, maxBytes = MAX_CHECKPOINT_BLOB_GRAPH_BYTES) {
|
|
65
|
+
return !stats.complete || stats.bytes > maxBytes;
|
|
66
|
+
}
|
|
59
67
|
const RESPONSE_REQUIRED_CHANNEL_BY_FIELD = new Map([
|
|
60
68
|
[2, "exec"],
|
|
61
69
|
[4, "kv"],
|
|
@@ -237,7 +245,7 @@ export function connectFrameError(payload) {
|
|
|
237
245
|
}
|
|
238
246
|
function rememberToolCatalog(sessionKey, tools) {
|
|
239
247
|
toolCatalogBySession.delete(sessionKey);
|
|
240
|
-
toolCatalogBySession.set(sessionKey, tools);
|
|
248
|
+
toolCatalogBySession.set(sessionKey, structuredClone(tools));
|
|
241
249
|
while (toolCatalogBySession.size > MAX_TURN_STATE_SESSIONS) {
|
|
242
250
|
const oldest = toolCatalogBySession.keys().next().value;
|
|
243
251
|
if (!oldest)
|
|
@@ -245,6 +253,17 @@ function rememberToolCatalog(sessionKey, tools) {
|
|
|
245
253
|
toolCatalogBySession.delete(oldest);
|
|
246
254
|
}
|
|
247
255
|
}
|
|
256
|
+
/** Restore the last real catalog solely for lifecycle turns such as compaction. */
|
|
257
|
+
export function restoreTurnToolCatalog(sessionKey, tools) {
|
|
258
|
+
if (!sessionKey || tools.length === 0)
|
|
259
|
+
return;
|
|
260
|
+
rememberToolCatalog(sessionKey, tools);
|
|
261
|
+
}
|
|
262
|
+
function snapshotToolCatalog(sessionKey) {
|
|
263
|
+
if (!sessionKey)
|
|
264
|
+
return [];
|
|
265
|
+
return structuredClone(toolCatalogBySession.get(sessionKey) ?? []);
|
|
266
|
+
}
|
|
248
267
|
function rememberPostCompactionRebase(sessionKey) {
|
|
249
268
|
postCompactionRebaseBySession.delete(sessionKey);
|
|
250
269
|
postCompactionRebaseBySession.add(sessionKey);
|
|
@@ -292,6 +311,11 @@ export function createCursorLanguageModel(modelId, providerId, options) {
|
|
|
292
311
|
// Host Path.cache / explicit override wins over XDG heuristics for this process.
|
|
293
312
|
if (options.cacheDir)
|
|
294
313
|
setHostCacheDirOverride(options.cacheDir);
|
|
314
|
+
// Start loading and pruning restart state as soon as the provider is built.
|
|
315
|
+
// startSession awaits this same per-cache-root initialization before binding.
|
|
316
|
+
void initializeConversationPersistence(opencodeGlobalCacheDir()).catch((error) => {
|
|
317
|
+
trace(`conversation persistence: startup load failed: ${String(error)}`);
|
|
318
|
+
});
|
|
295
319
|
return {
|
|
296
320
|
specificationVersion: "v3",
|
|
297
321
|
provider: providerId,
|
|
@@ -327,7 +351,6 @@ async function doStreamImpl(modelId, options, callOptions) {
|
|
|
327
351
|
baseUrl: resolveApiBaseURL(options),
|
|
328
352
|
});
|
|
329
353
|
const prompt = callOptions.prompt;
|
|
330
|
-
const promptTokens = estimatePromptTokens(prompt);
|
|
331
354
|
const retryPolicy = resolveRetryPolicy(options.retry);
|
|
332
355
|
// pumpWithRecovery owns the complete per-turn attempt budget. Opening a
|
|
333
356
|
// replacement session here must be a single attempt; otherwise setup retry
|
|
@@ -389,7 +412,6 @@ async function doStreamImpl(modelId, options, callOptions) {
|
|
|
389
412
|
initialSession: activeSession,
|
|
390
413
|
controller,
|
|
391
414
|
abortSignal: callOptions.abortSignal,
|
|
392
|
-
promptTokens,
|
|
393
415
|
retryPolicy,
|
|
394
416
|
recover: (recovery) => openSession({ recovery }),
|
|
395
417
|
onSession: (next) => { activeSession = next; },
|
|
@@ -429,7 +451,6 @@ export async function pumpWithRecovery(input) {
|
|
|
429
451
|
maxAttempts: (input.maxRecoveries ?? 1) + 1,
|
|
430
452
|
};
|
|
431
453
|
const maxRecoveries = retryPolicy.maxAttempts - 1;
|
|
432
|
-
const requestUsage = { outputChars: 0 };
|
|
433
454
|
input.onSession?.(session);
|
|
434
455
|
for (let attempt = 0;; attempt++) {
|
|
435
456
|
const pumpedSession = session;
|
|
@@ -439,8 +460,6 @@ export async function pumpWithRecovery(input) {
|
|
|
439
460
|
await pump(pumpedSession, input.controller, {
|
|
440
461
|
textId: crypto.randomUUID(),
|
|
441
462
|
reasoningId: crypto.randomUUID(),
|
|
442
|
-
promptTokens: input.promptTokens ?? 0,
|
|
443
|
-
requestUsage,
|
|
444
463
|
}, input.abortSignal);
|
|
445
464
|
return session;
|
|
446
465
|
}
|
|
@@ -473,7 +492,6 @@ export async function pumpWithRecovery(input) {
|
|
|
473
492
|
: { kind: "rebase" };
|
|
474
493
|
session = await input.recover(recovery);
|
|
475
494
|
if (recovery.kind === "resume") {
|
|
476
|
-
session.hadToolCallBoundary = pumpedSession.hadToolCallBoundary;
|
|
477
495
|
session.usageEstimate = { ...pumpedSession.usageEstimate };
|
|
478
496
|
session.editToolCalls = new Map(pumpedSession.editToolCalls);
|
|
479
497
|
}
|
|
@@ -489,6 +507,17 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
489
507
|
const prompt = callOptions.prompt;
|
|
490
508
|
const incomingTools = extractTools(callOptions);
|
|
491
509
|
const sessionKey = opencodeSessionKey(callOptions);
|
|
510
|
+
const cacheDir = opencodeGlobalCacheDir();
|
|
511
|
+
if (sessionKey) {
|
|
512
|
+
const restored = await hydrateConversationState(cacheDir, sessionKey).catch((error) => {
|
|
513
|
+
trace(`conversation persistence: restore failed sessionKey=${sessionKey}: ${String(error)}`);
|
|
514
|
+
return undefined;
|
|
515
|
+
});
|
|
516
|
+
if (restored?.postCompactionRebase)
|
|
517
|
+
rememberPostCompactionRebase(sessionKey);
|
|
518
|
+
if (restored?.toolCatalog.length)
|
|
519
|
+
restoreTurnToolCatalog(sessionKey, restored.toolCatalog);
|
|
520
|
+
}
|
|
492
521
|
const providerOptions = callOptions.providerOptions?.cursor;
|
|
493
522
|
// The classic plugin marks OpenCode's agent="compaction" through chat.params.
|
|
494
523
|
// OpenCode 2.0 removed that hook, so its plugin records the same fact against
|
|
@@ -513,17 +542,55 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
513
542
|
const allowTools = toolState.allowTools;
|
|
514
543
|
const discoveredSubagentCatalog = extractHostSubagentCatalog(cursorTools);
|
|
515
544
|
const resetState = resolveTurnConversationReset({ sessionKey, isCompaction });
|
|
516
|
-
|
|
517
|
-
|
|
545
|
+
let recovery = startOptions?.recovery;
|
|
546
|
+
let resumeRecovery = recovery?.kind === "resume" ? recovery : undefined;
|
|
547
|
+
let resuming = !!resumeRecovery;
|
|
518
548
|
// Compaction must not reuse the prior conversation; its first normal turn
|
|
519
549
|
// must also rebase so the summary-agent checkpoint cannot replace the normal
|
|
520
550
|
// system prompt and OpenCode's newly compacted history.
|
|
521
|
-
|
|
522
|
-
? { conversationId:
|
|
551
|
+
let bound = resuming
|
|
552
|
+
? { conversationId: resumeRecovery.conversationId, reset: false, previousId: undefined }
|
|
523
553
|
: bindConversationId(sessionKey, { reset: resetState.reset || recovery?.kind === "rebase" });
|
|
554
|
+
let conversationState = resuming
|
|
555
|
+
? resumeRecovery.checkpoint
|
|
556
|
+
: (bound.reset ? undefined : getCheckpoint(bound.conversationId));
|
|
557
|
+
let checkpointGraph = conversationState
|
|
558
|
+
? inspectConversationBlobGraph(bound.conversationId, conversationState)
|
|
559
|
+
: { count: 0, bytes: 0, complete: true };
|
|
560
|
+
let forcedResetReason;
|
|
561
|
+
if (conversationState && checkpointBlobGraphRequiresRebase(checkpointGraph)) {
|
|
562
|
+
const previousConversationId = bound.conversationId;
|
|
563
|
+
forcedResetReason = checkpointGraph.complete
|
|
564
|
+
? "oversized-checkpoint-graph"
|
|
565
|
+
: "incomplete-checkpoint-graph";
|
|
566
|
+
trace(`checkpoint state rejected: reason=${forcedResetReason} ` +
|
|
567
|
+
`conversationId=${previousConversationId} checkpointBytes=${conversationState.length} ` +
|
|
568
|
+
`blobCount=${checkpointGraph.count} blobBytes=${checkpointGraph.bytes} ` +
|
|
569
|
+
`limitBytes=${MAX_CHECKPOINT_BLOB_GRAPH_BYTES} ` +
|
|
570
|
+
`detail=${checkpointGraph.fallbackReason ?? "-"} action=rebase`);
|
|
571
|
+
// A recovery resume must become a history rebase so the interrupted user
|
|
572
|
+
// request remains in the seed. An ordinary turn keeps its normal user text.
|
|
573
|
+
if (recovery?.kind === "resume")
|
|
574
|
+
recovery = { kind: "rebase" };
|
|
575
|
+
resumeRecovery = undefined;
|
|
576
|
+
resuming = false;
|
|
577
|
+
if (!sessionKey) {
|
|
578
|
+
clearCheckpoint(previousConversationId);
|
|
579
|
+
clearConversationBlobs(previousConversationId);
|
|
580
|
+
}
|
|
581
|
+
bound = bindConversationId(sessionKey, { reset: true });
|
|
582
|
+
conversationState = undefined;
|
|
583
|
+
checkpointGraph = { count: 0, bytes: 0, complete: true };
|
|
584
|
+
}
|
|
524
585
|
const conversationId = bound.conversationId;
|
|
586
|
+
const conversationGroupId = resolveConversationGroupId(sessionKey, conversationId);
|
|
525
587
|
if (bound.reset) {
|
|
526
|
-
|
|
588
|
+
if (sessionKey) {
|
|
589
|
+
await clearPersistedConversationState(cacheDir, sessionKey, bound.previousId).catch((error) => {
|
|
590
|
+
trace(`conversation persistence: reset cleanup failed sessionKey=${sessionKey}: ${String(error)}`);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
trace(`conversation reset: reason=${forcedResetReason ?? (recovery?.kind === "rebase" ? "interrupted-run" : (resetState.reason ?? "unknown"))} ` +
|
|
527
594
|
`sessionKey=${sessionKey ?? "(none)"} ` +
|
|
528
595
|
`previousId=${bound.previousId ?? "-"} → conversationId=${conversationId}`);
|
|
529
596
|
}
|
|
@@ -604,7 +671,9 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
604
671
|
baseURL: agentBaseUrl,
|
|
605
672
|
headers: options.headers,
|
|
606
673
|
});
|
|
607
|
-
|
|
674
|
+
// Freeze RequestContext per conversation_id (CLI parity). Rebuilding every
|
|
675
|
+
// Run mutates volatile slices (git porcelain, layout) and breaks prompt cache.
|
|
676
|
+
const { context: requestContext, reused: requestContextReused } = await getOrBuildRequestContext(conversationId, { workspaceRoot, tools: cursorTools });
|
|
608
677
|
const contextSubagents = Array.isArray(requestContext.custom_subagents)
|
|
609
678
|
? requestContext.custom_subagents
|
|
610
679
|
.map((agent) => agent && typeof agent === "object" && typeof agent.name === "string"
|
|
@@ -628,15 +697,14 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
628
697
|
? requestContext.tools
|
|
629
698
|
: [];
|
|
630
699
|
// CLI parity: echo the last conversation_checkpoint_update as conversation_state.
|
|
631
|
-
// After compaction reset there is no checkpoint —
|
|
632
|
-
|
|
633
|
-
? recovery.checkpoint
|
|
634
|
-
: (bound.reset ? undefined : getCheckpoint(conversationId));
|
|
700
|
+
// After compaction or an unsafe checkpoint reset there is no checkpoint —
|
|
701
|
+
// seed a new Cursor conversation from OpenCode's authoritative history.
|
|
635
702
|
const reqBytes = buildRunRequest({
|
|
636
703
|
text: userText,
|
|
637
704
|
images,
|
|
638
705
|
modelId: cursorModelId,
|
|
639
706
|
conversationId,
|
|
707
|
+
conversationGroupId,
|
|
640
708
|
systemPrompt: conversationState ? undefined : systemPrompt,
|
|
641
709
|
history: conversationState ? undefined : history,
|
|
642
710
|
conversationState,
|
|
@@ -657,14 +725,31 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
657
725
|
? requestContext.hooks_additional_context
|
|
658
726
|
: "";
|
|
659
727
|
const historyChars = history.reduce((n, m) => n + m.content.length, 0);
|
|
728
|
+
const seedChars = conversationState
|
|
729
|
+
? 0
|
|
730
|
+
: (systemPrompt?.length ?? 0) + userText.length + historyChars;
|
|
731
|
+
const seedEstimateIn = estimateTokens(seedChars);
|
|
732
|
+
const checkpointEnvelopeEstimateIn = conversationState
|
|
733
|
+
? estimateTokens(userText.length + conversationState.length)
|
|
734
|
+
: 0;
|
|
735
|
+
const runRequestWireEstimateIn = estimateTokens(reqBytes.length);
|
|
736
|
+
const encodedRequestContext = encodeMessage("RequestContext", requestContext);
|
|
737
|
+
const priorTokenDetails = decodeConversationTokenDetails(conversationState);
|
|
738
|
+
const requestContextHash = sha(encodedRequestContext);
|
|
739
|
+
const systemPromptHash = systemPrompt ? sha(systemPrompt) : undefined;
|
|
660
740
|
const usageEstimate = {
|
|
661
|
-
|
|
741
|
+
// This is used only before Cursor supplies authoritative TurnEnded usage.
|
|
742
|
+
// Estimate the actual protobuf request, not history/system text omitted by
|
|
743
|
+
// checkpoint Runs. The opaque remote interpretation of KV blobs is logged
|
|
744
|
+
// separately and deliberately is not mislabeled as a token count.
|
|
745
|
+
inputTokens: runRequestWireEstimateIn,
|
|
662
746
|
outputTokens: 0,
|
|
663
747
|
cacheRead: 0,
|
|
664
748
|
cacheWrite: 0,
|
|
665
749
|
reasoningTokens: 0,
|
|
666
750
|
};
|
|
667
|
-
trace(`outbound Run: model=${cursorModelId} opencodeModel=${modelId}
|
|
751
|
+
trace(`outbound Run: model=${cursorModelId} opencodeModel=${modelId} ` +
|
|
752
|
+
`conversationId=${conversationId} conversationGroupId=${conversationGroupId} ` +
|
|
668
753
|
`params=${JSON.stringify(parameterValues ?? [])} ` +
|
|
669
754
|
`maxMode=${maxMode} systemPromptLen=${systemPrompt?.length ?? 0} ` +
|
|
670
755
|
`tools=${tools.length} incomingTools=${incomingTools.length} compaction=${isCompaction} ` +
|
|
@@ -672,14 +757,20 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
672
757
|
`availableModels=${_availableModels?.length ?? 0} userTextLen=${userText.length} ` +
|
|
673
758
|
`images=${images.length} imageBytes=${images.reduce((total, image) => total + image.data.length, 0)} ` +
|
|
674
759
|
`historyMsgs=${history.length} historyChars=${historyChars} ` +
|
|
675
|
-
`checkpointLen=${conversationState?.length ?? 0}
|
|
676
|
-
`
|
|
677
|
-
`
|
|
760
|
+
`checkpointLen=${conversationState?.length ?? 0} checkpointBlobCount=${checkpointGraph.count} ` +
|
|
761
|
+
`checkpointBlobBytes=${checkpointGraph.bytes} checkpointGraphComplete=${checkpointGraph.complete} ` +
|
|
762
|
+
`seedChars=${seedChars} seedEstimateIn=${seedEstimateIn} ` +
|
|
763
|
+
`checkpointEnvelopeEstimateIn=${checkpointEnvelopeEstimateIn} ` +
|
|
764
|
+
`reset=${bound.reset} ` +
|
|
765
|
+
`resume=${resuming} requestContextReused=${requestContextReused} ` +
|
|
766
|
+
`usageEstimateMode=run-request-wire usageEstimateIn=${usageEstimate.inputTokens} ` +
|
|
767
|
+
`runRequestBytes=${reqBytes.length} requestContextBytes=${encodedRequestContext.length}`);
|
|
678
768
|
if (hooksCtx)
|
|
679
769
|
trace(`outbound Run hooks_additional_context: ${hooksCtx}`);
|
|
680
770
|
trace(`hash run_request sha256=${sha(reqBytes)}`);
|
|
681
771
|
if (systemPrompt)
|
|
682
772
|
trace(`hash systemPrompt sha256=${sha(systemPrompt)}`);
|
|
773
|
+
trace(`hash requestContext sha256=${requestContextHash}`);
|
|
683
774
|
if (conversationState)
|
|
684
775
|
trace(`hash checkpoint sha256=${sha(conversationState)}`);
|
|
685
776
|
try {
|
|
@@ -693,8 +784,31 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
693
784
|
const session = {
|
|
694
785
|
sessionId: crypto.randomUUID(),
|
|
695
786
|
conversationId,
|
|
787
|
+
cacheDir,
|
|
696
788
|
resumeCheckpoint: undefined,
|
|
789
|
+
tokenDetails: priorTokenDetails,
|
|
790
|
+
tokenDetailsFresh: false,
|
|
791
|
+
cacheDiagnostics: {
|
|
792
|
+
sessionKey,
|
|
793
|
+
conversationId,
|
|
794
|
+
conversationGroupId,
|
|
795
|
+
modelId: cursorModelId,
|
|
796
|
+
priorTokenDetails,
|
|
797
|
+
startedWithCheckpoint: !!conversationState,
|
|
798
|
+
requestContextReused,
|
|
799
|
+
requestContextHash,
|
|
800
|
+
systemPromptHash,
|
|
801
|
+
checkpointUpdates: 0,
|
|
802
|
+
tokenDetailUpdates: 0,
|
|
803
|
+
pumpPasses: 0,
|
|
804
|
+
stepStarts: 0,
|
|
805
|
+
stepCompletes: 0,
|
|
806
|
+
displayToolCalls: 0,
|
|
807
|
+
execRequests: 0,
|
|
808
|
+
},
|
|
697
809
|
openCodeSessionId: sessionKey,
|
|
810
|
+
postCompactionRebase: isCompaction,
|
|
811
|
+
toolCatalog: snapshotToolCatalog(sessionKey),
|
|
698
812
|
stream,
|
|
699
813
|
frames: stream.frames()[Symbol.asyncIterator](),
|
|
700
814
|
pending: new Map(),
|
|
@@ -730,11 +844,10 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
730
844
|
if (session.closed)
|
|
731
845
|
return;
|
|
732
846
|
if (heartbeatWritePending) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}));
|
|
847
|
+
// The pending write has its own bounded drain wait and will close the
|
|
848
|
+
// session with the actual failing operation. Do not race it with another
|
|
849
|
+
// heartbeat or misattribute an existing KV backlog to the heartbeat.
|
|
850
|
+
trace(`heartbeat skipped: prior heartbeat write is still pending sessionId=${session.sessionId}`);
|
|
738
851
|
return;
|
|
739
852
|
}
|
|
740
853
|
heartbeatWritePending = true;
|
|
@@ -805,16 +918,39 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
805
918
|
|| pending.resultField === "background_shell_spawn_result"
|
|
806
919
|
? consumeCursorShellResult(r.toolCallId, r.output)
|
|
807
920
|
: undefined;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
921
|
+
const workspaceRoot = workspaceRootFromRequestContext(session.requestContext);
|
|
922
|
+
const correlatedEditCallId = pending.resultMetadata?.correlatedEditCallId;
|
|
923
|
+
const requestedPath = pending.resultMetadata?.path;
|
|
924
|
+
const correlatedEdit = !r.error
|
|
925
|
+
&& pending.resultField === "read_result"
|
|
926
|
+
&& pending.toolName === "read"
|
|
927
|
+
&& typeof correlatedEditCallId === "string"
|
|
928
|
+
&& typeof requestedPath === "string"
|
|
929
|
+
? session.editToolCalls?.get(correlatedEditCallId)
|
|
930
|
+
: undefined;
|
|
931
|
+
if (correlatedEdit) {
|
|
932
|
+
const absolutePath = path.resolve(workspaceRoot, requestedPath);
|
|
933
|
+
if (absolutePath === path.resolve(workspaceRoot, correlatedEdit.path)) {
|
|
934
|
+
frames = buildCompleteEditReadMessages(r.execId, absolutePath, requestedPath) ?? [];
|
|
935
|
+
if (frames.length > 0) {
|
|
936
|
+
correlatedEdit.completeRead = true;
|
|
937
|
+
trace(`continuation: upgraded authorized correlated edit read execId=${r.execId} ` +
|
|
938
|
+
`path=${JSON.stringify(requestedPath)}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (frames.length === 0) {
|
|
943
|
+
frames = buildExecClientMessages({
|
|
944
|
+
execId: r.execId,
|
|
945
|
+
resultField: pending.resultField,
|
|
946
|
+
output: shellResult?.output ?? r.output,
|
|
947
|
+
error: r.error,
|
|
948
|
+
toolName: pending.toolName ?? r.toolName,
|
|
949
|
+
resultMetadata: pending.resultMetadata,
|
|
950
|
+
shellOutcome: shellResult?.outcome,
|
|
951
|
+
workspaceRoot,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
818
954
|
}
|
|
819
955
|
catch (error) {
|
|
820
956
|
trace(`continuation: result encode FAILED execId=${r.execId} err=${error.message}`);
|
|
@@ -879,7 +1015,27 @@ function resolveExplicitAgentBaseURL(options) {
|
|
|
879
1015
|
function isTruthyEnv(value) {
|
|
880
1016
|
return value === "1" || value === "true";
|
|
881
1017
|
}
|
|
1018
|
+
const streamWriteChains = new WeakMap();
|
|
1019
|
+
/**
|
|
1020
|
+
* Keep awaited protocol writes ordered per Run stream. In particular, a large
|
|
1021
|
+
* KV get_blob reply must drain before another KV reply or heartbeat is queued;
|
|
1022
|
+
* otherwise one heartbeat observes the backlog created by dozens of ignored
|
|
1023
|
+
* false write() results and reports the wrong operation as the failure.
|
|
1024
|
+
*/
|
|
882
1025
|
async function writeWithBackpressure(stream, message, operation) {
|
|
1026
|
+
const previous = streamWriteChains.get(stream);
|
|
1027
|
+
const current = (previous ? previous.catch(() => undefined) : Promise.resolve())
|
|
1028
|
+
.then(() => writeWithBackpressureNow(stream, message, operation));
|
|
1029
|
+
streamWriteChains.set(stream, current);
|
|
1030
|
+
try {
|
|
1031
|
+
await current;
|
|
1032
|
+
}
|
|
1033
|
+
finally {
|
|
1034
|
+
if (streamWriteChains.get(stream) === current)
|
|
1035
|
+
streamWriteChains.delete(stream);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async function writeWithBackpressureNow(stream, message, operation) {
|
|
883
1039
|
let accepted;
|
|
884
1040
|
try {
|
|
885
1041
|
accepted = stream.write(message);
|
|
@@ -892,6 +1048,7 @@ async function writeWithBackpressure(stream, message, operation) {
|
|
|
892
1048
|
}
|
|
893
1049
|
if (accepted !== false)
|
|
894
1050
|
return;
|
|
1051
|
+
trace(`stream write backpressured: operation=${operation} bytes=${message.length}`);
|
|
895
1052
|
if (!stream.waitForDrain) {
|
|
896
1053
|
throw new CursorTransportError(`Cursor ${operation} write was backpressured`, {
|
|
897
1054
|
transient: false,
|
|
@@ -901,6 +1058,7 @@ async function writeWithBackpressure(stream, message, operation) {
|
|
|
901
1058
|
}
|
|
902
1059
|
try {
|
|
903
1060
|
await stream.waitForDrain(5_000);
|
|
1061
|
+
trace(`stream write drained: operation=${operation} bytes=${message.length}`);
|
|
904
1062
|
}
|
|
905
1063
|
catch (cause) {
|
|
906
1064
|
throw toCursorProviderError(cause, {
|
|
@@ -947,13 +1105,27 @@ async function nextFrameWithSemanticDeadline(session) {
|
|
|
947
1105
|
*/
|
|
948
1106
|
export async function pump(session, controller, ids, abortSignal) {
|
|
949
1107
|
sessionManager.registerSession(session);
|
|
1108
|
+
const cacheDiagnostics = session.cacheDiagnostics ??= {
|
|
1109
|
+
sessionKey: session.openCodeSessionId,
|
|
1110
|
+
conversationId: session.conversationId,
|
|
1111
|
+
priorTokenDetails: session.tokenDetails,
|
|
1112
|
+
startedWithCheckpoint: !!session.tokenDetails,
|
|
1113
|
+
requestContextReused: false,
|
|
1114
|
+
requestContextHash: "unavailable",
|
|
1115
|
+
checkpointUpdates: 0,
|
|
1116
|
+
tokenDetailUpdates: 0,
|
|
1117
|
+
pumpPasses: 0,
|
|
1118
|
+
stepStarts: 0,
|
|
1119
|
+
stepCompletes: 0,
|
|
1120
|
+
displayToolCalls: 0,
|
|
1121
|
+
execRequests: 0,
|
|
1122
|
+
};
|
|
1123
|
+
cacheDiagnostics.pumpPasses++;
|
|
950
1124
|
const { textId, reasoningId } = ids;
|
|
951
|
-
const promptTokens = ids.promptTokens ?? 0;
|
|
952
1125
|
const advertisedToolNames = advertisedToolNamesFromDescriptors(session.toolDescriptors);
|
|
953
1126
|
const advertisedToolNameSet = new Set(advertisedToolNames.map((name) => resolveCustomWebToolAlias(name, session.toolAliases)));
|
|
954
1127
|
let textStarted = false;
|
|
955
1128
|
let reasoningStarted = false;
|
|
956
|
-
const requestUsage = ids.requestUsage ?? { outputChars: 0 };
|
|
957
1129
|
const replaySafety = new AttemptReplaySafety(session.sessionId);
|
|
958
1130
|
const failRunProtocol = (message, code) => {
|
|
959
1131
|
replaySafety.markBarrier("unknown-or-malformed-frame");
|
|
@@ -961,6 +1133,10 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
961
1133
|
sessionManager.close(session, "remote-error", error);
|
|
962
1134
|
throw error;
|
|
963
1135
|
};
|
|
1136
|
+
const rethrowTransportWriteFailure = (error) => {
|
|
1137
|
+
if (error instanceof CursorProviderError && error.origin !== "protocol")
|
|
1138
|
+
throw error;
|
|
1139
|
+
};
|
|
964
1140
|
// OpenCode cancels the ReadableStream between turns (see the cancel handler
|
|
965
1141
|
// in doStreamImpl). The frames iterator can still yield a final `done` after
|
|
966
1142
|
// the cancel lands — controller.enqueue on a cancelled controller throws.
|
|
@@ -1016,13 +1192,17 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1016
1192
|
}
|
|
1017
1193
|
};
|
|
1018
1194
|
/**
|
|
1019
|
-
* Cursor's streamed edit handshake
|
|
1020
|
-
* replacement through write_args.
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1195
|
+
* Cursor's streamed edit handshake needs the complete target before it sends
|
|
1196
|
+
* the replacement through write_args. OpenCode's ordinary read tool caps at
|
|
1197
|
+
* 50 KB, so forwarding this private prerequisite read teaches Cursor's editor
|
|
1198
|
+
* that a large file ends at the cap and produces a destructive replacement.
|
|
1199
|
+
*
|
|
1200
|
+
* Answer a correlated workspace-file read directly up to Cursor's own 50 MB
|
|
1201
|
+
* edit limit. Missing targets remain empty-file successes so creation can
|
|
1202
|
+
* continue. External and otherwise ineligible reads retain OpenCode's normal
|
|
1203
|
+
* permission-aware path.
|
|
1024
1204
|
*/
|
|
1025
|
-
const
|
|
1205
|
+
const recoverCorrelatedEditRead = (parsed, displayCallId) => {
|
|
1026
1206
|
if (!displayCallId ||
|
|
1027
1207
|
parsed.resultField !== "read_result" ||
|
|
1028
1208
|
parsed.toolName !== "read" ||
|
|
@@ -1043,8 +1223,54 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1043
1223
|
const workspaceRoot = workspaceRootFromRequestContext(session.requestContext);
|
|
1044
1224
|
const resolvePath = (value) => path.resolve(workspaceRoot, value);
|
|
1045
1225
|
const absolutePath = resolvePath(requestedPath);
|
|
1046
|
-
if (absolutePath !== resolvePath(editPath)
|
|
1226
|
+
if (absolutePath !== resolvePath(editPath))
|
|
1047
1227
|
return false;
|
|
1228
|
+
let exists = true;
|
|
1229
|
+
try {
|
|
1230
|
+
fs.lstatSync(absolutePath);
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
const code = error && typeof error === "object" && "code" in error
|
|
1234
|
+
? error.code
|
|
1235
|
+
: undefined;
|
|
1236
|
+
if (code !== "ENOENT")
|
|
1237
|
+
return false;
|
|
1238
|
+
exists = false;
|
|
1239
|
+
}
|
|
1240
|
+
if (exists) {
|
|
1241
|
+
// Do not bypass OpenCode's external-directory permission boundary, even
|
|
1242
|
+
// through a symlink rooted in the workspace.
|
|
1243
|
+
try {
|
|
1244
|
+
const realRoot = fs.realpathSync(workspaceRoot);
|
|
1245
|
+
const realTarget = fs.realpathSync(absolutePath);
|
|
1246
|
+
const relative = path.relative(realRoot, realTarget);
|
|
1247
|
+
if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1248
|
+
parsed.resultMetadata = {
|
|
1249
|
+
...parsed.resultMetadata,
|
|
1250
|
+
correlatedEditCallId: displayCallId,
|
|
1251
|
+
};
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
const frames = buildCompleteEditReadMessages(parsed.id, absolutePath, requestedPath);
|
|
1255
|
+
if (!frames)
|
|
1256
|
+
return false;
|
|
1257
|
+
for (const frame of frames)
|
|
1258
|
+
session.stream.write(frame);
|
|
1259
|
+
const editCall = session.editToolCalls?.get(displayCallId);
|
|
1260
|
+
if (editCall)
|
|
1261
|
+
editCall.completeRead = true;
|
|
1262
|
+
trace(`exec: correlated edit read completed directly id=${parsed.id} ` +
|
|
1263
|
+
`path=${JSON.stringify(requestedPath)}; awaiting write_args`);
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
parsed.resultMetadata = {
|
|
1268
|
+
...parsed.resultMetadata,
|
|
1269
|
+
correlatedEditCallId: displayCallId,
|
|
1270
|
+
};
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1048
1274
|
try {
|
|
1049
1275
|
for (const frame of buildExecClientMessages({
|
|
1050
1276
|
execId: parsed.id,
|
|
@@ -1061,7 +1287,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1061
1287
|
return true;
|
|
1062
1288
|
}
|
|
1063
1289
|
catch (e) {
|
|
1064
|
-
const error = new Error(`Failed to recover Cursor edit read
|
|
1290
|
+
const error = new Error(`Failed to recover Cursor edit read: ${e.message}`);
|
|
1065
1291
|
trace(`exec: edit read recovery FAILED ${error.message}`);
|
|
1066
1292
|
safeError(error);
|
|
1067
1293
|
sessionManager.close(session);
|
|
@@ -1079,7 +1305,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1079
1305
|
*
|
|
1080
1306
|
* Read-only by construction — write/edit targets are never checked, so
|
|
1081
1307
|
* legitimate file creation (path resolves, file absent) is never denied. Runs
|
|
1082
|
-
* after
|
|
1308
|
+
* after recoverCorrelatedEditRead, which has already converted the edit
|
|
1083
1309
|
* handshake read into an empty-file success. EACCES/EPERM and other stat
|
|
1084
1310
|
* errors fall through to OpenCode so a genuine permission decision stands.
|
|
1085
1311
|
*/
|
|
@@ -1141,9 +1367,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1141
1367
|
textStarted = true;
|
|
1142
1368
|
}
|
|
1143
1369
|
session.usageEstimate.outputTokens += estimateTokens(text.length);
|
|
1144
|
-
|
|
1145
|
-
requestUsage.outputChars += text.length;
|
|
1146
|
-
}
|
|
1370
|
+
safeEnqueue({ type: "text-delta", id: textId, delta: text });
|
|
1147
1371
|
};
|
|
1148
1372
|
const emitReasoning = (text) => {
|
|
1149
1373
|
if (!text)
|
|
@@ -1154,33 +1378,32 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1154
1378
|
reasoningStarted = true;
|
|
1155
1379
|
}
|
|
1156
1380
|
session.usageEstimate.outputTokens += estimateTokens(text.length);
|
|
1157
|
-
|
|
1158
|
-
requestUsage.outputChars += text.length;
|
|
1159
|
-
}
|
|
1381
|
+
safeEnqueue({ type: "reasoning-delta", id: reasoningId, delta: text });
|
|
1160
1382
|
};
|
|
1161
|
-
const emitFinish = (te, reason) => {
|
|
1383
|
+
const emitFinish = (te, reason, settledUsage, settledSource) => {
|
|
1162
1384
|
closeOpenSpans();
|
|
1163
|
-
// TurnEnded is request-local only when this Run did not previously stop at
|
|
1164
|
-
// a tool boundary. On a held multi-step Run Cursor aggregates every model
|
|
1165
|
-
// invocation into these counters; treating that billed total as the final
|
|
1166
|
-
// request's prompt size makes OpenCode compact after ordinary tasks.
|
|
1167
|
-
const cumulativeTurnEnded = !!te && session.hadToolCallBoundary === true;
|
|
1168
|
-
if (te && !cumulativeTurnEnded) {
|
|
1169
|
-
session.usageEstimate = {
|
|
1170
|
-
inputTokens: turnEndedCounter(te, "input_tokens"),
|
|
1171
|
-
outputTokens: turnEndedCounter(te, "output_tokens"),
|
|
1172
|
-
cacheRead: turnEndedCounter(te, "cache_read"),
|
|
1173
|
-
cacheWrite: turnEndedCounter(te, "cache_write"),
|
|
1174
|
-
reasoningTokens: turnEndedCounter(te, "reasoning_tokens"),
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
1385
|
const est = session.usageEstimate;
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1386
|
+
// OpenCode persists and sums every AI SDK finish. Cursor does not expose a
|
|
1387
|
+
// final context total at tool boundaries, so charging the growing estimate
|
|
1388
|
+
// there multiplies one held Run across all of its steps. Emit zero at
|
|
1389
|
+
// intermediate boundaries, then prefer checkpoint tokenDetails exactly once
|
|
1390
|
+
// when TurnEnded closes the held Run. Cursor CLI keeps context occupancy in
|
|
1391
|
+
// agentStore tokenDetails; it does not reinterpret TurnEnded as occupancy.
|
|
1392
|
+
const tokenDetails = te ? session.tokenDetails : undefined;
|
|
1393
|
+
const contextSource = tokenDetails
|
|
1394
|
+
? session.tokenDetailsFresh
|
|
1395
|
+
? "checkpoint-current-run"
|
|
1396
|
+
: "checkpoint-previous-turn"
|
|
1397
|
+
: undefined;
|
|
1398
|
+
const counters = te ? cursorUsageCountersFromTurnEnded(te) : undefined;
|
|
1399
|
+
const usage = settledUsage ?? (te
|
|
1400
|
+
? tokenDetails
|
|
1401
|
+
? buildLanguageModelV3UsageFromCounters(counters, { contextTotalTokens: tokenDetails.usedTokens })
|
|
1402
|
+
: emptyLanguageModelV3Usage()
|
|
1403
|
+
: emptyLanguageModelV3Usage());
|
|
1404
|
+
const providerMetadata = te
|
|
1405
|
+
? cursorTurnEndedProviderMetadata(te, tokenDetails, contextSource)
|
|
1406
|
+
: undefined;
|
|
1184
1407
|
const reasonLabel = typeof reason === "object" && reason && "unified" in reason
|
|
1185
1408
|
? String(reason.unified ?? "unknown")
|
|
1186
1409
|
: String(reason);
|
|
@@ -1194,7 +1417,13 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1194
1417
|
`rawOut=${te ? turnEndedCounter(te, "output_tokens") : est.outputTokens} ` +
|
|
1195
1418
|
`rawCacheRead=${te ? turnEndedCounter(te, "cache_read") : est.cacheRead} ` +
|
|
1196
1419
|
`rawCacheWrite=${te ? turnEndedCounter(te, "cache_write") : est.cacheWrite} ` +
|
|
1197
|
-
`source=${te
|
|
1420
|
+
`source=${te
|
|
1421
|
+
? settledSource ?? (contextSource ?? "unavailable")
|
|
1422
|
+
: "intermediate-zero"}`);
|
|
1423
|
+
if (counters) {
|
|
1424
|
+
trace(formatTurnUsageValidation(counters, usage, tokenDetails, contextSource));
|
|
1425
|
+
trace(formatCursorCacheDiagnostics(counters, tokenDetails, cacheDiagnostics.priorTokenDetails, cacheDiagnostics));
|
|
1426
|
+
}
|
|
1198
1427
|
safeEnqueue({
|
|
1199
1428
|
type: "finish",
|
|
1200
1429
|
usage,
|
|
@@ -1325,9 +1554,20 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1325
1554
|
if (checkpointRaw != null) {
|
|
1326
1555
|
const bytes = normalizeCheckpointBytes(checkpointRaw);
|
|
1327
1556
|
if (bytes && bytes.length > 0) {
|
|
1557
|
+
cacheDiagnostics.checkpointUpdates++;
|
|
1328
1558
|
setCheckpoint(session.conversationId, bytes);
|
|
1329
1559
|
session.resumeCheckpoint = Uint8Array.from(bytes);
|
|
1330
|
-
|
|
1560
|
+
const tokenDetails = decodeConversationTokenDetails(bytes);
|
|
1561
|
+
if (tokenDetails) {
|
|
1562
|
+
cacheDiagnostics.tokenDetailUpdates++;
|
|
1563
|
+
session.tokenDetails = tokenDetails;
|
|
1564
|
+
session.tokenDetailsFresh = true;
|
|
1565
|
+
}
|
|
1566
|
+
const context = tokenDetails
|
|
1567
|
+
? ` context=${tokenDetails.usedTokens}/${tokenDetails.maxTokens} ` +
|
|
1568
|
+
`categories=${formatCursorTokenCategories(tokenDetails)}`
|
|
1569
|
+
: " context=unavailable";
|
|
1570
|
+
trace(`checkpoint: stored ${bytes.length}B for conversationId=${session.conversationId}${context}`);
|
|
1331
1571
|
}
|
|
1332
1572
|
}
|
|
1333
1573
|
if (iu?.text_delta) {
|
|
@@ -1338,11 +1578,25 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1338
1578
|
}
|
|
1339
1579
|
else if (iu?.turn_ended) {
|
|
1340
1580
|
trace(`turn_ended raw wire fields: ${debugWalkTurnEnded(payload)}`);
|
|
1341
|
-
|
|
1581
|
+
const turnEnded = iu.turn_ended;
|
|
1582
|
+
if (session.openCodeSessionId) {
|
|
1583
|
+
await persistConversationState(session.cacheDir ?? opencodeGlobalCacheDir(), {
|
|
1584
|
+
sessionKey: session.openCodeSessionId,
|
|
1585
|
+
conversationId: session.conversationId,
|
|
1586
|
+
requestContext: session.requestContext,
|
|
1587
|
+
toolCatalog: session.toolCatalog ?? [],
|
|
1588
|
+
postCompactionRebase: session.postCompactionRebase,
|
|
1589
|
+
}).catch((error) => {
|
|
1590
|
+
trace(`conversation persistence: TurnEnded save failed ` +
|
|
1591
|
+
`sessionKey=${session.openCodeSessionId}: ${String(error)}`);
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
emitFinish(turnEnded, { unified: "stop", raw: undefined });
|
|
1342
1595
|
sessionManager.close(session);
|
|
1343
1596
|
return;
|
|
1344
1597
|
}
|
|
1345
1598
|
else if (iu?.tool_call_started) {
|
|
1599
|
+
cacheDiagnostics.displayToolCalls++;
|
|
1346
1600
|
// Stash Cursor display ToolCall until exec claims it, or completed bridges it.
|
|
1347
1601
|
const started = iu.tool_call_started;
|
|
1348
1602
|
const callId = typeof started.call_id === "string" ? started.call_id : "";
|
|
@@ -1429,7 +1683,14 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1429
1683
|
}
|
|
1430
1684
|
}
|
|
1431
1685
|
}
|
|
1686
|
+
else if (iu?.step_started) {
|
|
1687
|
+
cacheDiagnostics.stepStarts++;
|
|
1688
|
+
}
|
|
1689
|
+
else if (iu?.step_completed) {
|
|
1690
|
+
cacheDiagnostics.stepCompletes++;
|
|
1691
|
+
}
|
|
1432
1692
|
else if (esm) {
|
|
1693
|
+
cacheDiagnostics.execRequests++;
|
|
1433
1694
|
const esmId = esm.id ?? 0;
|
|
1434
1695
|
if (esm.request_context_args) {
|
|
1435
1696
|
// Server turn-setup probe (#10). Reply with full OpenCode-sourced context.
|
|
@@ -1445,10 +1706,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1445
1706
|
}
|
|
1446
1707
|
try {
|
|
1447
1708
|
traceRequestContextPaths(`exec request_context reply id=${esmId}`, session.requestContext);
|
|
1448
|
-
session.stream
|
|
1709
|
+
await writeWithBackpressure(session.stream, buildRequestContextResult(esmId, session.requestContext), `request-context reply id=${esmId}`);
|
|
1449
1710
|
trace(`exec request_context: replied`);
|
|
1450
1711
|
}
|
|
1451
|
-
catch {
|
|
1712
|
+
catch (error) {
|
|
1713
|
+
rethrowTransportWriteFailure(error);
|
|
1452
1714
|
failRunProtocol("Cursor request-context reply failed", RUN_REPLY_FAILED);
|
|
1453
1715
|
}
|
|
1454
1716
|
}
|
|
@@ -1461,10 +1723,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1461
1723
|
? stateArgs.server_identifiers.join(",")
|
|
1462
1724
|
: "";
|
|
1463
1725
|
try {
|
|
1464
|
-
session.stream
|
|
1726
|
+
await writeWithBackpressure(session.stream, buildMcpStateResult(esmId, stateArgs, session.requestContext), `MCP-state reply id=${esmId}`);
|
|
1465
1727
|
trace(`exec mcp_state: replied id=${esmId} requested=[${requested}]`);
|
|
1466
1728
|
}
|
|
1467
|
-
catch {
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
rethrowTransportWriteFailure(error);
|
|
1468
1731
|
failRunProtocol("Cursor MCP-state reply failed", RUN_REPLY_FAILED);
|
|
1469
1732
|
}
|
|
1470
1733
|
}
|
|
@@ -1475,10 +1738,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1475
1738
|
const args = esm.list_mcp_resources_exec_args;
|
|
1476
1739
|
const server = typeof args.server === "string" ? args.server : "";
|
|
1477
1740
|
try {
|
|
1478
|
-
session.stream
|
|
1741
|
+
await writeWithBackpressure(session.stream, buildListMcpResourcesFallback(esmId), `list-MCP-resources reply id=${esmId}`);
|
|
1479
1742
|
trace(`exec list_mcp_resources: replied id=${esmId} server=${server || "(all)"} success{resources:[]}`);
|
|
1480
1743
|
}
|
|
1481
|
-
catch {
|
|
1744
|
+
catch (error) {
|
|
1745
|
+
rethrowTransportWriteFailure(error);
|
|
1482
1746
|
failRunProtocol("Cursor list_mcp_resources reply failed", RUN_REPLY_FAILED);
|
|
1483
1747
|
}
|
|
1484
1748
|
}
|
|
@@ -1488,10 +1752,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1488
1752
|
const server = typeof args.server === "string" ? args.server : "";
|
|
1489
1753
|
const uri = typeof args.uri === "string" ? args.uri : "";
|
|
1490
1754
|
try {
|
|
1491
|
-
session.stream
|
|
1755
|
+
await writeWithBackpressure(session.stream, buildReadMcpResourceFallback(esmId, server, uri), `read-MCP-resource reply id=${esmId}`);
|
|
1492
1756
|
trace(`exec read_mcp_resource: replied id=${esmId} server=${server} uri=${uri} error{server not found}`);
|
|
1493
1757
|
}
|
|
1494
|
-
catch {
|
|
1758
|
+
catch (error) {
|
|
1759
|
+
rethrowTransportWriteFailure(error);
|
|
1495
1760
|
failRunProtocol("Cursor read_mcp_resource reply failed", RUN_REPLY_FAILED);
|
|
1496
1761
|
}
|
|
1497
1762
|
}
|
|
@@ -1509,6 +1774,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1509
1774
|
const editCall = displayCallId
|
|
1510
1775
|
? session.editToolCalls?.get(displayCallId)
|
|
1511
1776
|
: undefined;
|
|
1777
|
+
// A complete correlated edit read is trusted provenance: unlike an
|
|
1778
|
+
// ordinary capped read it cannot have appended our warning marker,
|
|
1779
|
+
// and the source itself may legitimately contain the marker text.
|
|
1780
|
+
if (!editCall?.completeRead)
|
|
1781
|
+
rejectPartialReadMutation(parsed);
|
|
1512
1782
|
if (editCall) {
|
|
1513
1783
|
const remapped = remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNameSet, editCall.path, workspaceRootFromRequestContext(session.requestContext));
|
|
1514
1784
|
if (parsed.resultField === "write_result" && displayCallId) {
|
|
@@ -1560,7 +1830,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1560
1830
|
return;
|
|
1561
1831
|
continue;
|
|
1562
1832
|
}
|
|
1563
|
-
if (
|
|
1833
|
+
if (recoverCorrelatedEditRead(parsed, displayCallId))
|
|
1564
1834
|
continue;
|
|
1565
1835
|
if (rejectMissingReadTarget(parsed)) {
|
|
1566
1836
|
if (displayCallId)
|
|
@@ -1621,11 +1891,12 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1621
1891
|
if (handled.outcome === "acknowledged") {
|
|
1622
1892
|
replaySafety.markBarrier("stateful-interaction");
|
|
1623
1893
|
}
|
|
1624
|
-
session.stream.
|
|
1894
|
+
await writeWithBackpressure(session.stream, handled.reply, `interaction reply id=${handled.id}`);
|
|
1625
1895
|
trace(`interaction_query: replied id=${handled.id} variant=${handled.variantName} ` +
|
|
1626
1896
|
`field=${handled.variantField} outcome=${handled.outcome}`);
|
|
1627
1897
|
}
|
|
1628
|
-
catch {
|
|
1898
|
+
catch (error) {
|
|
1899
|
+
rethrowTransportWriteFailure(error);
|
|
1629
1900
|
failRunProtocol("Cursor interaction reply failed", RUN_REPLY_FAILED);
|
|
1630
1901
|
}
|
|
1631
1902
|
}
|
|
@@ -1640,12 +1911,14 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1640
1911
|
const handled = handleKvServerMessage(kv, session);
|
|
1641
1912
|
if (handled) {
|
|
1642
1913
|
try {
|
|
1643
|
-
session.stream.
|
|
1914
|
+
await writeWithBackpressure(session.stream, handled.reply, `KV ${handled.kind}_blob reply id=${handled.id}`);
|
|
1644
1915
|
trace(`kv replied: kind=${handled.kind} id=${handled.id} blobId=${handled.blobIdHex.slice(0, 16)}… ` +
|
|
1645
1916
|
`found=${handled.found} echoed=${!!handled.echoed} ` +
|
|
1917
|
+
`replyBytes=${handled.reply.length} replyBlobBytes=${handled.replyBlobBytes} ` +
|
|
1646
1918
|
`sessionBlobs=${session.blobs.size} convBlobs=${conversationBlobCount(session.conversationId)}`);
|
|
1647
1919
|
}
|
|
1648
|
-
catch {
|
|
1920
|
+
catch (error) {
|
|
1921
|
+
rethrowTransportWriteFailure(error);
|
|
1649
1922
|
failRunProtocol("Cursor KV reply failed", RUN_REPLY_FAILED);
|
|
1650
1923
|
}
|
|
1651
1924
|
}
|
|
@@ -1655,7 +1928,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1655
1928
|
}
|
|
1656
1929
|
}
|
|
1657
1930
|
catch (e) {
|
|
1658
|
-
if (e instanceof
|
|
1931
|
+
if (e instanceof CursorProviderError)
|
|
1659
1932
|
throw e;
|
|
1660
1933
|
if (requiredChannel) {
|
|
1661
1934
|
failRunProtocol(`Cursor ${requiredChannel} request could not be handled`, RUN_REQUEST_UNSUPPORTED);
|
|
@@ -1822,6 +2095,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1822
2095
|
// leaving the turn with no file-editing guidance at all.
|
|
1823
2096
|
instructions.push("- Use OpenCode `apply_patch` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available. Cursor-native write and edit requests are accepted and converted to `apply_patch` automatically.");
|
|
1824
2097
|
}
|
|
2098
|
+
if (names.has("edit") || names.has("write") || names.has("apply_patch")) {
|
|
2099
|
+
instructions.push("- Never use a read result as complete file content when it says the output is capped, partial, or requires another offset. Read the remaining ranges first, or make a targeted edit/patch from complete context; do not pass a partial read back as a whole-file replacement.");
|
|
2100
|
+
}
|
|
1825
2101
|
return [
|
|
1826
2102
|
`OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
|
|
1827
2103
|
`Workspace root: ${JSON.stringify(workspaceRoot)}. Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.`,
|
|
@@ -1841,25 +2117,19 @@ export function estimateTokens(chars) {
|
|
|
1841
2117
|
return 0;
|
|
1842
2118
|
return Math.ceil(chars / 4);
|
|
1843
2119
|
}
|
|
1844
|
-
/**
|
|
1845
|
-
export function
|
|
1846
|
-
const serializedContent = prompt
|
|
1847
|
-
.map((message) => typeof message.content === "string"
|
|
1848
|
-
? message.content
|
|
1849
|
-
: (JSON.stringify(message.content) ?? ""))
|
|
1850
|
-
.join("\n");
|
|
1851
|
-
return estimateTokens(serializedContent.length);
|
|
1852
|
-
}
|
|
1853
|
-
/** Preserve exact Cursor counters as diagnostics when they are cumulative across a multi-step Run. */
|
|
1854
|
-
export function cursorTurnEndedProviderMetadata(te) {
|
|
2120
|
+
/** Preserve exact request-local Cursor counters as diagnostics. */
|
|
2121
|
+
export function cursorTurnEndedProviderMetadata(te, tokenDetails, contextSource) {
|
|
1855
2122
|
return {
|
|
1856
2123
|
cursor: {
|
|
1857
|
-
usageVersion:
|
|
2124
|
+
usageVersion: 3,
|
|
1858
2125
|
inputTokensRaw: turnEndedCounter(te, "input_tokens"),
|
|
1859
2126
|
outputTokensRaw: turnEndedCounter(te, "output_tokens"),
|
|
1860
2127
|
cacheReadRaw: turnEndedCounter(te, "cache_read"),
|
|
1861
2128
|
cacheWriteRaw: turnEndedCounter(te, "cache_write"),
|
|
1862
2129
|
reasoningTokensRaw: turnEndedCounter(te, "reasoning_tokens"),
|
|
2130
|
+
...(tokenDetails
|
|
2131
|
+
? { context: cursorContextUsageMetadata(tokenDetails, contextSource) }
|
|
2132
|
+
: {}),
|
|
1863
2133
|
},
|
|
1864
2134
|
};
|
|
1865
2135
|
}
|
|
@@ -2072,6 +2342,7 @@ function foldStreamParts(parts) {
|
|
|
2072
2342
|
let reasoning = "";
|
|
2073
2343
|
const content = [];
|
|
2074
2344
|
let finishReason = { unified: "stop", raw: undefined };
|
|
2345
|
+
let providerMetadata;
|
|
2075
2346
|
let usage = {
|
|
2076
2347
|
inputTokens: { total: undefined, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
|
|
2077
2348
|
outputTokens: { total: undefined, text: undefined, reasoning: undefined },
|
|
@@ -2092,11 +2363,18 @@ function foldStreamParts(parts) {
|
|
|
2092
2363
|
else if (part.type === "finish") {
|
|
2093
2364
|
finishReason = part.finishReason;
|
|
2094
2365
|
usage = part.usage;
|
|
2366
|
+
providerMetadata = part.providerMetadata;
|
|
2095
2367
|
}
|
|
2096
2368
|
}
|
|
2097
2369
|
if (reasoning)
|
|
2098
2370
|
content.unshift({ type: "reasoning", text: reasoning });
|
|
2099
2371
|
if (text)
|
|
2100
2372
|
content.unshift({ type: "text", text });
|
|
2101
|
-
return {
|
|
2373
|
+
return {
|
|
2374
|
+
content,
|
|
2375
|
+
finishReason,
|
|
2376
|
+
usage,
|
|
2377
|
+
warnings: [],
|
|
2378
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
2379
|
+
};
|
|
2102
2380
|
}
|