pi-cursor-sdk 0.5.2 → 0.5.4

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 (114) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +62 -795
  3. package/dist/context.js +13 -7
  4. package/dist/cursor-agents-context.js +6 -6
  5. package/dist/cursor-api-key.js +1 -1
  6. package/dist/cursor-bridge-contract.js +1 -13
  7. package/dist/cursor-cloud-local-state.js +0 -1
  8. package/dist/cursor-config.js +1 -17
  9. package/dist/cursor-custom-subagent-definitions.js +1 -1
  10. package/dist/cursor-fallback-models.generated.js +665 -1
  11. package/dist/cursor-live-run-coordinator.js +1 -0
  12. package/dist/cursor-local-agent-ownership.js +17 -0
  13. package/dist/cursor-mcp-timeout-override.js +7 -18
  14. package/dist/cursor-native-tool-display-replay.js +5 -5
  15. package/dist/cursor-native-tools.js +11 -0
  16. package/dist/cursor-pi-tool-bridge-diagnostics.js +4 -0
  17. package/dist/cursor-pi-tool-bridge-env.js +25 -2
  18. package/dist/cursor-pi-tool-bridge-run.js +10 -3
  19. package/dist/cursor-pi-tool-bridge-server.js +98 -48
  20. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  21. package/dist/cursor-pi-tool-bridge.js +4 -12
  22. package/dist/cursor-provider-errors.js +2 -8
  23. package/dist/cursor-provider-live-run-drain.js +8 -3
  24. package/dist/cursor-provider-overflow.js +7 -21
  25. package/dist/cursor-provider-turn-prepare.js +8 -1
  26. package/dist/cursor-provider-turn-runner.js +17 -6
  27. package/dist/cursor-provider-turn-summary.js +1 -1
  28. package/dist/cursor-provider.js +1 -4
  29. package/dist/cursor-runtime-state.js +3 -3
  30. package/dist/cursor-sdk-event-debug.js +2 -2
  31. package/dist/cursor-sdk-output-filter.js +1 -1
  32. package/dist/cursor-sdk-process-error-guard.js +1 -2
  33. package/dist/cursor-session-agent-cleanup.js +10 -1
  34. package/dist/cursor-session-agent-lifecycle.js +2 -2
  35. package/dist/cursor-session-agent-lineage.js +11 -7
  36. package/dist/cursor-session-agent-resume.js +15 -9
  37. package/dist/cursor-session-agent.js +28 -15
  38. package/dist/cursor-session-scope.js +28 -21
  39. package/dist/cursor-session-settings.js +3 -3
  40. package/dist/cursor-session-store.js +2 -2
  41. package/dist/cursor-state.js +26 -29
  42. package/dist/cursor-tool-lifecycle.js +0 -3
  43. package/dist/cursor-tool-manifest.js +3 -1
  44. package/dist/cursor-tool-presentation-registry.js +2 -4
  45. package/dist/cursor-tool-visibility.js +0 -3
  46. package/dist/cursor-transcript-tool-specs.js +1 -2
  47. package/dist/index.js +21 -0
  48. package/dist/model-discovery.js +8 -10
  49. package/dist/model-list-cache.js +4 -9
  50. package/docs/cursor-live-smoke-checklist.md +4 -4
  51. package/docs/cursor-model-ux-spec.md +36 -19
  52. package/docs/cursor-native-tool-replay.md +8 -5
  53. package/docs/cursor-native-tool-visual-audit.md +1 -1
  54. package/docs/cursor-testing-lessons.md +39 -9
  55. package/docs/cursor-tool-surfaces.md +48 -4
  56. package/docs/development.md +93 -0
  57. package/docs/platform-smoke-implementation.md +3 -2
  58. package/docs/reference.md +806 -0
  59. package/package.json +13 -11
  60. package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
  61. package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
  62. package/scripts/platform-smoke/target-runtime.mjs +4 -2
  63. package/scripts/platform-smoke/targets.mjs +0 -1
  64. package/shared/cursor-sdk-output-filter.d.mts +2 -0
  65. package/shared/cursor-sdk-output-filter.mjs +92 -28
  66. package/shared/cursor-sensitive-text.d.mts +1 -0
  67. package/shared/cursor-sensitive-text.mjs +7 -0
  68. package/src/context.ts +18 -11
  69. package/src/cursor-agents-context.ts +6 -6
  70. package/src/cursor-api-key.ts +1 -1
  71. package/src/cursor-bridge-contract.ts +1 -15
  72. package/src/cursor-cloud-local-state.ts +0 -2
  73. package/src/cursor-config.ts +1 -24
  74. package/src/cursor-custom-subagent-definitions.ts +1 -1
  75. package/src/cursor-fallback-models.generated.ts +665 -1
  76. package/src/cursor-live-run-coordinator.ts +5 -0
  77. package/src/cursor-local-agent-ownership.ts +18 -0
  78. package/src/cursor-mcp-timeout-override.ts +7 -31
  79. package/src/cursor-native-tool-display-replay.ts +4 -6
  80. package/src/cursor-native-tools.ts +14 -0
  81. package/src/cursor-pi-tool-bridge-diagnostics.ts +17 -0
  82. package/src/cursor-pi-tool-bridge-env.ts +31 -2
  83. package/src/cursor-pi-tool-bridge-run.ts +10 -3
  84. package/src/cursor-pi-tool-bridge-server.ts +89 -45
  85. package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
  86. package/src/cursor-pi-tool-bridge.ts +2 -18
  87. package/src/cursor-provider-errors.ts +2 -10
  88. package/src/cursor-provider-live-run-drain.ts +8 -2
  89. package/src/cursor-provider-overflow.ts +6 -24
  90. package/src/cursor-provider-turn-prepare.ts +10 -1
  91. package/src/cursor-provider-turn-runner.ts +17 -6
  92. package/src/cursor-provider-turn-summary.ts +1 -1
  93. package/src/cursor-provider.ts +0 -5
  94. package/src/cursor-runtime-state.ts +3 -3
  95. package/src/cursor-sdk-event-debug.ts +2 -2
  96. package/src/cursor-sdk-output-filter.ts +2 -1
  97. package/src/cursor-sdk-process-error-guard.ts +1 -3
  98. package/src/cursor-session-agent-cleanup.ts +6 -1
  99. package/src/cursor-session-agent-lifecycle.ts +2 -2
  100. package/src/cursor-session-agent-lineage.ts +10 -5
  101. package/src/cursor-session-agent-resume.ts +14 -7
  102. package/src/cursor-session-agent.ts +31 -15
  103. package/src/cursor-session-scope.ts +34 -20
  104. package/src/cursor-session-settings.ts +3 -3
  105. package/src/cursor-session-store.ts +2 -2
  106. package/src/cursor-state.ts +34 -41
  107. package/src/cursor-tool-lifecycle.ts +0 -4
  108. package/src/cursor-tool-manifest.ts +4 -1
  109. package/src/cursor-tool-presentation-registry.ts +3 -5
  110. package/src/cursor-tool-visibility.ts +0 -4
  111. package/src/cursor-transcript-tool-specs.ts +0 -3
  112. package/src/index.ts +18 -0
  113. package/src/model-discovery.ts +7 -9
  114. package/src/model-list-cache.ts +4 -10
@@ -59,6 +59,7 @@ import type {
59
59
  } from "./cursor-provider-turn-types.js";
60
60
  import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
61
61
  import type { SessionCursorAgentLease } from "./cursor-session-agent.js";
62
+ import { assertCursorNativeToolsRuntime } from "./cursor-native-tools.js";
62
63
  import { prepareCursorSummaryProviderTurn } from "./cursor-provider-turn-summary.js";
63
64
 
64
65
  export interface PrepareCursorProviderTurnParams {
@@ -69,6 +70,8 @@ export interface PrepareCursorProviderTurnParams {
69
70
  throwIfAborted: () => void;
70
71
  /** Snapshot resolved once by the runner before draining; reused unchanged through prepare. */
71
72
  resolvedConfig: CursorResolvedSdkConfig;
73
+ /** Snapshotted before drain/acquisition, never re-read during creation/resume. */
74
+ nativeToolsEnabled: boolean;
72
75
  }
73
76
 
74
77
  interface PrepareCursorProviderTurnContext extends PrepareCursorProviderTurnParams {
@@ -252,7 +255,7 @@ async function prepareCursorCloudProviderTurn(
252
255
  async function prepareCursorLocalProviderTurn(
253
256
  prepareParams: PrepareCursorProviderTurnContext,
254
257
  ): Promise<LocalCursorProviderTurnPrepareResult> {
255
- const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled, agents, customSubagentNames } = prepareParams;
258
+ const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled, agents, customSubagentNames, nativeToolsEnabled } = prepareParams;
256
259
  const { model, context, options } = params;
257
260
 
258
261
  let restoreCursorSdkOutputFilter: (() => void) | undefined;
@@ -289,6 +292,7 @@ async function prepareCursorLocalProviderTurn(
289
292
  settingSources,
290
293
  localSafety,
291
294
  agents,
295
+ nativeToolsEnabled,
292
296
  localResume: resolvedConfig.local.resume.value,
293
297
  storeRoot: resolvedConfig.local.storeRoot.value,
294
298
  useHttp1ForAgent,
@@ -314,6 +318,7 @@ async function prepareCursorLocalProviderTurn(
314
318
  ...getCursorPromptOptions(model),
315
319
  nativeSourceRoles: params.request.nativeSourceRoles,
316
320
  agentMode,
321
+ nativeToolsEnabled,
317
322
  includePiBridgeGuidance,
318
323
  includePiAskQuestionGuidance: false,
319
324
  };
@@ -323,6 +328,7 @@ async function prepareCursorLocalProviderTurn(
323
328
  return {
324
329
  ...promptOptions,
325
330
  toolManifest: buildCursorToolManifestText({
331
+ nativeToolsEnabled,
326
332
  bridgeSnapshot: sessionAgentLease.bridgeRun?.snapshot,
327
333
  piBridgeEnabled: resolveCursorPiToolBridgeEnabled(),
328
334
  includePiBridgeGuidance,
@@ -373,6 +379,7 @@ async function prepareCursorLocalProviderTurn(
373
379
  agentMode,
374
380
  localForce: resolvedConfig.local.force.value,
375
381
  localResume: resolvedConfig.local.resume.value,
382
+ ...(nativeToolsEnabled ? {} : { nativeToolsEnabled: false, tools: ["mcp"] }),
376
383
  resumedAgent: sessionAgentLease.resumed,
377
384
  activeToolNames: activeToolNames ? [...activeToolNames] : [],
378
385
  sessionAgentScopeKey,
@@ -386,6 +393,7 @@ async function prepareCursorLocalProviderTurn(
386
393
  ? cursorLiveRuns.start({
387
394
  id: useNativeToolReplay ? nativeReplayId : bridgeRun?.id ?? nativeReplayId,
388
395
  agent,
396
+ nativeToolsEnabled,
389
397
  bridgeRun,
390
398
  sessionBridgeRun,
391
399
  sessionAgentScopeKey,
@@ -467,6 +475,7 @@ export async function prepareCursorProviderTurn(
467
475
  ): Promise<CursorProviderTurnPrepareResult> {
468
476
  const { params, resolvedConfig } = prepareParams;
469
477
  const { model, options } = params;
478
+ assertCursorNativeToolsRuntime(resolvedConfig.runtime.value, prepareParams.nativeToolsEnabled);
470
479
 
471
480
  const fastEnabled = resolvedConfig.runtime.value === "cloud" ? undefined : getEffectiveFastForModelId(model.id, params.scope.scopeKey);
472
481
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
@@ -1,5 +1,6 @@
1
+ import { assertCursorNativeToolsRuntime, resolveCursorNativeToolsEnabled } from "./cursor-native-tools.js";
1
2
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
2
- import { drainExistingCursorLiveRunBeforeSend } from "./cursor-provider-live-run-drain.js";
3
+ import { cursorLiveRuns, drainExistingCursorLiveRunBeforeSend } from "./cursor-provider-live-run-drain.js";
3
4
  import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
4
5
  import type { CursorRuntime } from "./cursor-config.js";
5
6
  import { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
@@ -68,6 +69,13 @@ export class CursorProviderTurnRunner {
68
69
  this.throwIfAborted();
69
70
  const { scope } = this.params;
70
71
  const cwd = scope.cwd;
72
+
73
+ // Resolved once here, before any drain await, so the drain decision and the
74
+ // prepare dispatch below always act on the same config snapshot.
75
+ const resolvedConfig = resolveCursorProviderTurnConfig(cwd, scope.projectTrusted, scope.scopeKey);
76
+ this.runtimeTarget = resolvedConfig.runtime.value;
77
+ const nativeToolsEnabled = resolveCursorNativeToolsEnabled();
78
+ assertCursorNativeToolsRuntime(this.runtimeTarget, nativeToolsEnabled);
71
79
  this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
72
80
  cwd,
73
81
  modelId: model.id,
@@ -76,13 +84,9 @@ export class CursorProviderTurnRunner {
76
84
  });
77
85
  sdkEventDebugRef.current = this.sdkEventDebug;
78
86
  this.sdkEventDebug?.recordContextSnapshot(context);
79
- // Resolved once here, before any drain await, so the drain decision and the
80
- // prepare dispatch below always act on the same config snapshot.
81
- const resolvedConfig = resolveCursorProviderTurnConfig(cwd, scope.projectTrusted, scope.scopeKey);
82
- this.runtimeTarget = resolvedConfig.runtime.value;
83
87
  if (resolvedConfig.runtime.value === "local" && this.params.request.purpose === "normal") {
84
88
  if (
85
- (await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug, scope.scopeKey, this.params.request.occupancyFloor)) ===
89
+ (await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug, scope.scopeKey, this.params.request.occupancyFloor, nativeToolsEnabled)) ===
86
90
  "stream_ended"
87
91
  ) {
88
92
  return;
@@ -98,6 +102,7 @@ export class CursorProviderTurnRunner {
98
102
  sdkEventDebug: this.sdkEventDebug,
99
103
  throwIfAborted: () => this.throwIfAborted(),
100
104
  resolvedConfig,
105
+ nativeToolsEnabled,
101
106
  });
102
107
 
103
108
  const usage = await this.params.usageRecorder.start({
@@ -173,6 +178,12 @@ export class CursorProviderTurnRunner {
173
178
  displayOnlyTraceBlock: finalized.displayOnlyTraceBlock,
174
179
  });
175
180
  } catch (error) {
181
+ // This body owns the session turn lease; a queued request aborted before
182
+ // entry must not release another request's run in handleOuterCatch().
183
+ if (!prepared && (error instanceof CursorLiveRunAbortError || options?.signal?.aborted)) {
184
+ const pending = cursorLiveRuns.getActiveForScope(this.params.scope.scopeKey);
185
+ if (pending && !pending.disposed) await cursorLiveRuns.release(pending);
186
+ }
176
187
  await runFinalizer.applyTerminalEvent({ kind: "error", prepared: started ?? prepared, error });
177
188
  } finally {
178
189
  await runFinalizer.cleanup(prepared, sendResult, liveCompletion);
@@ -40,7 +40,7 @@ export async function prepareCursorSummaryProviderTurn(
40
40
  configureCursorSdkHttp1(sdk, resolvedConfig.local.useHttp1ForAgent);
41
41
  restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
42
42
  sessionStore = (await openCursorSessionStoreForScope({
43
- cwd, scopeKey: params.scope.scopeKey, persistent: false,
43
+ cwd, scopeKey: params.scope.persistentScopeKey ?? params.scope.scopeKey, persistent: false,
44
44
  })).sessionStore;
45
45
  throwIfAborted();
46
46
  agent = await suppressCursorSdkOutput(() => sdk.Agent.create({
@@ -9,8 +9,6 @@ import {
9
9
  } from "@earendil-works/pi-ai";
10
10
  import {
11
11
  cursorLiveRuns,
12
- DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
13
- getPendingCursorLiveRun,
14
12
  hasTrailingUserMessagesAfterToolResults,
15
13
  releaseAllPendingCursorLiveRunsForTests,
16
14
  resetCursorNativeReplayIdleDisposeMs,
@@ -114,10 +112,7 @@ export function streamCursor(
114
112
  }
115
113
 
116
114
  export const __testUtils = {
117
- DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
118
115
  pendingCursorNativeRunCount: cursorLiveRuns.count,
119
- getPendingCursorLiveRun,
120
- getActiveCursorLiveRunForScope: cursorLiveRuns.getActiveForScope,
121
116
  hasTrailingUserMessagesAfterToolResults,
122
117
  setCursorNativeReplayIdleDisposeMs,
123
118
  resetCursorNativeReplayIdleDisposeMs,
@@ -39,9 +39,9 @@ import { getResolvedSessionCursorHttp1Enabled } from "./cursor-http1.js";
39
39
  import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
40
40
  import { cursorSettingsScopeForContext, getCursorSessionSettings } from "./cursor-session-settings.js";
41
41
 
42
- export const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
42
+ const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
43
43
 
44
- export const CURSOR_CLOUD_ACK_DISCLOSURE = [
44
+ const CURSOR_CLOUD_ACK_DISCLOSURE = [
45
45
  "Cursor Cloud executes this work remotely.",
46
46
  "Fresh context is used by default; prior Pi context is included only with explicit bootstrap opt-in.",
47
47
  "Pi-local tools and the Pi bridge are unavailable, and Pi environment variables are not forwarded.",
@@ -145,7 +145,7 @@ export function resolveCursorStatusRuntime(ctx: CursorRuntimeContext): CursorRun
145
145
  }
146
146
  }
147
147
 
148
- export function formatResolvedCursorRuntime(runtime: CursorResolvedSetting<CursorRuntime>): string {
148
+ function formatResolvedCursorRuntime(runtime: CursorResolvedSetting<CursorRuntime>): string {
149
149
  const cap = runtime.cappedBy;
150
150
  return cap
151
151
  ? `${runtime.value} (source: ${runtime.source} safety cap over ${cap.cappedSource} ${cap.cappedValue})`
@@ -171,7 +171,7 @@ export type DiscardedIncompleteStartedToolCallReason =
171
171
  | "sdk-failure"
172
172
  | "run-drain";
173
173
 
174
- export function hashCursorSdkCallId(callId: string): string {
174
+ function hashCursorSdkCallId(callId: string): string {
175
175
  return createHash("sha256").update(callId).digest("hex").slice(0, 8);
176
176
  }
177
177
 
@@ -182,7 +182,7 @@ export interface DiscardedIncompleteStartedToolCallRecord {
182
182
  reason: DiscardedIncompleteStartedToolCallReason;
183
183
  }
184
184
 
185
- export function serializeDiscardedIncompleteStartedToolCall(record: {
185
+ function serializeDiscardedIncompleteStartedToolCall(record: {
186
186
  toolName: string;
187
187
  callId: string;
188
188
  reason?: DiscardedIncompleteStartedToolCallReason;
@@ -1,5 +1,6 @@
1
1
  export {
2
- CURSOR_SDK_STARTUP_NOISE_PATTERNS,
2
+ __testUtils,
3
+ installCursorSdkProcessOutputFilter,
3
4
  withCursorSdkOutputNoticeHandler,
4
5
  installCursorSdkOutputFilter,
5
6
  isCursorSdkOutputSuppressed,
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { classifyCursorConnectError, isCursorSdkAbortConnectError, isCursorSdkConnectionStalledError } from "./cursor-provider-errors.js";
2
+ import { classifyCursorConnectError, isCursorSdkConnectionStalledError } from "./cursor-provider-errors.js";
3
3
 
4
4
  interface CursorSdkProcessErrorGuardToken {
5
5
  suppressAbortErrors: boolean;
@@ -147,8 +147,6 @@ export const __testUtils = {
147
147
  },
148
148
  };
149
149
 
150
- export { isCursorSdkAbortConnectError };
151
-
152
150
  export function installCursorSdkProcessErrorGuard(): CursorSdkProcessErrorGuard {
153
151
  const token: CursorSdkProcessErrorGuardToken = { suppressAbortErrors: false };
154
152
  activeProviderTurns.add(token);
@@ -5,6 +5,7 @@ import { fsyncExistingRegularFile } from "./cursor-durable-fs.js";
5
5
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
6
6
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
7
7
  import { cursorSessionScopeKeyForManager } from "./cursor-session-scope.js";
8
+ import { claimCursorLocalAgent } from "./cursor-local-agent-ownership.js";
8
9
  import {
9
10
  CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE,
10
11
  isCursorLocalAgentId,
@@ -323,7 +324,11 @@ export async function runCursorSessionAgentCleanupCommand(pi: LocalResumeCleanup
323
324
  openedStore = await openCursorSessionStore(ctx.cwd, identity, identities.workspaceRoot, identities.storeRoot);
324
325
  openedStores.set(identity.stateRoot, openedStore);
325
326
  }
326
- await operations.delete(agentId, { cwd: ctx.cwd, store: openedStore.store });
327
+ const release = claimCursorLocalAgent(identity, agentId);
328
+ if (!release) throw new Error("Cursor local agent is still owned by an active runtime");
329
+ try {
330
+ await operations.delete(agentId, { cwd: ctx.cwd, store: openedStore.store });
331
+ } finally { release(); }
327
332
  deletedAgentIds.push(agentId);
328
333
  } catch (error) {
329
334
  failedAgentIds.push({
@@ -7,7 +7,7 @@ import type {
7
7
  SessionStartEvent,
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import { clearCursorSdkHttp1 } from "./cursor-http1.js";
10
- import { cursorSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
10
+ import { cursorRuntimeSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
11
11
  import {
12
12
  disposeSessionCursorAgent,
13
13
  invalidateSessionAgent,
@@ -25,7 +25,7 @@ export interface CursorSessionAgentLifecycleExtensionApi {
25
25
 
26
26
  export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecycleExtensionApi): void {
27
27
  let scopeKey: string | undefined;
28
- pi.on("session_start", (_event, ctx) => { scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager); });
28
+ pi.on("session_start", (_event, ctx) => { scopeKey = cursorRuntimeSessionScopeKeyForManager(ctx.sessionManager); });
29
29
  onCursorSessionScopeKeyChange(pi, async (previousScopeKey) => {
30
30
  await disposeSessionCursorAgent(previousScopeKey);
31
31
  });
@@ -29,6 +29,7 @@ interface CursorSessionAgentLineageState {
29
29
  sessionId?: string;
30
30
  sessionFile?: string;
31
31
  scopeKey?: string;
32
+ runtimeScopeKey?: string;
32
33
  cwd?: string;
33
34
  recordedAgentIds: Set<string>;
34
35
  }
@@ -82,7 +83,7 @@ function readRecordedAgentIds(entries: readonly SessionEntry[], sessionId: strin
82
83
  export function recordCursorSessionAgentLineage(agentId: string, turnScopeKey?: string): void {
83
84
  const sessionState = turnScopeKey === undefined
84
85
  ? state
85
- : statesByScope.get(turnScopeKey) ?? (state.scopeKey === turnScopeKey ? state : undefined);
86
+ : statesByScope.get(turnScopeKey) ?? ((state.runtimeScopeKey ?? state.scopeKey) === turnScopeKey ? state : undefined);
86
87
  if (!sessionState) return;
87
88
  const { appendEntry, sessionId, sessionFile, scopeKey, cwd } = sessionState;
88
89
  if (!appendEntry || !sessionId || !scopeKey || !cwd) return;
@@ -115,22 +116,25 @@ export function registerCursorSessionAgentLineage(pi: CursorSessionAgentLineageE
115
116
  pi.on("session_start", (_event, ctx) => {
116
117
  state = sessionState;
117
118
  const record = sessionState;
118
- if (record.scopeKey && statesByScope.get(record.scopeKey) === record) statesByScope.delete(record.scopeKey);
119
+ if (record.runtimeScopeKey && statesByScope.get(record.runtimeScopeKey) === record) statesByScope.delete(record.runtimeScopeKey);
119
120
  record.appendEntry = pi.appendEntry;
120
121
  record.sessionId = ctx.sessionManager.getSessionId();
121
122
  record.sessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
122
- record.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
123
- statesByScope.set(record.scopeKey, record);
123
+ const scope = getCursorSessionScopeSnapshot(pi);
124
+ record.runtimeScopeKey = scope.scopeKey;
125
+ record.scopeKey = scope.persistentScopeKey ?? scope.scopeKey;
126
+ statesByScope.set(scope.scopeKey, record);
124
127
  record.cwd = ctx.cwd;
125
128
  record.recordedAgentIds = readRecordedAgentIds(ctx.sessionManager.getEntries(), record.sessionId);
126
129
  });
127
130
  pi.on("session_shutdown", () => {
128
131
  const state = sessionState;
129
- if (state.scopeKey && statesByScope.get(state.scopeKey) === state) statesByScope.delete(state.scopeKey);
132
+ if (state.runtimeScopeKey && statesByScope.get(state.runtimeScopeKey) === state) statesByScope.delete(state.runtimeScopeKey);
130
133
  state.appendEntry = undefined;
131
134
  state.sessionId = undefined;
132
135
  state.sessionFile = undefined;
133
136
  state.scopeKey = undefined;
137
+ state.runtimeScopeKey = undefined;
134
138
  state.cwd = undefined;
135
139
  state.recordedAgentIds = new Set();
136
140
  });
@@ -142,6 +146,7 @@ function resetStateForTests(): void {
142
146
  state.sessionId = undefined;
143
147
  state.sessionFile = undefined;
144
148
  state.scopeKey = undefined;
149
+ state.runtimeScopeKey = undefined;
145
150
  state.cwd = undefined;
146
151
  state.recordedAgentIds = new Set();
147
152
  }
@@ -60,6 +60,7 @@ interface PendingCursorSessionAgentResumeHandle {
60
60
 
61
61
  interface CursorSessionResumeState {
62
62
  appendEntry?: ExtensionAPI["appendEntry"];
63
+ runtimeScopeKey?: string;
63
64
  scopeKey: string;
64
65
  sessionFile?: string;
65
66
  sessionId?: string;
@@ -355,7 +356,7 @@ function restoreFromBranch(branch: readonly SessionEntry[], allEntries: readonly
355
356
 
356
357
  export function getMatchingCursorSessionAgentResumeHandle(poolKey: string, scopeKey?: string): CursorSessionAgentResumeEntryData | undefined {
357
358
  const state = getResumeState(scopeKey);
358
- if (scopeKey !== undefined && state.scopeKey !== scopeKey) return undefined;
359
+ if (scopeKey !== undefined && (state.runtimeScopeKey ?? state.scopeKey) !== scopeKey) return undefined;
359
360
  const handle = state.activeHandle;
360
361
  if (!handle || !isCursorLocalAgentId(handle.agentId)) return undefined;
361
362
  if (handle.poolKey !== poolKey) return undefined;
@@ -373,7 +374,7 @@ export function getMatchingCursorSessionAgentResumeHandle(poolKey: string, scope
373
374
 
374
375
  export function persistCursorSessionAgentResumeHandle(input: PendingCursorSessionAgentResumeHandle, scopeKey?: string): void {
375
376
  const state = getResumeState(scopeKey);
376
- if (scopeKey !== undefined && state.scopeKey !== scopeKey) return;
377
+ if (scopeKey !== undefined && (state.runtimeScopeKey ?? state.scopeKey) !== scopeKey) return;
377
378
  if (!isCursorLocalAgentId(input.agentId)) return;
378
379
  state.pendingHandle = {
379
380
  runtime: input.runtime,
@@ -427,8 +428,9 @@ interface CursorSessionAgentResumeExtensionApi {
427
428
  }
428
429
 
429
430
  export function registerCursorSessionAgentResume(pi: CursorSessionAgentResumeExtensionApi): void {
431
+ const scope = getCursorSessionScopeSnapshot(pi);
430
432
  const sessionState: CursorSessionResumeState = {
431
- appendEntry: pi.appendEntry, scopeKey: getCursorSessionScopeSnapshot(pi).scopeKey,
433
+ appendEntry: pi.appendEntry, scopeKey: scope.persistentScopeKey ?? scope.scopeKey,
432
434
  cwd: process.cwd(), branchPathHash: EMPTY_BRANCH_HASH,
433
435
  compactionGeneration: 0, unownedUserEntryIds: new Set(),
434
436
  };
@@ -439,9 +441,12 @@ export function registerCursorSessionAgentResume(pi: CursorSessionAgentResumeExt
439
441
  restoreFromBranch(branch, entries.length > 0 ? entries : branch, state);
440
442
  };
441
443
  pi.on("session_start", (_event, ctx) => {
442
- if (statesByScope.get(state.scopeKey) === state) statesByScope.delete(state.scopeKey);
443
- state.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
444
- statesByScope.set(state.scopeKey, state);
444
+ const key = state.runtimeScopeKey ?? state.scopeKey;
445
+ if (statesByScope.get(key) === state) statesByScope.delete(key);
446
+ const scope = getCursorSessionScopeSnapshot(pi);
447
+ state.runtimeScopeKey = scope.scopeKey;
448
+ state.scopeKey = scope.persistentScopeKey ?? scope.scopeKey;
449
+ statesByScope.set(scope.scopeKey, state);
445
450
  lastBoundState = state;
446
451
  state.sessionFile = ctx.sessionManager.getSessionFile?.() ?? undefined;
447
452
  state.sessionId = ctx.sessionManager.getSessionId?.() ?? undefined;
@@ -482,7 +487,8 @@ export function registerCursorSessionAgentResume(pi: CursorSessionAgentResumeExt
482
487
  state.branchPathHash = hashBranchStep(state.branchPathHash, event.compactionEntry);
483
488
  });
484
489
  pi.on("session_shutdown", () => {
485
- if (statesByScope.get(state.scopeKey) === state) statesByScope.delete(state.scopeKey);
490
+ const key = state.runtimeScopeKey ?? state.scopeKey;
491
+ if (statesByScope.get(key) === state) statesByScope.delete(key);
486
492
  state.appendEntry = undefined;
487
493
  state.pendingHandle = undefined;
488
494
  });
@@ -496,6 +502,7 @@ function resetStateForTests(): void {
496
502
  const state = lastBoundState;
497
503
  state.appendEntry = undefined;
498
504
  state.scopeKey = getCursorSessionScopeKey();
505
+ state.runtimeScopeKey = undefined;
499
506
  state.sessionFile = undefined;
500
507
  state.sessionId = undefined;
501
508
  state.cwd = process.cwd();
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import type { AgentModeOption, AgentOptions, LocalAgentOptions, LocalAgentStore, ModelSelection, SDKAgent, SettingSource } from "@cursor/sdk";
3
3
  import type { Context } from "@earendil-works/pi-ai";
4
4
  import { normalizeCursorCustomStoreRoot } from "./cursor-custom-store-path.js";
5
+ import { claimCursorLocalAgent } from "./cursor-local-agent-ownership.js";
5
6
  import {
6
7
  getRegisteredCursorPiToolBridge,
7
8
  type CursorPiToolBridge,
@@ -9,7 +10,7 @@ import {
9
10
  type CursorPiToolBridgeRun,
10
11
  } from "./cursor-pi-tool-bridge.js";
11
12
  import { computeCursorContextFingerprint } from "./context.js";
12
- import { getCursorSessionFile, getCursorSessionScopeGeneration, getCursorSessionScopeKey, type CursorTurnScope } from "./cursor-session-scope.js";
13
+ import { getCursorSessionScopeSnapshot, getCursorSessionScopeGeneration, getCursorSessionScopeKey, type CursorTurnScope } from "./cursor-session-scope.js";
13
14
  import {
14
15
  getMatchingCursorSessionAgentResumeHandle,
15
16
  persistCursorSessionAgentResumeHandle,
@@ -68,6 +69,7 @@ interface SessionCursorAgentReadyEntry extends SessionCursorAgentPoolEntryBase {
68
69
  resumeEnabled: boolean;
69
70
  resumed: boolean;
70
71
  resumeNotice?: string;
72
+ releaseAgentClaim: () => void;
71
73
  }
72
74
 
73
75
  interface SessionCursorAgentBusyEntry extends SessionCursorAgentPoolEntryBase {
@@ -78,6 +80,7 @@ interface SessionCursorAgentBusyEntry extends SessionCursorAgentPoolEntryBase {
78
80
  resumeEnabled: boolean;
79
81
  resumed: boolean;
80
82
  resumeNotice?: string;
83
+ releaseAgentClaim: () => void;
81
84
  completionSettled: Promise<void>;
82
85
  pendingCompletion: Promise<void>;
83
86
  releaseBusyWait: () => void;
@@ -137,6 +140,8 @@ interface SessionCursorAgentCreateParams {
137
140
  settingSources?: SettingSource[];
138
141
  localSafety?: CursorLocalSafetyOptions;
139
142
  agents?: AgentOptions["agents"];
143
+ /** Captured by the turn owner before asynchronous acquisition. */
144
+ nativeToolsEnabled?: boolean;
140
145
  useHttp1ForAgent?: boolean;
141
146
  onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void;
142
147
  debugRecorder?: CursorSdkEventDebugRecorder;
@@ -150,7 +155,7 @@ interface SessionCursorAgentCreateParams {
150
155
  const sessionAgentsByScope = new Map<string, SessionCursorAgentPoolEntry>();
151
156
  const invalidatedScopeKeys = new Set<string>();
152
157
  const deadTransportScopeKeys = new Set<string>();
153
- let deadTransportAgentDisposeTimeoutMs = 3000;
158
+ const DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS = 3000;
154
159
  const terminalDisposedScopeGenerations = new Map<string, number>();
155
160
  const scopeCreationGenerations = new Map<string, number>();
156
161
  const EMPTY_POOL_STATE: SessionCursorAgentPoolState = { status: "empty" };
@@ -238,7 +243,7 @@ function buildCustomSubagentPoolKeySuffix(agents: AgentOptions["agents"]): strin
238
243
 
239
244
  function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCreateParams): string {
240
245
  return [
241
- scopeKey,
246
+ params.scope?.persistentScopeKey ?? scopeKey,
242
247
  params.cwd,
243
248
  buildModelPoolKey(params.modelSelection),
244
249
  buildSettingSourcesPoolKey(params.settingSources),
@@ -251,6 +256,8 @@ function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCr
251
256
  buildApiKeyPoolKeyFingerprint(params.apiKey),
252
257
  buildBridgePoolKeySuffix("bridge" in params ? params.bridge : getRegisteredCursorPiToolBridge()),
253
258
  ...buildCustomSubagentPoolKeySuffix(params.agents),
259
+ // Omit for the default so historical durable keys remain byte-for-byte stable.
260
+ ...(params.nativeToolsEnabled === false ? ["tools:mcp-only"] : []),
254
261
  ].join("\0");
255
262
  }
256
263
 
@@ -262,20 +269,23 @@ async function disposePoolEntry(entry: SessionCursorAgentPoolEntry, options?: {
262
269
  } catch {
263
270
  // disposal failure should not block session replacement
264
271
  }
272
+ let disposal: Promise<unknown> | undefined;
265
273
  try {
266
- const disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]()).catch(() => undefined);
274
+ disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]());
267
275
  // A dead local transport may never settle SDK disposal; bound the wait so the
268
276
  // next acquire recreates instead of hanging on the dead agent.
269
277
  await (options?.deadTransport
270
278
  ? Promise.race([
271
279
  disposal,
272
- new Promise<void>((resolve) => setTimeout(resolve, deadTransportAgentDisposeTimeoutMs).unref?.()),
280
+ new Promise<void>((resolve) => setTimeout(resolve, DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS).unref?.()),
273
281
  ])
274
282
  : disposal);
275
283
  } catch {
276
284
  // disposal failure should not block session replacement
277
285
  }
278
286
  await entry.sessionStore.dispose().catch(() => undefined);
287
+ // Timeout/rejection is not proof of closure; only successful SDK disposal releases ownership.
288
+ void disposal?.then(entry.releaseAgentClaim, () => {});
279
289
  }
280
290
 
281
291
  async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?: boolean }): Promise<void> {
@@ -470,6 +480,7 @@ async function createSessionAgentEntry(
470
480
  ): Promise<SessionCursorAgentReadyEntry> {
471
481
  let bridgeRun: CursorPiToolBridgeRun | undefined;
472
482
  let sessionStore: OpenCursorSessionStore | undefined;
483
+ let releaseAgentClaim: (() => void) | undefined;
473
484
  try {
474
485
  const registeredBridge = params.bridge;
475
486
  if (registeredBridge) {
@@ -495,7 +506,7 @@ async function createSessionAgentEntry(
495
506
  const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey, scopeKey) : undefined;
496
507
  const storeSelection = await openCursorSessionStoreForScope({
497
508
  cwd: params.cwd,
498
- scopeKey,
509
+ scopeKey: params.scope?.persistentScopeKey ?? scopeKey,
499
510
  persistent: persistentStore,
500
511
  storeRoot: params.storeRoot,
501
512
  resume: resumeHandle ? { identity: resumeHandle.storeIdentity, agentId: resumeHandle.agentId } : undefined,
@@ -513,6 +524,7 @@ async function createSessionAgentEntry(
513
524
  localSafety: params.localSafety,
514
525
  store: sessionStore!.store,
515
526
  }),
527
+ ...(params.nativeToolsEnabled === false ? { tools: ["mcp"] } : {}),
516
528
  ...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
517
529
  ...(params.agents && Object.keys(params.agents).length ? { agents: params.agents } : {}),
518
530
  });
@@ -520,11 +532,16 @@ async function createSessionAgentEntry(
520
532
  let effectiveSendState = sendState;
521
533
  let resumed = false;
522
534
  if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
535
+ releaseAgentClaim = claimCursorLocalAgent(sessionStore.identity, resumeHandle.agentId);
536
+ }
537
+ if (resumeHandle && resumeAttemptAllowed && resumeAgent && releaseAgentClaim) {
523
538
  try {
524
539
  agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
525
540
  effectiveSendState = { ...resumeHandle.sendState };
526
541
  resumed = true;
527
542
  } catch {
543
+ releaseAgentClaim();
544
+ releaseAgentClaim = undefined;
528
545
  if (persistentStore) resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
529
546
  if (storeSelection.persistent && !cursorSessionStoreIdentitiesEqual(sessionStore.identity, storeSelection.identities.sessionStore)) {
530
547
  const { identities } = storeSelection;
@@ -537,6 +554,8 @@ async function createSessionAgentEntry(
537
554
  agent ??= await createAgent(buildAgentOptions());
538
555
  if (!agent) throw new Error("Cursor SDK agent creation returned no agent");
539
556
  if (!sessionStore) throw new Error("Cursor SDK session store was not opened");
557
+ releaseAgentClaim ??= claimCursorLocalAgent(sessionStore.identity, agent.agentId);
558
+ if (!releaseAgentClaim) throw new Error("Cursor SDK agent is already owned by another runtime");
540
559
 
541
560
  return {
542
561
  status: "ready",
@@ -550,12 +569,14 @@ async function createSessionAgentEntry(
550
569
  sendState: effectiveSendState,
551
570
  resumeEnabled: params.localResume === true,
552
571
  resumed,
572
+ releaseAgentClaim,
553
573
  ...(resumeNotice ? { resumeNotice } : {}),
554
574
  };
555
575
  } catch (error) {
556
576
  bridgeRun?.cancel("Cursor session agent create failed");
557
577
  await bridgeRun?.dispose().catch(() => undefined);
558
578
  await sessionStore?.dispose().catch(() => undefined);
579
+ releaseAgentClaim?.();
559
580
  throw error;
560
581
  }
561
582
  }
@@ -568,7 +589,7 @@ export {
568
589
  } from "./cursor-session-send-policy.js";
569
590
 
570
591
  export function invalidateSessionAgent(
571
- scopeKey: string = getCursorSessionScopeKey(),
592
+ scopeKey: string,
572
593
  options?: { deadTransport?: boolean },
573
594
  ): void {
574
595
  invalidatedScopeKeys.add(scopeKey);
@@ -576,9 +597,9 @@ export function invalidateSessionAgent(
576
597
  }
577
598
 
578
599
  export async function acquireSessionCursorAgent(input: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
579
- const params = { ...input, bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
580
- const scopeKey = params.scope?.scopeKey ?? getCursorSessionScopeKey();
581
- const persistentStore = (params.scope ? params.scope.sessionFile : getCursorSessionFile()) !== undefined;
600
+ const params = { ...input, scope: input.scope ?? getCursorSessionScopeSnapshot(), bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
601
+ const scopeKey = params.scope.scopeKey;
602
+ const persistentStore = params.scope.sessionFile !== undefined;
582
603
  params.storeRoot = persistentStore && params.storeRoot !== undefined ? normalizeCursorCustomStoreRoot(params.storeRoot) : undefined;
583
604
 
584
605
  while (true) {
@@ -709,11 +730,6 @@ export const __testUtils = {
709
730
  resetSessionCursorAgent,
710
731
  refreshSessionCursorAgentConfig,
711
732
  disposeAllSessionCursorAgents,
712
- setDeadTransportAgentDisposeTimeoutMs(ms: number): number {
713
- const previous = deadTransportAgentDisposeTimeoutMs;
714
- deadTransportAgentDisposeTimeoutMs = ms;
715
- return previous;
716
- },
717
733
  SessionCursorAgentCreationSupersededError,
718
734
  SessionCursorAgentScopeClosedError,
719
735
  };