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.
Files changed (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +123 -9
  3. package/docs/cursor-model-ux-spec.md +19 -11
  4. package/docs/cursor-native-tool-replay.md +4 -2
  5. package/docs/cursor-tool-surfaces.md +7 -1
  6. package/docs/platform-smoke.md +51 -20
  7. package/package.json +13 -2
  8. package/platform-smoke.config.mjs +1 -0
  9. package/scripts/cloud-runtime-smoke.d.mts +40 -1
  10. package/scripts/cloud-runtime-smoke.mjs +444 -325
  11. package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
  12. package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
  13. package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
  14. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
  15. package/scripts/lib/cloud-smoke-github.d.mts +78 -0
  16. package/scripts/lib/cloud-smoke-github.mjs +331 -0
  17. package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
  18. package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
  19. package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
  20. package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
  21. package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
  22. package/scripts/local-resume-cleanup-smoke.mjs +11 -2
  23. package/scripts/platform-smoke/card-detect.mjs +1 -0
  24. package/scripts/platform-smoke/scenarios.d.mts +22 -0
  25. package/scripts/platform-smoke/scenarios.mjs +17 -0
  26. package/scripts/platform-smoke/targets.mjs +1 -0
  27. package/src/cursor-agent-message-web-tools.ts +14 -6
  28. package/src/cursor-cloud-local-state.ts +536 -0
  29. package/src/cursor-cloud-options.ts +46 -70
  30. package/src/cursor-config.ts +160 -14
  31. package/src/cursor-http1.ts +65 -0
  32. package/src/cursor-live-run-coordinator.ts +8 -1
  33. package/src/cursor-pi-tool-bridge-abort.ts +10 -3
  34. package/src/cursor-pi-tool-bridge-constants.ts +6 -0
  35. package/src/cursor-pi-tool-bridge-env.ts +9 -0
  36. package/src/cursor-pi-tool-bridge-run.ts +27 -5
  37. package/src/cursor-pi-tool-bridge.ts +11 -2
  38. package/src/cursor-provider-errors.ts +72 -2
  39. package/src/cursor-provider-live-run-drain.ts +2 -1
  40. package/src/cursor-provider-run-finalizer.ts +8 -2
  41. package/src/cursor-provider-run-outcome.ts +7 -1
  42. package/src/cursor-provider-turn-finalize.ts +23 -4
  43. package/src/cursor-provider-turn-prepare.ts +17 -3
  44. package/src/cursor-provider-turn-runner.ts +5 -0
  45. package/src/cursor-provider-turn-send.ts +1 -1
  46. package/src/cursor-question-tool.ts +8 -0
  47. package/src/cursor-ripgrep-path.ts +32 -0
  48. package/src/cursor-runtime-state.ts +119 -100
  49. package/src/cursor-sdk-process-error-guard.ts +19 -0
  50. package/src/cursor-session-agent-cleanup.ts +66 -15
  51. package/src/cursor-session-agent-lifecycle.ts +9 -4
  52. package/src/cursor-session-agent-resume.ts +50 -7
  53. package/src/cursor-session-agent.ts +106 -67
  54. package/src/cursor-session-scope.ts +22 -2
  55. package/src/cursor-session-store.ts +155 -0
  56. 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 RESUME_ENTRY_VERSION = 1;
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 (record.version !== RESUME_ENTRY_VERSION || record.runtime !== "local") return undefined;
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: RESUME_ENTRY_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 previousAgentId = state.activeHandle?.agentId ?? state.lastBranchHandle?.agentId;
342
- const cleanupCandidateAgentIds = previousAgentId && previousAgentId !== pending.agentId ? [previousAgentId] : undefined;
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
- ...(cleanupCandidateAgentIds ? { cleanupCandidateAgentIds } : {}),
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
- if (registeredBridge) {
423
- bridgeRun = await registeredBridge.createRun({
424
- onToolRequest: params.onBridgeToolRequest,
425
- debugRecorder: params.debugRecorder,
426
- });
427
- if (!bridgeRun.enabled || !bridgeRun.mcpServers) {
428
- await bridgeRun.dispose();
429
- bridgeRun = undefined;
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
- const resolvedPoolKey = buildSessionAgentPoolKey(scopeKey, params);
434
- const resumeEligible = params.localResume === true && !params.forceCreate;
435
- let createAgent = params.createAgent;
436
- let resumeAgent = params.resumeAgent;
437
- if (!createAgent || (resumeEligible && !resumeAgent)) {
438
- const sdk = await loadCursorSdk();
439
- createAgent ??= sdk.Agent.create;
440
- resumeAgent ??= sdk.Agent.resume;
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
- try {
468
- agent ??= await createAgent(agentOptions);
469
- } catch (error) {
470
- if (bridgeRun) {
471
- bridgeRun.cancel("Cursor session agent create failed");
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 bridgeRun.dispose();
498
+ agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
499
+ effectiveSendState = { ...resumeHandle.sendState };
500
+ resumed = true;
474
501
  } catch {
475
- // bridge disposal failure should not mask agent create failure
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 type { ExtensionHandler, SessionInfoChangedEvent, SessionStartEvent } from "@earendil-works/pi-coding-agent";
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
+ };