pi-cursor-sdk 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +62 -795
  3. package/dist/context.js +13 -7
  4. package/dist/cursor-agents-context.js +6 -6
  5. package/dist/cursor-api-key.js +1 -1
  6. package/dist/cursor-bridge-contract.js +1 -13
  7. package/dist/cursor-cloud-local-state.js +0 -1
  8. package/dist/cursor-config.js +1 -17
  9. package/dist/cursor-custom-subagent-definitions.js +1 -1
  10. package/dist/cursor-fallback-models.generated.js +665 -1
  11. package/dist/cursor-live-run-coordinator.js +1 -0
  12. package/dist/cursor-local-agent-ownership.js +17 -0
  13. package/dist/cursor-mcp-timeout-override.js +7 -18
  14. package/dist/cursor-native-tool-display-replay.js +5 -5
  15. package/dist/cursor-native-tools.js +11 -0
  16. package/dist/cursor-pi-tool-bridge-diagnostics.js +4 -0
  17. package/dist/cursor-pi-tool-bridge-env.js +25 -2
  18. package/dist/cursor-pi-tool-bridge-run.js +10 -3
  19. package/dist/cursor-pi-tool-bridge-server.js +98 -48
  20. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  21. package/dist/cursor-pi-tool-bridge.js +4 -12
  22. package/dist/cursor-provider-errors.js +2 -8
  23. package/dist/cursor-provider-live-run-drain.js +8 -3
  24. package/dist/cursor-provider-overflow.js +7 -21
  25. package/dist/cursor-provider-turn-prepare.js +8 -1
  26. package/dist/cursor-provider-turn-runner.js +17 -6
  27. package/dist/cursor-provider-turn-summary.js +1 -1
  28. package/dist/cursor-provider.js +1 -4
  29. package/dist/cursor-runtime-state.js +3 -3
  30. package/dist/cursor-sdk-event-debug.js +2 -2
  31. package/dist/cursor-sdk-output-filter.js +1 -1
  32. package/dist/cursor-sdk-process-error-guard.js +1 -2
  33. package/dist/cursor-session-agent-cleanup.js +10 -1
  34. package/dist/cursor-session-agent-lifecycle.js +2 -2
  35. package/dist/cursor-session-agent-lineage.js +11 -7
  36. package/dist/cursor-session-agent-resume.js +15 -9
  37. package/dist/cursor-session-agent.js +28 -15
  38. package/dist/cursor-session-scope.js +28 -21
  39. package/dist/cursor-session-settings.js +3 -3
  40. package/dist/cursor-session-store.js +2 -2
  41. package/dist/cursor-state.js +26 -29
  42. package/dist/cursor-tool-lifecycle.js +0 -3
  43. package/dist/cursor-tool-manifest.js +3 -1
  44. package/dist/cursor-tool-presentation-registry.js +2 -4
  45. package/dist/cursor-tool-visibility.js +0 -3
  46. package/dist/cursor-transcript-tool-specs.js +1 -2
  47. package/dist/index.js +21 -0
  48. package/dist/model-discovery.js +8 -10
  49. package/dist/model-list-cache.js +4 -9
  50. package/docs/cursor-live-smoke-checklist.md +4 -4
  51. package/docs/cursor-model-ux-spec.md +36 -19
  52. package/docs/cursor-native-tool-replay.md +8 -5
  53. package/docs/cursor-native-tool-visual-audit.md +1 -1
  54. package/docs/cursor-testing-lessons.md +39 -9
  55. package/docs/cursor-tool-surfaces.md +48 -4
  56. package/docs/development.md +93 -0
  57. package/docs/platform-smoke-implementation.md +3 -2
  58. package/docs/reference.md +806 -0
  59. package/package.json +13 -11
  60. package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
  61. package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
  62. package/scripts/platform-smoke/target-runtime.mjs +4 -2
  63. package/scripts/platform-smoke/targets.mjs +0 -1
  64. package/shared/cursor-sdk-output-filter.d.mts +2 -0
  65. package/shared/cursor-sdk-output-filter.mjs +92 -28
  66. package/shared/cursor-sensitive-text.d.mts +1 -0
  67. package/shared/cursor-sensitive-text.mjs +7 -0
  68. package/src/context.ts +18 -11
  69. package/src/cursor-agents-context.ts +6 -6
  70. package/src/cursor-api-key.ts +1 -1
  71. package/src/cursor-bridge-contract.ts +1 -15
  72. package/src/cursor-cloud-local-state.ts +0 -2
  73. package/src/cursor-config.ts +1 -24
  74. package/src/cursor-custom-subagent-definitions.ts +1 -1
  75. package/src/cursor-fallback-models.generated.ts +665 -1
  76. package/src/cursor-live-run-coordinator.ts +5 -0
  77. package/src/cursor-local-agent-ownership.ts +18 -0
  78. package/src/cursor-mcp-timeout-override.ts +7 -31
  79. package/src/cursor-native-tool-display-replay.ts +4 -6
  80. package/src/cursor-native-tools.ts +14 -0
  81. package/src/cursor-pi-tool-bridge-diagnostics.ts +17 -0
  82. package/src/cursor-pi-tool-bridge-env.ts +31 -2
  83. package/src/cursor-pi-tool-bridge-run.ts +10 -3
  84. package/src/cursor-pi-tool-bridge-server.ts +89 -45
  85. package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
  86. package/src/cursor-pi-tool-bridge.ts +2 -18
  87. package/src/cursor-provider-errors.ts +2 -10
  88. package/src/cursor-provider-live-run-drain.ts +8 -2
  89. package/src/cursor-provider-overflow.ts +6 -24
  90. package/src/cursor-provider-turn-prepare.ts +10 -1
  91. package/src/cursor-provider-turn-runner.ts +17 -6
  92. package/src/cursor-provider-turn-summary.ts +1 -1
  93. package/src/cursor-provider.ts +0 -5
  94. package/src/cursor-runtime-state.ts +3 -3
  95. package/src/cursor-sdk-event-debug.ts +2 -2
  96. package/src/cursor-sdk-output-filter.ts +2 -1
  97. package/src/cursor-sdk-process-error-guard.ts +1 -3
  98. package/src/cursor-session-agent-cleanup.ts +6 -1
  99. package/src/cursor-session-agent-lifecycle.ts +2 -2
  100. package/src/cursor-session-agent-lineage.ts +10 -5
  101. package/src/cursor-session-agent-resume.ts +14 -7
  102. package/src/cursor-session-agent.ts +31 -15
  103. package/src/cursor-session-scope.ts +34 -20
  104. package/src/cursor-session-settings.ts +3 -3
  105. package/src/cursor-session-store.ts +2 -2
  106. package/src/cursor-state.ts +34 -41
  107. package/src/cursor-tool-lifecycle.ts +0 -4
  108. package/src/cursor-tool-manifest.ts +4 -1
  109. package/src/cursor-tool-presentation-registry.ts +3 -5
  110. package/src/cursor-tool-visibility.ts +0 -4
  111. package/src/cursor-transcript-tool-specs.ts +0 -3
  112. package/src/index.ts +18 -0
  113. package/src/model-discovery.ts +7 -9
  114. package/src/model-list-cache.ts +4 -10
@@ -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 {
@@ -33,7 +33,7 @@ export async function prepareCursorSummaryProviderTurn(prepareParams, selection)
33
33
  configureCursorSdkHttp1(sdk, resolvedConfig.local.useHttp1ForAgent);
34
34
  restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
35
35
  sessionStore = (await openCursorSessionStoreForScope({
36
- cwd, scopeKey: params.scope.scopeKey, persistent: false,
36
+ cwd, scopeKey: params.scope.persistentScopeKey ?? params.scope.scopeKey, persistent: false,
37
37
  })).sessionStore;
38
38
  throwIfAborted();
39
39
  agent = await suppressCursorSdkOutput(() => sdk.Agent.create({
@@ -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);
@@ -3,6 +3,7 @@ import { fsyncExistingRegularFile } from "./cursor-durable-fs.js";
3
3
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
4
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
5
5
  import { cursorSessionScopeKeyForManager } from "./cursor-session-scope.js";
6
+ import { claimCursorLocalAgent } from "./cursor-local-agent-ownership.js";
6
7
  import { CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE, isCursorLocalAgentId, parseCursorSessionAgentResumeEntryData, readResumableCursorSessionAgentIds, resolveCursorSessionRepoRoot, } from "./cursor-session-agent-resume.js";
7
8
  import { withCursorSessionStoreIdentities, openCursorSessionStore, resolveCursorSessionStoreIdentity, } from "./cursor-session-store.js";
8
9
  export const CURSOR_SESSION_AGENT_CLEANUP_ENTRY_TYPE = "cursor-sdk-agent-cleanup";
@@ -260,7 +261,15 @@ export async function runCursorSessionAgentCleanupCommand(pi, args, ctx, storeRo
260
261
  openedStore = await openCursorSessionStore(ctx.cwd, identity, identities.workspaceRoot, identities.storeRoot);
261
262
  openedStores.set(identity.stateRoot, openedStore);
262
263
  }
263
- await operations.delete(agentId, { cwd: ctx.cwd, store: openedStore.store });
264
+ const release = claimCursorLocalAgent(identity, agentId);
265
+ if (!release)
266
+ throw new Error("Cursor local agent is still owned by an active runtime");
267
+ try {
268
+ await operations.delete(agentId, { cwd: ctx.cwd, store: openedStore.store });
269
+ }
270
+ finally {
271
+ release();
272
+ }
264
273
  deletedAgentIds.push(agentId);
265
274
  }
266
275
  catch (error) {
@@ -1,9 +1,9 @@
1
1
  import { clearCursorSdkHttp1 } from "./cursor-http1.js";
2
- import { cursorSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
2
+ import { cursorRuntimeSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
3
3
  import { disposeSessionCursorAgent, invalidateSessionAgent, resetSessionCursorAgent, } from "./cursor-session-agent.js";
4
4
  export function registerCursorSessionAgentLifecycle(pi) {
5
5
  let scopeKey;
6
- pi.on("session_start", (_event, ctx) => { scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager); });
6
+ pi.on("session_start", (_event, ctx) => { scopeKey = cursorRuntimeSessionScopeKeyForManager(ctx.sessionManager); });
7
7
  onCursorSessionScopeKeyChange(pi, async (previousScopeKey) => {
8
8
  await disposeSessionCursorAgent(previousScopeKey);
9
9
  });
@@ -52,7 +52,7 @@ function readRecordedAgentIds(entries, sessionId) {
52
52
  export function recordCursorSessionAgentLineage(agentId, turnScopeKey) {
53
53
  const sessionState = turnScopeKey === undefined
54
54
  ? state
55
- : statesByScope.get(turnScopeKey) ?? (state.scopeKey === turnScopeKey ? state : undefined);
55
+ : statesByScope.get(turnScopeKey) ?? ((state.runtimeScopeKey ?? state.scopeKey) === turnScopeKey ? state : undefined);
56
56
  if (!sessionState)
57
57
  return;
58
58
  const { appendEntry, sessionId, sessionFile, scopeKey, cwd } = sessionState;
@@ -83,24 +83,27 @@ export function registerCursorSessionAgentLineage(pi) {
83
83
  pi.on("session_start", (_event, ctx) => {
84
84
  state = sessionState;
85
85
  const record = sessionState;
86
- if (record.scopeKey && statesByScope.get(record.scopeKey) === record)
87
- statesByScope.delete(record.scopeKey);
86
+ if (record.runtimeScopeKey && statesByScope.get(record.runtimeScopeKey) === record)
87
+ statesByScope.delete(record.runtimeScopeKey);
88
88
  record.appendEntry = pi.appendEntry;
89
89
  record.sessionId = ctx.sessionManager.getSessionId();
90
90
  record.sessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
91
- record.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
92
- statesByScope.set(record.scopeKey, record);
91
+ const scope = getCursorSessionScopeSnapshot(pi);
92
+ record.runtimeScopeKey = scope.scopeKey;
93
+ record.scopeKey = scope.persistentScopeKey ?? scope.scopeKey;
94
+ statesByScope.set(scope.scopeKey, record);
93
95
  record.cwd = ctx.cwd;
94
96
  record.recordedAgentIds = readRecordedAgentIds(ctx.sessionManager.getEntries(), record.sessionId);
95
97
  });
96
98
  pi.on("session_shutdown", () => {
97
99
  const state = sessionState;
98
- if (state.scopeKey && statesByScope.get(state.scopeKey) === state)
99
- statesByScope.delete(state.scopeKey);
100
+ if (state.runtimeScopeKey && statesByScope.get(state.runtimeScopeKey) === state)
101
+ statesByScope.delete(state.runtimeScopeKey);
100
102
  state.appendEntry = undefined;
101
103
  state.sessionId = undefined;
102
104
  state.sessionFile = undefined;
103
105
  state.scopeKey = undefined;
106
+ state.runtimeScopeKey = undefined;
104
107
  state.cwd = undefined;
105
108
  state.recordedAgentIds = new Set();
106
109
  });
@@ -111,6 +114,7 @@ function resetStateForTests() {
111
114
  state.sessionId = undefined;
112
115
  state.sessionFile = undefined;
113
116
  state.scopeKey = undefined;
117
+ state.runtimeScopeKey = undefined;
114
118
  state.cwd = undefined;
115
119
  state.recordedAgentIds = new Set();
116
120
  }
@@ -266,7 +266,7 @@ function restoreFromBranch(branch, allEntries, state) {
266
266
  }
267
267
  export function getMatchingCursorSessionAgentResumeHandle(poolKey, scopeKey) {
268
268
  const state = getResumeState(scopeKey);
269
- if (scopeKey !== undefined && state.scopeKey !== scopeKey)
269
+ if (scopeKey !== undefined && (state.runtimeScopeKey ?? state.scopeKey) !== scopeKey)
270
270
  return undefined;
271
271
  const handle = state.activeHandle;
272
272
  if (!handle || !isCursorLocalAgentId(handle.agentId))
@@ -292,7 +292,7 @@ export function getMatchingCursorSessionAgentResumeHandle(poolKey, scopeKey) {
292
292
  }
293
293
  export function persistCursorSessionAgentResumeHandle(input, scopeKey) {
294
294
  const state = getResumeState(scopeKey);
295
- if (scopeKey !== undefined && state.scopeKey !== scopeKey)
295
+ if (scopeKey !== undefined && (state.runtimeScopeKey ?? state.scopeKey) !== scopeKey)
296
296
  return;
297
297
  if (!isCursorLocalAgentId(input.agentId))
298
298
  return;
@@ -343,8 +343,9 @@ function flushPendingCursorSessionAgentResumeHandle(branch, state) {
343
343
  }
344
344
  }
345
345
  export function registerCursorSessionAgentResume(pi) {
346
+ const scope = getCursorSessionScopeSnapshot(pi);
346
347
  const sessionState = {
347
- appendEntry: pi.appendEntry, scopeKey: getCursorSessionScopeSnapshot(pi).scopeKey,
348
+ appendEntry: pi.appendEntry, scopeKey: scope.persistentScopeKey ?? scope.scopeKey,
348
349
  cwd: process.cwd(), branchPathHash: EMPTY_BRANCH_HASH,
349
350
  compactionGeneration: 0, unownedUserEntryIds: new Set(),
350
351
  };
@@ -355,10 +356,13 @@ export function registerCursorSessionAgentResume(pi) {
355
356
  restoreFromBranch(branch, entries.length > 0 ? entries : branch, state);
356
357
  };
357
358
  pi.on("session_start", (_event, ctx) => {
358
- if (statesByScope.get(state.scopeKey) === state)
359
- statesByScope.delete(state.scopeKey);
360
- state.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
361
- statesByScope.set(state.scopeKey, state);
359
+ const key = state.runtimeScopeKey ?? state.scopeKey;
360
+ if (statesByScope.get(key) === state)
361
+ statesByScope.delete(key);
362
+ const scope = getCursorSessionScopeSnapshot(pi);
363
+ state.runtimeScopeKey = scope.scopeKey;
364
+ state.scopeKey = scope.persistentScopeKey ?? scope.scopeKey;
365
+ statesByScope.set(scope.scopeKey, state);
362
366
  lastBoundState = state;
363
367
  state.sessionFile = ctx.sessionManager.getSessionFile?.() ?? undefined;
364
368
  state.sessionId = ctx.sessionManager.getSessionId?.() ?? undefined;
@@ -399,8 +403,9 @@ export function registerCursorSessionAgentResume(pi) {
399
403
  state.branchPathHash = hashBranchStep(state.branchPathHash, event.compactionEntry);
400
404
  });
401
405
  pi.on("session_shutdown", () => {
402
- if (statesByScope.get(state.scopeKey) === state)
403
- statesByScope.delete(state.scopeKey);
406
+ const key = state.runtimeScopeKey ?? state.scopeKey;
407
+ if (statesByScope.get(key) === state)
408
+ statesByScope.delete(key);
404
409
  state.appendEntry = undefined;
405
410
  state.pendingHandle = undefined;
406
411
  });
@@ -412,6 +417,7 @@ function resetStateForTests() {
412
417
  const state = lastBoundState;
413
418
  state.appendEntry = undefined;
414
419
  state.scopeKey = getCursorSessionScopeKey();
420
+ state.runtimeScopeKey = undefined;
415
421
  state.sessionFile = undefined;
416
422
  state.sessionId = undefined;
417
423
  state.cwd = process.cwd();
@@ -1,8 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { normalizeCursorCustomStoreRoot } from "./cursor-custom-store-path.js";
3
+ import { claimCursorLocalAgent } from "./cursor-local-agent-ownership.js";
3
4
  import { getRegisteredCursorPiToolBridge, } from "./cursor-pi-tool-bridge.js";
4
5
  import { computeCursorContextFingerprint } from "./context.js";
5
- import { getCursorSessionFile, getCursorSessionScopeGeneration, getCursorSessionScopeKey } from "./cursor-session-scope.js";
6
+ import { getCursorSessionScopeSnapshot, getCursorSessionScopeGeneration, getCursorSessionScopeKey } from "./cursor-session-scope.js";
6
7
  import { getMatchingCursorSessionAgentResumeHandle, persistCursorSessionAgentResumeHandle, } from "./cursor-session-agent-resume.js";
7
8
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
8
9
  import { cursorSessionStoreIdentitiesEqual, openCursorSessionStore, openCursorSessionStoreForScope, } from "./cursor-session-store.js";
@@ -41,7 +42,7 @@ function rethrowSupersededWhenReplacedByDifferentPoolIdentity(scopeKey, poolKey,
41
42
  const sessionAgentsByScope = new Map();
42
43
  const invalidatedScopeKeys = new Set();
43
44
  const deadTransportScopeKeys = new Set();
44
- let deadTransportAgentDisposeTimeoutMs = 3000;
45
+ const DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS = 3000;
45
46
  const terminalDisposedScopeGenerations = new Map();
46
47
  const scopeCreationGenerations = new Map();
47
48
  const EMPTY_POOL_STATE = { status: "empty" };
@@ -107,7 +108,7 @@ function buildCustomSubagentPoolKeySuffix(agents) {
107
108
  }
108
109
  function buildSessionAgentPoolKey(scopeKey, params) {
109
110
  return [
110
- scopeKey,
111
+ params.scope?.persistentScopeKey ?? scopeKey,
111
112
  params.cwd,
112
113
  buildModelPoolKey(params.modelSelection),
113
114
  buildSettingSourcesPoolKey(params.settingSources),
@@ -120,6 +121,8 @@ function buildSessionAgentPoolKey(scopeKey, params) {
120
121
  buildApiKeyPoolKeyFingerprint(params.apiKey),
121
122
  buildBridgePoolKeySuffix("bridge" in params ? params.bridge : getRegisteredCursorPiToolBridge()),
122
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"] : []),
123
126
  ].join("\0");
124
127
  }
125
128
  async function disposePoolEntry(entry, options) {
@@ -132,14 +135,15 @@ async function disposePoolEntry(entry, options) {
132
135
  catch {
133
136
  // disposal failure should not block session replacement
134
137
  }
138
+ let disposal;
135
139
  try {
136
- const disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]()).catch(() => undefined);
140
+ disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]());
137
141
  // A dead local transport may never settle SDK disposal; bound the wait so the
138
142
  // next acquire recreates instead of hanging on the dead agent.
139
143
  await (options?.deadTransport
140
144
  ? Promise.race([
141
145
  disposal,
142
- new Promise((resolve) => setTimeout(resolve, deadTransportAgentDisposeTimeoutMs).unref?.()),
146
+ new Promise((resolve) => setTimeout(resolve, DEAD_TRANSPORT_AGENT_DISPOSE_TIMEOUT_MS).unref?.()),
143
147
  ])
144
148
  : disposal);
145
149
  }
@@ -147,6 +151,8 @@ async function disposePoolEntry(entry, options) {
147
151
  // disposal failure should not block session replacement
148
152
  }
149
153
  await entry.sessionStore.dispose().catch(() => undefined);
154
+ // Timeout/rejection is not proof of closure; only successful SDK disposal releases ownership.
155
+ void disposal?.then(entry.releaseAgentClaim, () => { });
150
156
  }
151
157
  async function disposePoolEntryForScope(scopeKey, options) {
152
158
  invalidateScopeCreations(scopeKey);
@@ -296,6 +302,7 @@ async function tryLeaseReadyEntry(entry, scopeKey, params, poolKey, created) {
296
302
  async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, sendState, params) {
297
303
  let bridgeRun;
298
304
  let sessionStore;
305
+ let releaseAgentClaim;
299
306
  try {
300
307
  const registeredBridge = params.bridge;
301
308
  if (registeredBridge) {
@@ -320,7 +327,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
320
327
  const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey, scopeKey) : undefined;
321
328
  const storeSelection = await openCursorSessionStoreForScope({
322
329
  cwd: params.cwd,
323
- scopeKey,
330
+ scopeKey: params.scope?.persistentScopeKey ?? scopeKey,
324
331
  persistent: persistentStore,
325
332
  storeRoot: params.storeRoot,
326
333
  resume: resumeHandle ? { identity: resumeHandle.storeIdentity, agentId: resumeHandle.agentId } : undefined,
@@ -338,6 +345,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
338
345
  localSafety: params.localSafety,
339
346
  store: sessionStore.store,
340
347
  }),
348
+ ...(params.nativeToolsEnabled === false ? { tools: ["mcp"] } : {}),
341
349
  ...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
342
350
  ...(params.agents && Object.keys(params.agents).length ? { agents: params.agents } : {}),
343
351
  });
@@ -345,12 +353,17 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
345
353
  let effectiveSendState = sendState;
346
354
  let resumed = false;
347
355
  if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
356
+ releaseAgentClaim = claimCursorLocalAgent(sessionStore.identity, resumeHandle.agentId);
357
+ }
358
+ if (resumeHandle && resumeAttemptAllowed && resumeAgent && releaseAgentClaim) {
348
359
  try {
349
360
  agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
350
361
  effectiveSendState = { ...resumeHandle.sendState };
351
362
  resumed = true;
352
363
  }
353
364
  catch {
365
+ releaseAgentClaim();
366
+ releaseAgentClaim = undefined;
354
367
  if (persistentStore)
355
368
  resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
356
369
  if (storeSelection.persistent && !cursorSessionStoreIdentitiesEqual(sessionStore.identity, storeSelection.identities.sessionStore)) {
@@ -366,6 +379,9 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
366
379
  throw new Error("Cursor SDK agent creation returned no agent");
367
380
  if (!sessionStore)
368
381
  throw new Error("Cursor SDK session store was not opened");
382
+ releaseAgentClaim ??= claimCursorLocalAgent(sessionStore.identity, agent.agentId);
383
+ if (!releaseAgentClaim)
384
+ throw new Error("Cursor SDK agent is already owned by another runtime");
369
385
  return {
370
386
  status: "ready",
371
387
  poolKey: resolvedPoolKey,
@@ -378,6 +394,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
378
394
  sendState: effectiveSendState,
379
395
  resumeEnabled: params.localResume === true,
380
396
  resumed,
397
+ releaseAgentClaim,
381
398
  ...(resumeNotice ? { resumeNotice } : {}),
382
399
  };
383
400
  }
@@ -385,19 +402,20 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
385
402
  bridgeRun?.cancel("Cursor session agent create failed");
386
403
  await bridgeRun?.dispose().catch(() => undefined);
387
404
  await sessionStore?.dispose().catch(() => undefined);
405
+ releaseAgentClaim?.();
388
406
  throw error;
389
407
  }
390
408
  }
391
409
  export { buildCursorSessionSendPrompt, MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP, planCursorSessionSend, } from "./cursor-session-send-policy.js";
392
- export function invalidateSessionAgent(scopeKey = getCursorSessionScopeKey(), options) {
410
+ export function invalidateSessionAgent(scopeKey, options) {
393
411
  invalidatedScopeKeys.add(scopeKey);
394
412
  if (options?.deadTransport)
395
413
  deadTransportScopeKeys.add(scopeKey);
396
414
  }
397
415
  export async function acquireSessionCursorAgent(input) {
398
- const params = { ...input, bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
399
- const scopeKey = params.scope?.scopeKey ?? getCursorSessionScopeKey();
400
- const persistentStore = (params.scope ? params.scope.sessionFile : getCursorSessionFile()) !== undefined;
416
+ const params = { ...input, scope: input.scope ?? getCursorSessionScopeSnapshot(), bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
417
+ const scopeKey = params.scope.scopeKey;
418
+ const persistentStore = params.scope.sessionFile !== undefined;
401
419
  params.storeRoot = persistentStore && params.storeRoot !== undefined ? normalizeCursorCustomStoreRoot(params.storeRoot) : undefined;
402
420
  while (true) {
403
421
  assertScopeAcceptsAcquire(scopeKey);
@@ -519,11 +537,6 @@ export const __testUtils = {
519
537
  resetSessionCursorAgent,
520
538
  refreshSessionCursorAgentConfig,
521
539
  disposeAllSessionCursorAgents,
522
- setDeadTransportAgentDisposeTimeoutMs(ms) {
523
- const previous = deadTransportAgentDisposeTimeoutMs;
524
- deadTransportAgentDisposeTimeoutMs = ms;
525
- return previous;
526
- },
527
540
  SessionCursorAgentCreationSupersededError,
528
541
  SessionCursorAgentScopeClosedError,
529
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,
@@ -11,23 +11,31 @@ const state = {
11
11
  sessionName: undefined,
12
12
  projectTrusted: false,
13
13
  sessionGeneration: 0,
14
+ runtimeOwner: undefined,
14
15
  };
15
16
  const scopes = new Map();
16
17
  const scopeChangeHandlers = new Map();
17
18
  const scopeGenerations = new Map([[ANONYMOUS_SESSION_SCOPE_KEY, 0]]);
18
19
  const projectTrustResolutionCwds = new Set();
19
20
  let nextSessionGeneration = 1;
21
+ let nextRuntimeOwner = 1;
22
+ let runtimeOwners = new WeakMap();
23
+ function persistentScopeKeyFor(record) {
24
+ return record.sessionFile ?? (record.sessionId ? `${EPHEMERAL_SESSION_SCOPE_PREFIX}${record.sessionId}` : ANONYMOUS_SESSION_SCOPE_KEY);
25
+ }
20
26
  function scopeKeyFor(record) {
21
- if (record.sessionFile)
22
- return record.sessionFile;
23
- if (record.sessionId)
24
- return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${record.sessionId}`;
25
- return ANONYMOUS_SESSION_SCOPE_KEY;
27
+ const persistent = persistentScopeKeyFor(record);
28
+ return record.runtimeOwner === undefined ? persistent : `${persistent}\0runtime:${record.runtimeOwner}`;
26
29
  }
27
30
  export function cursorSessionScopeKeyForManager(manager) {
28
- return manager.getSessionFile?.() ?? (manager.getSessionId?.()
29
- ? `${EPHEMERAL_SESSION_SCOPE_PREFIX}${manager.getSessionId()}`
30
- : ANONYMOUS_SESSION_SCOPE_KEY);
31
+ return persistentScopeKeyFor({
32
+ sessionFile: manager.getSessionFile?.() ?? undefined, sessionId: manager.getSessionId?.(),
33
+ });
34
+ }
35
+ export function cursorRuntimeSessionScopeKeyForManager(manager) {
36
+ const persistent = cursorSessionScopeKeyForManager(manager);
37
+ const owner = runtimeOwners.get(manager);
38
+ return owner === undefined ? persistent : `${persistent}\0runtime:${owner}`;
31
39
  }
32
40
  export function getCursorSessionScopeSnapshot(pi) {
33
41
  if (!pi && scopes.size > 1)
@@ -39,6 +47,7 @@ export function getCursorSessionScopeSnapshot(pi) {
39
47
  return Object.freeze({
40
48
  cwd: record.sessionCwd,
41
49
  scopeKey,
50
+ persistentScopeKey: persistentScopeKeyFor(record),
42
51
  sessionFile: record.sessionFile,
43
52
  sessionId: record.sessionId,
44
53
  sessionName: record.sessionName,
@@ -49,37 +58,29 @@ export function getCursorSessionScopeSnapshot(pi) {
49
58
  export function hasLiveCursorSessions() {
50
59
  return scopes.size > 0;
51
60
  }
52
- export function getCursorSessionFile() {
53
- return state.sessionFile;
54
- }
55
- /** Stable pool key, with process-local anonymous fallback before session_start. */
61
+ /** Runtime ownership key; persisted identities are captured separately. */
56
62
  export function getCursorSessionScopeKey() {
57
63
  return scopeKeyFor(state);
58
64
  }
59
65
  export function getCursorSessionScopeGeneration(scopeKey = getCursorSessionScopeKey()) {
60
66
  return scopeGenerations.get(scopeKey) ?? 0;
61
67
  }
62
- export function getCursorSessionCwd() {
63
- return state.sessionCwd;
64
- }
65
68
  export function getCursorSessionProjectTrusted(pi) {
66
69
  return pi ? scopes.get(pi)?.projectTrusted === true : state.projectTrusted;
67
70
  }
68
- export function getCursorSessionName() {
69
- return state.sessionName;
70
- }
71
71
  function normalizeCursorSessionName(name) {
72
72
  if (name === undefined)
73
73
  return undefined;
74
74
  return truncateCursorDisplayLine(name, MAX_CURSOR_SESSION_NAME_LENGTH) || undefined;
75
75
  }
76
- function setCursorSessionScope(cwd, sessionFile, sessionId, projectTrusted = false, sessionName) {
76
+ function setCursorSessionScope(cwd, sessionFile, sessionId, projectTrusted = false, sessionName, runtimeOwner) {
77
77
  state.sessionCwd = cwd;
78
78
  state.sessionFile = sessionFile;
79
79
  state.sessionId = sessionId;
80
80
  state.sessionName = normalizeCursorSessionName(sessionName);
81
81
  state.projectTrusted = projectTrusted;
82
82
  state.sessionGeneration = nextSessionGeneration++;
83
+ state.runtimeOwner = runtimeOwner;
83
84
  scopeGenerations.set(scopeKeyFor(state), state.sessionGeneration);
84
85
  }
85
86
  function recordProjectTrustResolution(cwd) {
@@ -92,6 +93,7 @@ function resetCursorSessionScope() {
92
93
  Object.assign(state, {
93
94
  sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
94
95
  sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
96
+ runtimeOwner: undefined,
95
97
  });
96
98
  nextSessionGeneration = 1;
97
99
  scopeGenerations.clear();
@@ -99,6 +101,8 @@ function resetCursorSessionScope() {
99
101
  projectTrustResolutionCwds.clear();
100
102
  scopes.clear();
101
103
  scopeChangeHandlers.clear();
104
+ runtimeOwners = new WeakMap();
105
+ nextRuntimeOwner = 1;
102
106
  }
103
107
  export function onCursorSessionScopeKeyChange(pi, handler) {
104
108
  scopeChangeHandlers.set(pi, handler);
@@ -108,6 +112,7 @@ export function registerCursorSessionScope(pi) {
108
112
  const record = {
109
113
  sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
110
114
  sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
115
+ runtimeOwner: undefined,
111
116
  };
112
117
  scopes.set(pi, record);
113
118
  const trustedCwds = new Set();
@@ -118,8 +123,10 @@ export function registerCursorSessionScope(pi) {
118
123
  pi.on("session_start", async (_event, ctx) => {
119
124
  const previousScopeKey = scopeKeyFor(record);
120
125
  const wasBound = record.sessionGeneration > 0;
126
+ if (!runtimeOwners.has(ctx.sessionManager))
127
+ runtimeOwners.set(ctx.sessionManager, nextRuntimeOwner++);
121
128
  setCursorSessionScope(ctx.cwd, ctx.sessionManager?.getSessionFile?.() ?? undefined, ctx.sessionManager?.getSessionId?.() ?? undefined, ctx.isProjectTrusted?.() === true
122
- && (trustedCwds.has(resolve(ctx.cwd)) || projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined);
129
+ && (trustedCwds.has(resolve(ctx.cwd)) || projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined, runtimeOwners.get(ctx.sessionManager));
123
130
  Object.assign(record, state);
124
131
  scopes.set(pi, record);
125
132
  if (wasBound && previousScopeKey !== scopeKeyFor(record)) {
@@ -1,4 +1,4 @@
1
- import { cursorSessionScopeKeyForManager, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
1
+ import { cursorRuntimeSessionScopeKeyForManager, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
2
2
  function createCliSettings() {
3
3
  return {
4
4
  forceFast: false, forceNoFast: false,
@@ -34,7 +34,7 @@ export function getCursorSessionSettings(scopeKey) {
34
34
  }
35
35
  export function cursorSettingsScopeForContext(ctx) {
36
36
  const manager = ctx.sessionManager;
37
- return manager ? cursorSessionScopeKeyForManager(manager) : undefined;
37
+ return manager ? cursorRuntimeSessionScopeKeyForManager(manager) : undefined;
38
38
  }
39
39
  export function registerCursorSessionSettings(pi) {
40
40
  let scopeKey;
@@ -42,7 +42,7 @@ export function registerCursorSessionSettings(pi) {
42
42
  pi.on("session_start", (_event, ctx) => {
43
43
  if (scopeKey)
44
44
  settings.delete(scopeKey);
45
- scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager);
45
+ scopeKey = cursorRuntimeSessionScopeKeyForManager(ctx.sessionManager);
46
46
  legacySettings = getCursorSessionSettings(scopeKey);
47
47
  // Reload creates a new ExtensionAPI but retains the same public manager.
48
48
  // Keep the run's one-shot CLI consumption without retaining dead managers.
@@ -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)
@@ -143,7 +143,7 @@ function workspaceHashes(cwd) {
143
143
  }
144
144
  }
145
145
  function assertSafeWorkspaceLayout(cwd) {
146
- // ponytail: SDK 1.0.36 has no public read-only root resolver; use one when exposed.
146
+ // ponytail: SDK 1.0.37 has no public read-only root resolver; use one when exposed.
147
147
  // This pure layout is contract-verified against its factory and public getter;
148
148
  // guard it before that getter can rename MD5 history through an owned link.
149
149
  const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");