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.
Files changed (57) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +124 -10
  3. package/docs/cursor-model-ux-spec.md +19 -11
  4. package/docs/cursor-native-tool-replay.md +5 -3
  5. package/docs/cursor-testing-lessons.md +2 -2
  6. package/docs/cursor-tool-surfaces.md +7 -1
  7. package/docs/platform-smoke.md +51 -20
  8. package/package.json +13 -2
  9. package/platform-smoke.config.mjs +1 -0
  10. package/scripts/cloud-runtime-smoke.d.mts +40 -1
  11. package/scripts/cloud-runtime-smoke.mjs +444 -325
  12. package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
  13. package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
  14. package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
  15. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
  16. package/scripts/lib/cloud-smoke-github.d.mts +78 -0
  17. package/scripts/lib/cloud-smoke-github.mjs +331 -0
  18. package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
  19. package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
  20. package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
  21. package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
  22. package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
  23. package/scripts/local-resume-cleanup-smoke.mjs +11 -2
  24. package/scripts/platform-smoke/card-detect.mjs +1 -0
  25. package/scripts/platform-smoke/scenarios.d.mts +22 -0
  26. package/scripts/platform-smoke/scenarios.mjs +17 -0
  27. package/scripts/platform-smoke/targets.mjs +1 -0
  28. package/src/cursor-agent-message-web-tools.ts +14 -6
  29. package/src/cursor-cloud-local-state.ts +536 -0
  30. package/src/cursor-cloud-options.ts +46 -70
  31. package/src/cursor-config.ts +160 -14
  32. package/src/cursor-http1.ts +65 -0
  33. package/src/cursor-live-run-coordinator.ts +8 -1
  34. package/src/cursor-pi-tool-bridge-abort.ts +10 -3
  35. package/src/cursor-pi-tool-bridge-constants.ts +6 -0
  36. package/src/cursor-pi-tool-bridge-env.ts +9 -0
  37. package/src/cursor-pi-tool-bridge-run.ts +27 -5
  38. package/src/cursor-pi-tool-bridge.ts +11 -2
  39. package/src/cursor-provider-errors.ts +72 -2
  40. package/src/cursor-provider-live-run-drain.ts +2 -1
  41. package/src/cursor-provider-run-finalizer.ts +8 -2
  42. package/src/cursor-provider-run-outcome.ts +7 -1
  43. package/src/cursor-provider-turn-finalize.ts +23 -4
  44. package/src/cursor-provider-turn-prepare.ts +17 -3
  45. package/src/cursor-provider-turn-runner.ts +15 -1
  46. package/src/cursor-provider-turn-send.ts +1 -1
  47. package/src/cursor-question-tool.ts +8 -0
  48. package/src/cursor-ripgrep-path.ts +32 -0
  49. package/src/cursor-runtime-state.ts +119 -100
  50. package/src/cursor-sdk-process-error-guard.ts +62 -0
  51. package/src/cursor-session-agent-cleanup.ts +66 -15
  52. package/src/cursor-session-agent-lifecycle.ts +9 -4
  53. package/src/cursor-session-agent-resume.ts +50 -7
  54. package/src/cursor-session-agent.ts +131 -71
  55. package/src/cursor-session-scope.ts +22 -2
  56. package/src/cursor-session-store.ts +155 -0
  57. 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;
@@ -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
- await entry.agent[Symbol.asyncDispose]();
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
- if (registeredBridge) {
412
- bridgeRun = await registeredBridge.createRun({
413
- onToolRequest: params.onBridgeToolRequest,
414
- debugRecorder: params.debugRecorder,
415
- });
416
- if (!bridgeRun.enabled || !bridgeRun.mcpServers) {
417
- await bridgeRun.dispose();
418
- 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
+ }
419
458
  }
420
- }
421
459
 
422
- const resolvedPoolKey = buildSessionAgentPoolKey(scopeKey, params);
423
- const resumeEligible = params.localResume === true && !params.forceCreate;
424
- let createAgent = params.createAgent;
425
- let resumeAgent = params.resumeAgent;
426
- if (!createAgent || (resumeEligible && !resumeAgent)) {
427
- const sdk = await loadCursorSdk();
428
- createAgent ??= sdk.Agent.create;
429
- resumeAgent ??= sdk.Agent.resume;
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
- try {
457
- agent ??= await createAgent(agentOptions);
458
- } catch (error) {
459
- if (bridgeRun) {
460
- 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) {
461
497
  try {
462
- await bridgeRun.dispose();
498
+ agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
499
+ effectiveSendState = { ...resumeHandle.sendState };
500
+ resumed = true;
463
501
  } catch {
464
- // 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
+ }
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(scopeKey: string = getCursorSessionScopeKey()): void {
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 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
  };