@rahularya01/pi-cursor 1.0.0 → 1.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 +18 -0
- package/README.md +28 -4
- package/package.json +8 -5
- package/src/auth/cli-credentials.ts +30 -5
- package/src/auth/consent.ts +27 -0
- package/src/auth/index.ts +12 -0
- package/src/client/index.ts +1 -1
- package/src/diagnostics/diagnostics.ts +17 -0
- package/src/index.ts +54 -11
- package/src/stream/config.ts +69 -0
- package/src/stream/context-normalize.ts +104 -0
- package/src/stream/index.ts +34 -3
- package/src/stream/model-routing.ts +100 -0
- package/src/stream/native-core.ts +97 -456
- package/src/stream/protocol.ts +41 -0
- package/src/stream/recovery.ts +454 -0
- package/tsconfig.json +1 -1
|
@@ -122,6 +122,48 @@ export type {
|
|
|
122
122
|
CursorParameterizedVariant,
|
|
123
123
|
} from "../client/cursor-wire.js";
|
|
124
124
|
|
|
125
|
+
import { getCursorAgentUrl as resolveCursorAgentUrl } from "./config.js";
|
|
126
|
+
import {
|
|
127
|
+
isContextModeSideChannelText as isContextModeSideChannelTextImpl,
|
|
128
|
+
frameContextModeSideChannel as frameContextModeSideChannelImpl,
|
|
129
|
+
normalizeMessagesForCursor as normalizeMessagesForCursorImpl,
|
|
130
|
+
textContent as textContentImpl,
|
|
131
|
+
contentHasImageParts as contentHasImagePartsImpl,
|
|
132
|
+
type OpenAIMessage as NormalizedOpenAIMessage,
|
|
133
|
+
} from "./context-normalize.js";
|
|
134
|
+
import {
|
|
135
|
+
resolveModelId as resolveModelIdImpl,
|
|
136
|
+
resolveRequestedModelId as resolveRequestedModelIdImpl,
|
|
137
|
+
type CursorNativeModelRouting as ExtractedCursorNativeModelRouting,
|
|
138
|
+
type CursorResolvableModel as ExtractedCursorResolvableModel,
|
|
139
|
+
type ResolvedCursorModelRouting as ExtractedResolvedCursorModelRouting,
|
|
140
|
+
} from "./model-routing.js";
|
|
141
|
+
import {
|
|
142
|
+
planRecovery as planRecoveryImpl,
|
|
143
|
+
wrapRecoveredToolResults as wrapRecoveredToolResultsImpl,
|
|
144
|
+
lostToolContinuationErrorBody as lostToolContinuationErrorBodyImpl,
|
|
145
|
+
formatLostToolContinuationDiagnostic as formatLostToolContinuationDiagnosticImpl,
|
|
146
|
+
lostToolContinuationMessage as lostToolContinuationMessageImpl,
|
|
147
|
+
bridgeKeyPrefix as bridgeKeyPrefixImpl,
|
|
148
|
+
fingerprintCompletedTurns as fingerprintCompletedTurnsImpl,
|
|
149
|
+
stripInFlightResults as stripInFlightResultsImpl,
|
|
150
|
+
clearStoredMidPauseMetadata as clearStoredMidPauseMetadataImpl,
|
|
151
|
+
type FullHistoryRebuildReason,
|
|
152
|
+
type RecoveryDecision as ExtractedRecoveryDecision,
|
|
153
|
+
type PlanRecoveryInput as ExtractedPlanRecoveryInput,
|
|
154
|
+
type LostToolContinuationDiagnosticInput as ExtractedLostToolContinuationDiagnosticInput,
|
|
155
|
+
type StoredConversation as ExtractedStoredConversation,
|
|
156
|
+
type ParsedTurn as ExtractedParsedTurn,
|
|
157
|
+
type ParsedToolCallStep as ExtractedParsedToolCallStep,
|
|
158
|
+
type ParsedAssistantTextStep as ExtractedParsedAssistantTextStep,
|
|
159
|
+
type ParsedTurnStep as ExtractedParsedTurnStep,
|
|
160
|
+
type ParsedToolResult as ExtractedParsedToolResult,
|
|
161
|
+
type ParsedImageContent as ExtractedParsedImageContent,
|
|
162
|
+
type ToolResultInfo as ExtractedToolResultInfo,
|
|
163
|
+
} from "./recovery.js";
|
|
164
|
+
import { enhanceCursorStreamError, isAuthErrorMessage } from "./protocol.js";
|
|
165
|
+
import { setLastRecoverySkipReason } from "../diagnostics/diagnostics.js";
|
|
166
|
+
|
|
125
167
|
// Cursor CLI's local-image path scales/compresses images to <= 5 MiB
|
|
126
168
|
// and accepts only jpeg/png/gif/webp by magic bytes.
|
|
127
169
|
const CURSOR_CLI_MAX_IMAGE_BYTES = 5_242_880;
|
|
@@ -132,55 +174,9 @@ const CURSOR_SUPPORTED_IMAGE_MIME_TYPES = new Set([
|
|
|
132
174
|
"image/webp",
|
|
133
175
|
]);
|
|
134
176
|
const MAX_OPENAI_REQUEST_BODY_BYTES = 25 * 1024 * 1024;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
function normalizeCursorUrl(value: unknown): string | undefined {
|
|
140
|
-
if (typeof value !== "string") return undefined;
|
|
141
|
-
const trimmed = value.trim();
|
|
142
|
-
if (!trimmed) return undefined;
|
|
143
|
-
try {
|
|
144
|
-
const url = new URL(trimmed);
|
|
145
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") return undefined;
|
|
146
|
-
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
147
|
-
url.search = "";
|
|
148
|
-
url.hash = "";
|
|
149
|
-
return url.toString().replace(/\/$/, "");
|
|
150
|
-
} catch {
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function readCursorCliAgentUrl(): string | undefined {
|
|
156
|
-
const configDir = process.env.CURSOR_CONFIG_DIR?.trim() || pathJoin(homedir(), ".cursor");
|
|
157
|
-
try {
|
|
158
|
-
const config = JSON.parse(readFileSync(pathJoin(configDir, "cli-config.json"), "utf8")) as {
|
|
159
|
-
serverConfigCache?: {
|
|
160
|
-
agentUrlConfig?: { agentnUrl?: unknown; agentUrl?: unknown };
|
|
161
|
-
};
|
|
162
|
-
};
|
|
163
|
-
return (
|
|
164
|
-
normalizeCursorUrl(config.serverConfigCache?.agentUrlConfig?.agentnUrl) ??
|
|
165
|
-
normalizeCursorUrl(config.serverConfigCache?.agentUrlConfig?.agentUrl)
|
|
166
|
-
);
|
|
167
|
-
} catch {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function getCursorAgentUrl(): string {
|
|
173
|
-
const envUrl =
|
|
174
|
-
normalizeCursorUrl(process.env.PI_CURSOR_AGENT_URL) ??
|
|
175
|
-
normalizeCursorUrl(process.env.CURSOR_AGENT_URL);
|
|
176
|
-
if (envUrl) {
|
|
177
|
-
cachedCursorAgentUrl = envUrl;
|
|
178
|
-
return envUrl;
|
|
179
|
-
}
|
|
180
|
-
if (cachedCursorAgentUrl) return cachedCursorAgentUrl;
|
|
181
|
-
cachedCursorAgentUrl = readCursorCliAgentUrl() ?? DEFAULT_CURSOR_AGENT_URL;
|
|
182
|
-
return cachedCursorAgentUrl;
|
|
183
|
-
}
|
|
177
|
+
// URL resolution lives in ./config.ts
|
|
178
|
+
export { getCursorAgentUrl } from "./config.js";
|
|
179
|
+
const getCursorAgentUrl = resolveCursorAgentUrl;
|
|
184
180
|
|
|
185
181
|
// ── Types ──
|
|
186
182
|
|
|
@@ -869,6 +865,7 @@ export function getProxyPort(): number | undefined {
|
|
|
869
865
|
return proxyPort;
|
|
870
866
|
}
|
|
871
867
|
|
|
868
|
+
/** @deprecated Internal OpenAI-compatible proxy path. Prefer native streamSimple. Not part of the public provider surface. */
|
|
872
869
|
export async function startProxy(getAccessToken: () => Promise<string>): Promise<number> {
|
|
873
870
|
proxyAccessTokenProvider = getAccessToken;
|
|
874
871
|
if (proxyServer && proxyPort) return proxyPort;
|
|
@@ -1007,15 +1004,10 @@ function readBody(req: IncomingMessage): Promise<string> {
|
|
|
1007
1004
|
|
|
1008
1005
|
// ── Native pi streamSimple provider ──
|
|
1009
1006
|
|
|
1010
|
-
export
|
|
1011
|
-
modelId: string;
|
|
1012
|
-
parameters?: CursorModelParameter[];
|
|
1013
|
-
requiresMaxMode?: boolean;
|
|
1014
|
-
requestedMaxMode?: boolean;
|
|
1015
|
-
}
|
|
1007
|
+
export type CursorNativeModelRouting = ExtractedCursorNativeModelRouting;
|
|
1016
1008
|
|
|
1017
1009
|
export interface CursorNativeStreamConfig {
|
|
1018
|
-
getAccessToken(): Promise<string>;
|
|
1010
|
+
getAccessToken(options?: { forceRefresh?: boolean }): Promise<string>;
|
|
1019
1011
|
getNoReasoningEffortByModelId?(): Map<string, string>;
|
|
1020
1012
|
getRawModelRoutingByModelId?(): Map<string, Record<string, CursorNativeModelRouting>>;
|
|
1021
1013
|
}
|
|
@@ -1380,55 +1372,32 @@ function nativeRequestParameterError(body: ChatCompletionRequest): string | unde
|
|
|
1380
1372
|
}
|
|
1381
1373
|
|
|
1382
1374
|
function lostToolContinuationMessage(): string {
|
|
1383
|
-
return
|
|
1375
|
+
return lostToolContinuationMessageImpl();
|
|
1384
1376
|
}
|
|
1385
1377
|
|
|
1386
|
-
export
|
|
1387
|
-
bridgeKey: string;
|
|
1388
|
-
hadStoredCheckpoint: boolean;
|
|
1389
|
-
skipReason?: string;
|
|
1390
|
-
}
|
|
1378
|
+
export type LostToolContinuationDiagnosticInput = ExtractedLostToolContinuationDiagnosticInput;
|
|
1391
1379
|
|
|
1392
1380
|
export function lostToolContinuationErrorBody(input: LostToolContinuationDiagnosticInput): {
|
|
1393
1381
|
error: Record<string, unknown>;
|
|
1394
1382
|
} {
|
|
1395
|
-
return
|
|
1396
|
-
error: {
|
|
1397
|
-
message: lostToolContinuationMessage(),
|
|
1398
|
-
type: "invalid_state_error",
|
|
1399
|
-
code: "tool_continuation_lost",
|
|
1400
|
-
hadStoredCheckpoint: input.hadStoredCheckpoint,
|
|
1401
|
-
bridgeKeyPrefix: bridgeKeyPrefix(input.bridgeKey),
|
|
1402
|
-
...(input.skipReason ? { skipReason: input.skipReason } : {}),
|
|
1403
|
-
},
|
|
1404
|
-
};
|
|
1383
|
+
return lostToolContinuationErrorBodyImpl(input);
|
|
1405
1384
|
}
|
|
1406
1385
|
|
|
1407
1386
|
function bridgeKeyPrefix(bridgeKey: string): string {
|
|
1408
|
-
return bridgeKey
|
|
1387
|
+
return bridgeKeyPrefixImpl(bridgeKey);
|
|
1409
1388
|
}
|
|
1410
1389
|
|
|
1411
1390
|
export function formatLostToolContinuationDiagnostic(
|
|
1412
1391
|
input: LostToolContinuationDiagnosticInput,
|
|
1413
1392
|
): string {
|
|
1414
|
-
|
|
1415
|
-
return (
|
|
1416
|
-
`[diagnostic: hadStoredCheckpoint=${input.hadStoredCheckpoint} ` +
|
|
1417
|
-
`bridgeKeyPrefix=${bridgeKeyPrefix(input.bridgeKey)}${skipReason}]`
|
|
1418
|
-
);
|
|
1393
|
+
return formatLostToolContinuationDiagnosticImpl(input);
|
|
1419
1394
|
}
|
|
1420
1395
|
|
|
1421
1396
|
export function wrapRecoveredToolResults(
|
|
1422
1397
|
toolResults: Array<Pick<ToolResultInfo, "toolCallId" | "content">>,
|
|
1423
1398
|
recoveryId: string = crypto.randomUUID(),
|
|
1424
1399
|
): string {
|
|
1425
|
-
|
|
1426
|
-
const endDelimiter = `[End recovered tool output recovery:${recoveryId}]`;
|
|
1427
|
-
const blocks = toolResults.map(
|
|
1428
|
-
(r) =>
|
|
1429
|
-
`${startDelimiter}\nTool call id: ${r.toolCallId}\nResult:\n${r.content}\n${endDelimiter}`,
|
|
1430
|
-
);
|
|
1431
|
-
return blocks.join("\n\n");
|
|
1400
|
+
return wrapRecoveredToolResultsImpl(toolResults, recoveryId);
|
|
1432
1401
|
}
|
|
1433
1402
|
|
|
1434
1403
|
function collectToolResultImages(toolResults: ToolResultInfo[]): ParsedImageContent[] {
|
|
@@ -1449,45 +1418,6 @@ function parsedTurnHasImages(turn: ParsedTurn): boolean {
|
|
|
1449
1418
|
return (turn.userImages?.length ?? 0) > 0;
|
|
1450
1419
|
}
|
|
1451
1420
|
|
|
1452
|
-
type FullHistoryRebuildReason = "no_checkpoint" | "synthesized_after_idle";
|
|
1453
|
-
|
|
1454
|
-
export type RecoveryDecision =
|
|
1455
|
-
| {
|
|
1456
|
-
kind: "recover";
|
|
1457
|
-
hadStoredCheckpoint: true;
|
|
1458
|
-
checkpoint: Uint8Array;
|
|
1459
|
-
conversationId: string;
|
|
1460
|
-
blobStore: Map<string, Uint8Array>;
|
|
1461
|
-
wrappedText: string;
|
|
1462
|
-
}
|
|
1463
|
-
| {
|
|
1464
|
-
kind: "rebuild_full_history";
|
|
1465
|
-
hadStoredCheckpoint: boolean;
|
|
1466
|
-
conversationId: string;
|
|
1467
|
-
completedTurns: ParsedTurn[];
|
|
1468
|
-
inFlightTurn: ParsedTurn;
|
|
1469
|
-
toolResults: ToolResultInfo[];
|
|
1470
|
-
blobStore: Map<string, Uint8Array>;
|
|
1471
|
-
wrappedText: string;
|
|
1472
|
-
rebuildReason: FullHistoryRebuildReason;
|
|
1473
|
-
}
|
|
1474
|
-
| {
|
|
1475
|
-
kind: "skip";
|
|
1476
|
-
reason:
|
|
1477
|
-
| "no_stored_conversation"
|
|
1478
|
-
| "no_midpause_snapshot"
|
|
1479
|
-
| "stale_checkpoint"
|
|
1480
|
-
| "midpause_turn_count_mismatch"
|
|
1481
|
-
| "midpause_history_fingerprint_mismatch"
|
|
1482
|
-
| "midpause_metadata_stale"
|
|
1483
|
-
| "no_inflight_tool_continuation"
|
|
1484
|
-
| "session_mismatch"
|
|
1485
|
-
| "pending_tool_call_mismatch";
|
|
1486
|
-
hadStoredCheckpoint: boolean;
|
|
1487
|
-
expected?: string[];
|
|
1488
|
-
received?: string[];
|
|
1489
|
-
};
|
|
1490
|
-
|
|
1491
1421
|
type FullHistoryRebuildDecision = Extract<RecoveryDecision, { kind: "rebuild_full_history" }>;
|
|
1492
1422
|
|
|
1493
1423
|
function logFullHistoryRebuild(
|
|
@@ -1526,166 +1456,14 @@ function logFullHistoryRebuild(
|
|
|
1526
1456
|
emitMetric("metric.cursor_provider.rebuild_full_history", metricFields);
|
|
1527
1457
|
}
|
|
1528
1458
|
|
|
1529
|
-
export
|
|
1530
|
-
|
|
1531
|
-
toolResults: ToolResultInfo[];
|
|
1532
|
-
completedTurns: ParsedTurn[];
|
|
1533
|
-
inFlightTurn?: ParsedTurn;
|
|
1534
|
-
rebuildReason?: FullHistoryRebuildReason;
|
|
1535
|
-
sessionId?: string;
|
|
1536
|
-
requestId: string;
|
|
1537
|
-
convKey: string;
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
function setsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
1541
|
-
return a.size === b.size && [...a].every((id) => b.has(id));
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
function skipRecovery(
|
|
1545
|
-
reason: Extract<RecoveryDecision, { kind: "skip" }>["reason"],
|
|
1546
|
-
hadStoredCheckpoint: boolean,
|
|
1547
|
-
expected?: string[],
|
|
1548
|
-
received?: string[],
|
|
1549
|
-
): RecoveryDecision {
|
|
1550
|
-
return {
|
|
1551
|
-
kind: "skip",
|
|
1552
|
-
reason,
|
|
1553
|
-
hadStoredCheckpoint,
|
|
1554
|
-
...(expected !== undefined ? { expected } : {}),
|
|
1555
|
-
...(received !== undefined ? { received } : {}),
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
function validateExactToolResultMatch(
|
|
1560
|
-
expected: string[],
|
|
1561
|
-
received: string[],
|
|
1562
|
-
): { ok: true } | { ok: false; expected: string[]; received: string[] } {
|
|
1563
|
-
const expectedSet = new Set(expected);
|
|
1564
|
-
const receivedSet = new Set(received);
|
|
1565
|
-
const hasDuplicates =
|
|
1566
|
-
expectedSet.size !== expected.length || receivedSet.size !== received.length;
|
|
1567
|
-
if (hasDuplicates || !setsEqual(expectedSet, receivedSet)) {
|
|
1568
|
-
return { ok: false, expected, received };
|
|
1569
|
-
}
|
|
1570
|
-
return { ok: true };
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function planFullHistoryRebuild(
|
|
1574
|
-
input: PlanRecoveryInput & { stored: StoredConversation },
|
|
1575
|
-
hadStoredCheckpoint: boolean,
|
|
1576
|
-
rebuildReason: FullHistoryRebuildReason,
|
|
1577
|
-
): RecoveryDecision {
|
|
1578
|
-
const pendingToolCalls = input.stored.midPausePendingToolCalls;
|
|
1579
|
-
if (!pendingToolCalls?.length) {
|
|
1580
|
-
return skipRecovery("no_midpause_snapshot", hadStoredCheckpoint);
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
if (input.stored.sessionScoped && input.stored.sessionId !== input.sessionId) {
|
|
1584
|
-
// Older session-scoped rows without a recorded sessionId fail closed here.
|
|
1585
|
-
return skipRecovery("session_mismatch", hadStoredCheckpoint);
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
const currentTurnCount = input.completedTurns.length;
|
|
1589
|
-
if (input.stored.midPauseTurnCount !== currentTurnCount) {
|
|
1590
|
-
clearStoredMidPauseMetadata(input.stored);
|
|
1591
|
-
return skipRecovery("midpause_turn_count_mismatch", hadStoredCheckpoint);
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
const currentHistoryFingerprint = fingerprintCompletedTurns(input.completedTurns);
|
|
1595
|
-
if (input.stored.midPauseHistoryFingerprint !== currentHistoryFingerprint) {
|
|
1596
|
-
clearStoredMidPauseMetadata(input.stored);
|
|
1597
|
-
return skipRecovery("midpause_history_fingerprint_mismatch", hadStoredCheckpoint);
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
const recordedAtMs = input.stored.midPauseRecordedAtMs;
|
|
1601
|
-
const maxAgeMs = resolveMidPauseRebuildMaxAgeMs(
|
|
1602
|
-
process.env.PI_CURSOR_MIDPAUSE_REBUILD_MAX_AGE_MS,
|
|
1603
|
-
);
|
|
1604
|
-
if (recordedAtMs === undefined || Date.now() - recordedAtMs > maxAgeMs) {
|
|
1605
|
-
clearStoredMidPauseMetadata(input.stored);
|
|
1606
|
-
return skipRecovery("midpause_metadata_stale", hadStoredCheckpoint);
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
const strippedInFlightTurn = input.inFlightTurn
|
|
1610
|
-
? stripInFlightResults(input.inFlightTurn)
|
|
1611
|
-
: undefined;
|
|
1612
|
-
const inFlightToolCallIds =
|
|
1613
|
-
strippedInFlightTurn?.steps
|
|
1614
|
-
.filter((step): step is ParsedToolCallStep => step.kind === "toolCall")
|
|
1615
|
-
.map((step) => step.toolCallId) ?? [];
|
|
1616
|
-
if (!strippedInFlightTurn || inFlightToolCallIds.length === 0 || input.toolResults.length === 0) {
|
|
1617
|
-
return skipRecovery("no_inflight_tool_continuation", hadStoredCheckpoint);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
const pendingIds = pendingToolCalls.map((c) => c.toolCallId);
|
|
1621
|
-
const receivedIds = input.toolResults.map((r) => r.toolCallId);
|
|
1622
|
-
const pendingVsReceived = validateExactToolResultMatch(pendingIds, receivedIds);
|
|
1623
|
-
const inFlightVsReceived = validateExactToolResultMatch(inFlightToolCallIds, receivedIds);
|
|
1624
|
-
if (!pendingVsReceived.ok) {
|
|
1625
|
-
return skipRecovery(
|
|
1626
|
-
"pending_tool_call_mismatch",
|
|
1627
|
-
hadStoredCheckpoint,
|
|
1628
|
-
pendingVsReceived.expected,
|
|
1629
|
-
pendingVsReceived.received,
|
|
1630
|
-
);
|
|
1631
|
-
}
|
|
1632
|
-
if (!inFlightVsReceived.ok) {
|
|
1633
|
-
return skipRecovery(
|
|
1634
|
-
"pending_tool_call_mismatch",
|
|
1635
|
-
hadStoredCheckpoint,
|
|
1636
|
-
inFlightVsReceived.expected,
|
|
1637
|
-
inFlightVsReceived.received,
|
|
1638
|
-
);
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
return {
|
|
1642
|
-
kind: "rebuild_full_history",
|
|
1643
|
-
hadStoredCheckpoint,
|
|
1644
|
-
conversationId: input.stored.conversationId,
|
|
1645
|
-
completedTurns: input.completedTurns,
|
|
1646
|
-
inFlightTurn: strippedInFlightTurn,
|
|
1647
|
-
toolResults: input.toolResults,
|
|
1648
|
-
blobStore: input.stored.blobStore,
|
|
1649
|
-
wrappedText: wrapRecoveredToolResults(input.toolResults),
|
|
1650
|
-
rebuildReason,
|
|
1651
|
-
};
|
|
1652
|
-
}
|
|
1459
|
+
export type RecoveryDecision = ExtractedRecoveryDecision;
|
|
1460
|
+
export type PlanRecoveryInput = ExtractedPlanRecoveryInput;
|
|
1653
1461
|
|
|
1654
1462
|
export function planRecovery(input: PlanRecoveryInput): RecoveryDecision {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
}
|
|
1659
|
-
if (!input.stored.checkpoint) {
|
|
1660
|
-
return planFullHistoryRebuild(
|
|
1661
|
-
input as PlanRecoveryInput & { stored: StoredConversation },
|
|
1662
|
-
false,
|
|
1663
|
-
input.rebuildReason ?? "no_checkpoint",
|
|
1664
|
-
);
|
|
1665
|
-
}
|
|
1666
|
-
discardStaleCheckpointIfNeeded(
|
|
1667
|
-
input.stored,
|
|
1668
|
-
input.completedTurns,
|
|
1669
|
-
input.requestId,
|
|
1670
|
-
input.convKey,
|
|
1671
|
-
);
|
|
1672
|
-
if (!input.stored.checkpoint) {
|
|
1673
|
-
return skipRecovery("stale_checkpoint", hadStoredCheckpointPreDiscard);
|
|
1674
|
-
}
|
|
1675
|
-
const expected = (input.stored.midPausePendingToolCalls ?? []).map((c) => c.toolCallId);
|
|
1676
|
-
const received = input.toolResults.map((r) => r.toolCallId);
|
|
1677
|
-
const match = validateExactToolResultMatch(expected, received);
|
|
1678
|
-
if (!match.ok) {
|
|
1679
|
-
return skipRecovery("pending_tool_call_mismatch", true, match.expected, match.received);
|
|
1680
|
-
}
|
|
1681
|
-
return {
|
|
1682
|
-
kind: "recover",
|
|
1683
|
-
hadStoredCheckpoint: true,
|
|
1684
|
-
checkpoint: input.stored.checkpoint,
|
|
1685
|
-
conversationId: input.stored.conversationId,
|
|
1686
|
-
blobStore: input.stored.blobStore,
|
|
1687
|
-
wrappedText: wrapRecoveredToolResults(input.toolResults),
|
|
1688
|
-
};
|
|
1463
|
+
return planRecoveryImpl({
|
|
1464
|
+
...input,
|
|
1465
|
+
discardStaleCheckpoint: discardStaleCheckpointIfNeeded,
|
|
1466
|
+
});
|
|
1689
1467
|
}
|
|
1690
1468
|
|
|
1691
1469
|
export function createCursorNativeStream(
|
|
@@ -1935,6 +1713,7 @@ async function handleCursorNativeRequest(
|
|
|
1935
1713
|
});
|
|
1936
1714
|
return;
|
|
1937
1715
|
}
|
|
1716
|
+
setLastRecoverySkipReason(decision.reason);
|
|
1938
1717
|
debugLog("bridge.recovery_skipped", {
|
|
1939
1718
|
requestId,
|
|
1940
1719
|
bridgeKey,
|
|
@@ -2265,8 +2044,15 @@ function writeNativeStream(
|
|
|
2265
2044
|
const endError = parseConnectEndStream(endStreamBytes);
|
|
2266
2045
|
if (endError) {
|
|
2267
2046
|
streamError = endError;
|
|
2268
|
-
|
|
2269
|
-
|
|
2047
|
+
const enhanced = enhanceCursorStreamError(endError.message);
|
|
2048
|
+
debugLog("native.stream.cursor_error", {
|
|
2049
|
+
requestId,
|
|
2050
|
+
modelId,
|
|
2051
|
+
message: endError.message,
|
|
2052
|
+
enhanced,
|
|
2053
|
+
isAuthError: isAuthErrorMessage(endError.message),
|
|
2054
|
+
});
|
|
2055
|
+
writer.error(enhanced, "error", state);
|
|
2270
2056
|
}
|
|
2271
2057
|
},
|
|
2272
2058
|
);
|
|
@@ -2684,34 +2470,11 @@ function fingerprintImage(image: ParsedImageContent): Record<string, unknown> {
|
|
|
2684
2470
|
}
|
|
2685
2471
|
|
|
2686
2472
|
export function fingerprintCompletedTurns(turns: ParsedTurn[]): string {
|
|
2687
|
-
|
|
2688
|
-
userText: turn.userText,
|
|
2689
|
-
userImages: (turn.userImages ?? []).map(fingerprintImage),
|
|
2690
|
-
steps: turn.steps.map((step) => {
|
|
2691
|
-
if (step.kind === "assistantText") return { kind: step.kind, text: step.text };
|
|
2692
|
-
return {
|
|
2693
|
-
kind: step.kind,
|
|
2694
|
-
toolCallId: step.toolCallId,
|
|
2695
|
-
toolName: step.toolName,
|
|
2696
|
-
arguments: stableNormalizeForHash(step.arguments),
|
|
2697
|
-
result: step.result
|
|
2698
|
-
? {
|
|
2699
|
-
content: step.result.content,
|
|
2700
|
-
isError: step.result.isError,
|
|
2701
|
-
images: (step.result.images ?? []).map(fingerprintImage),
|
|
2702
|
-
}
|
|
2703
|
-
: undefined,
|
|
2704
|
-
};
|
|
2705
|
-
}),
|
|
2706
|
-
}));
|
|
2707
|
-
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
2473
|
+
return fingerprintCompletedTurnsImpl(turns);
|
|
2708
2474
|
}
|
|
2709
2475
|
|
|
2710
2476
|
function clearStoredMidPauseMetadata(stored: StoredConversation): void {
|
|
2711
|
-
|
|
2712
|
-
delete stored.midPauseTurnCount;
|
|
2713
|
-
delete stored.midPauseHistoryFingerprint;
|
|
2714
|
-
delete stored.midPauseRecordedAtMs;
|
|
2477
|
+
clearStoredMidPauseMetadataImpl(stored);
|
|
2715
2478
|
}
|
|
2716
2479
|
|
|
2717
2480
|
function clearStoredCheckpoint(stored: StoredConversation, clearBlobStore = false): void {
|
|
@@ -2833,45 +2596,11 @@ export function handleBridgeCloseMidPause(input: HandleBridgeCloseMidPauseInput)
|
|
|
2833
2596
|
return { committed: true };
|
|
2834
2597
|
}
|
|
2835
2598
|
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
* e.g. model="gpt-5.4" + effort="medium" → "gpt-5.4-medium"
|
|
2839
|
-
* model="gpt-5.4-fast" + effort="high" → "gpt-5.4-high-fast"
|
|
2840
|
-
* If no effort provided, returns model as-is.
|
|
2841
|
-
*/
|
|
2842
|
-
export function resolveModelId(model: string, reasoningEffort?: string): string {
|
|
2843
|
-
if (!reasoningEffort) return model;
|
|
2844
|
-
|
|
2845
|
-
let suffix = "";
|
|
2846
|
-
let base = model;
|
|
2847
|
-
if (base.endsWith("-fast")) {
|
|
2848
|
-
suffix = "-fast";
|
|
2849
|
-
base = base.slice(0, -5);
|
|
2850
|
-
} else if (base.endsWith("-thinking")) {
|
|
2851
|
-
suffix = "-thinking";
|
|
2852
|
-
base = base.slice(0, -9);
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
return `${base}-${reasoningEffort}${suffix}`;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
export interface ResolvedCursorModelRouting extends CursorNativeModelRouting {
|
|
2859
|
-
maxMode: boolean;
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
type CursorModelRoutingByEffort = Record<string, CursorNativeModelRouting>;
|
|
2863
|
-
|
|
2864
|
-
export interface CursorResolvableModel {
|
|
2865
|
-
id: string;
|
|
2866
|
-
[key: string]: unknown;
|
|
2867
|
-
}
|
|
2599
|
+
export type ResolvedCursorModelRouting = ExtractedResolvedCursorModelRouting;
|
|
2600
|
+
export type CursorResolvableModel = ExtractedCursorResolvableModel;
|
|
2868
2601
|
|
|
2869
|
-
function
|
|
2870
|
-
return (
|
|
2871
|
-
!!value &&
|
|
2872
|
-
typeof value === "object" &&
|
|
2873
|
-
typeof (value as { modelId?: unknown }).modelId === "string"
|
|
2874
|
-
);
|
|
2602
|
+
export function resolveModelId(model: string, reasoningEffort?: string): string {
|
|
2603
|
+
return resolveModelIdImpl(model, reasoningEffort);
|
|
2875
2604
|
}
|
|
2876
2605
|
|
|
2877
2606
|
export function resolveRequestedModelId(
|
|
@@ -2882,45 +2611,22 @@ export function resolveRequestedModelId(
|
|
|
2882
2611
|
export function resolveRequestedModelId(
|
|
2883
2612
|
model: CursorResolvableModel,
|
|
2884
2613
|
reasoningEffort?: string,
|
|
2885
|
-
routingByModelId?: Map<
|
|
2614
|
+
routingByModelId?: Map<
|
|
2615
|
+
string,
|
|
2616
|
+
Record<string, CursorNativeModelRouting> | CursorNativeModelRouting
|
|
2617
|
+
>,
|
|
2886
2618
|
): ResolvedCursorModelRouting;
|
|
2887
2619
|
export function resolveRequestedModelId(
|
|
2888
2620
|
model: string | CursorResolvableModel,
|
|
2889
2621
|
reasoningEffort?: string,
|
|
2890
2622
|
cursorModelIdOrRoutingByModelId?:
|
|
2891
|
-
string | Map<string,
|
|
2623
|
+
string | Map<string, Record<string, CursorNativeModelRouting> | CursorNativeModelRouting>,
|
|
2892
2624
|
): string | ResolvedCursorModelRouting {
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
if (trimmedCursorModelId) return trimmedCursorModelId;
|
|
2899
|
-
return resolveModelId(model, reasoningEffort);
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
const routingByModelId =
|
|
2903
|
-
cursorModelIdOrRoutingByModelId instanceof Map ? cursorModelIdOrRoutingByModelId : undefined;
|
|
2904
|
-
const configured = routingByModelId?.get(model.id);
|
|
2905
|
-
let routing: CursorNativeModelRouting | undefined;
|
|
2906
|
-
if (isCursorModelRouting(configured)) {
|
|
2907
|
-
routing = configured;
|
|
2908
|
-
} else if (configured) {
|
|
2909
|
-
routing =
|
|
2910
|
-
configured[reasoningEffort ?? ""] ??
|
|
2911
|
-
configured.none ??
|
|
2912
|
-
configured.medium ??
|
|
2913
|
-
configured.high ??
|
|
2914
|
-
Object.values(configured).find(isCursorModelRouting);
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
return {
|
|
2918
|
-
modelId: routing?.modelId ?? resolveModelId(model.id, reasoningEffort),
|
|
2919
|
-
maxMode: Boolean(routing?.requestedMaxMode ?? routing?.requiresMaxMode),
|
|
2920
|
-
parameters: routing?.parameters,
|
|
2921
|
-
requestedMaxMode: routing?.requestedMaxMode,
|
|
2922
|
-
requiresMaxMode: routing?.requiresMaxMode,
|
|
2923
|
-
};
|
|
2625
|
+
return resolveRequestedModelIdImpl(
|
|
2626
|
+
model as any,
|
|
2627
|
+
reasoningEffort,
|
|
2628
|
+
cursorModelIdOrRoutingByModelId as any,
|
|
2629
|
+
);
|
|
2924
2630
|
}
|
|
2925
2631
|
|
|
2926
2632
|
function deriveRequestLockKey(body: ChatCompletionRequest): string {
|
|
@@ -3260,6 +2966,7 @@ async function handleChatCompletion(
|
|
|
3260
2966
|
}
|
|
3261
2967
|
return;
|
|
3262
2968
|
}
|
|
2969
|
+
setLastRecoverySkipReason(decision.reason);
|
|
3263
2970
|
debugLog("bridge.recovery_skipped", {
|
|
3264
2971
|
requestId,
|
|
3265
2972
|
bridgeKey,
|
|
@@ -3680,90 +3387,19 @@ function cloneParsedImage(image: ParsedImageContent): ParsedImageContent {
|
|
|
3680
3387
|
}
|
|
3681
3388
|
|
|
3682
3389
|
function stripInFlightResults(turn: ParsedTurn): ParsedTurn {
|
|
3683
|
-
return
|
|
3684
|
-
userText: turn.userText,
|
|
3685
|
-
steps: turn.steps.map((step) => {
|
|
3686
|
-
if (step.kind === "assistantText") return { kind: "assistantText", text: step.text };
|
|
3687
|
-
return {
|
|
3688
|
-
kind: "toolCall",
|
|
3689
|
-
toolCallId: step.toolCallId,
|
|
3690
|
-
toolName: step.toolName,
|
|
3691
|
-
arguments: clonePlainValue(step.arguments) as Record<string, unknown>,
|
|
3692
|
-
};
|
|
3693
|
-
}),
|
|
3694
|
-
...(turn.userImages?.length ? { userImages: turn.userImages.map(cloneParsedImage) } : {}),
|
|
3695
|
-
};
|
|
3390
|
+
return stripInFlightResultsImpl(turn);
|
|
3696
3391
|
}
|
|
3697
3392
|
|
|
3698
|
-
/**
|
|
3699
|
-
* context-mode (and similar extensions) inject routing / resume / memory as a
|
|
3700
|
-
* trailing `role: "user"` message. Cursor's wire path collapses history into
|
|
3701
|
-
* turns and treats the *last* user message as the live task, so that injection
|
|
3702
|
-
* becomes the model request instead of the real user prompt — models then run
|
|
3703
|
-
* compaction recovery (ctx_doctor / ctx_stats / "what should we investigate")
|
|
3704
|
-
* instead of doing the work.
|
|
3705
|
-
*
|
|
3706
|
-
* Fold pure side-channel user messages into the system prompt and keep the
|
|
3707
|
-
* real user turns as the task.
|
|
3708
|
-
*/
|
|
3709
|
-
const CONTEXT_MODE_SIDE_CHANNEL_PRIORITY =
|
|
3710
|
-
"Provider infrastructure context only. Prioritize the user's actual request above. " +
|
|
3711
|
-
"Do not run compaction recovery, session investigation, or ctx_doctor/ctx_stats rituals " +
|
|
3712
|
-
"unless the user explicitly asked for that.";
|
|
3713
|
-
|
|
3714
3393
|
export function isContextModeSideChannelText(text: string): boolean {
|
|
3715
|
-
|
|
3716
|
-
if (!t) return false;
|
|
3717
|
-
return (
|
|
3718
|
-
/^context-mode active\b/i.test(t) ||
|
|
3719
|
-
t.includes("<session_state") ||
|
|
3720
|
-
t.includes("<session_resume") ||
|
|
3721
|
-
t.includes("<active_memory>") ||
|
|
3722
|
-
t.includes("Hierarchy: ctx_batch_execute") ||
|
|
3723
|
-
/<\/?session_mode\b/i.test(t)
|
|
3724
|
-
);
|
|
3394
|
+
return isContextModeSideChannelTextImpl(text);
|
|
3725
3395
|
}
|
|
3726
3396
|
|
|
3727
3397
|
export function frameContextModeSideChannel(text: string): string {
|
|
3728
|
-
return (
|
|
3729
|
-
`<provider_context source="context-mode">\n${text.trim()}\n</provider_context>\n\n` +
|
|
3730
|
-
CONTEXT_MODE_SIDE_CHANNEL_PRIORITY
|
|
3731
|
-
);
|
|
3398
|
+
return frameContextModeSideChannelImpl(text);
|
|
3732
3399
|
}
|
|
3733
3400
|
|
|
3734
3401
|
export function normalizeMessagesForCursor(messages: OpenAIMessage[]): OpenAIMessage[] {
|
|
3735
|
-
|
|
3736
|
-
const sideParts: string[] = [];
|
|
3737
|
-
const rest: OpenAIMessage[] = [];
|
|
3738
|
-
|
|
3739
|
-
for (const msg of messages) {
|
|
3740
|
-
if (msg.role === "system") {
|
|
3741
|
-
const text = textContent(msg.content);
|
|
3742
|
-
if (text) systemParts.push(text);
|
|
3743
|
-
continue;
|
|
3744
|
-
}
|
|
3745
|
-
|
|
3746
|
-
if (msg.role === "user") {
|
|
3747
|
-
const text = textContent(msg.content);
|
|
3748
|
-
// Keep multimodal user turns intact — only pure text side-channels move.
|
|
3749
|
-
if (isContextModeSideChannelText(text) && !contentHasImageParts(msg.content)) {
|
|
3750
|
-
sideParts.push(text);
|
|
3751
|
-
continue;
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
|
|
3755
|
-
rest.push(msg);
|
|
3756
|
-
}
|
|
3757
|
-
|
|
3758
|
-
if (sideParts.length === 0) {
|
|
3759
|
-
// Still collapse multiple system messages for a stable shape.
|
|
3760
|
-
if (systemParts.length === 0) return messages;
|
|
3761
|
-
return [{ role: "system", content: systemParts.join("\n") }, ...rest];
|
|
3762
|
-
}
|
|
3763
|
-
|
|
3764
|
-
const framed = frameContextModeSideChannel(sideParts.join("\n\n"));
|
|
3765
|
-
const system = systemParts.length > 0 ? `${systemParts.join("\n")}\n\n${framed}` : framed;
|
|
3766
|
-
return [{ role: "system", content: system }, ...rest];
|
|
3402
|
+
return normalizeMessagesForCursorImpl(messages as NormalizedOpenAIMessage[]) as OpenAIMessage[];
|
|
3767
3403
|
}
|
|
3768
3404
|
|
|
3769
3405
|
export function parseMessages(
|
|
@@ -4392,7 +4028,12 @@ function processServerMessage(
|
|
|
4392
4028
|
const query = msg.message.value as InteractionQuery;
|
|
4393
4029
|
const handled = handleCursorWebFetchInteractionQuery(query, sendFrame);
|
|
4394
4030
|
if (!handled) {
|
|
4395
|
-
debugLog("native.interaction_query.unhandled", {
|
|
4031
|
+
debugLog("native.interaction_query.unhandled", {
|
|
4032
|
+
id: query.id,
|
|
4033
|
+
queryCase: query.query.case,
|
|
4034
|
+
hint: "Unhandled Cursor InteractionQuery — wire may have new fields; check clientVersion via /cursor.doctor",
|
|
4035
|
+
clientVersion: process.env.PI_CURSOR_CLIENT_VERSION || "default",
|
|
4036
|
+
});
|
|
4396
4037
|
}
|
|
4397
4038
|
return handled;
|
|
4398
4039
|
}
|