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,15 +1,57 @@
1
1
  import { createServer } from "node:http";
2
+ import { resolveCursorPiToolBridgePortRange, resolveCursorPiToolBridgeUrlHost, } from "./cursor-pi-tool-bridge-env.js";
2
3
  import { asRecord } from "./cursor-record-utils.js";
3
4
  import { CursorPiToolBridgeRunImpl } from "./cursor-pi-tool-bridge-run.js";
4
5
  import { buildCursorPiToolBridgeSnapshot, buildCursorPiToolBridgeSurfaceSignature, createEmptySnapshot, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-snapshot.js";
5
- export const LOOPBACK_HOST = "127.0.0.1";
6
+ const LOOPBACK_HOST = "127.0.0.1";
6
7
  const HTTP_SERVER_CLOSE_GRACE_MS = 250;
8
+ const EPHEMERAL_BIND_ATTEMPTS = 10;
9
+ function listenOnce(server, port, host = LOOPBACK_HOST) {
10
+ return new Promise((resolve, reject) => {
11
+ const onError = (error) => {
12
+ server.off("listening", onListening);
13
+ reject(error);
14
+ };
15
+ const onListening = () => {
16
+ server.off("error", onError);
17
+ resolve();
18
+ };
19
+ server.once("error", onError);
20
+ server.once("listening", onListening);
21
+ server.listen({ port, host, ipv6Only: host === "::1" });
22
+ });
23
+ }
24
+ async function closeServers(servers) {
25
+ await Promise.all(servers.map(async (server) => {
26
+ if (!server.listening)
27
+ return;
28
+ await new Promise((resolve, reject) => {
29
+ let settled = false;
30
+ const settle = (error) => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ clearTimeout(closeTimer);
35
+ if (error)
36
+ reject(error);
37
+ else
38
+ resolve();
39
+ };
40
+ const closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS);
41
+ closeTimer.unref?.();
42
+ server.close((error) => settle(error ?? undefined));
43
+ server.closeIdleConnections();
44
+ server.closeAllConnections();
45
+ });
46
+ }));
47
+ }
7
48
  export class CursorPiToolBridgeRegistry {
8
49
  pi;
9
50
  env;
10
51
  runs = new Set();
11
52
  routes = new Map();
12
- httpServer;
53
+ httpServers = [];
54
+ advertisedHost;
13
55
  listenPromise;
14
56
  serverLifecycle = Promise.resolve();
15
57
  constructor(pi, env = process.env) {
@@ -36,7 +78,17 @@ export class CursorPiToolBridgeRegistry {
36
78
  : createEmptySnapshot();
37
79
  const run = new CursorPiToolBridgeRunImpl(this, this.env, snapshot, bridgeEnabled && snapshot.tools.length > 0, options);
38
80
  this.runs.add(run);
39
- await run.start();
81
+ try {
82
+ await run.start();
83
+ }
84
+ catch (error) {
85
+ try {
86
+ await run.dispose();
87
+ }
88
+ catch { /* Preserve the original startup failure, not a secondary cleanup error. */ }
89
+ this.runs.delete(run);
90
+ throw error;
91
+ }
40
92
  run.emitStartDiagnostics(bridgeEnabled);
41
93
  return run;
42
94
  }
@@ -51,9 +103,9 @@ export class CursorPiToolBridgeRegistry {
51
103
  await this.ensureHttpServer();
52
104
  this.routes.set(pathname, run);
53
105
  const address = this.getHttpServerAddress();
54
- if (!address)
106
+ if (!address || !this.advertisedHost)
55
107
  throw new Error("Cursor pi tool bridge HTTP server is not listening");
56
- return `http://${LOOPBACK_HOST}:${address.port}${pathname}`;
108
+ return `http://${this.advertisedHost}:${address.port}${pathname}`;
57
109
  });
58
110
  }
59
111
  async unregisterRun(pathname, run) {
@@ -71,7 +123,7 @@ export class CursorPiToolBridgeRegistry {
71
123
  return result;
72
124
  }
73
125
  getHttpServerAddress() {
74
- const address = asRecord(this.httpServer?.address());
126
+ const address = asRecord(this.httpServers[0]?.address());
75
127
  if (typeof address?.port !== "number")
76
128
  return undefined;
77
129
  return {
@@ -99,13 +151,17 @@ export class CursorPiToolBridgeRegistry {
99
151
  }
100
152
  async ensureHttpServer() {
101
153
  this.listenPromise ??= this.startHttpServer().catch((error) => {
102
- this.httpServer = undefined;
154
+ this.httpServers = [];
155
+ this.advertisedHost = undefined;
103
156
  this.listenPromise = undefined;
104
157
  throw error;
105
158
  });
106
159
  await this.listenPromise;
107
160
  }
108
161
  async startHttpServer() {
162
+ // A hostname is a listener policy, not a mutable per-run URL preference.
163
+ const advertisedHost = resolveCursorPiToolBridgeUrlHost(this.env);
164
+ const range = resolveCursorPiToolBridgePortRange(this.env);
109
165
  const [{ getRequestListener }, { createMcpHonoApp }] = await Promise.all([
110
166
  import("@hono/node-server"),
111
167
  import("@modelcontextprotocol/hono"),
@@ -115,51 +171,45 @@ export class CursorPiToolBridgeRegistry {
115
171
  const parsedBody = context.get("parsedBody");
116
172
  return this.handleHttpRequest(context.req.raw, parsedBody);
117
173
  });
118
- const server = createServer(getRequestListener(app.fetch, { overrideGlobalObjects: false }));
119
- this.httpServer = server;
120
- await new Promise((resolve, reject) => {
121
- const onError = (error) => {
122
- server.off("listening", onListening);
123
- reject(error);
124
- };
125
- const onListening = () => {
126
- server.off("error", onError);
127
- resolve();
128
- };
129
- server.once("error", onError);
130
- server.once("listening", onListening);
131
- server.listen(0, LOOPBACK_HOST);
132
- });
174
+ const listener = getRequestListener(app.fetch, { overrideGlobalObjects: false });
175
+ const attempts = range ? range.last - range.first + 1 : advertisedHost === "localhost" ? EPHEMERAL_BIND_ATTEMPTS : 1;
176
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
177
+ const servers = [createServer(listener)];
178
+ try {
179
+ await listenOnce(servers[0], range ? range.first + attempt : 0);
180
+ if (advertisedHost === "localhost") {
181
+ const port = servers[0].address().port;
182
+ const ipv6 = createServer(listener);
183
+ servers.push(ipv6);
184
+ // Own both explicit loopbacks before publishing any tokenized URL.
185
+ await listenOnce(ipv6, port, "::1");
186
+ }
187
+ this.httpServers = servers;
188
+ this.advertisedHost = advertisedHost;
189
+ return;
190
+ }
191
+ catch (error) {
192
+ try {
193
+ await closeServers(servers);
194
+ }
195
+ catch {
196
+ throw error;
197
+ } // Never retry or replace the bind failure when rollback fails.
198
+ if (error.code !== "EADDRINUSE" || (!range && advertisedHost !== "localhost"))
199
+ throw error;
200
+ }
201
+ }
202
+ if (range)
203
+ throw new Error(`Cursor pi tool bridge could not listen on any port in ${range.first}-${range.last} (all in use)`);
204
+ throw new Error("Cursor pi tool bridge could not own both localhost loopback listeners on an ephemeral port");
133
205
  }
134
206
  async closeHttpServer() {
135
207
  await this.listenPromise?.catch(() => undefined);
136
- const server = this.httpServer;
137
- if (!server)
138
- return;
139
- this.httpServer = undefined;
208
+ const servers = this.httpServers;
209
+ this.httpServers = [];
210
+ this.advertisedHost = undefined;
140
211
  this.listenPromise = undefined;
141
- await new Promise((resolve, reject) => {
142
- let settled = false;
143
- let closeTimer;
144
- const settle = (error) => {
145
- if (settled)
146
- return;
147
- settled = true;
148
- if (closeTimer)
149
- clearTimeout(closeTimer);
150
- if (error)
151
- reject(error);
152
- else
153
- resolve();
154
- };
155
- closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS);
156
- closeTimer.unref?.();
157
- server.close((error) => {
158
- settle(error ?? undefined);
159
- });
160
- server.closeIdleConnections();
161
- server.closeAllConnections();
162
- });
212
+ await closeServers(servers);
163
213
  }
164
214
  async handleHttpRequest(request, parsedBody) {
165
215
  const url = new URL(request.url);
@@ -1,5 +1,5 @@
1
1
  import { createMcpToolName, normalizeMcpInputSchema, stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
2
- export { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
2
+ export { CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
3
3
  import { isRegisteredCursorNativeToolName } from "./cursor-native-tool-display-state.js";
4
4
  import { isExcludedFromCursorBridgeExposure } from "./cursor-tool-presentation-registry.js";
5
5
  const OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "write", "edit", "grep", "find", "ls"]);
@@ -1,11 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV, CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX, serializeCursorPiToolBridgeDiagnostic, } from "./cursor-pi-tool-bridge-diagnostics.js";
3
- import { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, } from "./cursor-pi-tool-bridge-env.js";
2
+ import { CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX, serializeCursorPiToolBridgeDiagnostic, } from "./cursor-pi-tool-bridge-diagnostics.js";
4
3
  import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
5
- import { isCursorPiBridgeToolCallId, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
6
- import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
4
+ import { isCursorPiBridgeToolCallId } from "./cursor-pi-tool-bridge-constants.js";
5
+ import { CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
7
6
  export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
8
- export { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeCallTimeoutMs, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
7
+ export { resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeCallTimeoutMs, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
9
8
  export { buildCursorPiToolBridgeSnapshot, buildCursorPiToolBridgeSurfaceSignature, } from "./cursor-pi-tool-bridge-snapshot.js";
10
9
  const bridges = new Map();
11
10
  const activeCallsByPi = new Map();
@@ -110,13 +109,7 @@ export function getRegisteredCursorPiToolBridge(pi) {
110
109
  return pi ? bridges.get(pi) : registeredCursorPiToolBridge;
111
110
  }
112
111
  export const __testUtils = {
113
- CURSOR_PI_TOOL_BRIDGE_ENV,
114
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
115
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
116
- CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
117
112
  CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
118
- LOOPBACK_HOST,
119
- MCP_SERVER_NAME,
120
113
  createRegistry(pi, env = process.env) {
121
114
  return new CursorPiToolBridgeRegistry(pi, env);
122
115
  },
@@ -130,7 +123,6 @@ export const __testUtils = {
130
123
  return bridgeToolExecutionAbortTracker.getActiveCount();
131
124
  },
132
125
  buildWindowsBridgeBashAbortCommandForTests: buildWindowsBridgeBashAbortCommand,
133
- installWindowsBridgeBashAbortMarkerForTests: installWindowsBridgeBashAbortMarker,
134
126
  emitBridgeToolExecutionProcessAbortSignalForTests(signal) {
135
127
  bridgeToolExecutionAbortTracker.emitProcessAbortSignalForTests(signal);
136
128
  },
@@ -214,12 +214,6 @@ export function classifyCursorConnectError(error) {
214
214
  }
215
215
  return undefined;
216
216
  }
217
- export function isCursorSdkAbortConnectError(error) {
218
- return classifyCursorConnectError(error)?.kind === "abort";
219
- }
220
- export function isUnauthenticatedConnectError(error) {
221
- return classifyCursorConnectError(error)?.kind === "unauthenticated";
222
- }
223
217
  function isLikelyNetworkTimeout(message) {
224
218
  return (/\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN|NGHTTP2_ENHANCE_YOUR_CALM|ERR_HTTP2_STREAM_ERROR|ERR_HTTP2_SESSION_ERROR)\b/i.test(message) ||
225
219
  /\bConnectError\b.*\b(unavailable|deadline|timeout|timed out)\b/i.test(message) ||
@@ -9,7 +9,7 @@ import { trimCurrentTurnAlreadyEmittedCursorText } from "./cursor-run-final-text
9
9
  import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
10
10
  import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
11
11
  import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
12
- export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
12
+ const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
13
13
  function parseCursorNativeReplayIdleDisposeMs(raw) {
14
14
  if (!raw || /[^0-9]/.test(raw))
15
15
  return DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
@@ -36,7 +36,7 @@ export function createCursorNativeReplayId() {
36
36
  function getCursorNativeReplayIdFromToolCallId(toolCallId) {
37
37
  return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
38
38
  }
39
- export function getPendingCursorLiveRun(context, scopeKey) {
39
+ function getPendingCursorLiveRun(context, scopeKey) {
40
40
  return cursorLiveRuns.getPendingFromContext(context, getCursorNativeReplayIdFromToolCallId, scopeKey);
41
41
  }
42
42
  export function getActiveCursorLiveRunForCurrentScope(scopeKey) {
@@ -357,7 +357,7 @@ export async function drainCursorLiveRunTurn(stream, partial, model, context, ru
357
357
  });
358
358
  }
359
359
  }
360
- export async function drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, signal, turnDebugRecorder, scopeKey, occupancyFloor) {
360
+ export async function drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, signal, turnDebugRecorder, scopeKey, occupancyFloor, nativeToolsEnabled = true) {
361
361
  turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
362
362
  while (true) {
363
363
  const run = getPendingCursorLiveRun(context, scopeKey) ?? getActiveCursorLiveRunForCurrentScope(scopeKey);
@@ -369,6 +369,11 @@ export async function drainExistingCursorLiveRunBeforeSend(stream, partial, mode
369
369
  const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
370
370
  if (run.disposed)
371
371
  return "continue_send";
372
+ // Check under the drain lease, before consuming/delivering Pi results:
373
+ // resolving a bridge waiter can resume the SDK's old tool surface.
374
+ if ((run.nativeToolsEnabled !== false) !== nativeToolsEnabled) {
375
+ 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.");
376
+ }
372
377
  const consumed = cursorLiveRuns.consumeToolResults(run, context, getCursorNativeReplayIdFromToolCallId);
373
378
  await run.bridgeRun?.resolveToolResults(consumed.toolResults);
374
379
  const shouldChainUserInput = run.chainUserInputAfterCompletion || hasTrailingUserMessagesAfterToolResults(context);
@@ -12,7 +12,7 @@ import { CURSOR_PROVIDER } from "./cursor-model.js";
12
12
  *
13
13
  * See pi `docs/custom-provider.md` -> "Context Overflow Errors".
14
14
  */
15
- export const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
15
+ const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
16
16
  /**
17
17
  * Context-overflow phrases. These overlap pi's own `OVERFLOW_PATTERNS` so the
18
18
  * false-positive surface matches pi's built-in detection for other providers.
@@ -37,9 +37,9 @@ const CURSOR_THROTTLE_PATTERN = /too many requests|rate.?limit|throttl|retry.?af
37
37
  * Map a finalized Cursor assistant `errorMessage` to pi's overflow form.
38
38
  * Returns `undefined` when the message is not a Cursor context-overflow failure.
39
39
  *
40
- * Pure and idempotent so it is safe to call repeatedly and unit-test in isolation.
40
+ * Idempotent so repeated message delivery does not add another prefix.
41
41
  */
42
- export function normalizeCursorOverflowErrorMessage(errorMessage) {
42
+ function normalizeCursorOverflowErrorMessage(errorMessage) {
43
43
  const message = errorMessage?.trim();
44
44
  if (!message)
45
45
  return undefined;
@@ -53,22 +53,6 @@ export function normalizeCursorOverflowErrorMessage(errorMessage) {
53
53
  }
54
54
  return undefined;
55
55
  }
56
- /**
57
- * Rewrite a finalized assistant message into pi's overflow form when it is a
58
- * Cursor context-overflow failure. Returns the replacement message (for the
59
- * `message_end` result) or `undefined` to leave it unchanged.
60
- *
61
- * `isCursorProvider` carries the provider scoping decision out of the handler so
62
- * the full logic is unit-testable without a pi event harness.
63
- */
64
- export function rewriteCursorOverflowAssistantMessage(message, isCursorProvider) {
65
- if (!isCursorProvider || message.stopReason !== "error")
66
- return undefined;
67
- const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
68
- if (!rewritten)
69
- return undefined;
70
- return { ...message, errorMessage: rewritten };
71
- }
72
56
  /**
73
57
  * Register a `message_end` handler that rewrites Cursor context-overflow
74
58
  * failures into the `context_length_exceeded` form pi auto-compacts on.
@@ -82,9 +66,11 @@ export function registerCursorOverflowNormalization(pi) {
82
66
  if (message.role !== "assistant")
83
67
  return undefined;
84
68
  const isCursorProvider = message.provider === CURSOR_PROVIDER || ctx.model?.provider === CURSOR_PROVIDER;
85
- const rewritten = rewriteCursorOverflowAssistantMessage(message, isCursorProvider);
69
+ if (!isCursorProvider || message.stopReason !== "error")
70
+ return undefined;
71
+ const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
86
72
  if (!rewritten)
87
73
  return undefined;
88
- return { message: rewritten };
74
+ return { message: { ...message, errorMessage: rewritten } };
89
75
  });
90
76
  }
@@ -24,6 +24,7 @@ import { MISSING_CURSOR_API_KEY_MESSAGE } from "./cursor-provider-errors.js";
24
24
  import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
25
25
  import { resolveCursorApiKey } from "./cursor-api-key.js";
26
26
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
27
+ import { assertCursorNativeToolsRuntime } from "./cursor-native-tools.js";
27
28
  import { prepareCursorSummaryProviderTurn } from "./cursor-provider-turn-summary.js";
28
29
  function buildCursorCloudPromptContext(context, handoff, nativeSourceRoles) {
29
30
  if (handoff === "bootstrap")
@@ -182,7 +183,7 @@ async function prepareCursorCloudProviderTurn(prepareParams) {
182
183
  }
183
184
  }
184
185
  async function prepareCursorLocalProviderTurn(prepareParams) {
185
- const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled, agents, customSubagentNames } = prepareParams;
186
+ const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled, agents, customSubagentNames, nativeToolsEnabled } = prepareParams;
186
187
  const { model, context, options } = params;
187
188
  let restoreCursorSdkOutputFilter;
188
189
  let sessionAgentScopeKey;
@@ -212,6 +213,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
212
213
  settingSources,
213
214
  localSafety,
214
215
  agents,
216
+ nativeToolsEnabled,
215
217
  localResume: resolvedConfig.local.resume.value,
216
218
  storeRoot: resolvedConfig.local.storeRoot.value,
217
219
  useHttp1ForAgent,
@@ -236,6 +238,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
236
238
  ...getCursorPromptOptions(model),
237
239
  nativeSourceRoles: params.request.nativeSourceRoles,
238
240
  agentMode,
241
+ nativeToolsEnabled,
239
242
  includePiBridgeGuidance,
240
243
  includePiAskQuestionGuidance: false,
241
244
  };
@@ -245,6 +248,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
245
248
  return {
246
249
  ...promptOptions,
247
250
  toolManifest: buildCursorToolManifestText({
251
+ nativeToolsEnabled,
248
252
  bridgeSnapshot: sessionAgentLease.bridgeRun?.snapshot,
249
253
  piBridgeEnabled: resolveCursorPiToolBridgeEnabled(),
250
254
  includePiBridgeGuidance,
@@ -295,6 +299,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
295
299
  agentMode,
296
300
  localForce: resolvedConfig.local.force.value,
297
301
  localResume: resolvedConfig.local.resume.value,
302
+ ...(nativeToolsEnabled ? {} : { nativeToolsEnabled: false, tools: ["mcp"] }),
298
303
  resumedAgent: sessionAgentLease.resumed,
299
304
  activeToolNames: activeToolNames ? [...activeToolNames] : [],
300
305
  sessionAgentScopeKey,
@@ -308,6 +313,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
308
313
  ? cursorLiveRuns.start({
309
314
  id: useNativeToolReplay ? nativeReplayId : bridgeRun?.id ?? nativeReplayId,
310
315
  agent,
316
+ nativeToolsEnabled,
311
317
  bridgeRun,
312
318
  sessionBridgeRun,
313
319
  sessionAgentScopeKey,
@@ -387,6 +393,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
387
393
  export async function prepareCursorProviderTurn(prepareParams) {
388
394
  const { params, resolvedConfig } = prepareParams;
389
395
  const { model, options } = params;
396
+ assertCursorNativeToolsRuntime(resolvedConfig.runtime.value, prepareParams.nativeToolsEnabled);
390
397
  const fastEnabled = resolvedConfig.runtime.value === "cloud" ? undefined : getEffectiveFastForModelId(model.id, params.scope.scopeKey);
391
398
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
392
399
  if (resolvedConfig.runtime.value === "local" && params.request.purpose !== "normal") {
@@ -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 { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
5
6
  import { awaitFinalizeCursorRunOutcome } from "./cursor-provider-turn-finalize.js";
@@ -45,6 +46,12 @@ export class CursorProviderTurnRunner {
45
46
  this.throwIfAborted();
46
47
  const { scope } = this.params;
47
48
  const cwd = scope.cwd;
49
+ // Resolved once here, before any drain await, so the drain decision and the
50
+ // prepare dispatch below always act on the same config snapshot.
51
+ const resolvedConfig = resolveCursorProviderTurnConfig(cwd, scope.projectTrusted, scope.scopeKey);
52
+ this.runtimeTarget = resolvedConfig.runtime.value;
53
+ const nativeToolsEnabled = resolveCursorNativeToolsEnabled();
54
+ assertCursorNativeToolsRuntime(this.runtimeTarget, nativeToolsEnabled);
48
55
  this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
49
56
  cwd,
50
57
  modelId: model.id,
@@ -53,12 +60,8 @@ export class CursorProviderTurnRunner {
53
60
  });
54
61
  sdkEventDebugRef.current = this.sdkEventDebug;
55
62
  this.sdkEventDebug?.recordContextSnapshot(context);
56
- // Resolved once here, before any drain await, so the drain decision and the
57
- // prepare dispatch below always act on the same config snapshot.
58
- const resolvedConfig = resolveCursorProviderTurnConfig(cwd, scope.projectTrusted, scope.scopeKey);
59
- this.runtimeTarget = resolvedConfig.runtime.value;
60
63
  if (resolvedConfig.runtime.value === "local" && this.params.request.purpose === "normal") {
61
- if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug, scope.scopeKey, this.params.request.occupancyFloor)) ===
64
+ if ((await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug, scope.scopeKey, this.params.request.occupancyFloor, nativeToolsEnabled)) ===
62
65
  "stream_ended") {
63
66
  return;
64
67
  }
@@ -72,6 +75,7 @@ export class CursorProviderTurnRunner {
72
75
  sdkEventDebug: this.sdkEventDebug,
73
76
  throwIfAborted: () => this.throwIfAborted(),
74
77
  resolvedConfig,
78
+ nativeToolsEnabled,
75
79
  });
76
80
  const usage = await this.params.usageRecorder.start({
77
81
  agent: prepared.agent,
@@ -145,6 +149,13 @@ export class CursorProviderTurnRunner {
145
149
  });
146
150
  }
147
151
  catch (error) {
152
+ // This body owns the session turn lease; a queued request aborted before
153
+ // entry must not release another request's run in handleOuterCatch().
154
+ if (!prepared && (error instanceof CursorLiveRunAbortError || options?.signal?.aborted)) {
155
+ const pending = cursorLiveRuns.getActiveForScope(this.params.scope.scopeKey);
156
+ if (pending && !pending.disposed)
157
+ await cursorLiveRuns.release(pending);
158
+ }
148
159
  await runFinalizer.applyTerminalEvent({ kind: "error", prepared: started ?? prepared, error });
149
160
  }
150
161
  finally {
@@ -1,5 +1,5 @@
1
1
  import { createAssistantMessageEventStream, } from "@earendil-works/pi-ai";
2
- import { cursorLiveRuns, DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS, getPendingCursorLiveRun, hasTrailingUserMessagesAfterToolResults, releaseAllPendingCursorLiveRunsForTests, resetCursorNativeReplayIdleDisposeMs, setCursorNativeReplayIdleDisposeMs, } from "./cursor-provider-live-run-drain.js";
2
+ import { cursorLiveRuns, hasTrailingUserMessagesAfterToolResults, releaseAllPendingCursorLiveRunsForTests, resetCursorNativeReplayIdleDisposeMs, setCursorNativeReplayIdleDisposeMs, } from "./cursor-provider-live-run-drain.js";
3
3
  import { disposeAllSessionCursorAgents } from "./cursor-session-agent.js";
4
4
  import { attachCursorSdkEventDebugPiStreamTap } from "./cursor-sdk-event-debug.js";
5
5
  import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
@@ -66,10 +66,7 @@ export function streamCursor(model, context, options, ownership) {
66
66
  return stream;
67
67
  }
68
68
  export const __testUtils = {
69
- DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
70
69
  pendingCursorNativeRunCount: cursorLiveRuns.count,
71
- getPendingCursorLiveRun,
72
- getActiveCursorLiveRunForScope: cursorLiveRuns.getActiveForScope,
73
70
  hasTrailingUserMessagesAfterToolResults,
74
71
  setCursorNativeReplayIdleDisposeMs,
75
72
  resetCursorNativeReplayIdleDisposeMs,
@@ -4,8 +4,8 @@ import { asRecord } from "./cursor-record-utils.js";
4
4
  import { getResolvedSessionCursorHttp1Enabled } from "./cursor-http1.js";
5
5
  import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
6
6
  import { cursorSettingsScopeForContext, getCursorSessionSettings } from "./cursor-session-settings.js";
7
- export const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
8
- export const CURSOR_CLOUD_ACK_DISCLOSURE = [
7
+ const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
8
+ const CURSOR_CLOUD_ACK_DISCLOSURE = [
9
9
  "Cursor Cloud executes this work remotely.",
10
10
  "Fresh context is used by default; prior Pi context is included only with explicit bootstrap opt-in.",
11
11
  "Pi-local tools and the Pi bridge are unavailable, and Pi environment variables are not forwarded.",
@@ -76,7 +76,7 @@ export function resolveCursorStatusRuntime(ctx) {
76
76
  return { kind: "invalid", message: error instanceof Error ? error.message : String(error) };
77
77
  }
78
78
  }
79
- export function formatResolvedCursorRuntime(runtime) {
79
+ function formatResolvedCursorRuntime(runtime) {
80
80
  const cap = runtime.cappedBy;
81
81
  return cap
82
82
  ? `${runtime.value} (source: ${runtime.source} safety cap over ${cap.cappedSource} ${cap.cappedValue})`
@@ -60,10 +60,10 @@ export function resolveCursorSdkEventDebugEnabled(env = process.env) {
60
60
  return parseEnvBoolean(env[CURSOR_SDK_EVENT_DEBUG_ENV], false);
61
61
  }
62
62
  export const DISCARDED_INCOMPLETE_TOOL_CALL_REASON = "no-completion-at-run-end";
63
- export function hashCursorSdkCallId(callId) {
63
+ function hashCursorSdkCallId(callId) {
64
64
  return createHash("sha256").update(callId).digest("hex").slice(0, 8);
65
65
  }
66
- export function serializeDiscardedIncompleteStartedToolCall(record) {
66
+ function serializeDiscardedIncompleteStartedToolCall(record) {
67
67
  return {
68
68
  event: "discarded-incomplete-started-tool-call",
69
69
  toolName: record.toolName,
@@ -1 +1 @@
1
- export { CURSOR_SDK_STARTUP_NOISE_PATTERNS, withCursorSdkOutputNoticeHandler, installCursorSdkOutputFilter, isCursorSdkOutputSuppressed, isCursorSdkStartupNoise, suppressCursorSdkOutput, } from "../shared/cursor-sdk-output-filter.mjs";
1
+ export { __testUtils, installCursorSdkProcessOutputFilter, withCursorSdkOutputNoticeHandler, installCursorSdkOutputFilter, isCursorSdkOutputSuppressed, isCursorSdkStartupNoise, suppressCursorSdkOutput, } from "../shared/cursor-sdk-output-filter.mjs";
@@ -1,4 +1,4 @@
1
- import { classifyCursorConnectError, isCursorSdkAbortConnectError, isCursorSdkConnectionStalledError } from "./cursor-provider-errors.js";
1
+ import { classifyCursorConnectError, isCursorSdkConnectionStalledError } from "./cursor-provider-errors.js";
2
2
  // Cursor SDK controlled-exec tasks can reject after their originating provider turn
3
3
  // has ended. The exact closed-writable failure is therefore session-scoped; existing
4
4
  // ConnectRPC suppression remains scoped to active provider turns.
@@ -128,7 +128,6 @@ export const __testUtils = {
128
128
  lifecycleSessionGuards.clear();
129
129
  },
130
130
  };
131
- export { isCursorSdkAbortConnectError };
132
131
  export function installCursorSdkProcessErrorGuard() {
133
132
  const token = { suppressAbortErrors: false };
134
133
  activeProviderTurns.add(token);
@@ -42,7 +42,7 @@ function rethrowSupersededWhenReplacedByDifferentPoolIdentity(scopeKey, poolKey,
42
42
  const sessionAgentsByScope = new Map();
43
43
  const invalidatedScopeKeys = new Set();
44
44
  const deadTransportScopeKeys = new Set();
45
- let deadTransportAgentDisposeTimeoutMs = 3000;
45
+ const DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS = 3000;
46
46
  const terminalDisposedScopeGenerations = new Map();
47
47
  const scopeCreationGenerations = new Map();
48
48
  const EMPTY_POOL_STATE = { status: "empty" };
@@ -121,6 +121,8 @@ function buildSessionAgentPoolKey(scopeKey, params) {
121
121
  buildApiKeyPoolKeyFingerprint(params.apiKey),
122
122
  buildBridgePoolKeySuffix("bridge" in params ? params.bridge : getRegisteredCursorPiToolBridge()),
123
123
  ...buildCustomSubagentPoolKeySuffix(params.agents),
124
+ // Omit for the default so historical durable keys remain byte-for-byte stable.
125
+ ...(params.nativeToolsEnabled === false ? ["tools:mcp-only"] : []),
124
126
  ].join("\0");
125
127
  }
126
128
  async function disposePoolEntry(entry, options) {
@@ -141,7 +143,7 @@ async function disposePoolEntry(entry, options) {
141
143
  await (options?.deadTransport
142
144
  ? Promise.race([
143
145
  disposal,
144
- new Promise((resolve) => setTimeout(resolve, deadTransportAgentDisposeTimeoutMs).unref?.()),
146
+ new Promise((resolve) => setTimeout(resolve, DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS).unref?.()),
145
147
  ])
146
148
  : disposal);
147
149
  }
@@ -343,6 +345,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
343
345
  localSafety: params.localSafety,
344
346
  store: sessionStore.store,
345
347
  }),
348
+ ...(params.nativeToolsEnabled === false ? { tools: ["mcp"] } : {}),
346
349
  ...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
347
350
  ...(params.agents && Object.keys(params.agents).length ? { agents: params.agents } : {}),
348
351
  });
@@ -404,7 +407,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
404
407
  }
405
408
  }
406
409
  export { buildCursorSessionSendPrompt, MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP, planCursorSessionSend, } from "./cursor-session-send-policy.js";
407
- export function invalidateSessionAgent(scopeKey = getCursorSessionScopeKey(), options) {
410
+ export function invalidateSessionAgent(scopeKey, options) {
408
411
  invalidatedScopeKeys.add(scopeKey);
409
412
  if (options?.deadTransport)
410
413
  deadTransportScopeKeys.add(scopeKey);
@@ -534,11 +537,6 @@ export const __testUtils = {
534
537
  resetSessionCursorAgent,
535
538
  refreshSessionCursorAgentConfig,
536
539
  disposeAllSessionCursorAgents,
537
- setDeadTransportAgentDisposeTimeoutMs(ms) {
538
- const previous = deadTransportAgentDisposeTimeoutMs;
539
- deadTransportAgentDisposeTimeoutMs = ms;
540
- return previous;
541
- },
542
540
  SessionCursorAgentCreationSupersededError,
543
541
  SessionCursorAgentScopeClosedError,
544
542
  };
@@ -3,7 +3,7 @@ import { parseArgs } from "@earendil-works/pi-coding-agent";
3
3
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
4
4
  const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
5
5
  const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:";
6
- export const MAX_CURSOR_SESSION_NAME_LENGTH = 100;
6
+ const MAX_CURSOR_SESSION_NAME_LENGTH = 100;
7
7
  const state = {
8
8
  sessionCwd: process.cwd(),
9
9
  sessionFile: undefined,
@@ -65,15 +65,9 @@ export function getCursorSessionScopeKey() {
65
65
  export function getCursorSessionScopeGeneration(scopeKey = getCursorSessionScopeKey()) {
66
66
  return scopeGenerations.get(scopeKey) ?? 0;
67
67
  }
68
- export function getCursorSessionCwd() {
69
- return state.sessionCwd;
70
- }
71
68
  export function getCursorSessionProjectTrusted(pi) {
72
69
  return pi ? scopes.get(pi)?.projectTrusted === true : state.projectTrusted;
73
70
  }
74
- export function getCursorSessionName() {
75
- return state.sessionName;
76
- }
77
71
  function normalizeCursorSessionName(name) {
78
72
  if (name === undefined)
79
73
  return undefined;
@@ -9,7 +9,7 @@ let sdkOperationsForTests;
9
9
  const activeWorkspaceRoots = new Map();
10
10
  // ponytail: leases protect only extension-owned stores in this process. Other SDK
11
11
  // users/processes must stop before upgrade; a cross-process lock belongs in the SDK.
12
- export function hashCursorSessionStoreScope(scopeKey) {
12
+ function hashCursorSessionStoreScope(scopeKey) {
13
13
  return createHash("sha256")
14
14
  .update("pi-cursor-sdk-session-store\0")
15
15
  .update(scopeKey)