pi-cursor-sdk 0.2.0 → 0.3.7

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 (177) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +55 -51
  3. package/dist/bundled-context-windows.js +39 -0
  4. package/dist/context-window-cache.js +92 -0
  5. package/dist/context.js +353 -0
  6. package/dist/cursor-active-tools.js +3 -0
  7. package/dist/cursor-agent-message-web-tools.js +165 -0
  8. package/dist/cursor-agents-context-registration.js +17 -0
  9. package/dist/cursor-agents-context.js +127 -0
  10. package/dist/cursor-api-key.js +34 -0
  11. package/dist/cursor-bridge-contract.js +27 -0
  12. package/dist/cursor-cloud-lifecycle.js +650 -0
  13. package/dist/cursor-cloud-local-state.js +460 -0
  14. package/dist/cursor-cloud-options.js +145 -0
  15. package/dist/cursor-cloud-reporting.js +222 -0
  16. package/dist/cursor-compact-tool-summary.js +75 -0
  17. package/dist/cursor-config.js +613 -0
  18. package/dist/cursor-context-tools.js +6 -0
  19. package/dist/cursor-display-only-trace.js +11 -0
  20. package/dist/cursor-display-text.js +18 -0
  21. package/dist/cursor-durable-fs.js +53 -0
  22. package/dist/cursor-edit-diff.js +13 -0
  23. package/dist/cursor-env-boolean.js +19 -0
  24. package/dist/cursor-fallback-models.generated.js +7760 -0
  25. package/dist/cursor-fallback-warning.js +15 -0
  26. package/dist/cursor-http1.js +45 -0
  27. package/dist/cursor-incomplete-tool-visibility.js +83 -0
  28. package/dist/cursor-live-run-accounting.js +55 -0
  29. package/dist/cursor-live-run-coordinator.js +409 -0
  30. package/dist/cursor-mcp-timeout-override.js +116 -0
  31. package/dist/cursor-model-lifecycle.js +32 -0
  32. package/dist/cursor-model.js +5 -0
  33. package/dist/cursor-native-replay-routing.js +35 -0
  34. package/dist/cursor-native-replay-trace.js +28 -0
  35. package/dist/cursor-native-tool-display-registration.js +102 -0
  36. package/dist/cursor-native-tool-display-replay.js +428 -0
  37. package/dist/cursor-native-tool-display-state.js +70 -0
  38. package/dist/cursor-native-tool-display-tools.js +123 -0
  39. package/dist/cursor-native-tool-names.js +11 -0
  40. package/dist/cursor-partial-content-emitter.js +130 -0
  41. package/dist/cursor-pi-context.js +30 -0
  42. package/dist/cursor-pi-tool-bridge-abort.js +114 -0
  43. package/dist/cursor-pi-tool-bridge-constants.js +6 -0
  44. package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
  45. package/dist/cursor-pi-tool-bridge-env.js +18 -0
  46. package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
  47. package/dist/cursor-pi-tool-bridge-run.js +355 -0
  48. package/dist/cursor-pi-tool-bridge-server.js +163 -0
  49. package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
  50. package/dist/cursor-pi-tool-bridge-types.js +1 -0
  51. package/dist/cursor-pi-tool-bridge.js +123 -0
  52. package/dist/cursor-provider-errors.js +311 -0
  53. package/dist/cursor-provider-lazy.js +39 -0
  54. package/dist/cursor-provider-live-run-drain.js +420 -0
  55. package/dist/cursor-provider-overflow.js +90 -0
  56. package/dist/cursor-provider-run-finalizer.js +176 -0
  57. package/dist/cursor-provider-run-outcome.js +76 -0
  58. package/dist/cursor-provider-turn-coordinator.js +293 -0
  59. package/dist/cursor-provider-turn-display-router.js +158 -0
  60. package/dist/cursor-provider-turn-emit.js +41 -0
  61. package/dist/cursor-provider-turn-finalize.js +154 -0
  62. package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
  63. package/dist/cursor-provider-turn-prepare.js +379 -0
  64. package/dist/cursor-provider-turn-runner.js +146 -0
  65. package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
  66. package/dist/cursor-provider-turn-send.js +138 -0
  67. package/dist/cursor-provider-turn-shell-output.js +117 -0
  68. package/dist/cursor-provider-turn-tool-ledger.js +99 -0
  69. package/dist/cursor-provider-turn-types.js +1 -0
  70. package/dist/cursor-provider.js +72 -0
  71. package/dist/cursor-question-tool.js +194 -0
  72. package/dist/cursor-record-utils.js +61 -0
  73. package/dist/cursor-replay-activity-builders.js +114 -0
  74. package/dist/cursor-replay-source-names.js +27 -0
  75. package/dist/cursor-replay-summary-args.js +105 -0
  76. package/dist/cursor-replay-tool-details.js +167 -0
  77. package/dist/cursor-ripgrep-path.js +27 -0
  78. package/dist/cursor-run-final-text.js +63 -0
  79. package/dist/cursor-runtime-state.js +380 -0
  80. package/dist/cursor-sdk-billed-usage.js +86 -0
  81. package/dist/cursor-sdk-event-debug-constants.js +33 -0
  82. package/dist/cursor-sdk-event-debug-session.js +107 -0
  83. package/dist/cursor-sdk-event-debug.js +461 -0
  84. package/dist/cursor-sdk-output-filter.js +1 -0
  85. package/dist/cursor-sdk-process-error-guard.js +221 -0
  86. package/dist/cursor-sdk-runtime.js +3 -0
  87. package/dist/cursor-sensitive-text.js +23 -0
  88. package/dist/cursor-session-agent-cleanup.js +311 -0
  89. package/dist/cursor-session-agent-lifecycle.js +32 -0
  90. package/dist/cursor-session-agent-lineage.js +103 -0
  91. package/dist/cursor-session-agent-resume.js +417 -0
  92. package/dist/cursor-session-agent.js +507 -0
  93. package/dist/cursor-session-compaction-prep.js +21 -0
  94. package/dist/cursor-session-scope.js +113 -0
  95. package/dist/cursor-session-send-policy.js +18 -0
  96. package/dist/cursor-session-store.js +105 -0
  97. package/dist/cursor-session-turn-queue.js +50 -0
  98. package/dist/cursor-setting-sources.js +15 -0
  99. package/dist/cursor-skill-tool.js +230 -0
  100. package/dist/cursor-state.js +519 -0
  101. package/dist/cursor-task-presentation.js +59 -0
  102. package/dist/cursor-tool-lifecycle.js +94 -0
  103. package/dist/cursor-tool-manifest.js +34 -0
  104. package/dist/cursor-tool-presentation-registry.js +305 -0
  105. package/dist/cursor-tool-result-display-readers.js +158 -0
  106. package/dist/cursor-tool-transcript.js +32 -0
  107. package/dist/cursor-tool-visibility.js +28 -0
  108. package/dist/cursor-transcript-tool-formatters.js +618 -0
  109. package/dist/cursor-transcript-tool-specs.js +284 -0
  110. package/dist/cursor-transcript-utils.js +170 -0
  111. package/dist/cursor-usage-accounting.js +166 -0
  112. package/dist/cursor-web-tool-activity.js +23 -0
  113. package/dist/cursor-web-tool-args.js +12 -0
  114. package/dist/index.js +82 -0
  115. package/dist/model-discovery.js +305 -0
  116. package/dist/model-list-cache.js +156 -0
  117. package/docs/cursor-dogfood-checklist.md +2 -1
  118. package/docs/cursor-live-smoke-checklist.md +13 -12
  119. package/docs/cursor-model-ux-spec.md +27 -6
  120. package/docs/cursor-native-tool-replay.md +13 -13
  121. package/docs/cursor-native-tool-visual-audit.md +6 -3
  122. package/docs/cursor-testing-lessons.md +14 -8
  123. package/docs/cursor-tool-surfaces.md +5 -5
  124. package/docs/platform-smoke.md +3 -3
  125. package/package.json +19 -7
  126. package/platform-smoke.config.mjs +1 -1
  127. package/scripts/build.mjs +126 -0
  128. package/scripts/cloud-runtime-smoke.mjs +3 -1
  129. package/scripts/debug-provider-events.mjs +3 -1
  130. package/scripts/debug-sdk-events.mjs +1 -1
  131. package/scripts/isolated-cursor-smoke.sh +5 -4
  132. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
  133. package/scripts/lib/cursor-visual-render.mjs +1 -1
  134. package/scripts/lib/ensure-built.mjs +16 -0
  135. package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
  136. package/scripts/local-resume-smoke.mjs +3 -1
  137. package/scripts/platform-smoke/card-detect.mjs +1 -1
  138. package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
  139. package/scripts/platform-smoke/render-ansi.mjs +1 -1
  140. package/scripts/platform-smoke/targets.mjs +2 -2
  141. package/scripts/prepare.mjs +102 -0
  142. package/scripts/steering-rpc-smoke.mjs +4 -2
  143. package/scripts/tmux-live-smoke.sh +11 -11
  144. package/scripts/visual-tui-smoke-self-test.mjs +4 -1
  145. package/scripts/visual-tui-smoke.mjs +51 -7
  146. package/src/bundled-context-windows.ts +4 -1
  147. package/src/context.ts +27 -4
  148. package/src/cursor-agents-context-registration.ts +2 -2
  149. package/src/cursor-agents-context.ts +24 -10
  150. package/src/cursor-api-key.ts +5 -4
  151. package/src/cursor-cloud-reporting.ts +23 -2
  152. package/src/cursor-context-tools.ts +3 -1
  153. package/src/cursor-fallback-models.generated.ts +592 -204
  154. package/src/cursor-live-run-coordinator.ts +11 -6
  155. package/src/cursor-native-tool-display-registration.ts +10 -11
  156. package/src/cursor-partial-content-emitter.ts +13 -0
  157. package/src/cursor-pi-context.ts +44 -0
  158. package/src/cursor-provider-errors.ts +1 -1
  159. package/src/cursor-provider-lazy.ts +5 -4
  160. package/src/cursor-provider-live-run-drain.ts +14 -9
  161. package/src/cursor-provider-run-finalizer.ts +1 -0
  162. package/src/cursor-provider-turn-coordinator.ts +15 -0
  163. package/src/cursor-provider-turn-emit.ts +6 -0
  164. package/src/cursor-provider-turn-finalize.ts +12 -0
  165. package/src/cursor-provider-turn-prepare.ts +10 -5
  166. package/src/cursor-provider-turn-types.ts +2 -0
  167. package/src/cursor-sdk-billed-usage.ts +107 -0
  168. package/src/cursor-sdk-process-error-guard.ts +6 -4
  169. package/src/cursor-session-agent-lifecycle.ts +8 -9
  170. package/src/cursor-session-agent-resume.ts +23 -0
  171. package/src/cursor-session-compaction-prep.ts +4 -0
  172. package/src/cursor-skill-tool.ts +1 -1
  173. package/src/cursor-tool-manifest.ts +1 -1
  174. package/src/cursor-usage-accounting.ts +70 -9
  175. package/src/index.ts +1 -1
  176. package/tsconfig.build.json +8 -0
  177. package/tsconfig.json +16 -0
@@ -0,0 +1,507 @@
1
+ import { createHash } from "node:crypto";
2
+ import { getRegisteredCursorPiToolBridge, } from "./cursor-pi-tool-bridge.js";
3
+ import { computeCursorContextFingerprint } from "./context.js";
4
+ import { getCursorSessionFile, getCursorSessionScopeGeneration, getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
+ import { getMatchingCursorSessionAgentResumeHandle, persistCursorSessionAgentResumeHandle, } from "./cursor-session-agent-resume.js";
6
+ import { loadCursorSdk } from "./cursor-sdk-runtime.js";
7
+ import { cursorSessionStoreIdentitiesEqual, openCursorSessionStore, openCursorSessionStoreForScope, } from "./cursor-session-store.js";
8
+ class SessionCursorAgentCreationSupersededError extends Error {
9
+ constructor() {
10
+ super("Cursor session agent creation was superseded");
11
+ this.name = "SessionCursorAgentCreationSupersededError";
12
+ }
13
+ }
14
+ export class SessionCursorAgentScopeClosedError extends Error {
15
+ constructor() {
16
+ super("Cursor session agent scope is closed");
17
+ this.name = "SessionCursorAgentScopeClosedError";
18
+ }
19
+ }
20
+ function assertScopeAcceptsAcquire(scopeKey) {
21
+ const terminalGeneration = terminalDisposedScopeGenerations.get(scopeKey);
22
+ if (terminalGeneration === undefined)
23
+ return;
24
+ if (terminalGeneration >= getCursorSessionScopeGeneration(scopeKey)) {
25
+ throw new SessionCursorAgentScopeClosedError();
26
+ }
27
+ terminalDisposedScopeGenerations.delete(scopeKey);
28
+ }
29
+ function rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error) {
30
+ if (!(error instanceof SessionCursorAgentCreationSupersededError))
31
+ return;
32
+ const replacement = sessionAgentsByScope.get(scopeKey);
33
+ if (replacement && replacement.poolKey !== poolKey) {
34
+ throw error;
35
+ }
36
+ }
37
+ const sessionAgentsByScope = new Map();
38
+ const invalidatedScopeKeys = new Set();
39
+ const deadTransportScopeKeys = new Set();
40
+ let deadTransportAgentDisposeTimeoutMs = 3000;
41
+ const terminalDisposedScopeGenerations = new Map();
42
+ const scopeCreationGenerations = new Map();
43
+ const EMPTY_POOL_STATE = { status: "empty" };
44
+ const LOCAL_RESUME_FALLBACK_NOTICE = "Could not resume prior Cursor agent; continuing from current pi transcript in a new Cursor agent.";
45
+ let nextSessionAgentInstanceId = 1;
46
+ export function buildCursorLocalAgentOptions(options) {
47
+ return {
48
+ cwd: options.cwd,
49
+ ...(options.store ? { store: options.store } : {}),
50
+ ...(options.settingSources ? { settingSources: options.settingSources } : {}),
51
+ ...(options.localSafety?.autoReview === true ? { autoReview: true } : {}),
52
+ ...(options.localSafety?.sandboxEnabled === true ? { sandboxOptions: { enabled: true } } : {}),
53
+ };
54
+ }
55
+ function allocateSessionAgentInstanceId() {
56
+ return nextSessionAgentInstanceId++;
57
+ }
58
+ function getSessionCursorAgentPoolState(scopeKey) {
59
+ return sessionAgentsByScope.get(scopeKey) ?? EMPTY_POOL_STATE;
60
+ }
61
+ function isActivePoolEntry(entry) {
62
+ return entry?.status === "ready" || entry?.status === "busy";
63
+ }
64
+ function getScopeCreationGeneration(scopeKey) {
65
+ return scopeCreationGenerations.get(scopeKey) ?? 0;
66
+ }
67
+ function invalidateScopeCreations(scopeKey) {
68
+ scopeCreationGenerations.set(scopeKey, getScopeCreationGeneration(scopeKey) + 1);
69
+ }
70
+ function buildModelPoolKey(modelSelection) {
71
+ return JSON.stringify(modelSelection);
72
+ }
73
+ function buildSettingSourcesPoolKey(settingSources) {
74
+ return settingSources?.join(",") ?? "";
75
+ }
76
+ function buildLocalSafetyPoolKey(localSafety) {
77
+ return JSON.stringify({
78
+ autoReview: localSafety?.autoReview === true,
79
+ sandboxEnabled: localSafety?.sandboxEnabled === true,
80
+ });
81
+ }
82
+ function buildApiKeyPoolKeyFingerprint(apiKey) {
83
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
84
+ }
85
+ function buildBridgePoolKeySuffix() {
86
+ const registeredBridge = getRegisteredCursorPiToolBridge();
87
+ if (!registeredBridge)
88
+ return "bridge:absent";
89
+ return registeredBridge.getToolSurfaceSignature();
90
+ }
91
+ function buildSessionAgentPoolKey(scopeKey, params) {
92
+ return [
93
+ scopeKey,
94
+ params.cwd,
95
+ buildModelPoolKey(params.modelSelection),
96
+ buildSettingSourcesPoolKey(params.settingSources),
97
+ buildLocalSafetyPoolKey(params.localSafety),
98
+ params.useHttp1ForAgent === undefined
99
+ ? "http1:default"
100
+ : params.useHttp1ForAgent
101
+ ? "http1:on"
102
+ : "http1:off",
103
+ buildApiKeyPoolKeyFingerprint(params.apiKey),
104
+ buildBridgePoolKeySuffix(),
105
+ ].join("\0");
106
+ }
107
+ async function disposePoolEntry(entry, options) {
108
+ if (!isActivePoolEntry(entry))
109
+ return;
110
+ entry.bridgeRun?.cancel("Cursor session agent disposed");
111
+ try {
112
+ await entry.bridgeRun?.dispose();
113
+ }
114
+ catch {
115
+ // disposal failure should not block session replacement
116
+ }
117
+ try {
118
+ const disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]()).catch(() => undefined);
119
+ // A dead local transport may never settle SDK disposal; bound the wait so the
120
+ // next acquire recreates instead of hanging on the dead agent.
121
+ await (options?.deadTransport
122
+ ? Promise.race([
123
+ disposal,
124
+ new Promise((resolve) => setTimeout(resolve, deadTransportAgentDisposeTimeoutMs).unref?.()),
125
+ ])
126
+ : disposal);
127
+ }
128
+ catch {
129
+ // disposal failure should not block session replacement
130
+ }
131
+ await entry.sessionStore.dispose().catch(() => undefined);
132
+ }
133
+ async function disposePoolEntryForScope(scopeKey, options) {
134
+ invalidateScopeCreations(scopeKey);
135
+ if (options?.terminal) {
136
+ terminalDisposedScopeGenerations.set(scopeKey, getCursorSessionScopeGeneration(scopeKey));
137
+ }
138
+ const entry = sessionAgentsByScope.get(scopeKey);
139
+ invalidatedScopeKeys.delete(scopeKey);
140
+ const deadTransport = deadTransportScopeKeys.delete(scopeKey);
141
+ if (!entry)
142
+ return;
143
+ sessionAgentsByScope.delete(scopeKey);
144
+ if (entry.status === "busy") {
145
+ entry.releaseBusyWait();
146
+ }
147
+ if (entry.status === "creating") {
148
+ entry.creating.catch(() => {
149
+ // In-flight Agent.create was orphaned by scope disposal; active waiters surface errors elsewhere.
150
+ });
151
+ return;
152
+ }
153
+ await disposePoolEntry(entry, { deadTransport });
154
+ }
155
+ function createInitialSendState() {
156
+ return { bootstrapped: false, contextFingerprint: "", incrementalSendCount: 0 };
157
+ }
158
+ function bindBridgeToolRequest(entry, onBridgeToolRequest) {
159
+ entry.bridgeRun?.setOnToolRequest(onBridgeToolRequest);
160
+ }
161
+ function commitSessionAgentSendForLease(scopeKey, poolKey, instanceId, context, bootstrapped) {
162
+ const entry = sessionAgentsByScope.get(scopeKey);
163
+ if (!isActivePoolEntry(entry))
164
+ return;
165
+ if (entry.poolKey !== poolKey || entry.instanceId !== instanceId)
166
+ return;
167
+ entry.sendState.bootstrapped = bootstrapped || entry.sendState.bootstrapped;
168
+ entry.sendState.contextFingerprint = computeCursorContextFingerprint(context);
169
+ if (bootstrapped) {
170
+ entry.sendState.incrementalSendCount = 0;
171
+ }
172
+ else {
173
+ entry.sendState.incrementalSendCount += 1;
174
+ }
175
+ if (entry.resumeEnabled) {
176
+ persistCursorSessionAgentResumeHandle({
177
+ runtime: "local",
178
+ agentId: entry.agent.agentId,
179
+ poolKey: entry.poolKey,
180
+ sendState: entry.sendState,
181
+ storeIdentity: entry.sessionStore.identity,
182
+ });
183
+ }
184
+ }
185
+ function normalizeRunCompletion(completion) {
186
+ return Promise.resolve(completion).then(() => undefined, () => undefined);
187
+ }
188
+ function buildBusyPoolEntry(entry, completionSettled) {
189
+ let releaseBusyWait = () => { };
190
+ const releaseSignal = new Promise((resolve) => {
191
+ releaseBusyWait = () => resolve("released");
192
+ });
193
+ const pendingCompletion = Promise.race([
194
+ completionSettled.then(() => "completed"),
195
+ releaseSignal,
196
+ ]).then((outcome) => {
197
+ const current = sessionAgentsByScope.get(entry.scopeKey);
198
+ if (outcome === "completed" &&
199
+ current?.status === "busy" &&
200
+ current.poolKey === entry.poolKey &&
201
+ current.instanceId === entry.instanceId &&
202
+ current.pendingCompletion === pendingCompletion) {
203
+ sessionAgentsByScope.set(entry.scopeKey, { ...current, status: "ready" });
204
+ }
205
+ });
206
+ return {
207
+ ...entry,
208
+ status: "busy",
209
+ completionSettled,
210
+ pendingCompletion,
211
+ releaseBusyWait,
212
+ busyGeneration: getScopeCreationGeneration(entry.scopeKey),
213
+ };
214
+ }
215
+ function trackSessionAgentRunCompletionForLease(scopeKey, poolKey, instanceId, completion) {
216
+ const entry = sessionAgentsByScope.get(scopeKey);
217
+ if (!isActivePoolEntry(entry))
218
+ return;
219
+ if (entry.poolKey !== poolKey || entry.instanceId !== instanceId)
220
+ return;
221
+ const completionToTrack = normalizeRunCompletion(completion);
222
+ const completionSettled = (entry.status === "busy"
223
+ ? Promise.all([entry.completionSettled, completionToTrack]).then(() => undefined)
224
+ : completionToTrack);
225
+ if (entry.status === "busy") {
226
+ entry.releaseBusyWait();
227
+ }
228
+ sessionAgentsByScope.set(scopeKey, buildBusyPoolEntry(entry, completionSettled));
229
+ }
230
+ function leaseFromEntry(entry, scopeKey, params, created) {
231
+ entry.resumeEnabled = params.localResume === true;
232
+ bindBridgeToolRequest(entry, params.onBridgeToolRequest);
233
+ entry.bridgeRun?.setDebugRecorder(params.debugRecorder);
234
+ const resumeNotice = entry.resumeNotice;
235
+ entry.resumeNotice = undefined;
236
+ return {
237
+ scopeKey,
238
+ poolKey: entry.poolKey,
239
+ instanceId: entry.instanceId,
240
+ agent: entry.agent,
241
+ bridgeRun: entry.bridgeRun,
242
+ store: entry.sessionStore.store,
243
+ storeIdentity: entry.sessionStore.identity,
244
+ sendState: entry.sendState,
245
+ created,
246
+ resumed: entry.resumed,
247
+ ...(resumeNotice ? { resumeNotice } : {}),
248
+ commitSend: (context, bootstrapped) => {
249
+ commitSessionAgentSendForLease(scopeKey, entry.poolKey, entry.instanceId, context, bootstrapped);
250
+ },
251
+ trackRunCompletion: (completion) => {
252
+ trackSessionAgentRunCompletionForLease(scopeKey, entry.poolKey, entry.instanceId, completion);
253
+ },
254
+ };
255
+ }
256
+ function getCurrentReadyPoolEntry(scopeKey, poolKey) {
257
+ const current = sessionAgentsByScope.get(scopeKey);
258
+ if (current?.status !== "ready")
259
+ return undefined;
260
+ if (current.poolKey !== poolKey)
261
+ return undefined;
262
+ return current;
263
+ }
264
+ async function tryLeaseReadyEntry(entry, scopeKey, params, poolKey, created) {
265
+ if (entry.status === "busy") {
266
+ await entry.pendingCompletion;
267
+ }
268
+ assertScopeAcceptsAcquire(scopeKey);
269
+ if (invalidatedScopeKeys.has(scopeKey)) {
270
+ await disposePoolEntryForScope(scopeKey);
271
+ return undefined;
272
+ }
273
+ const readyEntry = getCurrentReadyPoolEntry(scopeKey, poolKey);
274
+ if (!readyEntry)
275
+ return undefined;
276
+ return leaseFromEntry(readyEntry, scopeKey, params, created);
277
+ }
278
+ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, sendState, params) {
279
+ let bridgeRun;
280
+ let sessionStore;
281
+ try {
282
+ const registeredBridge = getRegisteredCursorPiToolBridge();
283
+ if (registeredBridge) {
284
+ bridgeRun = await registeredBridge.createRun({
285
+ onToolRequest: params.onBridgeToolRequest,
286
+ debugRecorder: params.debugRecorder,
287
+ });
288
+ if (!bridgeRun.enabled || !bridgeRun.mcpServers) {
289
+ await bridgeRun.dispose();
290
+ bridgeRun = undefined;
291
+ }
292
+ }
293
+ const resolvedPoolKey = buildSessionAgentPoolKey(scopeKey, params);
294
+ const resumeEligible = params.localResume === true && !params.forceCreate;
295
+ let createAgent = params.createAgent;
296
+ let resumeAgent = params.resumeAgent;
297
+ if (!createAgent || (resumeEligible && !resumeAgent)) {
298
+ const sdk = await loadCursorSdk();
299
+ createAgent ??= sdk.Agent.create;
300
+ resumeAgent ??= sdk.Agent.resume;
301
+ }
302
+ const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey) : undefined;
303
+ const storeSelection = await openCursorSessionStoreForScope({
304
+ cwd: params.cwd,
305
+ scopeKey,
306
+ persistent: persistentStore,
307
+ hasResumeHandle: resumeHandle !== undefined,
308
+ resumeIdentity: resumeHandle?.storeIdentity,
309
+ });
310
+ sessionStore = storeSelection.sessionStore;
311
+ const { identities } = storeSelection;
312
+ const resumeAttemptAllowed = storeSelection.resumeAttemptAllowed;
313
+ let resumeNotice = storeSelection.resumeFallback ? LOCAL_RESUME_FALLBACK_NOTICE : undefined;
314
+ const buildAgentOptions = () => ({
315
+ apiKey: params.apiKey,
316
+ model: params.modelSelection,
317
+ mode: params.agentMode,
318
+ local: buildCursorLocalAgentOptions({
319
+ cwd: params.cwd,
320
+ settingSources: params.settingSources,
321
+ localSafety: params.localSafety,
322
+ store: sessionStore.store,
323
+ }),
324
+ ...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
325
+ });
326
+ let agent;
327
+ let effectiveSendState = sendState;
328
+ let resumed = false;
329
+ if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
330
+ try {
331
+ agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
332
+ effectiveSendState = { ...resumeHandle.sendState };
333
+ resumed = true;
334
+ }
335
+ catch {
336
+ if (persistentStore)
337
+ resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
338
+ if (!cursorSessionStoreIdentitiesEqual(sessionStore.identity, identities.sessionStore)) {
339
+ await sessionStore.dispose().catch(() => undefined);
340
+ sessionStore = await openCursorSessionStore(params.cwd, identities.sessionStore);
341
+ }
342
+ }
343
+ }
344
+ agent ??= await createAgent(buildAgentOptions());
345
+ if (!agent)
346
+ throw new Error("Cursor SDK agent creation returned no agent");
347
+ if (!sessionStore)
348
+ throw new Error("Cursor SDK session store was not opened");
349
+ return {
350
+ status: "ready",
351
+ poolKey: resolvedPoolKey,
352
+ instanceId,
353
+ scopeKey,
354
+ agent,
355
+ bridgeRun,
356
+ sessionStore,
357
+ sendState: effectiveSendState,
358
+ resumeEnabled: params.localResume === true,
359
+ resumed,
360
+ ...(resumeNotice ? { resumeNotice } : {}),
361
+ };
362
+ }
363
+ catch (error) {
364
+ bridgeRun?.cancel("Cursor session agent create failed");
365
+ await bridgeRun?.dispose().catch(() => undefined);
366
+ await sessionStore?.dispose().catch(() => undefined);
367
+ throw error;
368
+ }
369
+ }
370
+ export { buildCursorSessionSendPrompt, MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP, planCursorSessionSend, } from "./cursor-session-send-policy.js";
371
+ export function invalidateSessionAgent(scopeKey = getCursorSessionScopeKey(), options) {
372
+ invalidatedScopeKeys.add(scopeKey);
373
+ if (options?.deadTransport)
374
+ deadTransportScopeKeys.add(scopeKey);
375
+ }
376
+ export async function acquireSessionCursorAgent(params) {
377
+ const scopeKey = getCursorSessionScopeKey();
378
+ const persistentStore = getCursorSessionFile() !== undefined;
379
+ while (true) {
380
+ assertScopeAcceptsAcquire(scopeKey);
381
+ if (invalidatedScopeKeys.has(scopeKey)) {
382
+ await disposePoolEntryForScope(scopeKey);
383
+ }
384
+ const poolKey = buildSessionAgentPoolKey(scopeKey, params);
385
+ const state = getSessionCursorAgentPoolState(scopeKey);
386
+ if ((state.status === "ready" || state.status === "busy") && state.poolKey !== poolKey) {
387
+ await disposePoolEntryForScope(scopeKey);
388
+ continue;
389
+ }
390
+ if (state.status === "ready") {
391
+ return leaseFromEntry(state, scopeKey, params, false);
392
+ }
393
+ if (state.status === "busy") {
394
+ const busyGeneration = state.busyGeneration;
395
+ await state.pendingCompletion;
396
+ if (busyGeneration !== getScopeCreationGeneration(scopeKey))
397
+ continue;
398
+ continue;
399
+ }
400
+ if (state.status === "creating") {
401
+ if (state.poolKey !== poolKey) {
402
+ await disposePoolEntryForScope(scopeKey);
403
+ continue;
404
+ }
405
+ try {
406
+ await state.creating;
407
+ }
408
+ catch (error) {
409
+ if (error instanceof SessionCursorAgentCreationSupersededError) {
410
+ assertScopeAcceptsAcquire(scopeKey);
411
+ rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
412
+ continue;
413
+ }
414
+ throw error;
415
+ }
416
+ continue;
417
+ }
418
+ assertScopeAcceptsAcquire(scopeKey);
419
+ const creationGeneration = getScopeCreationGeneration(scopeKey);
420
+ const instanceId = allocateSessionAgentInstanceId();
421
+ const sendState = createInitialSendState();
422
+ let placeholder;
423
+ const creating = createSessionAgentEntry(scopeKey, persistentStore, instanceId, sendState, params).then(async (createdEntry) => {
424
+ const stillCurrent = sessionAgentsByScope.get(scopeKey) === placeholder &&
425
+ getScopeCreationGeneration(scopeKey) === placeholder.creationGeneration;
426
+ if (!stillCurrent) {
427
+ await disposePoolEntry(createdEntry);
428
+ if (sessionAgentsByScope.get(scopeKey) === placeholder) {
429
+ sessionAgentsByScope.delete(scopeKey);
430
+ }
431
+ throw new SessionCursorAgentCreationSupersededError();
432
+ }
433
+ sessionAgentsByScope.set(scopeKey, createdEntry);
434
+ return createdEntry;
435
+ });
436
+ placeholder = {
437
+ status: "creating",
438
+ poolKey,
439
+ instanceId,
440
+ scopeKey,
441
+ sendState,
442
+ creationGeneration,
443
+ creating,
444
+ };
445
+ sessionAgentsByScope.set(scopeKey, placeholder);
446
+ try {
447
+ const createdEntry = await creating;
448
+ const lease = await tryLeaseReadyEntry(createdEntry, scopeKey, params, poolKey, true);
449
+ if (lease)
450
+ return lease;
451
+ continue;
452
+ }
453
+ catch (error) {
454
+ if (sessionAgentsByScope.get(scopeKey) === placeholder) {
455
+ sessionAgentsByScope.delete(scopeKey);
456
+ }
457
+ if (error instanceof SessionCursorAgentCreationSupersededError) {
458
+ assertScopeAcceptsAcquire(scopeKey);
459
+ rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey, poolKey, error);
460
+ continue;
461
+ }
462
+ throw error;
463
+ }
464
+ }
465
+ }
466
+ export async function refreshSessionCursorAgentConfig(scopeKey = getCursorSessionScopeKey()) {
467
+ const entry = sessionAgentsByScope.get(scopeKey);
468
+ if (!entry || entry.status === "creating")
469
+ return "no-agent";
470
+ if (entry.status === "busy")
471
+ return "busy";
472
+ if (typeof entry.agent.reload !== "function")
473
+ return "unsupported";
474
+ await entry.agent.reload();
475
+ return "reloaded";
476
+ }
477
+ export async function resetSessionCursorAgent(scopeKey = getCursorSessionScopeKey()) {
478
+ await disposePoolEntryForScope(scopeKey);
479
+ }
480
+ export async function disposeSessionCursorAgent(scopeKey = getCursorSessionScopeKey()) {
481
+ await disposePoolEntryForScope(scopeKey, { terminal: true });
482
+ }
483
+ export async function disposeAllSessionCursorAgents() {
484
+ const scopeKeys = [...new Set([...sessionAgentsByScope.keys(), ...terminalDisposedScopeGenerations.keys()])];
485
+ await Promise.all(scopeKeys.map((scopeKey) => disposePoolEntryForScope(scopeKey, { terminal: true })));
486
+ invalidatedScopeKeys.clear();
487
+ deadTransportScopeKeys.clear();
488
+ terminalDisposedScopeGenerations.clear();
489
+ }
490
+ export const __testUtils = {
491
+ sessionAgentsByScope,
492
+ getSessionCursorAgentPoolState,
493
+ invalidateSessionAgent,
494
+ disposeSessionCursorAgent,
495
+ resetSessionCursorAgent,
496
+ refreshSessionCursorAgentConfig,
497
+ disposeAllSessionCursorAgents,
498
+ buildApiKeyPoolKeyFingerprint,
499
+ buildSessionAgentPoolKey,
500
+ setDeadTransportAgentDisposeTimeoutMs(ms) {
501
+ const previous = deadTransportAgentDisposeTimeoutMs;
502
+ deadTransportAgentDisposeTimeoutMs = ms;
503
+ return previous;
504
+ },
505
+ SessionCursorAgentCreationSupersededError,
506
+ SessionCursorAgentScopeClosedError,
507
+ };
@@ -0,0 +1,21 @@
1
+ import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
2
+ import { resetSessionCursorAgent } from "./cursor-session-agent.js";
3
+ import { suppressCursorSessionAgentResumeHandlePersist } from "./cursor-session-agent-resume.js";
4
+ import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
+ /**
6
+ * Prepare the pooled Cursor session agent for pi compaction summarization.
7
+ * Releases any scoped live-run drain state still tied to the pooled agent, then
8
+ * disposes the pool entry so summarization acquires a clean SDK agent.
9
+ * Suppresses local-resume handle persist so the summarizer send cannot leak
10
+ * a one-message handle into the next normal turn (#223).
11
+ */
12
+ export async function prepareCursorSessionForCompaction(scopeKey = getCursorSessionScopeKey()) {
13
+ suppressCursorSessionAgentResumeHandlePersist();
14
+ while (true) {
15
+ const run = cursorLiveRuns.getActiveForScope(scopeKey);
16
+ if (!run || run.disposed)
17
+ break;
18
+ await cursorLiveRuns.release(run);
19
+ }
20
+ await resetSessionCursorAgent(scopeKey);
21
+ }
@@ -0,0 +1,113 @@
1
+ import { resolve } from "node:path";
2
+ import { parseArgs } from "@earendil-works/pi-coding-agent";
3
+ import { truncateCursorDisplayLine } from "./cursor-display-text.js";
4
+ const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
5
+ const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:";
6
+ export const MAX_CURSOR_SESSION_NAME_LENGTH = 100;
7
+ const state = {
8
+ sessionCwd: process.cwd(),
9
+ sessionFile: undefined,
10
+ sessionId: undefined,
11
+ sessionName: undefined,
12
+ projectTrusted: false,
13
+ sessionGeneration: 0,
14
+ };
15
+ const scopeGenerations = new Map([[ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration]]);
16
+ const projectTrustResolutionCwds = new Set();
17
+ let nextSessionGeneration = 1;
18
+ let scopeChangeHandler;
19
+ /**
20
+ * Pi session file when known; used to scope reused Cursor SDK agents to one pi session.
21
+ */
22
+ export function getCursorSessionFile() {
23
+ return state.sessionFile;
24
+ }
25
+ /**
26
+ * Stable scope key for session-agent pooling. Falls back to a process-local anonymous key
27
+ * before the first session_start (tests and early startup).
28
+ */
29
+ export function getCursorSessionScopeKey() {
30
+ if (state.sessionFile)
31
+ return state.sessionFile;
32
+ if (state.sessionId)
33
+ return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${state.sessionId}`;
34
+ return ANONYMOUS_SESSION_SCOPE_KEY;
35
+ }
36
+ export function getCursorSessionScopeGeneration(scopeKey = getCursorSessionScopeKey()) {
37
+ return scopeGenerations.get(scopeKey) ?? 0;
38
+ }
39
+ /**
40
+ * Pi session cwd when known; falls back to process.cwd() before session_start.
41
+ * Updated on session_start only until pi threads cwd into streamSimple—mid-session cwd
42
+ * changes without a new session_start event are not reflected here.
43
+ */
44
+ export function getCursorSessionCwd() {
45
+ return state.sessionCwd;
46
+ }
47
+ export function getCursorSessionProjectTrusted() {
48
+ return state.projectTrusted;
49
+ }
50
+ export function getCursorSessionName() {
51
+ return state.sessionName;
52
+ }
53
+ function normalizeCursorSessionName(name) {
54
+ if (name === undefined)
55
+ return undefined;
56
+ return truncateCursorDisplayLine(name, MAX_CURSOR_SESSION_NAME_LENGTH) || undefined;
57
+ }
58
+ function setCursorSessionScope(cwd, sessionFile, sessionId, projectTrusted = false, sessionName) {
59
+ state.sessionCwd = cwd;
60
+ state.sessionFile = sessionFile;
61
+ state.sessionId = sessionId;
62
+ state.sessionName = normalizeCursorSessionName(sessionName);
63
+ state.projectTrusted = projectTrusted;
64
+ state.sessionGeneration = nextSessionGeneration;
65
+ nextSessionGeneration += 1;
66
+ scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration);
67
+ }
68
+ function recordProjectTrustResolution(cwd) {
69
+ projectTrustResolutionCwds.add(resolve(cwd));
70
+ }
71
+ function isCliProjectTrustApproved(args = process.argv.slice(2)) {
72
+ return parseArgs(args).projectTrustOverride === true;
73
+ }
74
+ function resetCursorSessionScope() {
75
+ state.sessionCwd = process.cwd();
76
+ state.sessionFile = undefined;
77
+ state.sessionId = undefined;
78
+ state.sessionName = undefined;
79
+ state.projectTrusted = false;
80
+ state.sessionGeneration = 0;
81
+ nextSessionGeneration = 1;
82
+ scopeGenerations.clear();
83
+ scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration);
84
+ projectTrustResolutionCwds.clear();
85
+ }
86
+ export function onCursorSessionScopeKeyChange(handler) {
87
+ scopeChangeHandler = handler;
88
+ }
89
+ export function registerCursorSessionScope(pi) {
90
+ pi.on("project_trust", (event) => {
91
+ recordProjectTrustResolution(event.cwd);
92
+ return { trusted: "undecided" };
93
+ });
94
+ pi.on("session_start", async (_event, ctx) => {
95
+ const previousScopeKey = getCursorSessionScopeKey();
96
+ setCursorSessionScope(ctx.cwd, ctx.sessionManager?.getSessionFile?.() ?? undefined, ctx.sessionManager?.getSessionId?.() ?? undefined, ctx.isProjectTrusted?.() === true
97
+ && (projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined);
98
+ if (previousScopeKey !== getCursorSessionScopeKey()) {
99
+ await scopeChangeHandler?.(previousScopeKey);
100
+ }
101
+ });
102
+ pi.on("session_info_changed", (event) => {
103
+ state.sessionName = normalizeCursorSessionName(event.name);
104
+ });
105
+ }
106
+ export const __testUtils = {
107
+ ANONYMOUS_SESSION_SCOPE_KEY,
108
+ EPHEMERAL_SESSION_SCOPE_PREFIX,
109
+ set: setCursorSessionScope,
110
+ recordProjectTrustResolution,
111
+ isCliProjectTrustApproved,
112
+ reset: resetCursorSessionScope,
113
+ };
@@ -0,0 +1,18 @@
1
+ import { buildCursorIncrementalPrompt, buildCursorPrompt, shouldBootstrapCursorContext, } from "./context.js";
2
+ // Long-lived SDK session agents can drift tool-call behavior; recreate the agent after this many successful incremental sends.
3
+ export const MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP = 20;
4
+ export function planCursorSessionSend(sendState, context) {
5
+ if (!sendState.bootstrapped) {
6
+ return { mode: "bootstrap", resetAgent: false, reason: "initial" };
7
+ }
8
+ if (sendState.incrementalSendCount >= MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP) {
9
+ return { mode: "bootstrap", resetAgent: true, reason: "incremental_threshold" };
10
+ }
11
+ if (shouldBootstrapCursorContext(sendState, context)) {
12
+ return { mode: "bootstrap", resetAgent: true, reason: "context_divergence" };
13
+ }
14
+ return { mode: "incremental", resetAgent: false, reason: "incremental" };
15
+ }
16
+ export function buildCursorSessionSendPrompt(context, options, plan) {
17
+ return plan.mode === "bootstrap" ? buildCursorPrompt(context, options) : buildCursorIncrementalPrompt(context, options);
18
+ }