pi-cursor-sdk 0.1.59 → 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 +33 -0
- package/README.md +124 -10
- package/docs/cursor-model-ux-spec.md +19 -11
- package/docs/cursor-native-tool-replay.md +5 -3
- package/docs/cursor-testing-lessons.md +2 -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 +15 -1
- 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 +62 -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 +131 -71
- 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;
|
|
@@ -126,9 +138,12 @@ interface SessionCursorAgentCreateParams {
|
|
|
126
138
|
|
|
127
139
|
const sessionAgentsByScope = new Map<string, SessionCursorAgentPoolEntry>();
|
|
128
140
|
const invalidatedScopeKeys = new Set<string>();
|
|
141
|
+
const deadTransportScopeKeys = new Set<string>();
|
|
142
|
+
let deadTransportAgentDisposeTimeoutMs = 3000;
|
|
129
143
|
const terminalDisposedScopeGenerations = new Map<string, number>();
|
|
130
144
|
const scopeCreationGenerations = new Map<string, number>();
|
|
131
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.";
|
|
132
147
|
let nextSessionAgentInstanceId = 1;
|
|
133
148
|
|
|
134
149
|
export interface CursorLocalSafetyOptions {
|
|
@@ -140,9 +155,11 @@ export function buildCursorLocalAgentOptions(options: {
|
|
|
140
155
|
cwd: string;
|
|
141
156
|
settingSources?: SettingSource[];
|
|
142
157
|
localSafety?: CursorLocalSafetyOptions;
|
|
158
|
+
store?: LocalAgentStore;
|
|
143
159
|
}): LocalAgentOptions {
|
|
144
160
|
return {
|
|
145
161
|
cwd: options.cwd,
|
|
162
|
+
...(options.store ? { store: options.store } : {}),
|
|
146
163
|
...(options.settingSources ? { settingSources: options.settingSources } : {}),
|
|
147
164
|
...(options.localSafety?.autoReview === true ? { autoReview: true } : {}),
|
|
148
165
|
...(options.localSafety?.sandboxEnabled === true ? { sandboxOptions: { enabled: true } } : {}),
|
|
@@ -201,12 +218,17 @@ function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCr
|
|
|
201
218
|
buildModelPoolKey(params.modelSelection),
|
|
202
219
|
buildSettingSourcesPoolKey(params.settingSources),
|
|
203
220
|
buildLocalSafetyPoolKey(params.localSafety),
|
|
221
|
+
params.useHttp1ForAgent === undefined
|
|
222
|
+
? "http1:default"
|
|
223
|
+
: params.useHttp1ForAgent
|
|
224
|
+
? "http1:on"
|
|
225
|
+
: "http1:off",
|
|
204
226
|
buildApiKeyPoolKeyFingerprint(params.apiKey),
|
|
205
227
|
buildBridgePoolKeySuffix(),
|
|
206
228
|
].join("\0");
|
|
207
229
|
}
|
|
208
230
|
|
|
209
|
-
async function disposePoolEntry(entry: SessionCursorAgentPoolEntry): Promise<void> {
|
|
231
|
+
async function disposePoolEntry(entry: SessionCursorAgentPoolEntry, options?: { deadTransport?: boolean }): Promise<void> {
|
|
210
232
|
if (!isActivePoolEntry(entry)) return;
|
|
211
233
|
entry.bridgeRun?.cancel("Cursor session agent disposed");
|
|
212
234
|
try {
|
|
@@ -215,10 +237,19 @@ async function disposePoolEntry(entry: SessionCursorAgentPoolEntry): Promise<voi
|
|
|
215
237
|
// disposal failure should not block session replacement
|
|
216
238
|
}
|
|
217
239
|
try {
|
|
218
|
-
|
|
240
|
+
const disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]()).catch(() => undefined);
|
|
241
|
+
// A dead local transport may never settle SDK disposal; bound the wait so the
|
|
242
|
+
// next acquire recreates instead of hanging on the dead agent.
|
|
243
|
+
await (options?.deadTransport
|
|
244
|
+
? Promise.race([
|
|
245
|
+
disposal,
|
|
246
|
+
new Promise<void>((resolve) => setTimeout(resolve, deadTransportAgentDisposeTimeoutMs).unref?.()),
|
|
247
|
+
])
|
|
248
|
+
: disposal);
|
|
219
249
|
} catch {
|
|
220
250
|
// disposal failure should not block session replacement
|
|
221
251
|
}
|
|
252
|
+
await entry.sessionStore.dispose().catch(() => undefined);
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?: boolean }): Promise<void> {
|
|
@@ -228,6 +259,7 @@ async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?:
|
|
|
228
259
|
}
|
|
229
260
|
const entry = sessionAgentsByScope.get(scopeKey);
|
|
230
261
|
invalidatedScopeKeys.delete(scopeKey);
|
|
262
|
+
const deadTransport = deadTransportScopeKeys.delete(scopeKey);
|
|
231
263
|
if (!entry) return;
|
|
232
264
|
sessionAgentsByScope.delete(scopeKey);
|
|
233
265
|
if (entry.status === "busy") {
|
|
@@ -239,7 +271,7 @@ async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?:
|
|
|
239
271
|
});
|
|
240
272
|
return;
|
|
241
273
|
}
|
|
242
|
-
await disposePoolEntry(entry);
|
|
274
|
+
await disposePoolEntry(entry, { deadTransport });
|
|
243
275
|
}
|
|
244
276
|
|
|
245
277
|
function createInitialSendState(): SessionCursorAgentSendState {
|
|
@@ -276,6 +308,7 @@ function commitSessionAgentSendForLease(
|
|
|
276
308
|
agentId: entry.agent.agentId,
|
|
277
309
|
poolKey: entry.poolKey,
|
|
278
310
|
sendState: entry.sendState,
|
|
311
|
+
storeIdentity: entry.sessionStore.identity,
|
|
279
312
|
});
|
|
280
313
|
}
|
|
281
314
|
}
|
|
@@ -360,6 +393,8 @@ function leaseFromEntry(
|
|
|
360
393
|
instanceId: entry.instanceId,
|
|
361
394
|
agent: entry.agent,
|
|
362
395
|
bridgeRun: entry.bridgeRun,
|
|
396
|
+
store: entry.sessionStore.store,
|
|
397
|
+
storeIdentity: entry.sessionStore.identity,
|
|
363
398
|
sendState: entry.sendState,
|
|
364
399
|
created,
|
|
365
400
|
resumed: entry.resumed,
|
|
@@ -402,84 +437,98 @@ async function tryLeaseReadyEntry(
|
|
|
402
437
|
|
|
403
438
|
async function createSessionAgentEntry(
|
|
404
439
|
scopeKey: string,
|
|
440
|
+
persistentStore: boolean,
|
|
405
441
|
instanceId: number,
|
|
406
442
|
sendState: SessionCursorAgentSendState,
|
|
407
443
|
params: SessionCursorAgentCreateParams,
|
|
408
444
|
): Promise<SessionCursorAgentReadyEntry> {
|
|
409
|
-
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
410
445
|
let bridgeRun: CursorPiToolBridgeRun | undefined;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|
|
419
458
|
}
|
|
420
|
-
}
|
|
421
459
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
const agentOptions = {
|
|
432
|
-
apiKey: params.apiKey,
|
|
433
|
-
model: params.modelSelection,
|
|
434
|
-
mode: params.agentMode,
|
|
435
|
-
local: buildCursorLocalAgentOptions({
|
|
436
|
-
cwd: params.cwd,
|
|
437
|
-
settingSources: params.settingSources,
|
|
438
|
-
localSafety: params.localSafety,
|
|
439
|
-
}),
|
|
440
|
-
...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
|
|
441
|
-
};
|
|
442
|
-
let agent: SDKAgent | undefined;
|
|
443
|
-
let effectiveSendState = sendState;
|
|
444
|
-
let resumed = false;
|
|
445
|
-
let resumeNotice: string | undefined;
|
|
446
|
-
const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey) : undefined;
|
|
447
|
-
if (resumeHandle && resumeAgent) {
|
|
448
|
-
try {
|
|
449
|
-
agent = await resumeAgent(resumeHandle.agentId, agentOptions);
|
|
450
|
-
effectiveSendState = { ...resumeHandle.sendState };
|
|
451
|
-
resumed = true;
|
|
452
|
-
} catch {
|
|
453
|
-
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;
|
|
454
468
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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) {
|
|
461
497
|
try {
|
|
462
|
-
await
|
|
498
|
+
agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
|
|
499
|
+
effectiveSendState = { ...resumeHandle.sendState };
|
|
500
|
+
resumed = true;
|
|
463
501
|
} catch {
|
|
464
|
-
|
|
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
|
+
}
|
|
465
507
|
}
|
|
466
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);
|
|
467
530
|
throw error;
|
|
468
531
|
}
|
|
469
|
-
if (!agent) throw new Error("Cursor SDK agent creation returned no agent");
|
|
470
|
-
|
|
471
|
-
return {
|
|
472
|
-
status: "ready",
|
|
473
|
-
poolKey: resolvedPoolKey,
|
|
474
|
-
instanceId,
|
|
475
|
-
scopeKey,
|
|
476
|
-
agent,
|
|
477
|
-
bridgeRun,
|
|
478
|
-
sendState: effectiveSendState,
|
|
479
|
-
resumeEnabled: params.localResume === true,
|
|
480
|
-
resumed,
|
|
481
|
-
...(resumeNotice ? { resumeNotice } : {}),
|
|
482
|
-
};
|
|
483
532
|
}
|
|
484
533
|
|
|
485
534
|
export {
|
|
@@ -489,12 +538,17 @@ export {
|
|
|
489
538
|
type CursorSessionSendPlan,
|
|
490
539
|
} from "./cursor-session-send-policy.js";
|
|
491
540
|
|
|
492
|
-
export function invalidateSessionAgent(
|
|
541
|
+
export function invalidateSessionAgent(
|
|
542
|
+
scopeKey: string = getCursorSessionScopeKey(),
|
|
543
|
+
options?: { deadTransport?: boolean },
|
|
544
|
+
): void {
|
|
493
545
|
invalidatedScopeKeys.add(scopeKey);
|
|
546
|
+
if (options?.deadTransport) deadTransportScopeKeys.add(scopeKey);
|
|
494
547
|
}
|
|
495
548
|
|
|
496
549
|
export async function acquireSessionCursorAgent(params: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
|
|
497
550
|
const scopeKey = getCursorSessionScopeKey();
|
|
551
|
+
const persistentStore = getCursorSessionFile() !== undefined;
|
|
498
552
|
|
|
499
553
|
while (true) {
|
|
500
554
|
assertScopeAcceptsAcquire(scopeKey);
|
|
@@ -544,7 +598,7 @@ export async function acquireSessionCursorAgent(params: SessionCursorAgentCreate
|
|
|
544
598
|
const instanceId = allocateSessionAgentInstanceId();
|
|
545
599
|
const sendState = createInitialSendState();
|
|
546
600
|
let placeholder: SessionCursorAgentCreatingEntry;
|
|
547
|
-
const creating = createSessionAgentEntry(scopeKey, instanceId, sendState, params).then(async (createdEntry) => {
|
|
601
|
+
const creating = createSessionAgentEntry(scopeKey, persistentStore, instanceId, sendState, params).then(async (createdEntry) => {
|
|
548
602
|
const stillCurrent =
|
|
549
603
|
sessionAgentsByScope.get(scopeKey) === placeholder &&
|
|
550
604
|
getScopeCreationGeneration(scopeKey) === placeholder.creationGeneration;
|
|
@@ -611,6 +665,7 @@ export async function disposeAllSessionCursorAgents(): Promise<void> {
|
|
|
611
665
|
const scopeKeys = [...new Set([...sessionAgentsByScope.keys(), ...terminalDisposedScopeGenerations.keys()])];
|
|
612
666
|
await Promise.all(scopeKeys.map((scopeKey) => disposePoolEntryForScope(scopeKey, { terminal: true })));
|
|
613
667
|
invalidatedScopeKeys.clear();
|
|
668
|
+
deadTransportScopeKeys.clear();
|
|
614
669
|
terminalDisposedScopeGenerations.clear();
|
|
615
670
|
}
|
|
616
671
|
|
|
@@ -624,6 +679,11 @@ export const __testUtils = {
|
|
|
624
679
|
disposeAllSessionCursorAgents,
|
|
625
680
|
buildApiKeyPoolKeyFingerprint,
|
|
626
681
|
buildSessionAgentPoolKey,
|
|
682
|
+
setDeadTransportAgentDisposeTimeoutMs(ms: number): number {
|
|
683
|
+
const previous = deadTransportAgentDisposeTimeoutMs;
|
|
684
|
+
deadTransportAgentDisposeTimeoutMs = ms;
|
|
685
|
+
return previous;
|
|
686
|
+
},
|
|
627
687
|
SessionCursorAgentCreationSupersededError,
|
|
628
688
|
SessionCursorAgentScopeClosedError,
|
|
629
689
|
};
|
|
@@ -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
|
};
|