pi-cursor-sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +65 -13
  3. package/dist/context-window-cache.js +0 -6
  4. package/dist/context.js +8 -0
  5. package/dist/cursor-cloud-lifecycle.js +122 -98
  6. package/dist/cursor-durable-fs.js +97 -3
  7. package/dist/cursor-fallback-models.generated.js +288 -1
  8. package/dist/cursor-fallback-warning.js +2 -2
  9. package/dist/cursor-http1.js +8 -8
  10. package/dist/cursor-live-run-coordinator.js +9 -13
  11. package/dist/cursor-native-replay-routing.js +1 -1
  12. package/dist/cursor-native-tool-display-registration.js +46 -22
  13. package/dist/cursor-native-tool-display-state.js +57 -22
  14. package/dist/cursor-native-tool-display-tools.js +5 -6
  15. package/dist/cursor-pi-tool-bridge-constants.js +23 -2
  16. package/dist/cursor-pi-tool-bridge-run.js +7 -3
  17. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  18. package/dist/cursor-pi-tool-bridge.js +31 -9
  19. package/dist/cursor-provider-binding.js +58 -0
  20. package/dist/cursor-provider-errors.js +28 -7
  21. package/dist/cursor-provider-lazy.js +23 -11
  22. package/dist/cursor-provider-live-run-drain.js +15 -12
  23. package/dist/cursor-provider-run-finalizer.js +19 -8
  24. package/dist/cursor-provider-turn-coordinator.js +1 -0
  25. package/dist/cursor-provider-turn-display-router.js +4 -0
  26. package/dist/cursor-provider-turn-emit.js +1 -0
  27. package/dist/cursor-provider-turn-finalize.js +29 -20
  28. package/dist/cursor-provider-turn-prepare.js +24 -11
  29. package/dist/cursor-provider-turn-runner.js +37 -11
  30. package/dist/cursor-provider-turn-send.js +22 -9
  31. package/dist/cursor-provider-turn-summary.js +89 -0
  32. package/dist/cursor-provider.js +9 -3
  33. package/dist/cursor-request-provenance.js +42 -0
  34. package/dist/cursor-ripgrep-path.js +14 -3
  35. package/dist/cursor-runtime-state.js +50 -43
  36. package/dist/cursor-sdk-billed-usage.js +54 -75
  37. package/dist/cursor-sdk-event-debug-session.js +5 -6
  38. package/dist/cursor-sdk-event-debug.js +9 -6
  39. package/dist/cursor-sdk-process-error-guard.js +8 -7
  40. package/dist/cursor-sdk-runtime.js +13 -0
  41. package/dist/cursor-session-agent-cleanup.js +2 -2
  42. package/dist/cursor-session-agent-lifecycle.js +18 -9
  43. package/dist/cursor-session-agent-lineage.js +28 -12
  44. package/dist/cursor-session-agent-resume.js +51 -35
  45. package/dist/cursor-session-agent.js +9 -9
  46. package/dist/cursor-session-compaction-prep.js +2 -7
  47. package/dist/cursor-session-scope.js +71 -39
  48. package/dist/cursor-session-settings.js +61 -0
  49. package/dist/cursor-skill-tool.js +32 -49
  50. package/dist/cursor-state.js +72 -62
  51. package/dist/cursor-usage-accounting.js +13 -70
  52. package/dist/cursor-usage-command.js +89 -0
  53. package/dist/cursor-usage-ledger.js +512 -0
  54. package/dist/index.js +11 -19
  55. package/dist/model-discovery.js +11 -19
  56. package/docs/cursor-dogfood-checklist.md +3 -3
  57. package/docs/cursor-live-smoke-checklist.md +15 -15
  58. package/docs/cursor-model-ux-spec.md +59 -18
  59. package/docs/cursor-native-tool-replay.md +5 -3
  60. package/docs/cursor-native-tool-visual-audit.md +8 -8
  61. package/docs/cursor-testing-lessons.md +47 -23
  62. package/docs/cursor-tool-surfaces.md +1 -1
  63. package/docs/platform-smoke-implementation.md +10 -6
  64. package/docs/platform-smoke.md +35 -46
  65. package/package.json +6 -6
  66. package/scripts/cloud-runtime-smoke.mjs +4 -2
  67. package/scripts/local-resume-smoke.mjs +1 -1
  68. package/scripts/platform-smoke/card-detect.mjs +2 -8
  69. package/scripts/platform-smoke/targets.mjs +8 -15
  70. package/scripts/platform-smoke/visual-evidence.mjs +7 -2
  71. package/scripts/tmux-live-smoke.sh +57 -90
  72. package/src/context-window-cache.ts +0 -6
  73. package/src/context.ts +8 -0
  74. package/src/cursor-cloud-lifecycle.ts +124 -98
  75. package/src/cursor-durable-fs.ts +65 -3
  76. package/src/cursor-fallback-models.generated.ts +288 -1
  77. package/src/cursor-fallback-warning.ts +2 -2
  78. package/src/cursor-http1.ts +8 -8
  79. package/src/cursor-live-run-coordinator.ts +12 -13
  80. package/src/cursor-native-replay-routing.ts +2 -1
  81. package/src/cursor-native-tool-display-registration.ts +48 -24
  82. package/src/cursor-native-tool-display-state.ts +65 -21
  83. package/src/cursor-native-tool-display-tools.ts +7 -4
  84. package/src/cursor-pi-tool-bridge-constants.ts +27 -2
  85. package/src/cursor-pi-tool-bridge-run.ts +11 -3
  86. package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
  87. package/src/cursor-pi-tool-bridge.ts +26 -9
  88. package/src/cursor-provider-binding.ts +62 -0
  89. package/src/cursor-provider-errors.ts +27 -7
  90. package/src/cursor-provider-lazy.ts +21 -15
  91. package/src/cursor-provider-live-run-drain.ts +20 -13
  92. package/src/cursor-provider-run-finalizer.ts +24 -15
  93. package/src/cursor-provider-turn-coordinator.ts +2 -0
  94. package/src/cursor-provider-turn-display-router.ts +5 -0
  95. package/src/cursor-provider-turn-emit.ts +3 -1
  96. package/src/cursor-provider-turn-finalize.ts +38 -23
  97. package/src/cursor-provider-turn-prepare.ts +23 -14
  98. package/src/cursor-provider-turn-runner.ts +40 -13
  99. package/src/cursor-provider-turn-send.ts +23 -15
  100. package/src/cursor-provider-turn-summary.ts +94 -0
  101. package/src/cursor-provider-turn-types.ts +37 -4
  102. package/src/cursor-provider.ts +25 -3
  103. package/src/cursor-request-provenance.ts +64 -0
  104. package/src/cursor-ripgrep-path.ts +15 -5
  105. package/src/cursor-runtime-state.ts +53 -48
  106. package/src/cursor-sdk-billed-usage.ts +54 -102
  107. package/src/cursor-sdk-event-debug-session.ts +5 -5
  108. package/src/cursor-sdk-event-debug.ts +10 -6
  109. package/src/cursor-sdk-process-error-guard.ts +7 -7
  110. package/src/cursor-sdk-runtime.ts +14 -0
  111. package/src/cursor-session-agent-cleanup.ts +2 -2
  112. package/src/cursor-session-agent-lifecycle.ts +14 -9
  113. package/src/cursor-session-agent-lineage.ts +26 -12
  114. package/src/cursor-session-agent-resume.ts +49 -36
  115. package/src/cursor-session-agent.ts +13 -10
  116. package/src/cursor-session-compaction-prep.ts +2 -7
  117. package/src/cursor-session-scope.ts +87 -39
  118. package/src/cursor-session-settings.ts +72 -0
  119. package/src/cursor-skill-tool.ts +37 -61
  120. package/src/cursor-state.ts +74 -67
  121. package/src/cursor-usage-accounting.ts +17 -82
  122. package/src/cursor-usage-command.ts +76 -0
  123. package/src/cursor-usage-ledger.ts +500 -0
  124. package/src/index.ts +14 -23
  125. package/src/model-discovery.ts +14 -22
@@ -1,5 +1,5 @@
1
1
  import { isCursorLocalAgentId } from "./cursor-session-agent-resume.js";
2
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
2
+ import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
3
3
  import { asRecord } from "./cursor-record-utils.js";
4
4
  export const CURSOR_SESSION_AGENT_LINEAGE_ENTRY_TYPE = "cursor-sdk-agent-lineage";
5
5
  const LINEAGE_ENTRY_VERSION = 1;
@@ -9,9 +9,10 @@ function isIsoTimestamp(value) {
9
9
  const timestamp = Date.parse(value);
10
10
  return !Number.isNaN(timestamp) && new Date(timestamp).toISOString() === value;
11
11
  }
12
- const state = {
12
+ let state = {
13
13
  recordedAgentIds: new Set(),
14
14
  };
15
+ const statesByScope = new Map();
15
16
  export function parseCursorSessionAgentLineageEntryData(value) {
16
17
  const record = asRecord(value);
17
18
  if (record?.version !== LINEAGE_ENTRY_VERSION ||
@@ -48,11 +49,16 @@ function readRecordedAgentIds(entries, sessionId) {
48
49
  }));
49
50
  }
50
51
  /** Best-effort forensic lineage at the local Agent.send() boundary. Independent of resume. */
51
- export function recordCursorSessionAgentLineage(agentId) {
52
- const { appendEntry, sessionId, sessionFile, scopeKey, cwd } = state;
52
+ export function recordCursorSessionAgentLineage(agentId, turnScopeKey) {
53
+ const sessionState = turnScopeKey === undefined
54
+ ? state
55
+ : statesByScope.get(turnScopeKey) ?? (state.scopeKey === turnScopeKey ? state : undefined);
56
+ if (!sessionState)
57
+ return;
58
+ const { appendEntry, sessionId, sessionFile, scopeKey, cwd } = sessionState;
53
59
  if (!appendEntry || !sessionId || !scopeKey || !cwd)
54
60
  return;
55
- if (!isCursorLocalAgentId(agentId) || state.recordedAgentIds.has(agentId))
61
+ if (!isCursorLocalAgentId(agentId) || sessionState.recordedAgentIds.has(agentId))
56
62
  return;
57
63
  const data = {
58
64
  version: LINEAGE_ENTRY_VERSION,
@@ -66,22 +72,31 @@ export function recordCursorSessionAgentLineage(agentId) {
66
72
  };
67
73
  try {
68
74
  appendEntry(CURSOR_SESSION_AGENT_LINEAGE_ENTRY_TYPE, data);
69
- state.recordedAgentIds.add(agentId);
75
+ sessionState.recordedAgentIds.add(agentId);
70
76
  }
71
77
  catch {
72
78
  // Lineage is forensic metadata; a failed stock pi append must not affect the session.
73
79
  }
74
80
  }
75
81
  export function registerCursorSessionAgentLineage(pi) {
82
+ const sessionState = { recordedAgentIds: new Set() };
76
83
  pi.on("session_start", (_event, ctx) => {
77
- state.appendEntry = pi.appendEntry;
78
- state.sessionId = ctx.sessionManager.getSessionId();
79
- state.sessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
80
- state.scopeKey = getCursorSessionScopeKey();
81
- state.cwd = ctx.cwd;
82
- state.recordedAgentIds = readRecordedAgentIds(ctx.sessionManager.getEntries(), state.sessionId);
84
+ state = sessionState;
85
+ const record = sessionState;
86
+ if (record.scopeKey && statesByScope.get(record.scopeKey) === record)
87
+ statesByScope.delete(record.scopeKey);
88
+ record.appendEntry = pi.appendEntry;
89
+ record.sessionId = ctx.sessionManager.getSessionId();
90
+ record.sessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
91
+ record.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
92
+ statesByScope.set(record.scopeKey, record);
93
+ record.cwd = ctx.cwd;
94
+ record.recordedAgentIds = readRecordedAgentIds(ctx.sessionManager.getEntries(), record.sessionId);
83
95
  });
84
96
  pi.on("session_shutdown", () => {
97
+ const state = sessionState;
98
+ if (state.scopeKey && statesByScope.get(state.scopeKey) === state)
99
+ statesByScope.delete(state.scopeKey);
85
100
  state.appendEntry = undefined;
86
101
  state.sessionId = undefined;
87
102
  state.sessionFile = undefined;
@@ -91,6 +106,7 @@ export function registerCursorSessionAgentLineage(pi) {
91
106
  });
92
107
  }
93
108
  function resetStateForTests() {
109
+ statesByScope.clear();
94
110
  state.appendEntry = undefined;
95
111
  state.sessionId = undefined;
96
112
  state.sessionFile = undefined;
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { asRecord } from "./cursor-record-utils.js";
4
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
4
+ import { getCursorSessionScopeSnapshot, getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
5
  export const CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE = "cursor-sdk-agent-resume";
6
6
  const LEGACY_RESUME_ENTRY_VERSION = 1;
7
7
  const RESUME_ENTRY_VERSION = 2;
@@ -11,22 +11,19 @@ const EMPTY_BRANCH_HASH = hashParts(["cursor-sdk-agent-resume-branch", "v1"]);
11
11
  export function isCursorLocalAgentId(value) {
12
12
  return typeof value === "string" && value.length <= MAX_LOCAL_AGENT_ID_LENGTH && /^agent-[A-Za-z0-9_-]+$/.test(value);
13
13
  }
14
- const state = {
14
+ let lastBoundState = {
15
15
  scopeKey: getCursorSessionScopeKey(),
16
16
  cwd: process.cwd(),
17
17
  branchPathHash: EMPTY_BRANCH_HASH,
18
18
  compactionGeneration: 0,
19
19
  unownedUserEntryIds: new Set(),
20
20
  };
21
- // Compaction summarizer sends commit a one-message resume handle. Drop it so the
22
- // next normal turn_end cannot persist that lineage (#223).
23
- let resumeHandlePersistSuppressed = false;
24
- export function suppressCursorSessionAgentResumeHandlePersist() {
25
- resumeHandlePersistSuppressed = true;
26
- state.pendingHandle = undefined;
27
- }
28
- export function allowCursorSessionAgentResumeHandlePersist() {
29
- resumeHandlePersistSuppressed = false;
21
+ const statesByScope = new Map();
22
+ function getResumeState(scopeKey) {
23
+ if (scopeKey !== undefined) {
24
+ return statesByScope.get(scopeKey) ?? lastBoundState;
25
+ }
26
+ return lastBoundState;
30
27
  }
31
28
  function hashParts(parts) {
32
29
  const hash = createHash("sha256");
@@ -142,7 +139,7 @@ function matchesResumeScope(data, scope) {
142
139
  data.cwd === scope.cwd &&
143
140
  data.repoRoot === scope.repoRoot;
144
141
  }
145
- function matchesCurrentSession(data, branchPathHash, compactionGeneration = state.compactionGeneration) {
142
+ function matchesCurrentSession(data, branchPathHash, compactionGeneration, state) {
146
143
  return matchesResumeScope(data, state) &&
147
144
  data.compactionGeneration === compactionGeneration &&
148
145
  data.branchPathHash === branchPathHash;
@@ -244,19 +241,19 @@ export function readResumableCursorSessionAgentIds(entries, scope) {
244
241
  }
245
242
  return [...agentIds].sort((a, b) => a.localeCompare(b));
246
243
  }
247
- function canRestoreHandleSpanEntry(entry) {
244
+ function canRestoreHandleSpanEntry(entry, state) {
248
245
  if (entry.type === "message" && entry.message.role === "user")
249
246
  return !state.unownedUserEntryIds.has(entry.id);
250
247
  return canResumeHandleSpanEntry(entry);
251
248
  }
252
- function restoreFromBranch(branch, allEntries = branch) {
249
+ function restoreFromBranch(branch, allEntries, state) {
253
250
  const resumeIndex = indexLatestResumeEntries(allEntries);
254
251
  let fold = { branchPathHash: EMPTY_BRANCH_HASH, compactionGeneration: 0 };
255
252
  let lastBranchHandle;
256
253
  for (const entry of branch) {
257
254
  const next = advanceResumeBranchState(entry, fold, resumeIndex, {
258
- matchesEntry: matchesCurrentSession,
259
- canSpanEntry: canRestoreHandleSpanEntry,
255
+ matchesEntry: (data, hash, generation) => matchesCurrentSession(data, hash, generation, state),
256
+ canSpanEntry: (entry) => canRestoreHandleSpanEntry(entry, state),
260
257
  });
261
258
  if (next.activeHandle && next.activeHandle !== fold.activeHandle)
262
259
  lastBranchHandle = next.activeHandle;
@@ -267,7 +264,10 @@ function restoreFromBranch(branch, allEntries = branch) {
267
264
  state.activeHandle = fold.activeHandle;
268
265
  state.lastBranchHandle = lastBranchHandle;
269
266
  }
270
- export function getMatchingCursorSessionAgentResumeHandle(poolKey) {
267
+ export function getMatchingCursorSessionAgentResumeHandle(poolKey, scopeKey) {
268
+ const state = getResumeState(scopeKey);
269
+ if (scopeKey !== undefined && state.scopeKey !== scopeKey)
270
+ return undefined;
271
271
  const handle = state.activeHandle;
272
272
  if (!handle || !isCursorLocalAgentId(handle.agentId))
273
273
  return undefined;
@@ -290,8 +290,9 @@ export function getMatchingCursorSessionAgentResumeHandle(poolKey) {
290
290
  sendState: { ...handle.sendState },
291
291
  };
292
292
  }
293
- export function persistCursorSessionAgentResumeHandle(input) {
294
- if (resumeHandlePersistSuppressed)
293
+ export function persistCursorSessionAgentResumeHandle(input, scopeKey) {
294
+ const state = getResumeState(scopeKey);
295
+ if (scopeKey !== undefined && state.scopeKey !== scopeKey)
295
296
  return;
296
297
  if (!isCursorLocalAgentId(input.agentId))
297
298
  return;
@@ -303,13 +304,8 @@ export function persistCursorSessionAgentResumeHandle(input) {
303
304
  storeIdentity: { ...input.storeIdentity },
304
305
  };
305
306
  }
306
- function flushPendingCursorSessionAgentResumeHandle(branch) {
307
- if (resumeHandlePersistSuppressed) {
308
- state.pendingHandle = undefined;
309
- restoreFromBranch(branch);
310
- return;
311
- }
312
- restoreFromBranch(branch);
307
+ function flushPendingCursorSessionAgentResumeHandle(branch, state) {
308
+ restoreFromBranch(branch, branch, state);
313
309
  const pending = state.pendingHandle;
314
310
  state.pendingHandle = undefined;
315
311
  if (!pending || !state.appendEntry)
@@ -347,14 +343,23 @@ function flushPendingCursorSessionAgentResumeHandle(branch) {
347
343
  }
348
344
  }
349
345
  export function registerCursorSessionAgentResume(pi) {
350
- state.appendEntry = pi.appendEntry;
346
+ const sessionState = {
347
+ appendEntry: pi.appendEntry, scopeKey: getCursorSessionScopeSnapshot(pi).scopeKey,
348
+ cwd: process.cwd(), branchPathHash: EMPTY_BRANCH_HASH,
349
+ compactionGeneration: 0, unownedUserEntryIds: new Set(),
350
+ };
351
+ const state = sessionState;
351
352
  const restoreFromSessionManager = (sessionManager) => {
352
353
  const branch = sessionManager.getBranch();
353
354
  const entries = sessionManager.getEntries();
354
- restoreFromBranch(branch, entries.length > 0 ? entries : branch);
355
+ restoreFromBranch(branch, entries.length > 0 ? entries : branch, state);
355
356
  };
356
357
  pi.on("session_start", (_event, ctx) => {
357
- state.scopeKey = getCursorSessionScopeKey();
358
+ if (statesByScope.get(state.scopeKey) === state)
359
+ statesByScope.delete(state.scopeKey);
360
+ state.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
361
+ statesByScope.set(state.scopeKey, state);
362
+ lastBoundState = state;
358
363
  state.sessionFile = ctx.sessionManager.getSessionFile?.() ?? undefined;
359
364
  state.sessionId = ctx.sessionManager.getSessionId?.() ?? undefined;
360
365
  state.cwd = ctx.cwd;
@@ -366,7 +371,7 @@ export function registerCursorSessionAgentResume(pi) {
366
371
  restoreFromSessionManager(ctx.sessionManager);
367
372
  });
368
373
  pi.on("turn_end", (_event, ctx) => {
369
- flushPendingCursorSessionAgentResumeHandle(ctx.sessionManager.getBranch());
374
+ flushPendingCursorSessionAgentResumeHandle(ctx.sessionManager.getBranch(), state);
370
375
  });
371
376
  pi.on("session_tree", (_event, ctx) => {
372
377
  for (const entry of ctx.sessionManager.getBranch()) {
@@ -375,9 +380,14 @@ export function registerCursorSessionAgentResume(pi) {
375
380
  }
376
381
  restoreFromSessionManager(ctx.sessionManager);
377
382
  });
383
+ pi.on("session_before_compact", () => {
384
+ state.pendingHandle = undefined;
385
+ });
386
+ pi.on("session_compact_failed", () => {
387
+ state.pendingHandle = undefined;
388
+ });
378
389
  pi.on("session_compact", (event, ctx) => {
379
390
  state.pendingHandle = undefined;
380
- resumeHandlePersistSuppressed = false;
381
391
  const branch = ctx.sessionManager.getBranch();
382
392
  if (branch.length > 0) {
383
393
  restoreFromSessionManager(ctx.sessionManager);
@@ -388,11 +398,18 @@ export function registerCursorSessionAgentResume(pi) {
388
398
  state.compactionGeneration += 1;
389
399
  state.branchPathHash = hashBranchStep(state.branchPathHash, event.compactionEntry);
390
400
  });
401
+ pi.on("session_shutdown", () => {
402
+ if (statesByScope.get(state.scopeKey) === state)
403
+ statesByScope.delete(state.scopeKey);
404
+ state.appendEntry = undefined;
405
+ state.pendingHandle = undefined;
406
+ });
391
407
  }
392
408
  function setStateForTests(next) {
393
- Object.assign(state, next);
409
+ Object.assign(getResumeState(), next);
394
410
  }
395
411
  function resetStateForTests() {
412
+ const state = lastBoundState;
396
413
  state.appendEntry = undefined;
397
414
  state.scopeKey = getCursorSessionScopeKey();
398
415
  state.sessionFile = undefined;
@@ -405,13 +422,12 @@ function resetStateForTests() {
405
422
  state.lastBranchHandle = undefined;
406
423
  state.pendingHandle = undefined;
407
424
  state.unownedUserEntryIds = new Set();
408
- resumeHandlePersistSuppressed = false;
425
+ statesByScope.clear();
409
426
  }
410
427
  export const __testUtils = {
411
428
  EMPTY_BRANCH_HASH,
412
429
  hashBranchStep,
413
430
  reset: resetStateForTests,
414
431
  set: setStateForTests,
415
- state,
416
- isResumeHandlePersistSuppressed: () => resumeHandlePersistSuppressed,
432
+ get state() { return getResumeState(); },
417
433
  };
@@ -82,8 +82,7 @@ function buildLocalSafetyPoolKey(localSafety) {
82
82
  function buildApiKeyPoolKeyFingerprint(apiKey) {
83
83
  return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
84
84
  }
85
- function buildBridgePoolKeySuffix() {
86
- const registeredBridge = getRegisteredCursorPiToolBridge();
85
+ function buildBridgePoolKeySuffix(registeredBridge) {
87
86
  if (!registeredBridge)
88
87
  return "bridge:absent";
89
88
  return registeredBridge.getToolSurfaceSignature();
@@ -101,7 +100,7 @@ function buildSessionAgentPoolKey(scopeKey, params) {
101
100
  ? "http1:on"
102
101
  : "http1:off",
103
102
  buildApiKeyPoolKeyFingerprint(params.apiKey),
104
- buildBridgePoolKeySuffix(),
103
+ buildBridgePoolKeySuffix("bridge" in params ? params.bridge : getRegisteredCursorPiToolBridge()),
105
104
  ].join("\0");
106
105
  }
107
106
  async function disposePoolEntry(entry, options) {
@@ -179,7 +178,7 @@ function commitSessionAgentSendForLease(scopeKey, poolKey, instanceId, context,
179
178
  poolKey: entry.poolKey,
180
179
  sendState: entry.sendState,
181
180
  storeIdentity: entry.sessionStore.identity,
182
- });
181
+ }, scopeKey);
183
182
  }
184
183
  }
185
184
  function normalizeRunCompletion(completion) {
@@ -279,7 +278,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
279
278
  let bridgeRun;
280
279
  let sessionStore;
281
280
  try {
282
- const registeredBridge = getRegisteredCursorPiToolBridge();
281
+ const registeredBridge = params.bridge;
283
282
  if (registeredBridge) {
284
283
  bridgeRun = await registeredBridge.createRun({
285
284
  onToolRequest: params.onBridgeToolRequest,
@@ -299,7 +298,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
299
298
  createAgent ??= sdk.Agent.create;
300
299
  resumeAgent ??= sdk.Agent.resume;
301
300
  }
302
- const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey) : undefined;
301
+ const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey, scopeKey) : undefined;
303
302
  const storeSelection = await openCursorSessionStoreForScope({
304
303
  cwd: params.cwd,
305
304
  scopeKey,
@@ -373,9 +372,10 @@ export function invalidateSessionAgent(scopeKey = getCursorSessionScopeKey(), op
373
372
  if (options?.deadTransport)
374
373
  deadTransportScopeKeys.add(scopeKey);
375
374
  }
376
- export async function acquireSessionCursorAgent(params) {
377
- const scopeKey = getCursorSessionScopeKey();
378
- const persistentStore = getCursorSessionFile() !== undefined;
375
+ export async function acquireSessionCursorAgent(input) {
376
+ const params = { ...input, bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
377
+ const scopeKey = params.scope?.scopeKey ?? getCursorSessionScopeKey();
378
+ const persistentStore = (params.scope ? params.scope.sessionFile : getCursorSessionFile()) !== undefined;
379
379
  while (true) {
380
380
  assertScopeAcceptsAcquire(scopeKey);
381
381
  if (invalidatedScopeKeys.has(scopeKey)) {
@@ -1,16 +1,11 @@
1
1
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
2
2
  import { resetSessionCursorAgent } from "./cursor-session-agent.js";
3
- import { suppressCursorSessionAgentResumeHandlePersist } from "./cursor-session-agent-resume.js";
4
3
  import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
4
  /**
6
- * Prepare the pooled Cursor session agent for pi compaction summarization.
7
- * Releases any scoped live-run drain state still tied to the pooled agent, then
8
- * disposes the pool entry so summarization acquires a clean SDK agent.
9
- * Suppresses local-resume handle persist so the summarizer send cannot leak
10
- * a one-message handle into the next normal turn (#223).
5
+ * Release scoped conversation runs and reset the pool before compaction.
6
+ * Summary execution owns a separate temporary agent, never this pool.
11
7
  */
12
8
  export async function prepareCursorSessionForCompaction(scopeKey = getCursorSessionScopeKey()) {
13
- suppressCursorSessionAgentResumeHandlePersist();
14
9
  while (true) {
15
10
  const run = cursorLiveRuns.getActiveForScope(scopeKey);
16
11
  if (!run || run.disposed)
@@ -12,40 +12,58 @@ const state = {
12
12
  projectTrusted: false,
13
13
  sessionGeneration: 0,
14
14
  };
15
- const scopeGenerations = new Map([[ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration]]);
15
+ const scopes = new Map();
16
+ const scopeChangeHandlers = new Map();
17
+ const scopeGenerations = new Map([[ANONYMOUS_SESSION_SCOPE_KEY, 0]]);
16
18
  const projectTrustResolutionCwds = new Set();
17
19
  let nextSessionGeneration = 1;
18
- let scopeChangeHandler;
19
- /**
20
- * Pi session file when known; used to scope reused Cursor SDK agents to one pi session.
21
- */
20
+ function scopeKeyFor(record) {
21
+ if (record.sessionFile)
22
+ return record.sessionFile;
23
+ if (record.sessionId)
24
+ return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${record.sessionId}`;
25
+ return ANONYMOUS_SESSION_SCOPE_KEY;
26
+ }
27
+ export function cursorSessionScopeKeyForManager(manager) {
28
+ return manager.getSessionFile?.() ?? (manager.getSessionId?.()
29
+ ? `${EPHEMERAL_SESSION_SCOPE_PREFIX}${manager.getSessionId()}`
30
+ : ANONYMOUS_SESSION_SCOPE_KEY);
31
+ }
32
+ export function getCursorSessionScopeSnapshot(pi) {
33
+ if (!pi && scopes.size > 1)
34
+ throw new Error("Cursor provider ownership requires a session-bound provider closure when multiple extensions are loaded.");
35
+ const record = pi ? scopes.get(pi) : state;
36
+ if (!record)
37
+ throw new Error("Cursor session binding is no longer active.");
38
+ const scopeKey = scopeKeyFor(record);
39
+ return Object.freeze({
40
+ cwd: record.sessionCwd,
41
+ scopeKey,
42
+ sessionFile: record.sessionFile,
43
+ sessionId: record.sessionId,
44
+ sessionName: record.sessionName,
45
+ projectTrusted: record.projectTrusted,
46
+ generation: getCursorSessionScopeGeneration(scopeKey),
47
+ });
48
+ }
49
+ export function hasLiveCursorSessions() {
50
+ return scopes.size > 0;
51
+ }
22
52
  export function getCursorSessionFile() {
23
53
  return state.sessionFile;
24
54
  }
25
- /**
26
- * Stable scope key for session-agent pooling. Falls back to a process-local anonymous key
27
- * before the first session_start (tests and early startup).
28
- */
55
+ /** Stable pool key, with process-local anonymous fallback before session_start. */
29
56
  export function getCursorSessionScopeKey() {
30
- if (state.sessionFile)
31
- return state.sessionFile;
32
- if (state.sessionId)
33
- return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${state.sessionId}`;
34
- return ANONYMOUS_SESSION_SCOPE_KEY;
57
+ return scopeKeyFor(state);
35
58
  }
36
59
  export function getCursorSessionScopeGeneration(scopeKey = getCursorSessionScopeKey()) {
37
60
  return scopeGenerations.get(scopeKey) ?? 0;
38
61
  }
39
- /**
40
- * Pi session cwd when known; falls back to process.cwd() before session_start.
41
- * Updated on session_start only until pi threads cwd into streamSimple—mid-session cwd
42
- * changes without a new session_start event are not reflected here.
43
- */
44
62
  export function getCursorSessionCwd() {
45
63
  return state.sessionCwd;
46
64
  }
47
- export function getCursorSessionProjectTrusted() {
48
- return state.projectTrusted;
65
+ export function getCursorSessionProjectTrusted(pi) {
66
+ return pi ? scopes.get(pi)?.projectTrusted === true : state.projectTrusted;
49
67
  }
50
68
  export function getCursorSessionName() {
51
69
  return state.sessionName;
@@ -61,9 +79,8 @@ function setCursorSessionScope(cwd, sessionFile, sessionId, projectTrusted = fal
61
79
  state.sessionId = sessionId;
62
80
  state.sessionName = normalizeCursorSessionName(sessionName);
63
81
  state.projectTrusted = projectTrusted;
64
- state.sessionGeneration = nextSessionGeneration;
65
- nextSessionGeneration += 1;
66
- scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration);
82
+ state.sessionGeneration = nextSessionGeneration++;
83
+ scopeGenerations.set(scopeKeyFor(state), state.sessionGeneration);
67
84
  }
68
85
  function recordProjectTrustResolution(cwd) {
69
86
  projectTrustResolutionCwds.add(resolve(cwd));
@@ -72,35 +89,50 @@ function isCliProjectTrustApproved(args = process.argv.slice(2)) {
72
89
  return parseArgs(args).projectTrustOverride === true;
73
90
  }
74
91
  function resetCursorSessionScope() {
75
- state.sessionCwd = process.cwd();
76
- state.sessionFile = undefined;
77
- state.sessionId = undefined;
78
- state.sessionName = undefined;
79
- state.projectTrusted = false;
80
- state.sessionGeneration = 0;
92
+ Object.assign(state, {
93
+ sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
94
+ sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
95
+ });
81
96
  nextSessionGeneration = 1;
82
97
  scopeGenerations.clear();
83
- scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration);
98
+ scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY, 0);
84
99
  projectTrustResolutionCwds.clear();
100
+ scopes.clear();
101
+ scopeChangeHandlers.clear();
85
102
  }
86
- export function onCursorSessionScopeKeyChange(handler) {
87
- scopeChangeHandler = handler;
103
+ export function onCursorSessionScopeKeyChange(pi, handler) {
104
+ scopeChangeHandlers.set(pi, handler);
88
105
  }
89
106
  export function registerCursorSessionScope(pi) {
107
+ // A fresh binding has no previous pooled scope to steal from the last-bound mirror.
108
+ const record = {
109
+ sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
110
+ sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
111
+ };
112
+ scopes.set(pi, record);
113
+ const trustedCwds = new Set();
90
114
  pi.on("project_trust", (event) => {
91
- recordProjectTrustResolution(event.cwd);
115
+ trustedCwds.add(resolve(event.cwd));
92
116
  return { trusted: "undecided" };
93
117
  });
94
118
  pi.on("session_start", async (_event, ctx) => {
95
- const previousScopeKey = getCursorSessionScopeKey();
119
+ const previousScopeKey = scopeKeyFor(record);
120
+ const wasBound = record.sessionGeneration > 0;
96
121
  setCursorSessionScope(ctx.cwd, ctx.sessionManager?.getSessionFile?.() ?? undefined, ctx.sessionManager?.getSessionId?.() ?? undefined, ctx.isProjectTrusted?.() === true
97
- && (projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined);
98
- if (previousScopeKey !== getCursorSessionScopeKey()) {
99
- await scopeChangeHandler?.(previousScopeKey);
122
+ && (trustedCwds.has(resolve(ctx.cwd)) || projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined);
123
+ Object.assign(record, state);
124
+ scopes.set(pi, record);
125
+ if (wasBound && previousScopeKey !== scopeKeyFor(record)) {
126
+ await scopeChangeHandlers.get(pi)?.(previousScopeKey);
100
127
  }
101
128
  });
102
129
  pi.on("session_info_changed", (event) => {
103
- state.sessionName = normalizeCursorSessionName(event.name);
130
+ record.sessionName = normalizeCursorSessionName(event.name);
131
+ Object.assign(state, record);
132
+ });
133
+ pi.on("session_shutdown", () => {
134
+ scopes.delete(pi);
135
+ scopeChangeHandlers.delete(pi);
104
136
  });
105
137
  }
106
138
  export const __testUtils = {
@@ -0,0 +1,61 @@
1
+ import { cursorSessionScopeKeyForManager, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
2
+ function createCliSettings() {
3
+ return {
4
+ forceFast: false, forceNoFast: false,
5
+ mode: { kind: "unset" },
6
+ config: {},
7
+ cloudEnvNames: undefined,
8
+ localForceConsumed: false,
9
+ };
10
+ }
11
+ function createSettings() {
12
+ return {
13
+ fast: new Map(),
14
+ mode: undefined,
15
+ runtime: undefined,
16
+ cloudAcknowledged: false,
17
+ http1: undefined,
18
+ projectTrusted: false,
19
+ cli: createCliSettings(),
20
+ };
21
+ }
22
+ const settings = new Map();
23
+ let cliOwners = new WeakMap();
24
+ let legacySettings = createSettings();
25
+ export function getCursorSessionSettings(scopeKey) {
26
+ if (scopeKey === undefined)
27
+ return legacySettings;
28
+ let value = settings.get(scopeKey);
29
+ if (!value) {
30
+ value = createSettings();
31
+ settings.set(scopeKey, value);
32
+ }
33
+ return value;
34
+ }
35
+ export function cursorSettingsScopeForContext(ctx) {
36
+ const manager = ctx.sessionManager;
37
+ return manager ? cursorSessionScopeKeyForManager(manager) : undefined;
38
+ }
39
+ export function registerCursorSessionSettings(pi) {
40
+ let scopeKey;
41
+ let cli = createCliSettings();
42
+ pi.on("session_start", (_event, ctx) => {
43
+ if (scopeKey)
44
+ settings.delete(scopeKey);
45
+ scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager);
46
+ legacySettings = getCursorSessionSettings(scopeKey);
47
+ // Reload creates a new ExtensionAPI but retains the same public manager.
48
+ // Keep the run's one-shot CLI consumption without retaining dead managers.
49
+ cli = cliOwners.get(ctx.sessionManager) ?? cli;
50
+ cliOwners.set(ctx.sessionManager, cli);
51
+ legacySettings.cli = cli;
52
+ legacySettings.projectTrusted = getCursorSessionProjectTrusted(pi);
53
+ });
54
+ pi.on("session_shutdown", () => { if (scopeKey)
55
+ settings.delete(scopeKey); });
56
+ }
57
+ export function resetCursorSessionSettingsForTests() {
58
+ settings.clear();
59
+ cliOwners = new WeakMap();
60
+ legacySettings = createSettings();
61
+ }