pi-cursor-sdk 0.1.60 → 0.1.61
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 +27 -0
- package/README.md +123 -9
- package/docs/cursor-model-ux-spec.md +19 -11
- package/docs/cursor-native-tool-replay.md +4 -2
- package/docs/cursor-tool-surfaces.md +7 -1
- package/docs/platform-smoke.md +51 -20
- package/package.json +13 -2
- package/platform-smoke.config.mjs +1 -0
- package/scripts/cloud-runtime-smoke.d.mts +40 -1
- package/scripts/cloud-runtime-smoke.mjs +444 -325
- package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
- package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
- package/scripts/lib/cloud-smoke-github.d.mts +78 -0
- package/scripts/lib/cloud-smoke-github.mjs +331 -0
- package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
- package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
- package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
- package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
- package/scripts/local-resume-cleanup-smoke.mjs +11 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -0
- package/scripts/platform-smoke/scenarios.d.mts +22 -0
- package/scripts/platform-smoke/scenarios.mjs +17 -0
- package/scripts/platform-smoke/targets.mjs +1 -0
- package/src/cursor-agent-message-web-tools.ts +14 -6
- package/src/cursor-cloud-local-state.ts +536 -0
- package/src/cursor-cloud-options.ts +46 -70
- package/src/cursor-config.ts +160 -14
- package/src/cursor-http1.ts +65 -0
- package/src/cursor-live-run-coordinator.ts +8 -1
- package/src/cursor-pi-tool-bridge-abort.ts +10 -3
- package/src/cursor-pi-tool-bridge-constants.ts +6 -0
- package/src/cursor-pi-tool-bridge-env.ts +9 -0
- package/src/cursor-pi-tool-bridge-run.ts +27 -5
- package/src/cursor-pi-tool-bridge.ts +11 -2
- package/src/cursor-provider-errors.ts +72 -2
- package/src/cursor-provider-live-run-drain.ts +2 -1
- package/src/cursor-provider-run-finalizer.ts +8 -2
- package/src/cursor-provider-run-outcome.ts +7 -1
- package/src/cursor-provider-turn-finalize.ts +23 -4
- package/src/cursor-provider-turn-prepare.ts +17 -3
- package/src/cursor-provider-turn-runner.ts +5 -0
- package/src/cursor-provider-turn-send.ts +1 -1
- package/src/cursor-question-tool.ts +8 -0
- package/src/cursor-ripgrep-path.ts +32 -0
- package/src/cursor-runtime-state.ts +119 -100
- package/src/cursor-sdk-process-error-guard.ts +19 -0
- package/src/cursor-session-agent-cleanup.ts +66 -15
- package/src/cursor-session-agent-lifecycle.ts +9 -4
- package/src/cursor-session-agent-resume.ts +50 -7
- package/src/cursor-session-agent.ts +106 -67
- package/src/cursor-session-scope.ts +22 -2
- package/src/cursor-session-store.ts +155 -0
- package/src/cursor-state.ts +162 -21
|
@@ -4,10 +4,12 @@ import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent
|
|
|
4
4
|
import type { SessionCursorAgentSendState } from "./cursor-session-agent.js";
|
|
5
5
|
import { asRecord } from "./cursor-record-utils.js";
|
|
6
6
|
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
7
|
+
import type { CursorSessionStoreIdentity } from "./cursor-session-store.js";
|
|
7
8
|
|
|
8
9
|
export const CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE = "cursor-sdk-agent-resume";
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const LEGACY_RESUME_ENTRY_VERSION = 1;
|
|
12
|
+
const RESUME_ENTRY_VERSION = 2;
|
|
11
13
|
const MAX_LOCAL_AGENT_ID_LENGTH = 256;
|
|
12
14
|
const EMPTY_BRANCH_HASH = hashParts(["cursor-sdk-agent-resume-branch", "v1"]);
|
|
13
15
|
|
|
@@ -24,8 +26,13 @@ export interface CursorSessionAgentResumeScope {
|
|
|
24
26
|
repoRoot?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export interface CursorSessionAgentCleanupCandidate {
|
|
30
|
+
agentId: string;
|
|
31
|
+
storeIdentity?: CursorSessionStoreIdentity;
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export interface CursorSessionAgentResumeEntryData {
|
|
28
|
-
version: 1;
|
|
35
|
+
version: 1 | 2;
|
|
29
36
|
runtime: "local";
|
|
30
37
|
agentId: string;
|
|
31
38
|
scopeKey: string;
|
|
@@ -38,7 +45,9 @@ export interface CursorSessionAgentResumeEntryData {
|
|
|
38
45
|
compactionGeneration: number;
|
|
39
46
|
sendState: SessionCursorAgentSendState;
|
|
40
47
|
createdAt: string;
|
|
48
|
+
storeIdentity?: CursorSessionStoreIdentity;
|
|
41
49
|
cleanupCandidateAgentIds?: string[];
|
|
50
|
+
cleanupCandidates?: CursorSessionAgentCleanupCandidate[];
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
interface PendingCursorSessionAgentResumeHandle {
|
|
@@ -46,6 +55,7 @@ interface PendingCursorSessionAgentResumeHandle {
|
|
|
46
55
|
agentId: string;
|
|
47
56
|
poolKey: string;
|
|
48
57
|
sendState: SessionCursorAgentSendState;
|
|
58
|
+
storeIdentity: CursorSessionStoreIdentity;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
interface CursorSessionResumeState {
|
|
@@ -109,10 +119,31 @@ function isSendState(value: unknown): value is SessionCursorAgentSendState {
|
|
|
109
119
|
typeof record.incrementalSendCount === "number";
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
function parseStoreIdentity(value: unknown): CursorSessionStoreIdentity | undefined {
|
|
123
|
+
const record = asRecord(value);
|
|
124
|
+
if (record?.version !== 1 || typeof record.stateRoot !== "string" || !record.stateRoot) return undefined;
|
|
125
|
+
return { version: 1, stateRoot: record.stateRoot };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseCleanupCandidates(value: unknown): CursorSessionAgentCleanupCandidate[] | undefined {
|
|
129
|
+
if (!Array.isArray(value)) return undefined;
|
|
130
|
+
const candidates = value.flatMap((item): CursorSessionAgentCleanupCandidate[] => {
|
|
131
|
+
const record = asRecord(item);
|
|
132
|
+
if (!isCursorLocalAgentId(record?.agentId)) return [];
|
|
133
|
+
const storeIdentity = record.storeIdentity === undefined ? undefined : parseStoreIdentity(record.storeIdentity);
|
|
134
|
+
if (record.storeIdentity !== undefined && !storeIdentity) return [];
|
|
135
|
+
return [{ agentId: record.agentId, ...(storeIdentity ? { storeIdentity } : {}) }];
|
|
136
|
+
});
|
|
137
|
+
return candidates.length ? candidates : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
export function parseCursorSessionAgentResumeEntryData(value: unknown): CursorSessionAgentResumeEntryData | undefined {
|
|
113
141
|
const record = asRecord(value);
|
|
114
142
|
if (!record) return undefined;
|
|
115
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
(record.version !== LEGACY_RESUME_ENTRY_VERSION && record.version !== RESUME_ENTRY_VERSION) ||
|
|
145
|
+
record.runtime !== "local"
|
|
146
|
+
) return undefined;
|
|
116
147
|
if (
|
|
117
148
|
!isCursorLocalAgentId(record.agentId) ||
|
|
118
149
|
typeof record.scopeKey !== "string" ||
|
|
@@ -126,11 +157,14 @@ export function parseCursorSessionAgentResumeEntryData(value: unknown): CursorSe
|
|
|
126
157
|
if (record.sessionFile !== undefined && typeof record.sessionFile !== "string") return undefined;
|
|
127
158
|
if (record.sessionId !== undefined && typeof record.sessionId !== "string") return undefined;
|
|
128
159
|
if (record.repoRoot !== undefined && typeof record.repoRoot !== "string") return undefined;
|
|
160
|
+
const storeIdentity = parseStoreIdentity(record.storeIdentity);
|
|
161
|
+
if (record.version === RESUME_ENTRY_VERSION && !storeIdentity) return undefined;
|
|
129
162
|
const cleanupCandidateAgentIds = Array.isArray(record.cleanupCandidateAgentIds)
|
|
130
163
|
? record.cleanupCandidateAgentIds.filter(isCursorLocalAgentId)
|
|
131
164
|
: undefined;
|
|
165
|
+
const cleanupCandidates = parseCleanupCandidates(record.cleanupCandidates);
|
|
132
166
|
return {
|
|
133
|
-
version:
|
|
167
|
+
version: record.version,
|
|
134
168
|
runtime: "local",
|
|
135
169
|
agentId: record.agentId,
|
|
136
170
|
scopeKey: record.scopeKey,
|
|
@@ -147,7 +181,9 @@ export function parseCursorSessionAgentResumeEntryData(value: unknown): CursorSe
|
|
|
147
181
|
incrementalSendCount: record.sendState.incrementalSendCount,
|
|
148
182
|
},
|
|
149
183
|
createdAt: record.createdAt,
|
|
184
|
+
...(storeIdentity ? { storeIdentity } : {}),
|
|
150
185
|
...(cleanupCandidateAgentIds?.length ? { cleanupCandidateAgentIds: [...new Set(cleanupCandidateAgentIds)] } : {}),
|
|
186
|
+
...(cleanupCandidates ? { cleanupCandidates } : {}),
|
|
151
187
|
};
|
|
152
188
|
}
|
|
153
189
|
|
|
@@ -330,6 +366,7 @@ export function persistCursorSessionAgentResumeHandle(input: PendingCursorSessio
|
|
|
330
366
|
agentId: input.agentId,
|
|
331
367
|
poolKey: input.poolKey,
|
|
332
368
|
sendState: { ...input.sendState },
|
|
369
|
+
storeIdentity: { ...input.storeIdentity },
|
|
333
370
|
};
|
|
334
371
|
}
|
|
335
372
|
|
|
@@ -338,8 +375,13 @@ function flushPendingCursorSessionAgentResumeHandle(branch: readonly SessionEntr
|
|
|
338
375
|
const pending = state.pendingHandle;
|
|
339
376
|
state.pendingHandle = undefined;
|
|
340
377
|
if (!pending || !state.appendEntry) return;
|
|
341
|
-
const
|
|
342
|
-
const
|
|
378
|
+
const previousHandle = state.activeHandle ?? state.lastBranchHandle;
|
|
379
|
+
const cleanupCandidates = state.sessionFile && previousHandle && previousHandle.agentId !== pending.agentId
|
|
380
|
+
? [{
|
|
381
|
+
agentId: previousHandle.agentId,
|
|
382
|
+
...(previousHandle.storeIdentity ? { storeIdentity: { ...previousHandle.storeIdentity } } : {}),
|
|
383
|
+
}]
|
|
384
|
+
: undefined;
|
|
343
385
|
const data: CursorSessionAgentResumeEntryData = {
|
|
344
386
|
version: RESUME_ENTRY_VERSION,
|
|
345
387
|
runtime: pending.runtime,
|
|
@@ -354,7 +396,8 @@ function flushPendingCursorSessionAgentResumeHandle(branch: readonly SessionEntr
|
|
|
354
396
|
compactionGeneration: state.compactionGeneration,
|
|
355
397
|
sendState: { ...pending.sendState },
|
|
356
398
|
createdAt: new Date().toISOString(),
|
|
357
|
-
|
|
399
|
+
storeIdentity: { ...pending.storeIdentity },
|
|
400
|
+
...(cleanupCandidates ? { cleanupCandidates } : {}),
|
|
358
401
|
};
|
|
359
402
|
try {
|
|
360
403
|
state.appendEntry<CursorSessionAgentResumeEntryData>(CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE, data);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import type { AgentModeOption, LocalAgentOptions, ModelSelection, SDKAgent, SettingSource } from "@cursor/sdk";
|
|
2
|
+
import type { AgentModeOption, LocalAgentOptions, LocalAgentStore, ModelSelection, SDKAgent, SettingSource } from "@cursor/sdk";
|
|
3
3
|
import type { Context } from "@earendil-works/pi-ai/compat";
|
|
4
4
|
import {
|
|
5
5
|
getRegisteredCursorPiToolBridge,
|
|
@@ -7,13 +7,20 @@ import {
|
|
|
7
7
|
type CursorPiToolBridgeRun,
|
|
8
8
|
} from "./cursor-pi-tool-bridge.js";
|
|
9
9
|
import { computeCursorContextFingerprint } from "./context.js";
|
|
10
|
-
import { getCursorSessionScopeGeneration, getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
10
|
+
import { getCursorSessionFile, getCursorSessionScopeGeneration, getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
11
11
|
import {
|
|
12
12
|
getMatchingCursorSessionAgentResumeHandle,
|
|
13
13
|
persistCursorSessionAgentResumeHandle,
|
|
14
14
|
} from "./cursor-session-agent-resume.js";
|
|
15
15
|
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
16
16
|
import { loadCursorSdk, type CursorSdkModule } from "./cursor-sdk-runtime.js";
|
|
17
|
+
import {
|
|
18
|
+
cursorSessionStoreIdentitiesEqual,
|
|
19
|
+
openCursorSessionStore,
|
|
20
|
+
openCursorSessionStoreForScope,
|
|
21
|
+
type CursorSessionStoreIdentity,
|
|
22
|
+
type OpenCursorSessionStore,
|
|
23
|
+
} from "./cursor-session-store.js";
|
|
17
24
|
|
|
18
25
|
export interface SessionCursorAgentSendState {
|
|
19
26
|
bootstrapped: boolean;
|
|
@@ -27,6 +34,8 @@ export interface SessionCursorAgentLease {
|
|
|
27
34
|
instanceId: number;
|
|
28
35
|
agent: SDKAgent;
|
|
29
36
|
bridgeRun?: CursorPiToolBridgeRun;
|
|
37
|
+
store: LocalAgentStore;
|
|
38
|
+
storeIdentity: CursorSessionStoreIdentity;
|
|
30
39
|
sendState: SessionCursorAgentSendState;
|
|
31
40
|
created: boolean;
|
|
32
41
|
resumed?: boolean;
|
|
@@ -52,6 +61,7 @@ interface SessionCursorAgentReadyEntry extends SessionCursorAgentPoolEntryBase {
|
|
|
52
61
|
status: "ready";
|
|
53
62
|
agent: SDKAgent;
|
|
54
63
|
bridgeRun?: CursorPiToolBridgeRun;
|
|
64
|
+
sessionStore: OpenCursorSessionStore;
|
|
55
65
|
resumeEnabled: boolean;
|
|
56
66
|
resumed: boolean;
|
|
57
67
|
resumeNotice?: string;
|
|
@@ -61,6 +71,7 @@ interface SessionCursorAgentBusyEntry extends SessionCursorAgentPoolEntryBase {
|
|
|
61
71
|
status: "busy";
|
|
62
72
|
agent: SDKAgent;
|
|
63
73
|
bridgeRun?: CursorPiToolBridgeRun;
|
|
74
|
+
sessionStore: OpenCursorSessionStore;
|
|
64
75
|
resumeEnabled: boolean;
|
|
65
76
|
resumed: boolean;
|
|
66
77
|
resumeNotice?: string;
|
|
@@ -116,6 +127,7 @@ interface SessionCursorAgentCreateParams {
|
|
|
116
127
|
modelSelection: ModelSelection;
|
|
117
128
|
settingSources?: SettingSource[];
|
|
118
129
|
localSafety?: CursorLocalSafetyOptions;
|
|
130
|
+
useHttp1ForAgent?: boolean;
|
|
119
131
|
onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
120
132
|
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
121
133
|
localResume?: boolean;
|
|
@@ -131,6 +143,7 @@ let deadTransportAgentDisposeTimeoutMs = 3000;
|
|
|
131
143
|
const terminalDisposedScopeGenerations = new Map<string, number>();
|
|
132
144
|
const scopeCreationGenerations = new Map<string, number>();
|
|
133
145
|
const EMPTY_POOL_STATE: SessionCursorAgentPoolState = { status: "empty" };
|
|
146
|
+
const LOCAL_RESUME_FALLBACK_NOTICE = "Could not resume prior Cursor agent; continuing from current pi transcript in a new Cursor agent.";
|
|
134
147
|
let nextSessionAgentInstanceId = 1;
|
|
135
148
|
|
|
136
149
|
export interface CursorLocalSafetyOptions {
|
|
@@ -142,9 +155,11 @@ export function buildCursorLocalAgentOptions(options: {
|
|
|
142
155
|
cwd: string;
|
|
143
156
|
settingSources?: SettingSource[];
|
|
144
157
|
localSafety?: CursorLocalSafetyOptions;
|
|
158
|
+
store?: LocalAgentStore;
|
|
145
159
|
}): LocalAgentOptions {
|
|
146
160
|
return {
|
|
147
161
|
cwd: options.cwd,
|
|
162
|
+
...(options.store ? { store: options.store } : {}),
|
|
148
163
|
...(options.settingSources ? { settingSources: options.settingSources } : {}),
|
|
149
164
|
...(options.localSafety?.autoReview === true ? { autoReview: true } : {}),
|
|
150
165
|
...(options.localSafety?.sandboxEnabled === true ? { sandboxOptions: { enabled: true } } : {}),
|
|
@@ -203,6 +218,11 @@ function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCr
|
|
|
203
218
|
buildModelPoolKey(params.modelSelection),
|
|
204
219
|
buildSettingSourcesPoolKey(params.settingSources),
|
|
205
220
|
buildLocalSafetyPoolKey(params.localSafety),
|
|
221
|
+
params.useHttp1ForAgent === undefined
|
|
222
|
+
? "http1:default"
|
|
223
|
+
: params.useHttp1ForAgent
|
|
224
|
+
? "http1:on"
|
|
225
|
+
: "http1:off",
|
|
206
226
|
buildApiKeyPoolKeyFingerprint(params.apiKey),
|
|
207
227
|
buildBridgePoolKeySuffix(),
|
|
208
228
|
].join("\0");
|
|
@@ -229,6 +249,7 @@ async function disposePoolEntry(entry: SessionCursorAgentPoolEntry, options?: {
|
|
|
229
249
|
} catch {
|
|
230
250
|
// disposal failure should not block session replacement
|
|
231
251
|
}
|
|
252
|
+
await entry.sessionStore.dispose().catch(() => undefined);
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?: boolean }): Promise<void> {
|
|
@@ -287,6 +308,7 @@ function commitSessionAgentSendForLease(
|
|
|
287
308
|
agentId: entry.agent.agentId,
|
|
288
309
|
poolKey: entry.poolKey,
|
|
289
310
|
sendState: entry.sendState,
|
|
311
|
+
storeIdentity: entry.sessionStore.identity,
|
|
290
312
|
});
|
|
291
313
|
}
|
|
292
314
|
}
|
|
@@ -371,6 +393,8 @@ function leaseFromEntry(
|
|
|
371
393
|
instanceId: entry.instanceId,
|
|
372
394
|
agent: entry.agent,
|
|
373
395
|
bridgeRun: entry.bridgeRun,
|
|
396
|
+
store: entry.sessionStore.store,
|
|
397
|
+
storeIdentity: entry.sessionStore.identity,
|
|
374
398
|
sendState: entry.sendState,
|
|
375
399
|
created,
|
|
376
400
|
resumed: entry.resumed,
|
|
@@ -413,84 +437,98 @@ async function tryLeaseReadyEntry(
|
|
|
413
437
|
|
|
414
438
|
async function createSessionAgentEntry(
|
|
415
439
|
scopeKey: string,
|
|
440
|
+
persistentStore: boolean,
|
|
416
441
|
instanceId: number,
|
|
417
442
|
sendState: SessionCursorAgentSendState,
|
|
418
443
|
params: SessionCursorAgentCreateParams,
|
|
419
444
|
): Promise<SessionCursorAgentReadyEntry> {
|
|
420
|
-
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
421
445
|
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
446
|
+
let sessionStore: OpenCursorSessionStore | undefined;
|
|
447
|
+
try {
|
|
448
|
+
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
449
|
+
if (registeredBridge) {
|
|
450
|
+
bridgeRun = await registeredBridge.createRun({
|
|
451
|
+
onToolRequest: params.onBridgeToolRequest,
|
|
452
|
+
debugRecorder: params.debugRecorder,
|
|
453
|
+
});
|
|
454
|
+
if (!bridgeRun.enabled || !bridgeRun.mcpServers) {
|
|
455
|
+
await bridgeRun.dispose();
|
|
456
|
+
bridgeRun = undefined;
|
|
457
|
+
}
|
|
430
458
|
}
|
|
431
|
-
}
|
|
432
459
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
const agentOptions = {
|
|
443
|
-
apiKey: params.apiKey,
|
|
444
|
-
model: params.modelSelection,
|
|
445
|
-
mode: params.agentMode,
|
|
446
|
-
local: buildCursorLocalAgentOptions({
|
|
447
|
-
cwd: params.cwd,
|
|
448
|
-
settingSources: params.settingSources,
|
|
449
|
-
localSafety: params.localSafety,
|
|
450
|
-
}),
|
|
451
|
-
...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
|
|
452
|
-
};
|
|
453
|
-
let agent: SDKAgent | undefined;
|
|
454
|
-
let effectiveSendState = sendState;
|
|
455
|
-
let resumed = false;
|
|
456
|
-
let resumeNotice: string | undefined;
|
|
457
|
-
const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey) : undefined;
|
|
458
|
-
if (resumeHandle && resumeAgent) {
|
|
459
|
-
try {
|
|
460
|
-
agent = await resumeAgent(resumeHandle.agentId, agentOptions);
|
|
461
|
-
effectiveSendState = { ...resumeHandle.sendState };
|
|
462
|
-
resumed = true;
|
|
463
|
-
} catch {
|
|
464
|
-
resumeNotice = "Could not resume prior Cursor agent; continuing from current pi transcript in a new Cursor agent.";
|
|
460
|
+
const resolvedPoolKey = buildSessionAgentPoolKey(scopeKey, params);
|
|
461
|
+
const resumeEligible = params.localResume === true && !params.forceCreate;
|
|
462
|
+
let createAgent = params.createAgent;
|
|
463
|
+
let resumeAgent = params.resumeAgent;
|
|
464
|
+
if (!createAgent || (resumeEligible && !resumeAgent)) {
|
|
465
|
+
const sdk = await loadCursorSdk();
|
|
466
|
+
createAgent ??= sdk.Agent.create;
|
|
467
|
+
resumeAgent ??= sdk.Agent.resume;
|
|
465
468
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey) : undefined;
|
|
470
|
+
const storeSelection = await openCursorSessionStoreForScope({
|
|
471
|
+
cwd: params.cwd,
|
|
472
|
+
scopeKey,
|
|
473
|
+
persistent: persistentStore,
|
|
474
|
+
hasResumeHandle: resumeHandle !== undefined,
|
|
475
|
+
resumeIdentity: resumeHandle?.storeIdentity,
|
|
476
|
+
});
|
|
477
|
+
sessionStore = storeSelection.sessionStore;
|
|
478
|
+
const { identities } = storeSelection;
|
|
479
|
+
const resumeAttemptAllowed = storeSelection.resumeAttemptAllowed;
|
|
480
|
+
let resumeNotice = storeSelection.resumeFallback ? LOCAL_RESUME_FALLBACK_NOTICE : undefined;
|
|
481
|
+
const buildAgentOptions = () => ({
|
|
482
|
+
apiKey: params.apiKey,
|
|
483
|
+
model: params.modelSelection,
|
|
484
|
+
mode: params.agentMode,
|
|
485
|
+
local: buildCursorLocalAgentOptions({
|
|
486
|
+
cwd: params.cwd,
|
|
487
|
+
settingSources: params.settingSources,
|
|
488
|
+
localSafety: params.localSafety,
|
|
489
|
+
store: sessionStore!.store,
|
|
490
|
+
}),
|
|
491
|
+
...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
|
|
492
|
+
});
|
|
493
|
+
let agent: SDKAgent | undefined;
|
|
494
|
+
let effectiveSendState = sendState;
|
|
495
|
+
let resumed = false;
|
|
496
|
+
if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
|
|
472
497
|
try {
|
|
473
|
-
await
|
|
498
|
+
agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
|
|
499
|
+
effectiveSendState = { ...resumeHandle.sendState };
|
|
500
|
+
resumed = true;
|
|
474
501
|
} catch {
|
|
475
|
-
|
|
502
|
+
if (persistentStore) resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
|
|
503
|
+
if (!cursorSessionStoreIdentitiesEqual(sessionStore.identity, identities.sessionStore)) {
|
|
504
|
+
await sessionStore.dispose().catch(() => undefined);
|
|
505
|
+
sessionStore = await openCursorSessionStore(params.cwd, identities.sessionStore);
|
|
506
|
+
}
|
|
476
507
|
}
|
|
477
508
|
}
|
|
509
|
+
agent ??= await createAgent(buildAgentOptions());
|
|
510
|
+
if (!agent) throw new Error("Cursor SDK agent creation returned no agent");
|
|
511
|
+
if (!sessionStore) throw new Error("Cursor SDK session store was not opened");
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
status: "ready",
|
|
515
|
+
poolKey: resolvedPoolKey,
|
|
516
|
+
instanceId,
|
|
517
|
+
scopeKey,
|
|
518
|
+
agent,
|
|
519
|
+
bridgeRun,
|
|
520
|
+
sessionStore,
|
|
521
|
+
sendState: effectiveSendState,
|
|
522
|
+
resumeEnabled: params.localResume === true,
|
|
523
|
+
resumed,
|
|
524
|
+
...(resumeNotice ? { resumeNotice } : {}),
|
|
525
|
+
};
|
|
526
|
+
} catch (error) {
|
|
527
|
+
bridgeRun?.cancel("Cursor session agent create failed");
|
|
528
|
+
await bridgeRun?.dispose().catch(() => undefined);
|
|
529
|
+
await sessionStore?.dispose().catch(() => undefined);
|
|
478
530
|
throw error;
|
|
479
531
|
}
|
|
480
|
-
if (!agent) throw new Error("Cursor SDK agent creation returned no agent");
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
status: "ready",
|
|
484
|
-
poolKey: resolvedPoolKey,
|
|
485
|
-
instanceId,
|
|
486
|
-
scopeKey,
|
|
487
|
-
agent,
|
|
488
|
-
bridgeRun,
|
|
489
|
-
sendState: effectiveSendState,
|
|
490
|
-
resumeEnabled: params.localResume === true,
|
|
491
|
-
resumed,
|
|
492
|
-
...(resumeNotice ? { resumeNotice } : {}),
|
|
493
|
-
};
|
|
494
532
|
}
|
|
495
533
|
|
|
496
534
|
export {
|
|
@@ -510,6 +548,7 @@ export function invalidateSessionAgent(
|
|
|
510
548
|
|
|
511
549
|
export async function acquireSessionCursorAgent(params: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
|
|
512
550
|
const scopeKey = getCursorSessionScopeKey();
|
|
551
|
+
const persistentStore = getCursorSessionFile() !== undefined;
|
|
513
552
|
|
|
514
553
|
while (true) {
|
|
515
554
|
assertScopeAcceptsAcquire(scopeKey);
|
|
@@ -559,7 +598,7 @@ export async function acquireSessionCursorAgent(params: SessionCursorAgentCreate
|
|
|
559
598
|
const instanceId = allocateSessionAgentInstanceId();
|
|
560
599
|
const sendState = createInitialSendState();
|
|
561
600
|
let placeholder: SessionCursorAgentCreatingEntry;
|
|
562
|
-
const creating = createSessionAgentEntry(scopeKey, instanceId, sendState, params).then(async (createdEntry) => {
|
|
601
|
+
const creating = createSessionAgentEntry(scopeKey, persistentStore, instanceId, sendState, params).then(async (createdEntry) => {
|
|
563
602
|
const stillCurrent =
|
|
564
603
|
sessionAgentsByScope.get(scopeKey) === placeholder &&
|
|
565
604
|
getScopeCreationGeneration(scopeKey) === placeholder.creationGeneration;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { parseArgs } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ExtensionHandler, ProjectTrustHandler, SessionInfoChangedEvent, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
3
5
|
|
|
4
6
|
interface CursorSessionScopeExtensionApi {
|
|
7
|
+
on(event: "project_trust", handler: ProjectTrustHandler): void;
|
|
5
8
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
6
9
|
on(event: "session_info_changed", handler: ExtensionHandler<SessionInfoChangedEvent>): void;
|
|
7
10
|
}
|
|
@@ -22,6 +25,7 @@ const state = {
|
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
const scopeGenerations = new Map<string, number>([[ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration]]);
|
|
28
|
+
const projectTrustResolutionCwds = new Set<string>();
|
|
25
29
|
let nextSessionGeneration = 1;
|
|
26
30
|
let scopeChangeHandler: CursorSessionScopeChangeHandler | undefined;
|
|
27
31
|
|
|
@@ -85,6 +89,14 @@ function setCursorSessionScope(
|
|
|
85
89
|
scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration);
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
function recordProjectTrustResolution(cwd: string): void {
|
|
93
|
+
projectTrustResolutionCwds.add(resolve(cwd));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isCliProjectTrustApproved(args = process.argv.slice(2)): boolean {
|
|
97
|
+
return parseArgs(args).projectTrustOverride === true;
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
function resetCursorSessionScope(): void {
|
|
89
101
|
state.sessionCwd = process.cwd();
|
|
90
102
|
state.sessionFile = undefined;
|
|
@@ -95,6 +107,7 @@ function resetCursorSessionScope(): void {
|
|
|
95
107
|
nextSessionGeneration = 1;
|
|
96
108
|
scopeGenerations.clear();
|
|
97
109
|
scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration);
|
|
110
|
+
projectTrustResolutionCwds.clear();
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeHandler): void {
|
|
@@ -102,13 +115,18 @@ export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeH
|
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi): void {
|
|
118
|
+
pi.on("project_trust", (event) => {
|
|
119
|
+
recordProjectTrustResolution(event.cwd);
|
|
120
|
+
return { trusted: "undecided" };
|
|
121
|
+
});
|
|
105
122
|
pi.on("session_start", async (_event, ctx) => {
|
|
106
123
|
const previousScopeKey = getCursorSessionScopeKey();
|
|
107
124
|
setCursorSessionScope(
|
|
108
125
|
ctx.cwd,
|
|
109
126
|
ctx.sessionManager?.getSessionFile?.() ?? undefined,
|
|
110
127
|
ctx.sessionManager?.getSessionId?.() ?? undefined,
|
|
111
|
-
ctx.isProjectTrusted?.() === true
|
|
128
|
+
ctx.isProjectTrusted?.() === true
|
|
129
|
+
&& (projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()),
|
|
112
130
|
ctx.sessionManager?.getSessionName?.() ?? undefined,
|
|
113
131
|
);
|
|
114
132
|
if (previousScopeKey !== getCursorSessionScopeKey()) {
|
|
@@ -124,5 +142,7 @@ export const __testUtils = {
|
|
|
124
142
|
ANONYMOUS_SESSION_SCOPE_KEY,
|
|
125
143
|
EPHEMERAL_SESSION_SCOPE_PREFIX,
|
|
126
144
|
set: setCursorSessionScope,
|
|
145
|
+
recordProjectTrustResolution,
|
|
146
|
+
isCliProjectTrustApproved,
|
|
127
147
|
reset: resetCursorSessionScope,
|
|
128
148
|
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, toNamespacedPath } from "node:path";
|
|
5
|
+
import type { LocalAgentStore } from "@cursor/sdk";
|
|
6
|
+
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
7
|
+
|
|
8
|
+
export interface CursorSessionStoreIdentity {
|
|
9
|
+
readonly version: 1;
|
|
10
|
+
readonly stateRoot: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OpenCursorSessionStore {
|
|
14
|
+
identity: CursorSessionStoreIdentity;
|
|
15
|
+
store: LocalAgentStore;
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CursorSessionStoreSelection {
|
|
20
|
+
sessionStore: OpenCursorSessionStore;
|
|
21
|
+
identities: {
|
|
22
|
+
defaultStore: CursorSessionStoreIdentity;
|
|
23
|
+
sessionStore: CursorSessionStoreIdentity;
|
|
24
|
+
};
|
|
25
|
+
resumeAttemptAllowed: boolean;
|
|
26
|
+
resumeFallback: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CursorSessionStoreSdkOperations {
|
|
30
|
+
getDefaultStateRoot(cwd: string): string | Promise<string>;
|
|
31
|
+
openSqliteStore(options: { workspaceRef: string; stateRoot: string }): Promise<LocalAgentStore & { dispose(): Promise<void> }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let sdkOperationsForTests: CursorSessionStoreSdkOperations | undefined;
|
|
35
|
+
|
|
36
|
+
export function hashCursorSessionStoreScope(scopeKey: string): string {
|
|
37
|
+
return createHash("sha256")
|
|
38
|
+
.update("pi-cursor-sdk-session-store\0")
|
|
39
|
+
.update(scopeKey)
|
|
40
|
+
.digest("hex")
|
|
41
|
+
.slice(0, 32);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildCursorSessionStateRoot(defaultStateRoot: string, scopeKey: string, persistent: boolean): string {
|
|
45
|
+
const baseRoot = persistent ? defaultStateRoot : join(tmpdir(), `pi-cursor-sdk-${randomUUID()}`);
|
|
46
|
+
return join(baseRoot, "pi-sessions", hashCursorSessionStoreScope(scopeKey));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getSdkOperations(): Promise<CursorSessionStoreSdkOperations> {
|
|
50
|
+
if (sdkOperationsForTests) return sdkOperationsForTests;
|
|
51
|
+
const [{ getDefaultSdkStateRoot }, { SqliteLocalAgentStore }] = await Promise.all([
|
|
52
|
+
loadCursorSdk(),
|
|
53
|
+
import("@cursor/sdk/sqlite"),
|
|
54
|
+
]);
|
|
55
|
+
return {
|
|
56
|
+
getDefaultStateRoot: getDefaultSdkStateRoot,
|
|
57
|
+
openSqliteStore: (options) => SqliteLocalAgentStore.open(options),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function getCursorSessionStoreIdentities(
|
|
62
|
+
cwd: string,
|
|
63
|
+
scopeKey: string,
|
|
64
|
+
persistent: boolean,
|
|
65
|
+
): Promise<{ defaultStore: CursorSessionStoreIdentity; sessionStore: CursorSessionStoreIdentity }> {
|
|
66
|
+
const defaultStateRoot = await (await getSdkOperations()).getDefaultStateRoot(cwd);
|
|
67
|
+
return {
|
|
68
|
+
defaultStore: { version: 1, stateRoot: defaultStateRoot },
|
|
69
|
+
sessionStore: {
|
|
70
|
+
version: 1,
|
|
71
|
+
stateRoot: buildCursorSessionStateRoot(defaultStateRoot, scopeKey, persistent),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function cursorSessionStoreIdentitiesEqual(
|
|
77
|
+
left: CursorSessionStoreIdentity,
|
|
78
|
+
right: CursorSessionStoreIdentity,
|
|
79
|
+
): boolean {
|
|
80
|
+
return left.version === right.version && left.stateRoot === right.stateRoot;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function openOwnedCursorSessionStore(
|
|
84
|
+
cwd: string,
|
|
85
|
+
identity: CursorSessionStoreIdentity,
|
|
86
|
+
removalRoot?: string,
|
|
87
|
+
): Promise<OpenCursorSessionStore> {
|
|
88
|
+
const openedIdentity = Object.freeze({ ...identity });
|
|
89
|
+
let store: LocalAgentStore & { dispose(): Promise<void> };
|
|
90
|
+
try {
|
|
91
|
+
store = await (await getSdkOperations()).openSqliteStore({
|
|
92
|
+
workspaceRef: cwd,
|
|
93
|
+
stateRoot: toNamespacedPath(openedIdentity.stateRoot),
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (removalRoot) await rm(removalRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
identity: openedIdentity,
|
|
101
|
+
store,
|
|
102
|
+
dispose: async () => {
|
|
103
|
+
try {
|
|
104
|
+
await store.dispose();
|
|
105
|
+
} finally {
|
|
106
|
+
if (removalRoot) await rm(removalRoot, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function openCursorSessionStore(
|
|
113
|
+
cwd: string,
|
|
114
|
+
identity: CursorSessionStoreIdentity,
|
|
115
|
+
): Promise<OpenCursorSessionStore> {
|
|
116
|
+
return openOwnedCursorSessionStore(cwd, identity);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function openCursorSessionStoreForScope(options: {
|
|
120
|
+
cwd: string;
|
|
121
|
+
scopeKey: string;
|
|
122
|
+
persistent: boolean;
|
|
123
|
+
hasResumeHandle: boolean;
|
|
124
|
+
resumeIdentity?: CursorSessionStoreIdentity;
|
|
125
|
+
}): Promise<CursorSessionStoreSelection> {
|
|
126
|
+
const identities = await getCursorSessionStoreIdentities(options.cwd, options.scopeKey, options.persistent);
|
|
127
|
+
const requestedResumeIdentity = options.hasResumeHandle
|
|
128
|
+
? options.resumeIdentity ?? (options.persistent ? identities.defaultStore : undefined)
|
|
129
|
+
: undefined;
|
|
130
|
+
const resumableIdentities = options.persistent
|
|
131
|
+
? [identities.defaultStore, identities.sessionStore]
|
|
132
|
+
: [identities.sessionStore];
|
|
133
|
+
const resumeIdentity = requestedResumeIdentity && resumableIdentities
|
|
134
|
+
.find((identity) => cursorSessionStoreIdentitiesEqual(identity, requestedResumeIdentity));
|
|
135
|
+
let resumeAttemptAllowed = options.hasResumeHandle && resumeIdentity !== undefined;
|
|
136
|
+
let resumeFallback = options.persistent && options.hasResumeHandle && !resumeIdentity;
|
|
137
|
+
const selectedIdentity = resumeIdentity ?? identities.sessionStore;
|
|
138
|
+
const removalRoot = options.persistent ? undefined : dirname(dirname(identities.sessionStore.stateRoot));
|
|
139
|
+
let sessionStore: OpenCursorSessionStore;
|
|
140
|
+
try {
|
|
141
|
+
sessionStore = await openOwnedCursorSessionStore(options.cwd, selectedIdentity, removalRoot);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (!resumeIdentity || cursorSessionStoreIdentitiesEqual(resumeIdentity, identities.sessionStore)) throw error;
|
|
144
|
+
resumeAttemptAllowed = false;
|
|
145
|
+
resumeFallback = true;
|
|
146
|
+
sessionStore = await openOwnedCursorSessionStore(options.cwd, identities.sessionStore);
|
|
147
|
+
}
|
|
148
|
+
return { sessionStore, identities, resumeAttemptAllowed, resumeFallback };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const __testUtils = {
|
|
152
|
+
setSdkOperations(operations: CursorSessionStoreSdkOperations | undefined): void {
|
|
153
|
+
sdkOperationsForTests = operations;
|
|
154
|
+
},
|
|
155
|
+
};
|