pi-cursor-sdk 0.5.3 → 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 (94) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +62 -816
  3. package/dist/context.js +11 -5
  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-fallback-models.generated.js +264 -1
  10. package/dist/cursor-live-run-coordinator.js +1 -0
  11. package/dist/cursor-mcp-timeout-override.js +6 -17
  12. package/dist/cursor-native-tool-display-replay.js +5 -5
  13. package/dist/cursor-native-tools.js +11 -0
  14. package/dist/cursor-pi-tool-bridge-env.js +25 -2
  15. package/dist/cursor-pi-tool-bridge-server.js +98 -48
  16. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  17. package/dist/cursor-pi-tool-bridge.js +4 -12
  18. package/dist/cursor-provider-errors.js +0 -6
  19. package/dist/cursor-provider-live-run-drain.js +8 -3
  20. package/dist/cursor-provider-overflow.js +7 -21
  21. package/dist/cursor-provider-turn-prepare.js +8 -1
  22. package/dist/cursor-provider-turn-runner.js +17 -6
  23. package/dist/cursor-provider.js +1 -4
  24. package/dist/cursor-runtime-state.js +3 -3
  25. package/dist/cursor-sdk-event-debug.js +2 -2
  26. package/dist/cursor-sdk-output-filter.js +1 -1
  27. package/dist/cursor-sdk-process-error-guard.js +1 -2
  28. package/dist/cursor-session-agent.js +6 -8
  29. package/dist/cursor-session-scope.js +1 -7
  30. package/dist/cursor-session-store.js +1 -1
  31. package/dist/cursor-state.js +9 -23
  32. package/dist/cursor-tool-lifecycle.js +0 -3
  33. package/dist/cursor-tool-manifest.js +3 -1
  34. package/dist/cursor-tool-presentation-registry.js +2 -4
  35. package/dist/cursor-tool-visibility.js +0 -3
  36. package/dist/cursor-transcript-tool-specs.js +1 -2
  37. package/dist/index.js +21 -0
  38. package/dist/model-discovery.js +0 -1
  39. package/dist/model-list-cache.js +4 -9
  40. package/docs/cursor-live-smoke-checklist.md +3 -3
  41. package/docs/cursor-model-ux-spec.md +16 -8
  42. package/docs/cursor-native-tool-replay.md +2 -2
  43. package/docs/cursor-native-tool-visual-audit.md +1 -1
  44. package/docs/cursor-testing-lessons.md +15 -3
  45. package/docs/cursor-tool-surfaces.md +32 -3
  46. package/docs/development.md +93 -0
  47. package/docs/platform-smoke-implementation.md +3 -2
  48. package/docs/reference.md +806 -0
  49. package/package.json +6 -4
  50. package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
  51. package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
  52. package/scripts/platform-smoke/target-runtime.mjs +4 -2
  53. package/scripts/platform-smoke/targets.mjs +0 -1
  54. package/shared/cursor-sdk-output-filter.d.mts +2 -0
  55. package/shared/cursor-sdk-output-filter.mjs +92 -28
  56. package/shared/cursor-sensitive-text.d.mts +1 -0
  57. package/shared/cursor-sensitive-text.mjs +7 -0
  58. package/src/context.ts +16 -9
  59. package/src/cursor-agents-context.ts +6 -6
  60. package/src/cursor-api-key.ts +1 -1
  61. package/src/cursor-bridge-contract.ts +1 -15
  62. package/src/cursor-cloud-local-state.ts +0 -2
  63. package/src/cursor-config.ts +1 -24
  64. package/src/cursor-fallback-models.generated.ts +264 -1
  65. package/src/cursor-live-run-coordinator.ts +5 -0
  66. package/src/cursor-mcp-timeout-override.ts +6 -30
  67. package/src/cursor-native-tool-display-replay.ts +4 -6
  68. package/src/cursor-native-tools.ts +14 -0
  69. package/src/cursor-pi-tool-bridge-env.ts +31 -2
  70. package/src/cursor-pi-tool-bridge-server.ts +89 -45
  71. package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
  72. package/src/cursor-pi-tool-bridge.ts +2 -18
  73. package/src/cursor-provider-errors.ts +0 -8
  74. package/src/cursor-provider-live-run-drain.ts +8 -2
  75. package/src/cursor-provider-overflow.ts +6 -24
  76. package/src/cursor-provider-turn-prepare.ts +10 -1
  77. package/src/cursor-provider-turn-runner.ts +17 -6
  78. package/src/cursor-provider.ts +0 -5
  79. package/src/cursor-runtime-state.ts +3 -3
  80. package/src/cursor-sdk-event-debug.ts +2 -2
  81. package/src/cursor-sdk-output-filter.ts +2 -1
  82. package/src/cursor-sdk-process-error-guard.ts +1 -3
  83. package/src/cursor-session-agent.ts +8 -8
  84. package/src/cursor-session-scope.ts +1 -9
  85. package/src/cursor-session-store.ts +1 -1
  86. package/src/cursor-state.ts +9 -36
  87. package/src/cursor-tool-lifecycle.ts +0 -4
  88. package/src/cursor-tool-manifest.ts +4 -1
  89. package/src/cursor-tool-presentation-registry.ts +3 -5
  90. package/src/cursor-tool-visibility.ts +0 -4
  91. package/src/cursor-transcript-tool-specs.ts +0 -3
  92. package/src/index.ts +18 -0
  93. package/src/model-discovery.ts +0 -1
  94. package/src/model-list-cache.ts +4 -10
@@ -1,18 +1,12 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import {
3
- CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
4
3
  CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
5
4
  type CursorPiToolBridgeDiagnosticEvent,
6
5
  serializeCursorPiToolBridgeDiagnostic,
7
6
  } from "./cursor-pi-tool-bridge-diagnostics.js";
8
- import {
9
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
10
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
11
- CURSOR_PI_TOOL_BRIDGE_ENV,
12
- } from "./cursor-pi-tool-bridge-env.js";
13
7
  import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
14
- import { isCursorPiBridgeToolCallId, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
15
- import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
8
+ import { isCursorPiBridgeToolCallId } from "./cursor-pi-tool-bridge-constants.js";
9
+ import { CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
16
10
  import type {
17
11
  CursorPiToolBridge,
18
12
  CursorPiToolBridgeExtensionApi,
@@ -34,9 +28,6 @@ export type {
34
28
  export type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-diagnostics.js";
35
29
  export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
36
30
  export {
37
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
38
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
39
- CURSOR_PI_TOOL_BRIDGE_ENV,
40
31
  resolveCursorPiToolBridgeBuiltinsEnabled,
41
32
  resolveCursorPiToolBridgeCallTimeoutMs,
42
33
  resolveCursorPiToolBridgeEnabled,
@@ -145,13 +136,7 @@ export function getRegisteredCursorPiToolBridge(pi?: object): CursorPiToolBridge
145
136
  }
146
137
 
147
138
  export const __testUtils = {
148
- CURSOR_PI_TOOL_BRIDGE_ENV,
149
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
150
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
151
- CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
152
139
  CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
153
- LOOPBACK_HOST,
154
- MCP_SERVER_NAME,
155
140
  createRegistry(
156
141
  pi: CursorPiToolBridgeSnapshotApi,
157
142
  env: Record<string, string | undefined> = process.env,
@@ -168,7 +153,6 @@ export const __testUtils = {
168
153
  return bridgeToolExecutionAbortTracker.getActiveCount();
169
154
  },
170
155
  buildWindowsBridgeBashAbortCommandForTests: buildWindowsBridgeBashAbortCommand,
171
- installWindowsBridgeBashAbortMarkerForTests: installWindowsBridgeBashAbortMarker,
172
156
  emitBridgeToolExecutionProcessAbortSignalForTests(signal: NodeJS.Signals) {
173
157
  bridgeToolExecutionAbortTracker.emitProcessAbortSignalForTests(signal);
174
158
  },
@@ -258,14 +258,6 @@ export function classifyCursorConnectError(error: unknown): CursorConnectErrorCl
258
258
  return undefined;
259
259
  }
260
260
 
261
- export function isCursorSdkAbortConnectError(error: unknown): boolean {
262
- return classifyCursorConnectError(error)?.kind === "abort";
263
- }
264
-
265
- export function isUnauthenticatedConnectError(error: unknown): boolean {
266
- return classifyCursorConnectError(error)?.kind === "unauthenticated";
267
- }
268
-
269
261
  function isLikelyNetworkTimeout(message: string): boolean {
270
262
  return (
271
263
  /\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN|NGHTTP2_ENHANCE_YOUR_CALM|ERR_HTTP2_STREAM_ERROR|ERR_HTTP2_SESSION_ERROR)\b/i.test(
@@ -29,7 +29,7 @@ import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js
29
29
  import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
30
30
  import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
31
31
 
32
- export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
32
+ const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
33
33
  function parseCursorNativeReplayIdleDisposeMs(raw: string | undefined): number {
34
34
  if (!raw || /[^0-9]/.test(raw)) return DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
35
35
  const value = Number(raw);
@@ -69,7 +69,7 @@ function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | und
69
69
  return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
70
70
  }
71
71
 
72
- export function getPendingCursorLiveRun(context: Context, scopeKey: string): CursorLiveRun | undefined {
72
+ function getPendingCursorLiveRun(context: Context, scopeKey: string): CursorLiveRun | undefined {
73
73
  return cursorLiveRuns.getPendingFromContext(context, getCursorNativeReplayIdFromToolCallId, scopeKey);
74
74
  }
75
75
 
@@ -449,6 +449,7 @@ export async function drainExistingCursorLiveRunBeforeSend(
449
449
  turnDebugRecorder: CursorSdkEventDebugRecorder | undefined,
450
450
  scopeKey: string,
451
451
  occupancyFloor?: number,
452
+ nativeToolsEnabled = true,
452
453
  ): Promise<LiveRunPreSendOutcome> {
453
454
  turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
454
455
  while (true) {
@@ -461,6 +462,11 @@ export async function drainExistingCursorLiveRunBeforeSend(
461
462
  try {
462
463
  const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
463
464
  if (run.disposed) return "continue_send" as const;
465
+ // Check under the drain lease, before consuming/delivering Pi results:
466
+ // resolving a bridge waiter can resume the SDK's old tool surface.
467
+ if ((run.nativeToolsEnabled !== false) !== nativeToolsEnabled) {
468
+ throw new Error("Cursor native tool policy changed while a local run is pending; restore the prior policy to finish it or run /cursor-abort before retrying. Already-executed work is not undone.");
469
+ }
464
470
  const consumed = cursorLiveRuns.consumeToolResults(run, context, getCursorNativeReplayIdFromToolCallId);
465
471
  await run.bridgeRun?.resolveToolResults(consumed.toolResults);
466
472
  const shouldChainUserInput = run.chainUserInputAfterCompletion || hasTrailingUserMessagesAfterToolResults(context);
@@ -1,5 +1,4 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { AssistantMessage } from "@earendil-works/pi-ai";
3
2
  import { CURSOR_PROVIDER } from "./cursor-model.js";
4
3
 
5
4
  /**
@@ -15,7 +14,7 @@ import { CURSOR_PROVIDER } from "./cursor-model.js";
15
14
  *
16
15
  * See pi `docs/custom-provider.md` -> "Context Overflow Errors".
17
16
  */
18
- export const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
17
+ const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
19
18
 
20
19
  /**
21
20
  * Context-overflow phrases. These overlap pi's own `OVERFLOW_PATTERNS` so the
@@ -43,9 +42,9 @@ const CURSOR_THROTTLE_PATTERN = /too many requests|rate.?limit|throttl|retry.?af
43
42
  * Map a finalized Cursor assistant `errorMessage` to pi's overflow form.
44
43
  * Returns `undefined` when the message is not a Cursor context-overflow failure.
45
44
  *
46
- * Pure and idempotent so it is safe to call repeatedly and unit-test in isolation.
45
+ * Idempotent so repeated message delivery does not add another prefix.
47
46
  */
48
- export function normalizeCursorOverflowErrorMessage(errorMessage: string | undefined): string | undefined {
47
+ function normalizeCursorOverflowErrorMessage(errorMessage: string | undefined): string | undefined {
49
48
  const message = errorMessage?.trim();
50
49
  if (!message) return undefined;
51
50
  // Idempotent: never double-prefix an already-normalized message.
@@ -57,24 +56,6 @@ export function normalizeCursorOverflowErrorMessage(errorMessage: string | undef
57
56
  return undefined;
58
57
  }
59
58
 
60
- /**
61
- * Rewrite a finalized assistant message into pi's overflow form when it is a
62
- * Cursor context-overflow failure. Returns the replacement message (for the
63
- * `message_end` result) or `undefined` to leave it unchanged.
64
- *
65
- * `isCursorProvider` carries the provider scoping decision out of the handler so
66
- * the full logic is unit-testable without a pi event harness.
67
- */
68
- export function rewriteCursorOverflowAssistantMessage(
69
- message: AssistantMessage,
70
- isCursorProvider: boolean,
71
- ): AssistantMessage | undefined {
72
- if (!isCursorProvider || message.stopReason !== "error") return undefined;
73
- const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
74
- if (!rewritten) return undefined;
75
- return { ...message, errorMessage: rewritten };
76
- }
77
-
78
59
  export type CursorOverflowExtensionApi = Pick<ExtensionAPI, "on">;
79
60
 
80
61
  /**
@@ -89,8 +70,9 @@ export function registerCursorOverflowNormalization(pi: CursorOverflowExtensionA
89
70
  const message = event.message;
90
71
  if (message.role !== "assistant") return undefined;
91
72
  const isCursorProvider = message.provider === CURSOR_PROVIDER || ctx.model?.provider === CURSOR_PROVIDER;
92
- const rewritten = rewriteCursorOverflowAssistantMessage(message, isCursorProvider);
73
+ if (!isCursorProvider || message.stopReason !== "error") return undefined;
74
+ const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
93
75
  if (!rewritten) return undefined;
94
- return { message: rewritten };
76
+ return { message: { ...message, errorMessage: rewritten } };
95
77
  });
96
78
  }
@@ -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);
@@ -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);
@@ -140,6 +140,8 @@ interface SessionCursorAgentCreateParams {
140
140
  settingSources?: SettingSource[];
141
141
  localSafety?: CursorLocalSafetyOptions;
142
142
  agents?: AgentOptions["agents"];
143
+ /** Captured by the turn owner before asynchronous acquisition. */
144
+ nativeToolsEnabled?: boolean;
143
145
  useHttp1ForAgent?: boolean;
144
146
  onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void;
145
147
  debugRecorder?: CursorSdkEventDebugRecorder;
@@ -153,7 +155,7 @@ interface SessionCursorAgentCreateParams {
153
155
  const sessionAgentsByScope = new Map<string, SessionCursorAgentPoolEntry>();
154
156
  const invalidatedScopeKeys = new Set<string>();
155
157
  const deadTransportScopeKeys = new Set<string>();
156
- let deadTransportAgentDisposeTimeoutMs = 3000;
158
+ const DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS = 3000;
157
159
  const terminalDisposedScopeGenerations = new Map<string, number>();
158
160
  const scopeCreationGenerations = new Map<string, number>();
159
161
  const EMPTY_POOL_STATE: SessionCursorAgentPoolState = { status: "empty" };
@@ -254,6 +256,8 @@ function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCr
254
256
  buildApiKeyPoolKeyFingerprint(params.apiKey),
255
257
  buildBridgePoolKeySuffix("bridge" in params ? params.bridge : getRegisteredCursorPiToolBridge()),
256
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"] : []),
257
261
  ].join("\0");
258
262
  }
259
263
 
@@ -273,7 +277,7 @@ async function disposePoolEntry(entry: SessionCursorAgentPoolEntry, options?: {
273
277
  await (options?.deadTransport
274
278
  ? Promise.race([
275
279
  disposal,
276
- new Promise<void>((resolve) => setTimeout(resolve, deadTransportAgentDisposeTimeoutMs).unref?.()),
280
+ new Promise<void>((resolve) => setTimeout(resolve, DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS).unref?.()),
277
281
  ])
278
282
  : disposal);
279
283
  } catch {
@@ -520,6 +524,7 @@ async function createSessionAgentEntry(
520
524
  localSafety: params.localSafety,
521
525
  store: sessionStore!.store,
522
526
  }),
527
+ ...(params.nativeToolsEnabled === false ? { tools: ["mcp"] } : {}),
523
528
  ...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
524
529
  ...(params.agents && Object.keys(params.agents).length ? { agents: params.agents } : {}),
525
530
  });
@@ -584,7 +589,7 @@ export {
584
589
  } from "./cursor-session-send-policy.js";
585
590
 
586
591
  export function invalidateSessionAgent(
587
- scopeKey: string = getCursorSessionScopeKey(),
592
+ scopeKey: string,
588
593
  options?: { deadTransport?: boolean },
589
594
  ): void {
590
595
  invalidatedScopeKeys.add(scopeKey);
@@ -725,11 +730,6 @@ export const __testUtils = {
725
730
  resetSessionCursorAgent,
726
731
  refreshSessionCursorAgentConfig,
727
732
  disposeAllSessionCursorAgents,
728
- setDeadTransportAgentDisposeTimeoutMs(ms: number): number {
729
- const previous = deadTransportAgentDisposeTimeoutMs;
730
- deadTransportAgentDisposeTimeoutMs = ms;
731
- return previous;
732
- },
733
733
  SessionCursorAgentCreationSupersededError,
734
734
  SessionCursorAgentScopeClosedError,
735
735
  };
@@ -12,7 +12,7 @@ interface CursorSessionScopeExtensionApi {
12
12
 
13
13
  const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
14
14
  const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:";
15
- export const MAX_CURSOR_SESSION_NAME_LENGTH = 100;
15
+ const MAX_CURSOR_SESSION_NAME_LENGTH = 100;
16
16
 
17
17
  type CursorSessionScopeChangeHandler = (previousScopeKey: string) => Promise<void> | void;
18
18
 
@@ -103,18 +103,10 @@ export function getCursorSessionScopeGeneration(scopeKey: string = getCursorSess
103
103
  return scopeGenerations.get(scopeKey) ?? 0;
104
104
  }
105
105
 
106
- export function getCursorSessionCwd(): string {
107
- return state.sessionCwd;
108
- }
109
-
110
106
  export function getCursorSessionProjectTrusted(pi?: object): boolean {
111
107
  return pi ? scopes.get(pi)?.projectTrusted === true : state.projectTrusted;
112
108
  }
113
109
 
114
- export function getCursorSessionName(): string | undefined {
115
- return state.sessionName;
116
- }
117
-
118
110
  function normalizeCursorSessionName(name: string | undefined): string | undefined {
119
111
  if (name === undefined) return undefined;
120
112
  return truncateCursorDisplayLine(name, MAX_CURSOR_SESSION_NAME_LENGTH) || undefined;
@@ -55,7 +55,7 @@ const activeWorkspaceRoots = new Map<string, { root: Promise<string>; users: num
55
55
  // ponytail: leases protect only extension-owned stores in this process. Other SDK
56
56
  // users/processes must stop before upgrade; a cross-process lock belongs in the SDK.
57
57
 
58
- export function hashCursorSessionStoreScope(scopeKey: string): string {
58
+ function hashCursorSessionStoreScope(scopeKey: string): string {
59
59
  return createHash("sha256")
60
60
  .update("pi-cursor-sdk-session-store\0")
61
61
  .update(scopeKey)
@@ -44,11 +44,7 @@ import {
44
44
  updateCursorSdkConfig,
45
45
  } from "./cursor-config.js";
46
46
  import {
47
- consumeCursorLocalForceOverride,
48
- CURSOR_RUNTIME_ENTRY_TYPE,
49
47
  formatCursorStatus,
50
- getCursorCliConfig,
51
- getCursorSessionConfig,
52
48
  registerCursorCloudRuntimeControls,
53
49
  resetCursorRuntimeStateForTests,
54
50
  resolveCursorStatusRuntime,
@@ -57,19 +53,11 @@ import {
57
53
  type CursorRuntimeStateExtensionApi,
58
54
  } from "./cursor-runtime-state.js";
59
55
 
60
- export {
61
- consumeCursorLocalForceOverride,
62
- getCursorCliConfig,
63
- getCursorSessionConfig,
64
- } from "./cursor-runtime-state.js";
65
-
66
56
  export const CURSOR_FOOTER_ENV = "PI_CURSOR_FOOTER";
67
57
 
68
58
  const FAST_ENTRY_TYPE = "cursor-fast-state";
69
59
  const MODE_ENTRY_TYPE = "cursor-mode-state";
70
60
 
71
- export type CursorAgentMode = AgentModeOption;
72
-
73
61
  const DEFAULT_CURSOR_AGENT_MODE: AgentModeOption = "agent";
74
62
 
75
63
  interface CursorFastEntryData {
@@ -91,11 +79,11 @@ const authoritativeGlobalFastPreferenceIds = new Set<string>();
91
79
  let globalFastPreferences = new Map<string, boolean>();
92
80
  const invalidCursorModeNotifiedSessionScopeKeys = new Set<string>();
93
81
 
94
- export function isCursorAgentMode(value: unknown): value is AgentModeOption {
82
+ function isCursorAgentMode(value: unknown): value is AgentModeOption {
95
83
  return value === "agent" || value === "plan";
96
84
  }
97
85
 
98
- export function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined {
86
+ function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined {
99
87
  if (typeof raw !== "string") return undefined;
100
88
  const mode = raw.trim();
101
89
  return isCursorAgentMode(mode) ? mode : undefined;
@@ -115,17 +103,13 @@ function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
115
103
  return isCursorAgentMode(asRecord(value)?.mode);
116
104
  }
117
105
 
118
- function getConfigPath(): string {
119
- return getCursorSdkUserConfigPath();
120
- }
121
-
122
106
  function loadGlobalFastPreferences(): Map<string, boolean> {
123
107
  return cursorFastDefaultsFromConfig(loadCursorSdkUserConfig());
124
108
  }
125
109
 
126
110
  function saveGlobalFastPreference(modelId: string, fast: boolean): void {
127
111
  updateCursorSdkConfig(
128
- getConfigPath(),
112
+ getCursorSdkUserConfigPath(),
129
113
  (current) => {
130
114
  const fastDefaults = { ...asRecord(current.fastDefaults), [modelId]: fast };
131
115
  return {
@@ -141,7 +125,7 @@ function saveGlobalFastPreference(modelId: string, fast: boolean): void {
141
125
 
142
126
  function saveGlobalCursorHttp1Enabled(enabled: boolean): void {
143
127
  updateCursorSdkConfig(
144
- getConfigPath(),
128
+ getCursorSdkUserConfigPath(),
145
129
  (current) => mergeCursorSdkConfigForUpdate(current, {
146
130
  local: { useHttp1ForAgent: enabled },
147
131
  }),
@@ -227,15 +211,15 @@ function formatInvalidCursorMode(raw: string): string {
227
211
  return `Invalid --cursor-mode "${raw}". Use "agent" or "plan".`;
228
212
  }
229
213
 
230
- export type CursorAgentModeResolution =
214
+ type CursorAgentModeResolution =
231
215
  | { kind: "valid"; mode: AgentModeOption }
232
216
  | { kind: "invalid"; raw: string; message: string };
233
217
 
234
- export function getStoredCursorAgentMode(scopeKey?: string): AgentModeOption {
218
+ function getStoredCursorAgentMode(scopeKey?: string): AgentModeOption {
235
219
  return getCursorSessionSettings(scopeKey).mode ?? DEFAULT_CURSOR_AGENT_MODE;
236
220
  }
237
221
 
238
- export function resolveCursorAgentMode(scopeKey?: string): CursorAgentModeResolution {
222
+ function resolveCursorAgentMode(scopeKey?: string): CursorAgentModeResolution {
239
223
  const cliCursorModeState = getCursorSessionSettings(scopeKey).cli.mode;
240
224
  switch (cliCursorModeState.kind) {
241
225
  case "valid":
@@ -383,10 +367,10 @@ function formatEffectiveCursorSettingSourcesLabel(raw: string | undefined = proc
383
367
  return `${rawLabel} (effective: ${effectiveLabel})`;
384
368
  }
385
369
 
386
- export function formatCursorToolsDebugReport(
370
+ function formatCursorToolsDebugReport(
387
371
  pi: Pick<ExtensionAPI, "getActiveTools" | "getAllTools">,
388
- env: Record<string, string | undefined> = process.env,
389
372
  ): string {
373
+ const env = process.env;
390
374
  const bridgeEnabled = resolveCursorPiToolBridgeEnabled(env);
391
375
  const manifestEnabled = resolveCursorToolManifestEnabled(env);
392
376
  const lines = [
@@ -689,16 +673,5 @@ function resetCursorModeStateForTests(): void {
689
673
  }
690
674
 
691
675
  export const __testUtils = {
692
- FAST_ENTRY_TYPE,
693
- MODE_ENTRY_TYPE,
694
- CURSOR_HTTP1_ENTRY_TYPE,
695
- RUNTIME_ENTRY_TYPE: CURSOR_RUNTIME_ENTRY_TYPE,
696
- DEFAULT_CURSOR_AGENT_MODE,
697
- getConfigPath,
698
- loadGlobalFastPreferences,
699
- get sessionFastPreferences() { return getCursorSessionSettings().fast; },
700
- getSessionCursorAgentMode: () => getCursorSessionSettings().mode,
701
- getCliCursorAgentMode: () => { const mode = getCursorSessionSettings().cli.mode; return mode.kind === "valid" ? mode.mode : undefined; },
702
- getCliCursorModeState: () => getCursorSessionSettings().cli.mode,
703
676
  resetCursorModeStateForTests,
704
677
  };
@@ -86,10 +86,6 @@ function buildCursorToolLifecycleLabelFromVisibility(
86
86
  }
87
87
  }
88
88
 
89
- export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string): string | undefined {
90
- return buildCursorToolLifecycleLabelFromVisibility(classifyCursorToolVisibility(toolCall), apiKey);
91
- }
92
-
93
89
  export function formatCursorToolLifecycleProgressText(toolCall: unknown, apiKey?: string): string | undefined {
94
90
  const visibility = classifyCursorToolVisibility(toolCall);
95
91
  const label = buildCursorToolLifecycleLabelFromVisibility(visibility, apiKey);
@@ -18,6 +18,7 @@ export function resolveCursorToolManifestEnabled(
18
18
 
19
19
  export function buildCursorToolManifestText(options: {
20
20
  bridgeSnapshot?: CursorPiToolBridgeSnapshot;
21
+ nativeToolsEnabled?: boolean;
21
22
  /** When false, bridge is off via PI_CURSOR_PI_TOOL_BRIDGE=0 (not merely empty). */
22
23
  piBridgeEnabled?: boolean;
23
24
  includePiBridgeGuidance?: boolean;
@@ -26,7 +27,9 @@ export function buildCursorToolManifestText(options: {
26
27
  const includePiBridgeGuidance = options.includePiBridgeGuidance !== false;
27
28
  const lines = [
28
29
  "Callable tool surfaces this run:",
29
- `- Cursor host/MCP: ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}; configured MCP depends on Cursor settings.`,
30
+ options.nativeToolsEnabled === false
31
+ ? "- Cursor native tools (including task/subagents): disabled (PI_CURSOR_NATIVE_TOOLS=0). MCP remains available; configured MCP depends on Cursor settings."
32
+ : `- Cursor host/MCP: ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}; configured MCP depends on Cursor settings.`,
30
33
  "- Pi tool toggles affect pi tools/bridge exposure only; they do not disable Cursor host/configured MCP tools.",
31
34
  ];
32
35
  const bridgeTools = includePiBridgeGuidance ? options.bridgeSnapshot?.tools ?? [] : [];