pi-cursor-sdk 0.5.2 → 0.5.3

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 (47) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +38 -17
  3. package/dist/context.js +2 -2
  4. package/dist/cursor-custom-subagent-definitions.js +1 -1
  5. package/dist/cursor-fallback-models.generated.js +402 -1
  6. package/dist/cursor-local-agent-ownership.js +17 -0
  7. package/dist/cursor-mcp-timeout-override.js +1 -1
  8. package/dist/cursor-pi-tool-bridge-diagnostics.js +4 -0
  9. package/dist/cursor-pi-tool-bridge-run.js +10 -3
  10. package/dist/cursor-provider-errors.js +2 -2
  11. package/dist/cursor-provider-turn-summary.js +1 -1
  12. package/dist/cursor-session-agent-cleanup.js +10 -1
  13. package/dist/cursor-session-agent-lifecycle.js +2 -2
  14. package/dist/cursor-session-agent-lineage.js +11 -7
  15. package/dist/cursor-session-agent-resume.js +15 -9
  16. package/dist/cursor-session-agent.js +22 -7
  17. package/dist/cursor-session-scope.js +27 -14
  18. package/dist/cursor-session-settings.js +3 -3
  19. package/dist/cursor-session-store.js +1 -1
  20. package/dist/cursor-state.js +17 -6
  21. package/dist/model-discovery.js +8 -9
  22. package/docs/cursor-live-smoke-checklist.md +2 -2
  23. package/docs/cursor-model-ux-spec.md +23 -14
  24. package/docs/cursor-native-tool-replay.md +6 -3
  25. package/docs/cursor-native-tool-visual-audit.md +1 -1
  26. package/docs/cursor-testing-lessons.md +24 -6
  27. package/docs/cursor-tool-surfaces.md +16 -1
  28. package/package.json +8 -8
  29. package/src/context.ts +2 -2
  30. package/src/cursor-custom-subagent-definitions.ts +1 -1
  31. package/src/cursor-fallback-models.generated.ts +402 -1
  32. package/src/cursor-local-agent-ownership.ts +18 -0
  33. package/src/cursor-mcp-timeout-override.ts +1 -1
  34. package/src/cursor-pi-tool-bridge-diagnostics.ts +17 -0
  35. package/src/cursor-pi-tool-bridge-run.ts +10 -3
  36. package/src/cursor-provider-errors.ts +2 -2
  37. package/src/cursor-provider-turn-summary.ts +1 -1
  38. package/src/cursor-session-agent-cleanup.ts +6 -1
  39. package/src/cursor-session-agent-lifecycle.ts +2 -2
  40. package/src/cursor-session-agent-lineage.ts +10 -5
  41. package/src/cursor-session-agent-resume.ts +14 -7
  42. package/src/cursor-session-agent.ts +23 -7
  43. package/src/cursor-session-scope.ts +33 -11
  44. package/src/cursor-session-settings.ts +3 -3
  45. package/src/cursor-session-store.ts +1 -1
  46. package/src/cursor-state.ts +25 -5
  47. package/src/model-discovery.ts +7 -8
@@ -0,0 +1,18 @@
1
+ import type { CursorSessionStoreIdentity } from "./cursor-session-store.js";
2
+
3
+ // ponytail: process-local claims protect extension-owned handles and cleanup.
4
+ // Cross-process SDK ownership requires an upstream store/agent lease contract.
5
+ // Unproven disposal retains the exact handle claim until process exit.
6
+ const claims = new Set<string>();
7
+
8
+ export function claimCursorLocalAgent(identity: CursorSessionStoreIdentity, agentId: string): (() => void) | undefined {
9
+ const key = JSON.stringify([identity.stateRoot, agentId]);
10
+ if (claims.has(key)) return undefined;
11
+ claims.add(key);
12
+ let released = false;
13
+ return () => {
14
+ if (released) return;
15
+ released = true;
16
+ claims.delete(key);
17
+ };
18
+ }
@@ -91,7 +91,7 @@ export function isCursorSdkMcpToolTimeoutStack(stack: string | undefined): boole
91
91
 
92
92
  export function isCursorSdkMcpConnectTimeoutStack(stack: string | undefined): boolean {
93
93
  if (!stack || !isCursorSdkMcpProtocolTimeoutStack(stack)) return false;
94
- // SDK 1.0.36 minifies the Client class name; the protocol method and SDK
94
+ // The SDK minifies the Client class name; the protocol method and SDK
95
95
  // frame, not that constructor's spelling, establish this timeout purpose.
96
96
  return /^\s*at (?:[\w$]+\.)?(?:connect|listTools|getTools)\s*\(.*(?:node_modules[/\\]@cursor[/\\]sdk|@cursor\/sdk\/dist)/m.test(stack);
97
97
  }
@@ -49,6 +49,17 @@ interface CursorPiToolBridgeRunDisposedDiagnostic extends CursorPiToolBridgeLife
49
49
  event: "run_disposed";
50
50
  }
51
51
 
52
+ interface CursorPiToolBridgeMcpInitializedDiagnostic {
53
+ event: "mcp_initialized";
54
+ runId: string;
55
+ }
56
+
57
+ interface CursorPiToolBridgeMcpToolsListDiagnostic {
58
+ event: "mcp_tools_list";
59
+ runId: string;
60
+ toolCount: number;
61
+ }
62
+
52
63
  export interface CursorPiToolBridgeRequestDiagnosticFields {
53
64
  runId: string;
54
65
  bridgeCallId: string;
@@ -79,6 +90,8 @@ export type CursorPiToolBridgeDiagnosticEvent =
79
90
  | CursorPiToolBridgeToolsExposedDiagnostic
80
91
  | CursorPiToolBridgeRunCancelledDiagnostic
81
92
  | CursorPiToolBridgeRunDisposedDiagnostic
93
+ | CursorPiToolBridgeMcpInitializedDiagnostic
94
+ | CursorPiToolBridgeMcpToolsListDiagnostic
82
95
  | CursorPiToolBridgeRequestQueuedDiagnostic
83
96
  | CursorPiToolBridgeRequestResolvedDiagnostic
84
97
  | CursorPiToolBridgeRequestRejectedDiagnostic;
@@ -89,6 +102,10 @@ function assertNeverDiagnosticEvent(_event: never): never {
89
102
 
90
103
  export function serializeCursorPiToolBridgeDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): Record<string, unknown> {
91
104
  switch (event.event) {
105
+ case "mcp_initialized":
106
+ return { event: event.event, runId: event.runId };
107
+ case "mcp_tools_list":
108
+ return { event: event.event, runId: event.runId, toolCount: event.toolCount };
92
109
  case "run_created":
93
110
  return {
94
111
  event: event.event,
@@ -274,9 +274,16 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
274
274
  sessionIdGenerator: randomUUID,
275
275
  });
276
276
 
277
- server.setRequestHandler("tools/list", async () => ({
278
- tools: this.snapshot.tools.map(snapshotToolToMcpTool),
279
- }));
277
+ // ponytail: server-side receipts only, not SDK/model consumption or ongoing
278
+ // connection health; upgrade when Cursor exposes public MCP lifecycle callbacks.
279
+ server.oninitialized = () => {
280
+ this.emitDiagnostic({ event: "mcp_initialized", runId: this.id });
281
+ };
282
+ server.setRequestHandler("tools/list", async () => {
283
+ const tools = this.snapshot.tools.map(snapshotToolToMcpTool);
284
+ this.emitDiagnostic({ event: "mcp_tools_list", runId: this.id, toolCount: tools.length });
285
+ return { tools };
286
+ });
280
287
  server.setRequestHandler("tools/call", async (request, context) => {
281
288
  return this.enqueueToolRequest(
282
289
  request.params.name,
@@ -168,7 +168,7 @@ function isCursorSdkStallAbortNetworkError(code: unknown, evidence: string, stac
168
168
  }
169
169
 
170
170
  /**
171
- * @cursor/sdk@1.0.36 throws internal RetriableError (name/kind "RetriableError") with message
171
+ * @cursor/sdk@1.0.37 throws internal RetriableError (name/kind "RetriableError") with message
172
172
  * "Connection stalled" or "Connection stalled repeatedly" after fetchWithRetry exhausts stalls.
173
173
  * The ConnectError is only the cause; the top-level error is not a ConnectError.
174
174
  */
@@ -229,7 +229,7 @@ export function classifyCursorConnectError(error: unknown): CursorConnectErrorCl
229
229
 
230
230
  const directAbort = (code === 1 || code === "canceled") &&
231
231
  (causeName === "AbortError" || /AbortError/.test(stack));
232
- // The vendored 1.0.36 transport wraps a canceled ConnectError as Unknown.
232
+ // The vendored SDK transport can wrap a canceled ConnectError as Unknown.
233
233
  // Require the observed canceled -> AbortError cause chain, not generic text.
234
234
  const wrappedAbort = (code === 2 || code === "unknown") &&
235
235
  causeName === "ConnectError" &&
@@ -40,7 +40,7 @@ export async function prepareCursorSummaryProviderTurn(
40
40
  configureCursorSdkHttp1(sdk, resolvedConfig.local.useHttp1ForAgent);
41
41
  restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
42
42
  sessionStore = (await openCursorSessionStoreForScope({
43
- cwd, scopeKey: params.scope.scopeKey, persistent: false,
43
+ cwd, scopeKey: params.scope.persistentScopeKey ?? params.scope.scopeKey, persistent: false,
44
44
  })).sessionStore;
45
45
  throwIfAborted();
46
46
  agent = await suppressCursorSdkOutput(() => sdk.Agent.create({
@@ -5,6 +5,7 @@ import { fsyncExistingRegularFile } from "./cursor-durable-fs.js";
5
5
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
6
6
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
7
7
  import { cursorSessionScopeKeyForManager } from "./cursor-session-scope.js";
8
+ import { claimCursorLocalAgent } from "./cursor-local-agent-ownership.js";
8
9
  import {
9
10
  CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE,
10
11
  isCursorLocalAgentId,
@@ -323,7 +324,11 @@ export async function runCursorSessionAgentCleanupCommand(pi: LocalResumeCleanup
323
324
  openedStore = await openCursorSessionStore(ctx.cwd, identity, identities.workspaceRoot, identities.storeRoot);
324
325
  openedStores.set(identity.stateRoot, openedStore);
325
326
  }
326
- await operations.delete(agentId, { cwd: ctx.cwd, store: openedStore.store });
327
+ const release = claimCursorLocalAgent(identity, agentId);
328
+ if (!release) throw new Error("Cursor local agent is still owned by an active runtime");
329
+ try {
330
+ await operations.delete(agentId, { cwd: ctx.cwd, store: openedStore.store });
331
+ } finally { release(); }
327
332
  deletedAgentIds.push(agentId);
328
333
  } catch (error) {
329
334
  failedAgentIds.push({
@@ -7,7 +7,7 @@ import type {
7
7
  SessionStartEvent,
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import { clearCursorSdkHttp1 } from "./cursor-http1.js";
10
- import { cursorSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
10
+ import { cursorRuntimeSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
11
11
  import {
12
12
  disposeSessionCursorAgent,
13
13
  invalidateSessionAgent,
@@ -25,7 +25,7 @@ export interface CursorSessionAgentLifecycleExtensionApi {
25
25
 
26
26
  export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecycleExtensionApi): void {
27
27
  let scopeKey: string | undefined;
28
- pi.on("session_start", (_event, ctx) => { scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager); });
28
+ pi.on("session_start", (_event, ctx) => { scopeKey = cursorRuntimeSessionScopeKeyForManager(ctx.sessionManager); });
29
29
  onCursorSessionScopeKeyChange(pi, async (previousScopeKey) => {
30
30
  await disposeSessionCursorAgent(previousScopeKey);
31
31
  });
@@ -29,6 +29,7 @@ interface CursorSessionAgentLineageState {
29
29
  sessionId?: string;
30
30
  sessionFile?: string;
31
31
  scopeKey?: string;
32
+ runtimeScopeKey?: string;
32
33
  cwd?: string;
33
34
  recordedAgentIds: Set<string>;
34
35
  }
@@ -82,7 +83,7 @@ function readRecordedAgentIds(entries: readonly SessionEntry[], sessionId: strin
82
83
  export function recordCursorSessionAgentLineage(agentId: string, turnScopeKey?: string): void {
83
84
  const sessionState = turnScopeKey === undefined
84
85
  ? state
85
- : statesByScope.get(turnScopeKey) ?? (state.scopeKey === turnScopeKey ? state : undefined);
86
+ : statesByScope.get(turnScopeKey) ?? ((state.runtimeScopeKey ?? state.scopeKey) === turnScopeKey ? state : undefined);
86
87
  if (!sessionState) return;
87
88
  const { appendEntry, sessionId, sessionFile, scopeKey, cwd } = sessionState;
88
89
  if (!appendEntry || !sessionId || !scopeKey || !cwd) return;
@@ -115,22 +116,25 @@ export function registerCursorSessionAgentLineage(pi: CursorSessionAgentLineageE
115
116
  pi.on("session_start", (_event, ctx) => {
116
117
  state = sessionState;
117
118
  const record = sessionState;
118
- if (record.scopeKey && statesByScope.get(record.scopeKey) === record) statesByScope.delete(record.scopeKey);
119
+ if (record.runtimeScopeKey && statesByScope.get(record.runtimeScopeKey) === record) statesByScope.delete(record.runtimeScopeKey);
119
120
  record.appendEntry = pi.appendEntry;
120
121
  record.sessionId = ctx.sessionManager.getSessionId();
121
122
  record.sessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
122
- record.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
123
- statesByScope.set(record.scopeKey, record);
123
+ const scope = getCursorSessionScopeSnapshot(pi);
124
+ record.runtimeScopeKey = scope.scopeKey;
125
+ record.scopeKey = scope.persistentScopeKey ?? scope.scopeKey;
126
+ statesByScope.set(scope.scopeKey, record);
124
127
  record.cwd = ctx.cwd;
125
128
  record.recordedAgentIds = readRecordedAgentIds(ctx.sessionManager.getEntries(), record.sessionId);
126
129
  });
127
130
  pi.on("session_shutdown", () => {
128
131
  const state = sessionState;
129
- if (state.scopeKey && statesByScope.get(state.scopeKey) === state) statesByScope.delete(state.scopeKey);
132
+ if (state.runtimeScopeKey && statesByScope.get(state.runtimeScopeKey) === state) statesByScope.delete(state.runtimeScopeKey);
130
133
  state.appendEntry = undefined;
131
134
  state.sessionId = undefined;
132
135
  state.sessionFile = undefined;
133
136
  state.scopeKey = undefined;
137
+ state.runtimeScopeKey = undefined;
134
138
  state.cwd = undefined;
135
139
  state.recordedAgentIds = new Set();
136
140
  });
@@ -142,6 +146,7 @@ function resetStateForTests(): void {
142
146
  state.sessionId = undefined;
143
147
  state.sessionFile = undefined;
144
148
  state.scopeKey = undefined;
149
+ state.runtimeScopeKey = undefined;
145
150
  state.cwd = undefined;
146
151
  state.recordedAgentIds = new Set();
147
152
  }
@@ -60,6 +60,7 @@ interface PendingCursorSessionAgentResumeHandle {
60
60
 
61
61
  interface CursorSessionResumeState {
62
62
  appendEntry?: ExtensionAPI["appendEntry"];
63
+ runtimeScopeKey?: string;
63
64
  scopeKey: string;
64
65
  sessionFile?: string;
65
66
  sessionId?: string;
@@ -355,7 +356,7 @@ function restoreFromBranch(branch: readonly SessionEntry[], allEntries: readonly
355
356
 
356
357
  export function getMatchingCursorSessionAgentResumeHandle(poolKey: string, scopeKey?: string): CursorSessionAgentResumeEntryData | undefined {
357
358
  const state = getResumeState(scopeKey);
358
- if (scopeKey !== undefined && state.scopeKey !== scopeKey) return undefined;
359
+ if (scopeKey !== undefined && (state.runtimeScopeKey ?? state.scopeKey) !== scopeKey) return undefined;
359
360
  const handle = state.activeHandle;
360
361
  if (!handle || !isCursorLocalAgentId(handle.agentId)) return undefined;
361
362
  if (handle.poolKey !== poolKey) return undefined;
@@ -373,7 +374,7 @@ export function getMatchingCursorSessionAgentResumeHandle(poolKey: string, scope
373
374
 
374
375
  export function persistCursorSessionAgentResumeHandle(input: PendingCursorSessionAgentResumeHandle, scopeKey?: string): void {
375
376
  const state = getResumeState(scopeKey);
376
- if (scopeKey !== undefined && state.scopeKey !== scopeKey) return;
377
+ if (scopeKey !== undefined && (state.runtimeScopeKey ?? state.scopeKey) !== scopeKey) return;
377
378
  if (!isCursorLocalAgentId(input.agentId)) return;
378
379
  state.pendingHandle = {
379
380
  runtime: input.runtime,
@@ -427,8 +428,9 @@ interface CursorSessionAgentResumeExtensionApi {
427
428
  }
428
429
 
429
430
  export function registerCursorSessionAgentResume(pi: CursorSessionAgentResumeExtensionApi): void {
431
+ const scope = getCursorSessionScopeSnapshot(pi);
430
432
  const sessionState: CursorSessionResumeState = {
431
- appendEntry: pi.appendEntry, scopeKey: getCursorSessionScopeSnapshot(pi).scopeKey,
433
+ appendEntry: pi.appendEntry, scopeKey: scope.persistentScopeKey ?? scope.scopeKey,
432
434
  cwd: process.cwd(), branchPathHash: EMPTY_BRANCH_HASH,
433
435
  compactionGeneration: 0, unownedUserEntryIds: new Set(),
434
436
  };
@@ -439,9 +441,12 @@ export function registerCursorSessionAgentResume(pi: CursorSessionAgentResumeExt
439
441
  restoreFromBranch(branch, entries.length > 0 ? entries : branch, state);
440
442
  };
441
443
  pi.on("session_start", (_event, ctx) => {
442
- if (statesByScope.get(state.scopeKey) === state) statesByScope.delete(state.scopeKey);
443
- state.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
444
- statesByScope.set(state.scopeKey, state);
444
+ const key = state.runtimeScopeKey ?? state.scopeKey;
445
+ if (statesByScope.get(key) === state) statesByScope.delete(key);
446
+ const scope = getCursorSessionScopeSnapshot(pi);
447
+ state.runtimeScopeKey = scope.scopeKey;
448
+ state.scopeKey = scope.persistentScopeKey ?? scope.scopeKey;
449
+ statesByScope.set(scope.scopeKey, state);
445
450
  lastBoundState = state;
446
451
  state.sessionFile = ctx.sessionManager.getSessionFile?.() ?? undefined;
447
452
  state.sessionId = ctx.sessionManager.getSessionId?.() ?? undefined;
@@ -482,7 +487,8 @@ export function registerCursorSessionAgentResume(pi: CursorSessionAgentResumeExt
482
487
  state.branchPathHash = hashBranchStep(state.branchPathHash, event.compactionEntry);
483
488
  });
484
489
  pi.on("session_shutdown", () => {
485
- if (statesByScope.get(state.scopeKey) === state) statesByScope.delete(state.scopeKey);
490
+ const key = state.runtimeScopeKey ?? state.scopeKey;
491
+ if (statesByScope.get(key) === state) statesByScope.delete(key);
486
492
  state.appendEntry = undefined;
487
493
  state.pendingHandle = undefined;
488
494
  });
@@ -496,6 +502,7 @@ function resetStateForTests(): void {
496
502
  const state = lastBoundState;
497
503
  state.appendEntry = undefined;
498
504
  state.scopeKey = getCursorSessionScopeKey();
505
+ state.runtimeScopeKey = undefined;
499
506
  state.sessionFile = undefined;
500
507
  state.sessionId = undefined;
501
508
  state.cwd = process.cwd();
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import type { AgentModeOption, AgentOptions, LocalAgentOptions, LocalAgentStore, ModelSelection, SDKAgent, SettingSource } from "@cursor/sdk";
3
3
  import type { Context } from "@earendil-works/pi-ai";
4
4
  import { normalizeCursorCustomStoreRoot } from "./cursor-custom-store-path.js";
5
+ import { claimCursorLocalAgent } from "./cursor-local-agent-ownership.js";
5
6
  import {
6
7
  getRegisteredCursorPiToolBridge,
7
8
  type CursorPiToolBridge,
@@ -9,7 +10,7 @@ import {
9
10
  type CursorPiToolBridgeRun,
10
11
  } from "./cursor-pi-tool-bridge.js";
11
12
  import { computeCursorContextFingerprint } from "./context.js";
12
- import { getCursorSessionFile, getCursorSessionScopeGeneration, getCursorSessionScopeKey, type CursorTurnScope } from "./cursor-session-scope.js";
13
+ import { getCursorSessionScopeSnapshot, getCursorSessionScopeGeneration, getCursorSessionScopeKey, type CursorTurnScope } from "./cursor-session-scope.js";
13
14
  import {
14
15
  getMatchingCursorSessionAgentResumeHandle,
15
16
  persistCursorSessionAgentResumeHandle,
@@ -68,6 +69,7 @@ interface SessionCursorAgentReadyEntry extends SessionCursorAgentPoolEntryBase {
68
69
  resumeEnabled: boolean;
69
70
  resumed: boolean;
70
71
  resumeNotice?: string;
72
+ releaseAgentClaim: () => void;
71
73
  }
72
74
 
73
75
  interface SessionCursorAgentBusyEntry extends SessionCursorAgentPoolEntryBase {
@@ -78,6 +80,7 @@ interface SessionCursorAgentBusyEntry extends SessionCursorAgentPoolEntryBase {
78
80
  resumeEnabled: boolean;
79
81
  resumed: boolean;
80
82
  resumeNotice?: string;
83
+ releaseAgentClaim: () => void;
81
84
  completionSettled: Promise<void>;
82
85
  pendingCompletion: Promise<void>;
83
86
  releaseBusyWait: () => void;
@@ -238,7 +241,7 @@ function buildCustomSubagentPoolKeySuffix(agents: AgentOptions["agents"]): strin
238
241
 
239
242
  function buildSessionAgentPoolKey(scopeKey: string, params: SessionCursorAgentCreateParams): string {
240
243
  return [
241
- scopeKey,
244
+ params.scope?.persistentScopeKey ?? scopeKey,
242
245
  params.cwd,
243
246
  buildModelPoolKey(params.modelSelection),
244
247
  buildSettingSourcesPoolKey(params.settingSources),
@@ -262,8 +265,9 @@ async function disposePoolEntry(entry: SessionCursorAgentPoolEntry, options?: {
262
265
  } catch {
263
266
  // disposal failure should not block session replacement
264
267
  }
268
+ let disposal: Promise<unknown> | undefined;
265
269
  try {
266
- const disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]()).catch(() => undefined);
270
+ disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]());
267
271
  // A dead local transport may never settle SDK disposal; bound the wait so the
268
272
  // next acquire recreates instead of hanging on the dead agent.
269
273
  await (options?.deadTransport
@@ -276,6 +280,8 @@ async function disposePoolEntry(entry: SessionCursorAgentPoolEntry, options?: {
276
280
  // disposal failure should not block session replacement
277
281
  }
278
282
  await entry.sessionStore.dispose().catch(() => undefined);
283
+ // Timeout/rejection is not proof of closure; only successful SDK disposal releases ownership.
284
+ void disposal?.then(entry.releaseAgentClaim, () => {});
279
285
  }
280
286
 
281
287
  async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?: boolean }): Promise<void> {
@@ -470,6 +476,7 @@ async function createSessionAgentEntry(
470
476
  ): Promise<SessionCursorAgentReadyEntry> {
471
477
  let bridgeRun: CursorPiToolBridgeRun | undefined;
472
478
  let sessionStore: OpenCursorSessionStore | undefined;
479
+ let releaseAgentClaim: (() => void) | undefined;
473
480
  try {
474
481
  const registeredBridge = params.bridge;
475
482
  if (registeredBridge) {
@@ -495,7 +502,7 @@ async function createSessionAgentEntry(
495
502
  const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey, scopeKey) : undefined;
496
503
  const storeSelection = await openCursorSessionStoreForScope({
497
504
  cwd: params.cwd,
498
- scopeKey,
505
+ scopeKey: params.scope?.persistentScopeKey ?? scopeKey,
499
506
  persistent: persistentStore,
500
507
  storeRoot: params.storeRoot,
501
508
  resume: resumeHandle ? { identity: resumeHandle.storeIdentity, agentId: resumeHandle.agentId } : undefined,
@@ -520,11 +527,16 @@ async function createSessionAgentEntry(
520
527
  let effectiveSendState = sendState;
521
528
  let resumed = false;
522
529
  if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
530
+ releaseAgentClaim = claimCursorLocalAgent(sessionStore.identity, resumeHandle.agentId);
531
+ }
532
+ if (resumeHandle && resumeAttemptAllowed && resumeAgent && releaseAgentClaim) {
523
533
  try {
524
534
  agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
525
535
  effectiveSendState = { ...resumeHandle.sendState };
526
536
  resumed = true;
527
537
  } catch {
538
+ releaseAgentClaim();
539
+ releaseAgentClaim = undefined;
528
540
  if (persistentStore) resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
529
541
  if (storeSelection.persistent && !cursorSessionStoreIdentitiesEqual(sessionStore.identity, storeSelection.identities.sessionStore)) {
530
542
  const { identities } = storeSelection;
@@ -537,6 +549,8 @@ async function createSessionAgentEntry(
537
549
  agent ??= await createAgent(buildAgentOptions());
538
550
  if (!agent) throw new Error("Cursor SDK agent creation returned no agent");
539
551
  if (!sessionStore) throw new Error("Cursor SDK session store was not opened");
552
+ releaseAgentClaim ??= claimCursorLocalAgent(sessionStore.identity, agent.agentId);
553
+ if (!releaseAgentClaim) throw new Error("Cursor SDK agent is already owned by another runtime");
540
554
 
541
555
  return {
542
556
  status: "ready",
@@ -550,12 +564,14 @@ async function createSessionAgentEntry(
550
564
  sendState: effectiveSendState,
551
565
  resumeEnabled: params.localResume === true,
552
566
  resumed,
567
+ releaseAgentClaim,
553
568
  ...(resumeNotice ? { resumeNotice } : {}),
554
569
  };
555
570
  } catch (error) {
556
571
  bridgeRun?.cancel("Cursor session agent create failed");
557
572
  await bridgeRun?.dispose().catch(() => undefined);
558
573
  await sessionStore?.dispose().catch(() => undefined);
574
+ releaseAgentClaim?.();
559
575
  throw error;
560
576
  }
561
577
  }
@@ -576,9 +592,9 @@ export function invalidateSessionAgent(
576
592
  }
577
593
 
578
594
  export async function acquireSessionCursorAgent(input: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
579
- const params = { ...input, bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
580
- const scopeKey = params.scope?.scopeKey ?? getCursorSessionScopeKey();
581
- const persistentStore = (params.scope ? params.scope.sessionFile : getCursorSessionFile()) !== undefined;
595
+ const params = { ...input, scope: input.scope ?? getCursorSessionScopeSnapshot(), bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
596
+ const scopeKey = params.scope.scopeKey;
597
+ const persistentStore = params.scope.sessionFile !== undefined;
582
598
  params.storeRoot = persistentStore && params.storeRoot !== undefined ? normalizeCursorCustomStoreRoot(params.storeRoot) : undefined;
583
599
 
584
600
  while (true) {
@@ -19,6 +19,8 @@ type CursorSessionScopeChangeHandler = (previousScopeKey: string) => Promise<voi
19
19
  export interface CursorTurnScope {
20
20
  readonly cwd: string;
21
21
  readonly scopeKey: string;
22
+ /** Durable identity only; never use it to own live runtime resources. */
23
+ readonly persistentScopeKey?: string;
22
24
  readonly sessionFile: string | undefined;
23
25
  readonly sessionId: string | undefined;
24
26
  readonly sessionName: string | undefined;
@@ -33,6 +35,7 @@ const state = {
33
35
  sessionName: undefined as string | undefined,
34
36
  projectTrusted: false,
35
37
  sessionGeneration: 0,
38
+ runtimeOwner: undefined as number | undefined,
36
39
  };
37
40
  type ScopeRecord = typeof state;
38
41
  const scopes = new Map<object, ScopeRecord>();
@@ -40,20 +43,34 @@ const scopeChangeHandlers = new Map<object, CursorSessionScopeChangeHandler>();
40
43
  const scopeGenerations = new Map<string, number>([[ANONYMOUS_SESSION_SCOPE_KEY, 0]]);
41
44
  const projectTrustResolutionCwds = new Set<string>();
42
45
  let nextSessionGeneration = 1;
46
+ let nextRuntimeOwner = 1;
47
+ let runtimeOwners = new WeakMap<object, number>();
48
+
49
+ function persistentScopeKeyFor(record: Pick<ScopeRecord, "sessionFile" | "sessionId">): string {
50
+ return record.sessionFile ?? (record.sessionId ? `${EPHEMERAL_SESSION_SCOPE_PREFIX}${record.sessionId}` : ANONYMOUS_SESSION_SCOPE_KEY);
51
+ }
43
52
 
44
53
  function scopeKeyFor(record: ScopeRecord): string {
45
- if (record.sessionFile) return record.sessionFile;
46
- if (record.sessionId) return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${record.sessionId}`;
47
- return ANONYMOUS_SESSION_SCOPE_KEY;
54
+ const persistent = persistentScopeKeyFor(record);
55
+ return record.runtimeOwner === undefined ? persistent : `${persistent}\0runtime:${record.runtimeOwner}`;
48
56
  }
49
57
 
50
58
  export function cursorSessionScopeKeyForManager(manager: {
51
59
  getSessionFile?(): string | undefined | null;
52
60
  getSessionId?(): string | undefined;
53
61
  }): string {
54
- return manager.getSessionFile?.() ?? (manager.getSessionId?.()
55
- ? `${EPHEMERAL_SESSION_SCOPE_PREFIX}${manager.getSessionId()}`
56
- : ANONYMOUS_SESSION_SCOPE_KEY);
62
+ return persistentScopeKeyFor({
63
+ sessionFile: manager.getSessionFile?.() ?? undefined, sessionId: manager.getSessionId?.(),
64
+ });
65
+ }
66
+
67
+ export function cursorRuntimeSessionScopeKeyForManager(manager: {
68
+ getSessionFile?(): string | undefined | null;
69
+ getSessionId?(): string | undefined;
70
+ }): string {
71
+ const persistent = cursorSessionScopeKeyForManager(manager);
72
+ const owner = runtimeOwners.get(manager);
73
+ return owner === undefined ? persistent : `${persistent}\0runtime:${owner}`;
57
74
  }
58
75
 
59
76
  export function getCursorSessionScopeSnapshot(pi?: object): CursorTurnScope {
@@ -64,6 +81,7 @@ export function getCursorSessionScopeSnapshot(pi?: object): CursorTurnScope {
64
81
  return Object.freeze({
65
82
  cwd: record.sessionCwd,
66
83
  scopeKey,
84
+ persistentScopeKey: persistentScopeKeyFor(record),
67
85
  sessionFile: record.sessionFile,
68
86
  sessionId: record.sessionId,
69
87
  sessionName: record.sessionName,
@@ -76,11 +94,7 @@ export function hasLiveCursorSessions(): boolean {
76
94
  return scopes.size > 0;
77
95
  }
78
96
 
79
- export function getCursorSessionFile(): string | undefined {
80
- return state.sessionFile;
81
- }
82
-
83
- /** Stable pool key, with process-local anonymous fallback before session_start. */
97
+ /** Runtime ownership key; persisted identities are captured separately. */
84
98
  export function getCursorSessionScopeKey(): string {
85
99
  return scopeKeyFor(state);
86
100
  }
@@ -112,6 +126,7 @@ function setCursorSessionScope(
112
126
  sessionId?: string,
113
127
  projectTrusted = false,
114
128
  sessionName?: string,
129
+ runtimeOwner?: number,
115
130
  ): void {
116
131
  state.sessionCwd = cwd;
117
132
  state.sessionFile = sessionFile;
@@ -119,6 +134,7 @@ function setCursorSessionScope(
119
134
  state.sessionName = normalizeCursorSessionName(sessionName);
120
135
  state.projectTrusted = projectTrusted;
121
136
  state.sessionGeneration = nextSessionGeneration++;
137
+ state.runtimeOwner = runtimeOwner;
122
138
  scopeGenerations.set(scopeKeyFor(state), state.sessionGeneration);
123
139
  }
124
140
 
@@ -134,6 +150,7 @@ function resetCursorSessionScope(): void {
134
150
  Object.assign(state, {
135
151
  sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
136
152
  sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
153
+ runtimeOwner: undefined,
137
154
  });
138
155
  nextSessionGeneration = 1;
139
156
  scopeGenerations.clear();
@@ -141,6 +158,8 @@ function resetCursorSessionScope(): void {
141
158
  projectTrustResolutionCwds.clear();
142
159
  scopes.clear();
143
160
  scopeChangeHandlers.clear();
161
+ runtimeOwners = new WeakMap();
162
+ nextRuntimeOwner = 1;
144
163
  }
145
164
 
146
165
  export function onCursorSessionScopeKeyChange(pi: object, handler: CursorSessionScopeChangeHandler): void {
@@ -152,6 +171,7 @@ export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi):
152
171
  const record: ScopeRecord = {
153
172
  sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
154
173
  sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
174
+ runtimeOwner: undefined,
155
175
  };
156
176
  scopes.set(pi, record);
157
177
  const trustedCwds = new Set<string>();
@@ -162,6 +182,7 @@ export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi):
162
182
  pi.on("session_start", async (_event, ctx) => {
163
183
  const previousScopeKey = scopeKeyFor(record);
164
184
  const wasBound = record.sessionGeneration > 0;
185
+ if (!runtimeOwners.has(ctx.sessionManager)) runtimeOwners.set(ctx.sessionManager, nextRuntimeOwner++);
165
186
  setCursorSessionScope(
166
187
  ctx.cwd,
167
188
  ctx.sessionManager?.getSessionFile?.() ?? undefined,
@@ -169,6 +190,7 @@ export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi):
169
190
  ctx.isProjectTrusted?.() === true
170
191
  && (trustedCwds.has(resolve(ctx.cwd)) || projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()),
171
192
  ctx.sessionManager?.getSessionName?.() ?? undefined,
193
+ runtimeOwners.get(ctx.sessionManager),
172
194
  );
173
195
  Object.assign(record, state);
174
196
  scopes.set(pi, record);
@@ -1,7 +1,7 @@
1
1
  import type { AgentModeOption } from "@cursor/sdk";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { CursorExplicitSdkConfig, CursorRuntime } from "./cursor-config.js";
4
- import { cursorSessionScopeKeyForManager, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
4
+ import { cursorRuntimeSessionScopeKeyForManager, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
5
5
 
6
6
  export type CursorCliModeState =
7
7
  | { kind: "unset" }
@@ -45,7 +45,7 @@ export function getCursorSessionSettings(scopeKey?: string) {
45
45
 
46
46
  export function cursorSettingsScopeForContext(ctx: Partial<Pick<ExtensionContext, "sessionManager">>): string | undefined {
47
47
  const manager = ctx.sessionManager;
48
- return manager ? cursorSessionScopeKeyForManager(manager) : undefined;
48
+ return manager ? cursorRuntimeSessionScopeKeyForManager(manager) : undefined;
49
49
  }
50
50
 
51
51
  export function registerCursorSessionSettings(pi: Pick<ExtensionAPI, "on">): void {
@@ -53,7 +53,7 @@ export function registerCursorSessionSettings(pi: Pick<ExtensionAPI, "on">): voi
53
53
  let cli = createCliSettings();
54
54
  pi.on("session_start", (_event, ctx) => {
55
55
  if (scopeKey) settings.delete(scopeKey);
56
- scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager);
56
+ scopeKey = cursorRuntimeSessionScopeKeyForManager(ctx.sessionManager);
57
57
  legacySettings = getCursorSessionSettings(scopeKey);
58
58
  // Reload creates a new ExtensionAPI but retains the same public manager.
59
59
  // Keep the run's one-shot CLI consumption without retaining dead managers.
@@ -194,7 +194,7 @@ function workspaceHashes(cwd: string): { current: string; legacy: string | undef
194
194
  }
195
195
 
196
196
  function assertSafeWorkspaceLayout(cwd: string): void {
197
- // ponytail: SDK 1.0.36 has no public read-only root resolver; use one when exposed.
197
+ // ponytail: SDK 1.0.37 has no public read-only root resolver; use one when exposed.
198
198
  // This pure layout is contract-verified against its factory and public getter;
199
199
  // guard it before that getter can rename MD5 history through an owned link.
200
200
  const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -1,7 +1,7 @@
1
1
  import type { AgentModeOption } from "@cursor/sdk";
2
2
  import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
- buildCursorToolManifestText,
4
+ CURSOR_HOST_TOOL_MANIFEST_SUMMARY,
5
5
  CURSOR_TOOL_MANIFEST_ENV,
6
6
  resolveCursorToolManifestEnabled,
7
7
  } from "./cursor-tool-manifest.js";
@@ -17,8 +17,10 @@ import {
17
17
  import {
18
18
  buildCursorPiToolBridgeSnapshot,
19
19
  CURSOR_PI_TOOL_BRIDGE_ENV,
20
+ resolveCursorPiToolBridgeBuiltinsEnabled,
20
21
  resolveCursorPiToolBridgeEnabled,
21
22
  } from "./cursor-pi-tool-bridge-snapshot.js";
23
+ import { CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV } from "./cursor-pi-tool-bridge-diagnostics.js";
22
24
  import {
23
25
  CURSOR_SETTING_SOURCES_ENV,
24
26
  DEFAULT_CURSOR_SETTING_SOURCES,
@@ -388,7 +390,7 @@ export function formatCursorToolsDebugReport(
388
390
  const bridgeEnabled = resolveCursorPiToolBridgeEnabled(env);
389
391
  const manifestEnabled = resolveCursorToolManifestEnabled(env);
390
392
  const lines = [
391
- "Cursor tool surfaces (current session):",
393
+ "Cursor tool surfaces (current registry snapshot; not a live run):",
392
394
  `${CURSOR_PI_TOOL_BRIDGE_ENV}: ${bridgeEnabled ? "enabled" : "disabled"}`,
393
395
  `${CURSOR_TOOL_MANIFEST_ENV}: ${manifestEnabled ? "enabled" : "disabled"}`,
394
396
  `${CURSOR_SETTING_SOURCES_ENV}: ${formatEffectiveCursorSettingSourcesLabel(env[CURSOR_SETTING_SOURCES_ENV])}`,
@@ -397,13 +399,31 @@ export function formatCursorToolsDebugReport(
397
399
  let bridgeSnapshot;
398
400
  if (bridgeEnabled) {
399
401
  try {
400
- bridgeSnapshot = buildCursorPiToolBridgeSnapshot(pi);
402
+ bridgeSnapshot = buildCursorPiToolBridgeSnapshot(pi, {
403
+ exposeOverlappingBuiltins: resolveCursorPiToolBridgeBuiltinsEnabled(env),
404
+ });
401
405
  } catch {
402
406
  lines.push("Pi bridge snapshot: unavailable (extension tool APIs required).");
403
407
  }
404
408
  }
405
409
 
406
- lines.push(buildCursorToolManifestText({ bridgeSnapshot, piBridgeEnabled: bridgeEnabled }));
410
+ lines.push(
411
+ `Cursor host/MCP: ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}; configured MCP depends on Cursor settings.`,
412
+ "Pi tool toggles affect pi tools/bridge exposure only; they do not disable Cursor host/configured MCP tools.",
413
+ );
414
+ if (!bridgeEnabled) {
415
+ lines.push("Pi bridge: disabled (PI_CURSOR_PI_TOOL_BRIDGE=0).");
416
+ } else if (bridgeSnapshot) {
417
+ const names = bridgeSnapshot.tools.map((tool) => tool.mcpToolName).sort();
418
+ lines.push(`Pi bridge exposure eligibility: ${names.length ? names.join(", ") : "no eligible pi__* tools"}.`);
419
+ }
420
+ lines.push(
421
+ "This snapshot does not prove a run's endpoint attachment, MCP initialization, catalog fetch, or tool calls.",
422
+ `For per-run observations, enable ${CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV}=1 before the turn.`,
423
+ "run_created/tools_exposed record exposure only; mcp_initialized records a validated notifications/initialized receipt; mcp_tools_list records catalog-handler output (toolCount), not client/model consumption or ongoing connection health.",
424
+ "request_queued/resolved/rejected record actual bridge call handling; absent receipts do not diagnose why a client/model made no request.",
425
+ "Not callable: cursor-replay-* IDs, pi history names, transcript labels.",
426
+ );
407
427
  return lines.join("\n");
408
428
  }
409
429
 
@@ -496,7 +516,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
496
516
  });
497
517
 
498
518
  pi.registerCommand("cursor-tools", {
499
- description: "Show live Cursor tool surfaces for this session (maintainer debug)",
519
+ description: "Show current-registry Cursor tool exposure eligibility (maintainer debug)",
500
520
  handler: async (_args, ctx) => {
501
521
  emitCursorToolsDebugReport(pi, ctx);
502
522
  },