@bitkyc08/opencodex 2.28.0 → 2.29.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 +9 -1
- package/gui/dist/assets/index-BNESwCzn.js +102 -0
- package/gui/dist/assets/index-CH7ncHCC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +3 -1
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/discovery.ts +25 -0
- package/src/adapters/cursor/live-transport.ts +19 -2
- package/src/adapters/cursor/native-exec.ts +47 -2
- package/src/adapters/cursor/protobuf-request.ts +80 -23
- package/src/adapters/cursor/request-builder.ts +119 -5
- package/src/adapters/cursor/transport.ts +5 -0
- package/src/adapters/cursor/types.ts +13 -0
- package/src/adapters/cursor.ts +109 -3
- package/src/adapters/google-antigravity-replay.ts +31 -4
- package/src/adapters/google.ts +323 -38
- package/src/adapters/openai-chat.ts +19 -0
- package/src/adapters/openai-responses.ts +218 -37
- package/src/bridge.ts +38 -20
- package/src/claude/desktop-3p.ts +15 -6
- package/src/cli/agent.ts +44 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/dispatch.ts +24 -1
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +40 -38
- package/src/cli/integrations.ts +1 -1
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +119 -20
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/inject.ts +46 -14
- package/src/codex/journal.ts +22 -8
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +134 -66
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/registry.ts +25 -2
- package/src/integrations/writer.ts +32 -1
- package/src/lab/public/signature.ts +25 -1
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/redact.ts +2 -2
- package/src/oauth/log.ts +3 -1
- package/src/providers/derive.ts +9 -0
- package/src/providers/fastwire.ts +24 -18
- package/src/providers/openai-tiers.ts +60 -1
- package/src/providers/registry.ts +14 -7
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/responses/compaction.ts +18 -0
- package/src/responses/custom-tool-compat.ts +70 -5
- package/src/responses/namespace-tool-compat.ts +356 -0
- package/src/responses/parser.ts +2 -2
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/reasoning-replay-cache.ts +125 -7
- package/src/responses/spill-store.ts +6 -1
- package/src/responses/state.ts +11 -0
- package/src/router.ts +14 -0
- package/src/routing/compatibility/behavior.ts +1 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +57 -9
- package/src/server/management/config-routes.ts +134 -15
- package/src/server/management/integration-routes.ts +8 -55
- package/src/server/management/model-routes.ts +23 -1
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/vision-sidecar-options.ts +18 -11
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/responses/core.ts +588 -78
- package/src/server/responses/responses-field-backfill.ts +7 -11
- package/src/server/responses/terminal-guard.ts +22 -11
- package/src/server/responses-custom-tool-repair.ts +3 -1
- package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
- package/src/server/responses-tool-search-repair.ts +64 -14
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/types/config.ts +26 -5
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +26 -0
- package/src/types.ts +1 -0
- package/src/usage/log.ts +2 -0
- package/src/vision/index.ts +8 -9
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +139 -12
- package/src/web-search/loop.ts +45 -5
- package/src/web-search/parse.ts +34 -23
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-D2sP-biU.js +0 -102
- package/gui/dist/assets/index-DQsMZzI5.css +0 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { fromBinary } from "@bufbuild/protobuf";
|
|
3
|
+
import { ConversationStateStructureSchema, type ConversationStateStructure } from "./gen/agent_pb";
|
|
4
|
+
import {
|
|
5
|
+
createCursorBlobCheckpointLease,
|
|
6
|
+
pinCursorBlobIdsForCheckpoint,
|
|
7
|
+
releaseCursorBlobRequestScope,
|
|
8
|
+
type CursorBlobRequestScopeToken,
|
|
9
|
+
} from "./native-exec";
|
|
10
|
+
|
|
11
|
+
export const CURSOR_CHECKPOINT_TTL_MS = 15 * 60_000;
|
|
12
|
+
export const CURSOR_CHECKPOINT_MAX_ENTRIES = 64;
|
|
13
|
+
export const CURSOR_CHECKPOINT_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
export type CursorCheckpointInvalidationReason =
|
|
16
|
+
| "missing_ref"
|
|
17
|
+
| "expired"
|
|
18
|
+
| "decode_failed"
|
|
19
|
+
| "conversation_changed"
|
|
20
|
+
| "identity_changed"
|
|
21
|
+
| "model_changed"
|
|
22
|
+
| "compaction"
|
|
23
|
+
| "trailing_tool_result"
|
|
24
|
+
| "force_fresh"
|
|
25
|
+
| "upstream_invalid_argument"
|
|
26
|
+
| "lineage_mismatch";
|
|
27
|
+
|
|
28
|
+
export interface CursorCheckpointSnapshot {
|
|
29
|
+
ref: string;
|
|
30
|
+
conversationId: string;
|
|
31
|
+
identityScope: string;
|
|
32
|
+
modelId: string;
|
|
33
|
+
checkpointBytes: Uint8Array;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
lastAccessAt: number;
|
|
36
|
+
blobLease?: CursorBlobRequestScopeToken;
|
|
37
|
+
coveredMessageCount?: number;
|
|
38
|
+
prefixDigest?: string;
|
|
39
|
+
systemDigest?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CursorCheckpointStore {
|
|
43
|
+
snapshots: Map<string, CursorCheckpointSnapshot>;
|
|
44
|
+
prefixIndex: Map<string, Set<string>>;
|
|
45
|
+
totalBytes: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const store: CursorCheckpointStore = {
|
|
49
|
+
snapshots: new Map(),
|
|
50
|
+
prefixIndex: new Map(),
|
|
51
|
+
totalBytes: 0,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let nowFn = (): number => Date.now();
|
|
55
|
+
let scheduleFn = (fn: () => void, ms: number): ReturnType<typeof setTimeout> => {
|
|
56
|
+
const timer = setTimeout(fn, ms);
|
|
57
|
+
timer.unref?.();
|
|
58
|
+
return timer;
|
|
59
|
+
};
|
|
60
|
+
let clearScheduleFn = (timer: ReturnType<typeof setTimeout>): void => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
};
|
|
63
|
+
let pruneTimer: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
+
|
|
65
|
+
function now(): number {
|
|
66
|
+
return nowFn();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function installCursorCheckpointClockForTests(input: {
|
|
70
|
+
now?: () => number;
|
|
71
|
+
schedule?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
72
|
+
clear?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
73
|
+
}): void {
|
|
74
|
+
if (input.now) nowFn = input.now;
|
|
75
|
+
if (input.schedule) scheduleFn = input.schedule;
|
|
76
|
+
if (input.clear) clearScheduleFn = input.clear;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resetCursorCheckpointClockForTests(): void {
|
|
80
|
+
nowFn = () => Date.now();
|
|
81
|
+
scheduleFn = (fn, ms) => {
|
|
82
|
+
const timer = setTimeout(fn, ms);
|
|
83
|
+
timer.unref?.();
|
|
84
|
+
return timer;
|
|
85
|
+
};
|
|
86
|
+
clearScheduleFn = timer => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
};
|
|
89
|
+
stopPruneTimer();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stopPruneTimer(): void {
|
|
93
|
+
if (pruneTimer !== undefined) clearScheduleFn(pruneTimer);
|
|
94
|
+
pruneTimer = undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function schedulePrune(at = now()): void {
|
|
98
|
+
stopPruneTimer();
|
|
99
|
+
let nextExpiry = Number.POSITIVE_INFINITY;
|
|
100
|
+
for (const snapshot of store.snapshots.values()) {
|
|
101
|
+
nextExpiry = Math.min(nextExpiry, snapshot.lastAccessAt + CURSOR_CHECKPOINT_TTL_MS);
|
|
102
|
+
}
|
|
103
|
+
if (!Number.isFinite(nextExpiry)) return;
|
|
104
|
+
pruneTimer = scheduleFn(() => {
|
|
105
|
+
pruneTimer = undefined;
|
|
106
|
+
prune();
|
|
107
|
+
schedulePrune();
|
|
108
|
+
}, Math.max(0, nextExpiry - at));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function prune(at = now()): void {
|
|
112
|
+
for (const [ref, snapshot] of store.snapshots) {
|
|
113
|
+
if (at - snapshot.lastAccessAt > CURSOR_CHECKPOINT_TTL_MS) deleteSnapshot(ref);
|
|
114
|
+
}
|
|
115
|
+
while (store.snapshots.size > CURSOR_CHECKPOINT_MAX_ENTRIES || store.totalBytes > CURSOR_CHECKPOINT_MAX_TOTAL_BYTES) {
|
|
116
|
+
const oldest = store.snapshots.keys().next().value;
|
|
117
|
+
if (oldest === undefined) break;
|
|
118
|
+
deleteSnapshot(oldest);
|
|
119
|
+
}
|
|
120
|
+
schedulePrune(at);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function indexPrefix(digest: string | undefined, ref: string): void {
|
|
124
|
+
if (!digest) return;
|
|
125
|
+
const refs = store.prefixIndex.get(digest) ?? new Set<string>();
|
|
126
|
+
refs.add(ref);
|
|
127
|
+
store.prefixIndex.set(digest, refs);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function unindexPrefix(digest: string | undefined, ref: string): void {
|
|
131
|
+
if (!digest) return;
|
|
132
|
+
const refs = store.prefixIndex.get(digest);
|
|
133
|
+
if (!refs) return;
|
|
134
|
+
refs.delete(ref);
|
|
135
|
+
if (refs.size === 0) store.prefixIndex.delete(digest);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function deleteSnapshot(ref: string): void {
|
|
139
|
+
const existing = store.snapshots.get(ref);
|
|
140
|
+
if (!existing) return;
|
|
141
|
+
if (existing.blobLease) releaseCursorBlobRequestScope(existing.blobLease);
|
|
142
|
+
unindexPrefix(existing.prefixDigest, ref);
|
|
143
|
+
store.snapshots.delete(ref);
|
|
144
|
+
store.totalBytes = Math.max(0, store.totalBytes - existing.checkpointBytes.byteLength);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function collectStateBlobIds(state: ConversationStateStructure, ids: Uint8Array[]): void {
|
|
148
|
+
ids.push(
|
|
149
|
+
...state.rootPromptMessagesJson,
|
|
150
|
+
...state.turns,
|
|
151
|
+
...state.turnsOld,
|
|
152
|
+
...state.todos,
|
|
153
|
+
...state.summaryArchives,
|
|
154
|
+
);
|
|
155
|
+
if (state.summary) ids.push(state.summary);
|
|
156
|
+
if (state.summaryArchive) ids.push(state.summaryArchive);
|
|
157
|
+
if (state.plan) ids.push(state.plan);
|
|
158
|
+
for (const value of Object.values(state.fileStates)) ids.push(value);
|
|
159
|
+
for (const value of Object.values(state.fileStatesV2)) {
|
|
160
|
+
if (value.content) ids.push(value.content);
|
|
161
|
+
if (value.initialContent) ids.push(value.initialContent);
|
|
162
|
+
}
|
|
163
|
+
for (const nested of Object.values(state.subagentStates)) {
|
|
164
|
+
if (nested.conversationState) collectStateBlobIds(nested.conversationState, ids);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectCheckpointBlobIds(checkpointBytes: Uint8Array): Uint8Array[] | undefined {
|
|
169
|
+
try {
|
|
170
|
+
const ids: Uint8Array[] = [];
|
|
171
|
+
collectStateBlobIds(fromBinary(ConversationStateStructureSchema, checkpointBytes), ids);
|
|
172
|
+
return ids.filter(id => id.byteLength > 0);
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function cursorCheckpointRefHash(ref: string): string {
|
|
179
|
+
return createHash("sha256").update("ocx:cursor:ckpt-ref:").update(ref).digest("hex").slice(0, 16);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function commitCursorCheckpoint(input: {
|
|
183
|
+
conversationId: string;
|
|
184
|
+
identityScope?: string;
|
|
185
|
+
modelId: string;
|
|
186
|
+
checkpointBytes: Uint8Array;
|
|
187
|
+
coveredMessageCount?: number;
|
|
188
|
+
prefixDigest?: string;
|
|
189
|
+
systemDigest?: string;
|
|
190
|
+
}): string | undefined {
|
|
191
|
+
if (!input.conversationId || !input.modelId || input.checkpointBytes.byteLength === 0) return undefined;
|
|
192
|
+
if (input.checkpointBytes.byteLength > CURSOR_CHECKPOINT_MAX_TOTAL_BYTES) return undefined;
|
|
193
|
+
prune();
|
|
194
|
+
const createdAt = now();
|
|
195
|
+
const ref = createHash("sha256")
|
|
196
|
+
.update("ocx:cursor:ckpt:")
|
|
197
|
+
.update(input.conversationId)
|
|
198
|
+
.update("|")
|
|
199
|
+
.update(input.identityScope?.trim() || "local")
|
|
200
|
+
.update("|")
|
|
201
|
+
.update(input.modelId)
|
|
202
|
+
.update("|")
|
|
203
|
+
.update(String(createdAt))
|
|
204
|
+
.update("|")
|
|
205
|
+
.update(input.checkpointBytes)
|
|
206
|
+
.digest("hex")
|
|
207
|
+
.slice(0, 32);
|
|
208
|
+
const snapshot: CursorCheckpointSnapshot = {
|
|
209
|
+
ref,
|
|
210
|
+
conversationId: input.conversationId,
|
|
211
|
+
identityScope: input.identityScope?.trim() || "local",
|
|
212
|
+
modelId: input.modelId,
|
|
213
|
+
checkpointBytes: input.checkpointBytes.slice(),
|
|
214
|
+
createdAt,
|
|
215
|
+
lastAccessAt: createdAt,
|
|
216
|
+
...(input.coveredMessageCount !== undefined ? { coveredMessageCount: input.coveredMessageCount } : {}),
|
|
217
|
+
...(input.prefixDigest ? { prefixDigest: input.prefixDigest } : {}),
|
|
218
|
+
...(input.systemDigest ? { systemDigest: input.systemDigest } : {}),
|
|
219
|
+
};
|
|
220
|
+
const blobIds = collectCheckpointBlobIds(input.checkpointBytes);
|
|
221
|
+
if (blobIds === undefined) return undefined;
|
|
222
|
+
if (blobIds.length > 0) {
|
|
223
|
+
const lease = createCursorBlobCheckpointLease(ref);
|
|
224
|
+
if (!pinCursorBlobIdsForCheckpoint(blobIds, lease)) {
|
|
225
|
+
releaseCursorBlobRequestScope(lease);
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
snapshot.blobLease = lease;
|
|
229
|
+
}
|
|
230
|
+
deleteSnapshot(ref);
|
|
231
|
+
store.snapshots.set(ref, snapshot);
|
|
232
|
+
store.totalBytes += snapshot.checkpointBytes.byteLength;
|
|
233
|
+
indexPrefix(snapshot.prefixDigest, ref);
|
|
234
|
+
prune(createdAt);
|
|
235
|
+
return store.snapshots.has(ref) ? ref : undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function getCursorCheckpointForPrefix(input: {
|
|
239
|
+
prefixDigest: string;
|
|
240
|
+
systemDigest: string;
|
|
241
|
+
coveredMessageCount: number;
|
|
242
|
+
identityScope?: string;
|
|
243
|
+
modelId: string;
|
|
244
|
+
}): CursorCheckpointSnapshot | undefined {
|
|
245
|
+
prune();
|
|
246
|
+
const refs = store.prefixIndex.get(input.prefixDigest);
|
|
247
|
+
if (!refs || refs.size !== 1) return undefined;
|
|
248
|
+
const [ref] = refs;
|
|
249
|
+
if (!ref) return undefined;
|
|
250
|
+
const snapshot = getCursorCheckpoint(ref);
|
|
251
|
+
if (!snapshot) return undefined;
|
|
252
|
+
const identityScope = input.identityScope?.trim() || "local";
|
|
253
|
+
if (snapshot.systemDigest !== input.systemDigest) return undefined;
|
|
254
|
+
if (snapshot.coveredMessageCount !== input.coveredMessageCount) return undefined;
|
|
255
|
+
if (snapshot.identityScope !== identityScope) return undefined;
|
|
256
|
+
if (snapshot.modelId !== input.modelId) return undefined;
|
|
257
|
+
return snapshot;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function getLatestCursorCheckpoint(
|
|
261
|
+
match: (snapshot: CursorCheckpointSnapshot) => boolean,
|
|
262
|
+
): CursorCheckpointSnapshot | undefined {
|
|
263
|
+
prune();
|
|
264
|
+
let found: CursorCheckpointSnapshot | undefined;
|
|
265
|
+
for (const snapshot of store.snapshots.values()) {
|
|
266
|
+
if (match(snapshot)) found = snapshot;
|
|
267
|
+
}
|
|
268
|
+
return found ? getCursorCheckpoint(found.ref) : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function getCursorCheckpoint(ref: string | undefined): CursorCheckpointSnapshot | undefined {
|
|
272
|
+
if (!ref) return undefined;
|
|
273
|
+
prune();
|
|
274
|
+
const snapshot = store.snapshots.get(ref);
|
|
275
|
+
if (!snapshot) return undefined;
|
|
276
|
+
const at = now();
|
|
277
|
+
if (at - snapshot.lastAccessAt > CURSOR_CHECKPOINT_TTL_MS) {
|
|
278
|
+
deleteSnapshot(ref);
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
snapshot.lastAccessAt = at;
|
|
282
|
+
store.snapshots.delete(ref);
|
|
283
|
+
store.snapshots.set(ref, snapshot);
|
|
284
|
+
return snapshot;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function invalidateCursorCheckpoint(ref: string | undefined): void {
|
|
288
|
+
if (!ref) return;
|
|
289
|
+
deleteSnapshot(ref);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function clearCursorCheckpointsForTests(): void {
|
|
293
|
+
stopPruneTimer();
|
|
294
|
+
for (const ref of [...store.snapshots.keys()]) deleteSnapshot(ref);
|
|
295
|
+
store.snapshots.clear();
|
|
296
|
+
store.prefixIndex.clear();
|
|
297
|
+
store.totalBytes = 0;
|
|
298
|
+
resetCursorCheckpointClockForTests();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function cursorCheckpointStoreMetricsForTests(): { count: number; totalBytes: number } {
|
|
302
|
+
return { count: store.snapshots.size, totalBytes: store.totalBytes };
|
|
303
|
+
}
|
|
@@ -146,6 +146,24 @@ export function isCursorExternalWireModel(modelId: string): boolean {
|
|
|
146
146
|
return !isCursorNativeWireModel(modelId);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Native composer models whose tool-result continuation must still be sent as a
|
|
151
|
+
* userMessageAction with the plain "Continue:" text instead of a bare resumeAction.
|
|
152
|
+
*
|
|
153
|
+
* Observed on live Cursor Connect traffic (2026-08-20): `composer-2.5` (the
|
|
154
|
+
* standard, non-fast build) resumes a tool-result turn with server-side native
|
|
155
|
+
* tool calls (read/grep/exec) instead of answering, or completes with zero text
|
|
156
|
+
* (empty `content` + `stop`). `composer-2.5-fast` answers correctly on the same
|
|
157
|
+
* resumeAction path, so only the affected id is listed here. Sending the same
|
|
158
|
+
* continuation as an explicit user message (external path) makes the model
|
|
159
|
+
* answer reliably.
|
|
160
|
+
*/
|
|
161
|
+
export function cursorNeedsExternalToolContinuation(modelId: string): boolean {
|
|
162
|
+
if (isCursorExternalWireModel(modelId)) return true;
|
|
163
|
+
const wire = cursorCodexToWireModelId(modelId).trim().toLowerCase();
|
|
164
|
+
return wire === "composer-2.5";
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
function stripCursorEffortSuffix(wireModelId: string): string {
|
|
150
168
|
const suffixes = [...CANONICAL_EFFORT_SUFFIXES].sort((a, b) => b.length - a.length);
|
|
151
169
|
for (const suffix of suffixes) {
|
|
@@ -155,6 +173,13 @@ function stripCursorEffortSuffix(wireModelId: string): string {
|
|
|
155
173
|
return wireModelId;
|
|
156
174
|
}
|
|
157
175
|
|
|
176
|
+
/** Compare Cursor wire models without effort suffix or the grok cursor- request prefix. */
|
|
177
|
+
export function cursorCheckpointModelAffinityId(modelId: string): string {
|
|
178
|
+
const wire = cursorCodexToWireModelId(modelId).trim().toLowerCase();
|
|
179
|
+
const withoutPrefix = wire.startsWith("cursor-") ? wire.slice("cursor-".length) : wire;
|
|
180
|
+
return stripCursorEffortSuffix(withoutPrefix);
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
export function isCursorRouterModelId(modelId: string): boolean {
|
|
159
184
|
return (CURSOR_ROUTER_MODEL_IDS as readonly string[]).includes(modelId);
|
|
160
185
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
CreatePlanRequestResponseSchema,
|
|
34
34
|
CreatePlanResultSchema,
|
|
35
35
|
CreatePlanSuccessSchema,
|
|
36
|
+
ConversationStateStructureSchema,
|
|
36
37
|
ExaFetchRequestResponseSchema,
|
|
37
38
|
ExaFetchRequestResponse_ApprovedSchema,
|
|
38
39
|
ExaSearchRequestResponseSchema,
|
|
@@ -439,11 +440,12 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
439
440
|
private framesReceived = 0;
|
|
440
441
|
private sawAssistantText = false;
|
|
441
442
|
private firstFrameAt?: number;
|
|
442
|
-
|
|
443
|
+
private firstFrameLogged = false;
|
|
443
444
|
/** Stable session identifier sent as x-session-id; mirrors IDE session semantics. */
|
|
444
445
|
private readonly sessionId: string;
|
|
445
446
|
/** Per-transport owner for native-exec / background shells. Must not share conversationId. */
|
|
446
447
|
private readonly shellOwnerId = crypto.randomUUID();
|
|
448
|
+
private capturedCheckpointBytes?: Uint8Array;
|
|
447
449
|
|
|
448
450
|
constructor(private readonly input: CursorTransportFactoryInput) {
|
|
449
451
|
this.sessionId = input.sessionId?.trim() || crypto.randomUUID();
|
|
@@ -1129,7 +1131,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1129
1131
|
}
|
|
1130
1132
|
releaseBacklogLease();
|
|
1131
1133
|
settler.settleFinish();
|
|
1132
|
-
}
|
|
1134
|
+
}).catch((err) => {
|
|
1133
1135
|
failAndClear(err instanceof Error ? err : new Error(String(err)));
|
|
1134
1136
|
});
|
|
1135
1137
|
};
|
|
@@ -1187,6 +1189,10 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1187
1189
|
}, HEARTBEAT_MS);
|
|
1188
1190
|
}
|
|
1189
1191
|
|
|
1192
|
+
capturedConversationCheckpoint(): Uint8Array | undefined {
|
|
1193
|
+
return this.capturedCheckpointBytes;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1190
1196
|
private async handleServerMessage(
|
|
1191
1197
|
message: AgentServerMessage,
|
|
1192
1198
|
state: ReturnType<typeof createCursorProtobufEventState>,
|
|
@@ -1194,6 +1200,13 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1194
1200
|
): Promise<void> {
|
|
1195
1201
|
if (!this.stream && !this.http1Connection) return;
|
|
1196
1202
|
debugProviderDiagnostic("cursor", "frame", describeCursorServerFrame(message));
|
|
1203
|
+
if (message.message.case === "conversationCheckpointUpdate") {
|
|
1204
|
+
try {
|
|
1205
|
+
this.capturedCheckpointBytes = toBinary(ConversationStateStructureSchema, message.message.value);
|
|
1206
|
+
} catch {
|
|
1207
|
+
this.capturedCheckpointBytes = undefined;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1197
1210
|
if (message.message.case === "kvServerMessage") {
|
|
1198
1211
|
this.writeConnectFrame(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
|
|
1199
1212
|
return;
|
|
@@ -1403,3 +1416,7 @@ function cursorConnectErrorCode(payload: Uint8Array): string | undefined {
|
|
|
1403
1416
|
export function createLiveCursorTransport(input: CursorTransportFactoryInput): CursorTransport {
|
|
1404
1417
|
return new LiveCursorTransport(input);
|
|
1405
1418
|
}
|
|
1419
|
+
|
|
1420
|
+
export function capturedCursorCheckpointBytes(transport: CursorTransport): Uint8Array | undefined {
|
|
1421
|
+
return transport.capturedConversationCheckpoint?.();
|
|
1422
|
+
}
|
|
@@ -117,6 +117,7 @@ interface CursorBlobLimits {
|
|
|
117
117
|
interface CursorBlobRequestScopeState {
|
|
118
118
|
keys: Set<string>;
|
|
119
119
|
sealed: boolean;
|
|
120
|
+
kind: "request" | "checkpoint";
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
const DEFAULT_BLOB_LIMITS: CursorBlobLimits = {
|
|
@@ -376,7 +377,7 @@ export function createCursorBlobRequestScope(): CursorBlobRequestScopeToken {
|
|
|
376
377
|
// IDENTITY, not just pin counts (review C2-2: identical descriptions made
|
|
377
378
|
// scope-swap bugs invisible to deep comparison).
|
|
378
379
|
const scope = Symbol(`cursor-blob-request-${++blobScopeSequence}`);
|
|
379
|
-
blobRequestScopes.set(scope, { keys: new Set(), sealed: false });
|
|
380
|
+
blobRequestScopes.set(scope, { keys: new Set(), sealed: false, kind: "request" });
|
|
380
381
|
return scope;
|
|
381
382
|
}
|
|
382
383
|
|
|
@@ -402,6 +403,48 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque
|
|
|
402
403
|
return blobId;
|
|
403
404
|
}
|
|
404
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Long-lived pin for blobs referenced by an active Cursor conversation checkpoint.
|
|
408
|
+
* Unlike a request scope, this lease is not sealed and is not released by getBlob hydration.
|
|
409
|
+
*/
|
|
410
|
+
export function createCursorBlobCheckpointLease(label: string): CursorBlobRequestScopeToken {
|
|
411
|
+
const scope = Symbol("cursor-blob-checkpoint-" + (++blobScopeSequence) + "-" + label.slice(0, 16));
|
|
412
|
+
blobRequestScopes.set(scope, { keys: new Set(), sealed: false, kind: "checkpoint" });
|
|
413
|
+
return scope;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function pinCursorBlobIdsForCheckpoint(
|
|
417
|
+
blobIds: readonly Uint8Array[],
|
|
418
|
+
lease: CursorBlobRequestScopeToken,
|
|
419
|
+
): boolean {
|
|
420
|
+
const state = blobRequestScopes.get(lease);
|
|
421
|
+
if (!state || state.sealed) return false;
|
|
422
|
+
const added: Array<{ entry: { requestPins: Set<CursorBlobRequestScopeToken> }; key: string }> = [];
|
|
423
|
+
for (const blobId of blobIds) {
|
|
424
|
+
if (blobId.byteLength === 0) continue;
|
|
425
|
+
const k = key(blobId);
|
|
426
|
+
const entry = blobs.get(k);
|
|
427
|
+
if (!entry || (isExpired(entry, Date.now()) && entry.requestPins.size === 0 && entry.provenance !== "remote-setBlobArgs")) {
|
|
428
|
+
for (const pinned of added) {
|
|
429
|
+
pinned.entry.requestPins.delete(lease);
|
|
430
|
+
state.keys.delete(pinned.key);
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
if (!entry.requestPins.has(lease)) {
|
|
435
|
+
entry.requestPins.add(lease);
|
|
436
|
+
state.keys.add(k);
|
|
437
|
+
added.push({ entry, key: k });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
reconcileBlobClassAccountingAndEnforce();
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function hasCursorBlob(blobId: Uint8Array): boolean {
|
|
445
|
+
return getBlob(key(blobId)) !== undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
405
448
|
export interface CursorBlobMetrics {
|
|
406
449
|
count: number;
|
|
407
450
|
totalBytes: number;
|
|
@@ -574,7 +617,9 @@ export function handleCursorNativeKv(
|
|
|
574
617
|
if (kvMsg.message.case === "getBlobArgs") {
|
|
575
618
|
const blobKey = key(kvMsg.message.value.blobId);
|
|
576
619
|
const blobData = getBlob(blobKey);
|
|
577
|
-
if (blobData
|
|
620
|
+
if (blobData && requestScope && blobRequestScopes.get(requestScope)?.kind === "request") {
|
|
621
|
+
releaseHydratedBlob(blobKey, requestScope);
|
|
622
|
+
}
|
|
578
623
|
return clientBytes({
|
|
579
624
|
message: {
|
|
580
625
|
case: "kvClientMessage",
|
|
@@ -4,7 +4,7 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
|
4
4
|
import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
|
|
5
5
|
import { namespacedToolName } from "../../types";
|
|
6
6
|
import type { CursorRunRequest } from "./types";
|
|
7
|
-
import { isCursorExternalWireModel } from "./discovery";
|
|
7
|
+
import { cursorNeedsExternalToolContinuation, isCursorExternalWireModel } from "./discovery";
|
|
8
8
|
import { normalizeCursorToolResultText } from "./tool-result-normalize";
|
|
9
9
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
10
10
|
import {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
ConversationActionSchema,
|
|
26
26
|
ConversationStepSchema,
|
|
27
27
|
ConversationStateStructureSchema,
|
|
28
|
+
type ConversationStateStructure,
|
|
28
29
|
ConversationTurnStructureSchema,
|
|
29
30
|
McpArgsSchema,
|
|
30
31
|
McpSuccessSchema,
|
|
@@ -793,8 +794,11 @@ function buildPreparedCursorRunRequest(
|
|
|
793
794
|
const lastRawIsToolResult = request.rawMessages?.at(-1)?.role === "toolResult";
|
|
794
795
|
// Native models resume the remembered Cursor conversation. External wire
|
|
795
796
|
// models continue as userMessageAction so history-blob tool results stay
|
|
796
|
-
// visible without a ResumeAction.
|
|
797
|
-
|
|
797
|
+
// visible without a ResumeAction. Some native composer ids are also routed
|
|
798
|
+
// through the external continuation path (cursorNeedsExternalToolContinuation)
|
|
799
|
+
// because a bare resumeAction makes them continue exploring with native tools
|
|
800
|
+
// instead of answering (observed on composer-2.5; see discovery.ts).
|
|
801
|
+
const externalToolContinuation = lastRawIsToolResult && cursorNeedsExternalToolContinuation(request.modelId);
|
|
798
802
|
const actionCase = (externalToolContinuation || (!lastRawIsToolResult && text.trim().length > 0))
|
|
799
803
|
? "userMessageAction"
|
|
800
804
|
: "resumeAction";
|
|
@@ -820,9 +824,70 @@ function buildPreparedCursorRunRequest(
|
|
|
820
824
|
}),
|
|
821
825
|
},
|
|
822
826
|
});
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
827
|
+
let continuationMode: "full-replay" | "checkpoint" = "full-replay";
|
|
828
|
+
let checkpointInvalidationReason = request.checkpointInvalidationReason;
|
|
829
|
+
let conversationState: ConversationStateStructure | undefined;
|
|
830
|
+
let rootPromptMessagesState: ReturnType<typeof rootPromptMessages> | undefined;
|
|
831
|
+
if (request.checkpointBytes && request.checkpointBytes.byteLength > 0) {
|
|
832
|
+
try {
|
|
833
|
+
conversationState = fromBinary(ConversationStateStructureSchema, request.checkpointBytes);
|
|
834
|
+
continuationMode = "checkpoint";
|
|
835
|
+
const suffixStart = request.checkpointSuffixStart;
|
|
836
|
+
if (
|
|
837
|
+
typeof suffixStart === "number"
|
|
838
|
+
&& Number.isSafeInteger(suffixStart)
|
|
839
|
+
&& suffixStart >= 0
|
|
840
|
+
&& request.rawMessages
|
|
841
|
+
&& suffixStart < request.rawMessages.length
|
|
842
|
+
) {
|
|
843
|
+
const suffixRequest: CursorRunRequest = {
|
|
844
|
+
...request,
|
|
845
|
+
system: [],
|
|
846
|
+
rawMessages: request.rawMessages.slice(suffixStart),
|
|
847
|
+
};
|
|
848
|
+
const suffixRoots = rootPromptMessages(suffixRequest, requestScope);
|
|
849
|
+
const suffixTurns = conversationTurns(suffixRequest, requestScope, suffixRoots.historyMessageStart);
|
|
850
|
+
const suffixSystemCount = systemPromptBlobs(suffixRequest).length;
|
|
851
|
+
const suffixHistoryIds = suffixRoots.ids.slice(suffixSystemCount);
|
|
852
|
+
const suffixHistorySerialized = suffixRoots.serialized.slice(suffixSystemCount);
|
|
853
|
+
conversationState = create(ConversationStateStructureSchema, {
|
|
854
|
+
...conversationState,
|
|
855
|
+
rootPromptMessagesJson: [
|
|
856
|
+
...conversationState.rootPromptMessagesJson,
|
|
857
|
+
...suffixHistoryIds,
|
|
858
|
+
],
|
|
859
|
+
turns: [
|
|
860
|
+
...conversationState.turns,
|
|
861
|
+
...suffixTurns,
|
|
862
|
+
],
|
|
863
|
+
});
|
|
864
|
+
rootPromptMessagesState = {
|
|
865
|
+
ids: suffixHistoryIds,
|
|
866
|
+
byteLength: suffixRoots.byteLength,
|
|
867
|
+
historyMessageStart: suffixRoots.historyMessageStart,
|
|
868
|
+
serialized: suffixHistorySerialized,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
} catch {
|
|
872
|
+
checkpointInvalidationReason = "decode_failed";
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (!conversationState) {
|
|
876
|
+
rootPromptMessagesState = rootPromptMessages(request, requestScope);
|
|
877
|
+
conversationState = create(ConversationStateStructureSchema, {
|
|
878
|
+
rootPromptMessagesJson: rootPromptMessagesState.ids,
|
|
879
|
+
turns: conversationTurns(request, requestScope, rootPromptMessagesState.historyMessageStart),
|
|
880
|
+
todos: [],
|
|
881
|
+
pendingToolCalls: [],
|
|
882
|
+
previousWorkspaceUris: [],
|
|
883
|
+
fileStates: {},
|
|
884
|
+
fileStatesV2: {},
|
|
885
|
+
summaryArchives: [],
|
|
886
|
+
turnTimings: [],
|
|
887
|
+
subagentStates: {},
|
|
888
|
+
readPaths: [],
|
|
889
|
+
});
|
|
890
|
+
}
|
|
826
891
|
// Hoisted out of the mcp_tools spread below so the estimate can read the same
|
|
827
892
|
// filtered definitions the wire carries. Both helpers are pure.
|
|
828
893
|
const visibleTools = cursorToolsForActivePrompt(request.tools, rawText, request.toolChoice);
|
|
@@ -834,9 +899,13 @@ function buildPreparedCursorRunRequest(
|
|
|
834
899
|
turnType: lastRawIsToolResult ? "tool-continuation" : "initial",
|
|
835
900
|
externalModel: isCursorExternalWireModel(request.modelId),
|
|
836
901
|
rawMessages: request.rawMessages?.length ?? 0,
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
902
|
+
continuationMode,
|
|
903
|
+
checkpointPresent: continuationMode === "checkpoint",
|
|
904
|
+
checkpointBytes: continuationMode === "checkpoint" ? request.checkpointBytes?.byteLength : undefined,
|
|
905
|
+
checkpointInvalidationReason,
|
|
906
|
+
rootBlobs: conversationState.rootPromptMessagesJson.length,
|
|
907
|
+
rootBytes: rootPromptMessagesState?.byteLength ?? 0,
|
|
908
|
+
turnBlobs: conversationState.turns.length,
|
|
840
909
|
tools: request.tools?.length ?? 0,
|
|
841
910
|
});
|
|
842
911
|
|
|
@@ -847,19 +916,7 @@ function buildPreparedCursorRunRequest(
|
|
|
847
916
|
const hasExplicitModelParameters = (request.requestedModelParameters?.length ?? 0) > 0;
|
|
848
917
|
const runRequest = create(AgentRunRequestSchema, {
|
|
849
918
|
conversationId: request.conversationId,
|
|
850
|
-
conversationState
|
|
851
|
-
rootPromptMessagesJson: rootPromptMessageIds,
|
|
852
|
-
turns: turnIds,
|
|
853
|
-
todos: [],
|
|
854
|
-
pendingToolCalls: [],
|
|
855
|
-
previousWorkspaceUris: [],
|
|
856
|
-
fileStates: {},
|
|
857
|
-
fileStatesV2: {},
|
|
858
|
-
summaryArchives: [],
|
|
859
|
-
turnTimings: [],
|
|
860
|
-
subagentStates: {},
|
|
861
|
-
readPaths: [],
|
|
862
|
-
}),
|
|
919
|
+
conversationState,
|
|
863
920
|
action,
|
|
864
921
|
// Explicit model-picker parameters follow current Cursor clients and use requested_model alone.
|
|
865
922
|
// Keep legacy model_details for flat model ids and the already-live Router path; sending both for
|
|
@@ -906,7 +963,7 @@ function buildPreparedCursorRunRequest(
|
|
|
906
963
|
// Same instances that produced `bytes`, so the estimate cannot count history or
|
|
907
964
|
// tools the payload dropped — the defect that blocked PR #376.
|
|
908
965
|
const modelVisibleParts = [
|
|
909
|
-
...rootPromptMessagesState
|
|
966
|
+
...(rootPromptMessagesState?.serialized ?? []),
|
|
910
967
|
...(actionCase === "userMessageAction" ? [actionText] : []),
|
|
911
968
|
...mcpToolDefs.map(modelVisibleToolText),
|
|
912
969
|
];
|