pi-cursor-sdk 0.5.3 → 0.5.5

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 (141) hide show
  1. package/CHANGELOG.md +61 -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 +424 -1
  10. package/dist/cursor-live-run-coordinator.js +1 -0
  11. package/dist/cursor-local-run-terminal-error.js +57 -0
  12. package/dist/cursor-mcp-timeout-override.js +6 -17
  13. package/dist/cursor-native-tool-display-replay.js +5 -5
  14. package/dist/cursor-native-tools.js +11 -0
  15. package/dist/cursor-pi-tool-bridge-env.js +25 -2
  16. package/dist/cursor-pi-tool-bridge-server.js +98 -48
  17. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  18. package/dist/cursor-pi-tool-bridge.js +4 -12
  19. package/dist/cursor-provider-errors.js +0 -6
  20. package/dist/cursor-provider-live-run-drain.js +8 -3
  21. package/dist/cursor-provider-overflow.js +7 -21
  22. package/dist/cursor-provider-turn-finalize.js +2 -0
  23. package/dist/cursor-provider-turn-prepare.js +8 -1
  24. package/dist/cursor-provider-turn-runner.js +17 -6
  25. package/dist/cursor-provider-turn-send.js +3 -0
  26. package/dist/cursor-provider.js +1 -4
  27. package/dist/cursor-runtime-state.js +3 -3
  28. package/dist/cursor-sdk-event-debug.js +2 -2
  29. package/dist/cursor-sdk-output-filter.js +1 -1
  30. package/dist/cursor-sdk-process-error-guard.js +1 -2
  31. package/dist/cursor-session-agent.js +6 -8
  32. package/dist/cursor-session-scope.js +1 -7
  33. package/dist/cursor-session-store.js +3 -2
  34. package/dist/cursor-state.js +9 -23
  35. package/dist/cursor-tool-lifecycle.js +0 -3
  36. package/dist/cursor-tool-manifest.js +3 -1
  37. package/dist/cursor-tool-presentation-registry.js +2 -4
  38. package/dist/cursor-tool-visibility.js +0 -3
  39. package/dist/cursor-transcript-tool-specs.js +1 -2
  40. package/dist/index.js +21 -0
  41. package/dist/model-discovery.js +0 -1
  42. package/dist/model-list-cache.js +4 -9
  43. package/docs/cursor-live-smoke-checklist.md +4 -4
  44. package/docs/cursor-model-ux-spec.md +18 -8
  45. package/docs/cursor-native-tool-replay.md +2 -2
  46. package/docs/cursor-native-tool-visual-audit.md +1 -1
  47. package/docs/cursor-testing-lessons.md +22 -4
  48. package/docs/cursor-tool-surfaces.md +32 -3
  49. package/docs/development.md +101 -0
  50. package/docs/platform-smoke-implementation.md +3 -2
  51. package/docs/reference.md +810 -0
  52. package/node_modules/@modelcontextprotocol/core/node_modules/zod/package.json +1 -1
  53. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  54. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  55. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  56. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/compile.ts +9 -0
  57. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/schemas.ts +35 -1
  58. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/util.ts +13 -0
  59. package/node_modules/@modelcontextprotocol/core/node_modules/zod/src/v4/core/versions.ts +1 -1
  60. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/compile.cjs +8 -0
  61. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/compile.js +8 -0
  62. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.cjs +30 -0
  63. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.d.cts +3 -1
  64. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.d.ts +3 -1
  65. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/schemas.js +30 -0
  66. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.cjs +15 -0
  67. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.d.cts +2 -0
  68. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.d.ts +2 -0
  69. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/util.js +14 -0
  70. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/versions.cjs +1 -1
  71. package/node_modules/@modelcontextprotocol/core/node_modules/zod/v4/core/versions.js +1 -1
  72. package/node_modules/@modelcontextprotocol/server/node_modules/zod/package.json +1 -1
  73. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  74. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  75. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  76. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/compile.ts +9 -0
  77. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/schemas.ts +35 -1
  78. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/util.ts +13 -0
  79. package/node_modules/@modelcontextprotocol/server/node_modules/zod/src/v4/core/versions.ts +1 -1
  80. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/compile.cjs +8 -0
  81. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/compile.js +8 -0
  82. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.cjs +30 -0
  83. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.d.cts +3 -1
  84. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.d.ts +3 -1
  85. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/schemas.js +30 -0
  86. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.cjs +15 -0
  87. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.d.cts +2 -0
  88. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.d.ts +2 -0
  89. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/util.js +14 -0
  90. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/versions.cjs +1 -1
  91. package/node_modules/@modelcontextprotocol/server/node_modules/zod/v4/core/versions.js +1 -1
  92. package/package.json +14 -6
  93. package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
  94. package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
  95. package/scripts/platform-smoke/target-runtime.mjs +4 -2
  96. package/scripts/platform-smoke/targets.mjs +0 -1
  97. package/scripts/steering-rpc-smoke.mjs +1 -1
  98. package/shared/cursor-sdk-output-filter.d.mts +2 -0
  99. package/shared/cursor-sdk-output-filter.mjs +92 -28
  100. package/shared/cursor-sensitive-text.d.mts +1 -0
  101. package/shared/cursor-sensitive-text.mjs +7 -0
  102. package/src/context.ts +16 -9
  103. package/src/cursor-agents-context.ts +6 -6
  104. package/src/cursor-api-key.ts +1 -1
  105. package/src/cursor-bridge-contract.ts +1 -15
  106. package/src/cursor-cloud-local-state.ts +0 -2
  107. package/src/cursor-config.ts +1 -24
  108. package/src/cursor-fallback-models.generated.ts +424 -1
  109. package/src/cursor-live-run-coordinator.ts +5 -0
  110. package/src/cursor-local-run-terminal-error.ts +58 -0
  111. package/src/cursor-mcp-timeout-override.ts +6 -30
  112. package/src/cursor-native-tool-display-replay.ts +4 -6
  113. package/src/cursor-native-tools.ts +14 -0
  114. package/src/cursor-pi-tool-bridge-env.ts +31 -2
  115. package/src/cursor-pi-tool-bridge-server.ts +89 -45
  116. package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
  117. package/src/cursor-pi-tool-bridge.ts +2 -18
  118. package/src/cursor-provider-errors.ts +0 -8
  119. package/src/cursor-provider-live-run-drain.ts +8 -2
  120. package/src/cursor-provider-overflow.ts +6 -24
  121. package/src/cursor-provider-turn-finalize.ts +2 -0
  122. package/src/cursor-provider-turn-prepare.ts +10 -1
  123. package/src/cursor-provider-turn-runner.ts +17 -6
  124. package/src/cursor-provider-turn-send.ts +2 -0
  125. package/src/cursor-provider.ts +0 -5
  126. package/src/cursor-runtime-state.ts +3 -3
  127. package/src/cursor-sdk-event-debug.ts +2 -2
  128. package/src/cursor-sdk-output-filter.ts +2 -1
  129. package/src/cursor-sdk-process-error-guard.ts +1 -3
  130. package/src/cursor-session-agent.ts +8 -8
  131. package/src/cursor-session-scope.ts +1 -9
  132. package/src/cursor-session-store.ts +3 -2
  133. package/src/cursor-state.ts +9 -36
  134. package/src/cursor-tool-lifecycle.ts +0 -4
  135. package/src/cursor-tool-manifest.ts +4 -1
  136. package/src/cursor-tool-presentation-registry.ts +3 -5
  137. package/src/cursor-tool-visibility.ts +0 -4
  138. package/src/cursor-transcript-tool-specs.ts +0 -3
  139. package/src/index.ts +18 -0
  140. package/src/model-discovery.ts +0 -1
  141. package/src/model-list-cache.ts +4 -10
@@ -8,9 +8,9 @@ import { inferImageMimeType } from "./cursor-tool-result-display-readers.js";
8
8
  import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-transcript-utils.js";
9
9
  import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayCallSummary, shouldShowCursorReplayCollapsedExpandHint, } from "./cursor-tool-presentation-registry.js";
10
10
  import { CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE, isCursorReplayGenerateImageDetails, isCursorReplayActivityDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
11
- export { isCursorReplayNativeEditDetails, isCursorReplayGenerateImageDetails, isCursorReplayActivityDetails, isCursorReplayNativeWriteDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
11
+ export { isCursorReplayNativeEditDetails, isCursorReplayNativeWriteDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
12
12
  export const CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES = 8;
13
- export const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
13
+ const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
14
14
  export const CURSOR_REPLAY_PREVIEW_MAX_LINE_CHARS = 240;
15
15
  const CURSOR_REPLAY_HIGHLIGHT_MAX_CHARS = 12000;
16
16
  export const cursorReplayToolSchema = Type.Object({}, { additionalProperties: true });
@@ -40,7 +40,7 @@ function buildImageReplayComponent(text, imageData, mimeType, filename, theme) {
40
40
  },
41
41
  };
42
42
  }
43
- export function getCursorReplayPath(args, details) {
43
+ function getCursorReplayPath(args, details) {
44
44
  const argPath = args?.path;
45
45
  return details?.path ?? (typeof argPath === "string" && argPath.trim() ? argPath : "unknown");
46
46
  }
@@ -202,7 +202,7 @@ function formatCursorReplayActivityPreview(details, text, theme, maxLines, strip
202
202
  }
203
203
  return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
204
204
  }
205
- export function formatCursorReplayPreview(text, theme, maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, stripHeader = true) {
205
+ function formatCursorReplayPreview(text, theme, maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, stripHeader = true) {
206
206
  const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
207
207
  if (!body)
208
208
  return undefined;
@@ -240,7 +240,7 @@ function getCursorReplayCardTitle(toolName, args) {
240
240
  }
241
241
  return "Cursor activity";
242
242
  }
243
- export function renderCursorReplayCall(toolName, args, theme, isPartial) {
243
+ function renderCursorReplayCall(toolName, args, theme, isPartial) {
244
244
  if (!isPartial)
245
245
  return new Text("", 0, 0);
246
246
  let text = theme.fg("toolTitle", theme.bold(`${getCursorReplayCardTitle(toolName, args)} `));
@@ -0,0 +1,11 @@
1
+ import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
+ export const CURSOR_NATIVE_TOOLS_ENV = "PI_CURSOR_NATIVE_TOOLS";
3
+ /** Default preserves the SDK toolset; false selects the public MCP-only main loop. */
4
+ export function resolveCursorNativeToolsEnabled(env = process.env) {
5
+ return parseEnvBoolean(env[CURSOR_NATIVE_TOOLS_ENV], true);
6
+ }
7
+ export function assertCursorNativeToolsRuntime(runtime, nativeToolsEnabled) {
8
+ if (runtime === "cloud" && !nativeToolsEnabled) {
9
+ throw new Error("PI_CURSOR_NATIVE_TOOLS=0 requires local runtime; Cursor Cloud does not support the SDK tools restriction. Select local runtime or remove the opt-out.");
10
+ }
11
+ }
@@ -1,8 +1,8 @@
1
1
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
2
  import { resolveCursorMcpToolTimeoutMs } from "./cursor-mcp-timeout-override.js";
3
3
  export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
4
- export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
5
- export const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
4
+ const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
5
+ const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
6
6
  export function resolveCursorPiToolBridgeEnabled(env = process.env) {
7
7
  return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
8
8
  }
@@ -16,3 +16,26 @@ export function resolveCursorPiToolBridgeCallTimeoutMs(env = process.env) {
16
16
  return mcpToolTimeoutMs;
17
17
  return Math.min(Math.max(Math.trunc(parsed), 1), mcpToolTimeoutMs);
18
18
  }
19
+ const CURSOR_PI_TOOL_BRIDGE_HOST_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_HOST";
20
+ const CURSOR_PI_TOOL_BRIDGE_PORT_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_PORT";
21
+ const DEFAULT_URL_HOST = "127.0.0.1";
22
+ const PORT_PATTERN = /^\d{1,5}$/;
23
+ export function resolveCursorPiToolBridgeUrlHost(env = process.env) {
24
+ return env[CURSOR_PI_TOOL_BRIDGE_HOST_ENV]?.trim().toLowerCase() === "localhost" ? "localhost" : DEFAULT_URL_HOST;
25
+ }
26
+ function parsePort(text) {
27
+ if (!PORT_PATTERN.test(text))
28
+ return undefined;
29
+ const port = Number(text);
30
+ return port >= 1 && port <= 65535 ? port : undefined;
31
+ }
32
+ export function resolveCursorPiToolBridgePortRange(env = process.env) {
33
+ const parts = env[CURSOR_PI_TOOL_BRIDGE_PORT_ENV]?.trim().split("-");
34
+ if (!parts || parts.length > 2)
35
+ return undefined;
36
+ const first = parsePort(parts[0]);
37
+ const last = parts.length === 2 ? parsePort(parts[1]) : first;
38
+ if (first === undefined || last === undefined || first > last)
39
+ return undefined;
40
+ return { first, last };
41
+ }
@@ -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
  }
@@ -4,6 +4,7 @@ import { getCheckpointContextWindow, saveCachedContextWindow } from "./context-w
4
4
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
5
5
  import { isCursorRunFinishedSuccessfully, resolveCursorRunOutcome, } from "./cursor-provider-run-outcome.js";
6
6
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
7
+ import { resolveCursorLocalRunTerminalError } from "./cursor-local-run-terminal-error.js";
7
8
  export async function cacheSdkContextWindow(agentId, modelId, cwd, store) {
8
9
  try {
9
10
  const { createAgentPlatform } = await loadCursorSdk();
@@ -91,6 +92,7 @@ export async function awaitFinalizeCursorRunOutcome(params) {
91
92
  let waitResult;
92
93
  try {
93
94
  waitResult = params.waitResult ?? (await params.run.wait());
95
+ waitResult = resolveCursorLocalRunTerminalError(params.run, waitResult, params.signal?.aborted === true);
94
96
  }
95
97
  catch (error) {
96
98
  await recordCursorProviderTerminalUsage(params.prepared, params.signal?.aborted ? "abort" : "error");
@@ -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 {
@@ -3,6 +3,7 @@ import { createCursorCloudLifecyclePersistenceError } from "./cursor-cloud-lifec
3
3
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
4
4
  import { consumeCursorLocalForceOverride } from "./cursor-runtime-state.js";
5
5
  import { recordCursorSessionAgentLineage } from "./cursor-session-agent-lineage.js";
6
+ import { watchCursorLocalRunTerminalError } from "./cursor-local-run-terminal-error.js";
6
7
  const CLOUD_LEDGER_CANCEL_TIMEOUT_MS = 5000;
7
8
  async function requestBoundedCloudRunCancellation(run) {
8
9
  let timer;
@@ -105,6 +106,8 @@ export async function sendCursorProviderTurn(sendParams) {
105
106
  recordCursorSessionAgentLineage(agent.agentId, params.scope.scopeKey);
106
107
  }
107
108
  const run = await runPromise;
109
+ if (prepared.runtimeTarget === "local")
110
+ watchCursorLocalRunTerminalError(prepared.store, run);
108
111
  sdkRun = run;
109
112
  runtime.sdkRun = run;
110
113
  try {
@@ -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})`