pi-cursor-sdk 0.1.59 → 0.1.61
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.
- package/CHANGELOG.md +33 -0
- package/README.md +124 -10
- package/docs/cursor-model-ux-spec.md +19 -11
- package/docs/cursor-native-tool-replay.md +5 -3
- package/docs/cursor-testing-lessons.md +2 -2
- package/docs/cursor-tool-surfaces.md +7 -1
- package/docs/platform-smoke.md +51 -20
- package/package.json +13 -2
- package/platform-smoke.config.mjs +1 -0
- package/scripts/cloud-runtime-smoke.d.mts +40 -1
- package/scripts/cloud-runtime-smoke.mjs +444 -325
- package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
- package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
- package/scripts/lib/cloud-smoke-github.d.mts +78 -0
- package/scripts/lib/cloud-smoke-github.mjs +331 -0
- package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
- package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
- package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
- package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
- package/scripts/local-resume-cleanup-smoke.mjs +11 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -0
- package/scripts/platform-smoke/scenarios.d.mts +22 -0
- package/scripts/platform-smoke/scenarios.mjs +17 -0
- package/scripts/platform-smoke/targets.mjs +1 -0
- package/src/cursor-agent-message-web-tools.ts +14 -6
- package/src/cursor-cloud-local-state.ts +536 -0
- package/src/cursor-cloud-options.ts +46 -70
- package/src/cursor-config.ts +160 -14
- package/src/cursor-http1.ts +65 -0
- package/src/cursor-live-run-coordinator.ts +8 -1
- package/src/cursor-pi-tool-bridge-abort.ts +10 -3
- package/src/cursor-pi-tool-bridge-constants.ts +6 -0
- package/src/cursor-pi-tool-bridge-env.ts +9 -0
- package/src/cursor-pi-tool-bridge-run.ts +27 -5
- package/src/cursor-pi-tool-bridge.ts +11 -2
- package/src/cursor-provider-errors.ts +72 -2
- package/src/cursor-provider-live-run-drain.ts +2 -1
- package/src/cursor-provider-run-finalizer.ts +8 -2
- package/src/cursor-provider-run-outcome.ts +7 -1
- package/src/cursor-provider-turn-finalize.ts +23 -4
- package/src/cursor-provider-turn-prepare.ts +17 -3
- package/src/cursor-provider-turn-runner.ts +15 -1
- package/src/cursor-provider-turn-send.ts +1 -1
- package/src/cursor-question-tool.ts +8 -0
- package/src/cursor-ripgrep-path.ts +32 -0
- package/src/cursor-runtime-state.ts +119 -100
- package/src/cursor-sdk-process-error-guard.ts +62 -0
- package/src/cursor-session-agent-cleanup.ts +66 -15
- package/src/cursor-session-agent-lifecycle.ts +9 -4
- package/src/cursor-session-agent-resume.ts +50 -7
- package/src/cursor-session-agent.ts +131 -71
- package/src/cursor-session-scope.ts +22 -2
- package/src/cursor-session-store.ts +155 -0
- package/src/cursor-state.ts +162 -21
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CURSOR_AUTO_REVIEW_ENV,
|
|
8
8
|
CURSOR_CLOUD_ACK_ENV,
|
|
9
9
|
CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV,
|
|
10
|
+
CURSOR_CLOUD_AUTO_CREATE_PR_ENV,
|
|
10
11
|
CURSOR_CLOUD_BRANCH_ENV,
|
|
11
12
|
CURSOR_CLOUD_CONTEXT_ENV,
|
|
12
13
|
CURSOR_CLOUD_DIRECT_PUSH_ENV,
|
|
@@ -15,19 +16,18 @@ import {
|
|
|
15
16
|
CURSOR_CLOUD_ENV_NAME_ENV,
|
|
16
17
|
CURSOR_CLOUD_ENV_TYPE_ENV,
|
|
17
18
|
CURSOR_CLOUD_REPO_ENV,
|
|
19
|
+
CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV,
|
|
18
20
|
CURSOR_LOCAL_FORCE_ENV,
|
|
19
21
|
CURSOR_LOCAL_RESUME_ENV,
|
|
20
22
|
CURSOR_RUNTIME_ENV,
|
|
21
23
|
CURSOR_SANDBOX_ENV,
|
|
24
|
+
getCursorSdkProjectConfigPath,
|
|
25
|
+
getCursorSdkUserConfigPath,
|
|
22
26
|
loadCursorSdkConfig,
|
|
23
|
-
|
|
24
|
-
loadCursorSdkUserConfig,
|
|
25
|
-
mergeCursorSdkConfig,
|
|
26
|
-
parseCursorSdkConfig,
|
|
27
|
+
mergeCursorSdkConfigForUpdate,
|
|
27
28
|
parseExplicitCursorCloudEnvNames,
|
|
28
29
|
resolveCursorSdkConfig,
|
|
29
|
-
|
|
30
|
-
saveCursorSdkUserConfig,
|
|
30
|
+
updateCursorSdkConfig,
|
|
31
31
|
type CursorExplicitSdkConfig,
|
|
32
32
|
type CursorResolvedSdkConfig,
|
|
33
33
|
type CursorResolvedSetting,
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
type CursorSdkConfig,
|
|
36
36
|
} from "./cursor-config.js";
|
|
37
37
|
import { asRecord } from "./cursor-record-utils.js";
|
|
38
|
+
import { getResolvedSessionCursorHttp1Enabled } from "./cursor-http1.js";
|
|
39
|
+
import { getCursorSessionCwd, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
|
|
38
40
|
|
|
39
41
|
export const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
|
|
40
42
|
|
|
@@ -57,28 +59,18 @@ export type CursorRuntimeStateExtensionApi = Pick<
|
|
|
57
59
|
"appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on"
|
|
58
60
|
>;
|
|
59
61
|
|
|
60
|
-
type CursorRuntimeContext = Pick<ExtensionContext, "cwd"
|
|
62
|
+
type CursorRuntimeContext = Pick<ExtensionContext, "cwd">;
|
|
61
63
|
|
|
62
64
|
type CursorStatusRefresh = (ctx: ExtensionContext) => void;
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
interface CursorCliConfigSnapshot {
|
|
67
|
+
config: CursorExplicitSdkConfig;
|
|
68
|
+
cloudEnvNames?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let cliCursorSnapshot: CursorCliConfigSnapshot = { config: {} };
|
|
67
72
|
let cliLocalForceConsumed = false;
|
|
68
|
-
let cliLocalResume = false;
|
|
69
|
-
let cliNoLocalResume = false;
|
|
70
73
|
let envLocalForceConsumed = false;
|
|
71
|
-
let cliCursorRuntime: string | undefined;
|
|
72
|
-
let cliCursorCloudRepo: string | undefined;
|
|
73
|
-
let cliCursorCloudBranch: string | undefined;
|
|
74
|
-
let cliCursorCloudContext: string | undefined;
|
|
75
|
-
let cliCursorCloudDirectPush = false;
|
|
76
|
-
let cliCursorCloudAllowLocalState = false;
|
|
77
|
-
let cliCursorCloudEnv: string | undefined;
|
|
78
|
-
let cliCursorCloudEnvFromFiles = false;
|
|
79
|
-
let cliCursorCloudEnvType: string | undefined;
|
|
80
|
-
let cliCursorCloudEnvName: string | undefined;
|
|
81
|
-
let cliCursorCloudAck = false;
|
|
82
74
|
let sessionCursorRuntime: CursorRuntime | undefined;
|
|
83
75
|
let sessionCursorCloudAcknowledged = false;
|
|
84
76
|
|
|
@@ -94,43 +86,23 @@ function stringFlagValue(value: boolean | string | undefined): string | undefine
|
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
export function getCursorCliConfig(): CursorExplicitSdkConfig {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
...(cliCursorCloudDirectPush ? { directPush: true } : {}),
|
|
102
|
-
...(cliCursorCloudAllowLocalState ? { allowLocalState: true } : {}),
|
|
103
|
-
envNames: parseExplicitCursorCloudEnvNames(cliCursorCloudEnv, "--cursor-cloud-env"),
|
|
104
|
-
...(cliCursorCloudEnvFromFiles ? { envFromFiles: true } : {}),
|
|
105
|
-
environment: {
|
|
106
|
-
type: cliCursorCloudEnvType,
|
|
107
|
-
name: cliCursorCloudEnvName,
|
|
108
|
-
},
|
|
109
|
-
...(cliCursorCloudAck ? { acknowledged: true } : {}),
|
|
110
|
-
},
|
|
111
|
-
local: {
|
|
112
|
-
...(cliAutoReview ? { autoReview: true } : {}),
|
|
113
|
-
...(cliSandbox ? { sandboxOptions: { enabled: true } } : {}),
|
|
114
|
-
...(cliLocalForce ? { force: true } : {}),
|
|
115
|
-
...(cliNoLocalResume ? { resume: false } : cliLocalResume ? { resume: true } : {}),
|
|
116
|
-
},
|
|
117
|
-
}) ?? {};
|
|
118
|
-
return {
|
|
119
|
-
...parsed,
|
|
120
|
-
...(cliCursorRuntime ? { runtime: cliCursorRuntime } : {}),
|
|
121
|
-
...(cliCursorCloudContext
|
|
122
|
-
? { cloud: { ...parsed.cloud, contextHandoff: cliCursorCloudContext } }
|
|
123
|
-
: {}),
|
|
124
|
-
};
|
|
89
|
+
const config = structuredClone(cliCursorSnapshot.config);
|
|
90
|
+
const envNames = parseExplicitCursorCloudEnvNames(cliCursorSnapshot.cloudEnvNames, "--cursor-cloud-env");
|
|
91
|
+
if (envNames) config.cloud = { ...config.cloud, envNames };
|
|
92
|
+
return config;
|
|
125
93
|
}
|
|
126
94
|
|
|
127
95
|
export function getCursorSessionConfig(): CursorSdkConfig {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
96
|
+
const useHttp1ForAgent = getResolvedSessionCursorHttp1Enabled();
|
|
97
|
+
return {
|
|
98
|
+
...(sessionCursorRuntime
|
|
99
|
+
? {
|
|
100
|
+
runtime: sessionCursorRuntime,
|
|
101
|
+
...(sessionCursorCloudAcknowledged ? { cloud: { acknowledged: true } } : {}),
|
|
102
|
+
}
|
|
103
|
+
: {}),
|
|
104
|
+
...(useHttp1ForAgent === undefined ? {} : { local: { useHttp1ForAgent } }),
|
|
105
|
+
};
|
|
134
106
|
}
|
|
135
107
|
|
|
136
108
|
export function resolveEffectiveCursorConfig(options: {
|
|
@@ -147,16 +119,28 @@ export function resolveEffectiveCursorConfig(options: {
|
|
|
147
119
|
}
|
|
148
120
|
|
|
149
121
|
export function resolveEffectiveCursorConfigForContext(ctx: CursorRuntimeContext): CursorResolvedSdkConfig {
|
|
150
|
-
return resolveEffectiveCursorConfig({
|
|
122
|
+
return resolveEffectiveCursorConfig({
|
|
123
|
+
cwd: ctx.cwd,
|
|
124
|
+
projectTrusted: getCursorSessionCwd() === ctx.cwd && getCursorSessionProjectTrusted(),
|
|
125
|
+
});
|
|
151
126
|
}
|
|
152
127
|
|
|
153
128
|
export type CursorRuntimeResolution =
|
|
154
|
-
| {
|
|
129
|
+
| {
|
|
130
|
+
kind: "valid";
|
|
131
|
+
runtime: CursorResolvedSetting<CursorRuntime>;
|
|
132
|
+
useHttp1ForAgent: CursorResolvedSetting<boolean>;
|
|
133
|
+
}
|
|
155
134
|
| { kind: "invalid"; message: string };
|
|
156
135
|
|
|
157
136
|
export function resolveCursorStatusRuntime(ctx: CursorRuntimeContext): CursorRuntimeResolution {
|
|
158
137
|
try {
|
|
159
|
-
|
|
138
|
+
const config = resolveEffectiveCursorConfigForContext(ctx);
|
|
139
|
+
return {
|
|
140
|
+
kind: "valid",
|
|
141
|
+
runtime: config.runtime,
|
|
142
|
+
useHttp1ForAgent: config.local.useHttp1ForAgent,
|
|
143
|
+
};
|
|
160
144
|
} catch (error) {
|
|
161
145
|
return { kind: "invalid", message: error instanceof Error ? error.message : String(error) };
|
|
162
146
|
}
|
|
@@ -173,8 +157,10 @@ export function formatCursorStatus(
|
|
|
173
157
|
runtime: CursorRuntime | "invalid",
|
|
174
158
|
fast: boolean | undefined,
|
|
175
159
|
mode: "agent" | "plan" | "invalid",
|
|
160
|
+
useHttp1ForAgent = false,
|
|
176
161
|
): string {
|
|
177
162
|
const parts = [`cursor:${runtime}`, fast === true ? "fast:on" : fast === false ? "fast:off" : "fast:n/a"];
|
|
163
|
+
if (runtime === "local" && useHttp1ForAgent) parts.push("http1");
|
|
178
164
|
if (mode === "invalid") parts.push("mode invalid");
|
|
179
165
|
else if (mode === "plan") parts.push("plan");
|
|
180
166
|
return parts.join(" · ");
|
|
@@ -183,7 +169,10 @@ export function formatCursorStatus(
|
|
|
183
169
|
export function consumeCursorLocalForceOverride(resolved: { value: boolean; source: string }): boolean {
|
|
184
170
|
if (!resolved.value) return false;
|
|
185
171
|
if (resolved.source === "cli" && !cliLocalForceConsumed) {
|
|
186
|
-
|
|
172
|
+
if (cliCursorSnapshot.config.local) {
|
|
173
|
+
const { force: _, ...local } = cliCursorSnapshot.config.local;
|
|
174
|
+
cliCursorSnapshot.config.local = local;
|
|
175
|
+
}
|
|
187
176
|
cliLocalForceConsumed = true;
|
|
188
177
|
return true;
|
|
189
178
|
}
|
|
@@ -207,22 +196,45 @@ export function restoreSessionCursorRuntimeState(branch: readonly SessionEntry[]
|
|
|
207
196
|
}
|
|
208
197
|
|
|
209
198
|
export function restoreCursorCliState(pi: Pick<ExtensionAPI, "getFlag">): void {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
199
|
+
const runtime = stringFlagValue(pi.getFlag("cursor-runtime"));
|
|
200
|
+
const repo = stringFlagValue(pi.getFlag("cursor-cloud-repo"));
|
|
201
|
+
const branch = stringFlagValue(pi.getFlag("cursor-cloud-branch"));
|
|
202
|
+
const contextHandoff = stringFlagValue(pi.getFlag("cursor-cloud-context"));
|
|
203
|
+
const environmentType = stringFlagValue(pi.getFlag("cursor-cloud-env-type"));
|
|
204
|
+
const environmentName = stringFlagValue(pi.getFlag("cursor-cloud-env-name"));
|
|
205
|
+
const cloudEnvNames = stringFlagValue(pi.getFlag("cursor-cloud-env"));
|
|
206
|
+
const cloud: NonNullable<CursorExplicitSdkConfig["cloud"]> = {
|
|
207
|
+
...(repo ? { repo } : {}),
|
|
208
|
+
...(branch ? { branch } : {}),
|
|
209
|
+
...(contextHandoff ? { contextHandoff } : {}),
|
|
210
|
+
...(pi.getFlag("cursor-cloud-direct-push") === true ? { directPush: true } : {}),
|
|
211
|
+
...(pi.getFlag("cursor-cloud-auto-create-pr") === true ? { autoCreatePR: true } : {}),
|
|
212
|
+
...(pi.getFlag("cursor-cloud-skip-reviewer-request") === true ? { skipReviewerRequest: true } : {}),
|
|
213
|
+
...(pi.getFlag("cursor-cloud-allow-local-state") === true ? { allowLocalState: true } : {}),
|
|
214
|
+
...(pi.getFlag("cursor-cloud-env-from-files") === true ? { envFromFiles: true } : {}),
|
|
215
|
+
...(environmentType || environmentName
|
|
216
|
+
? { environment: { ...(environmentType ? { type: environmentType } : {}), ...(environmentName ? { name: environmentName } : {}) } }
|
|
217
|
+
: {}),
|
|
218
|
+
...(pi.getFlag("cursor-cloud-ack") === true ? { acknowledged: true } : {}),
|
|
219
|
+
};
|
|
220
|
+
const local: NonNullable<CursorExplicitSdkConfig["local"]> = {
|
|
221
|
+
...(pi.getFlag("cursor-auto-review") === true ? { autoReview: true } : {}),
|
|
222
|
+
...(pi.getFlag("cursor-sandbox") === true ? { sandboxOptions: { enabled: true } } : {}),
|
|
223
|
+
...(!cliLocalForceConsumed && pi.getFlag("cursor-local-force") === true ? { force: true } : {}),
|
|
224
|
+
...(pi.getFlag("cursor-no-local-resume") === true
|
|
225
|
+
? { resume: false }
|
|
226
|
+
: pi.getFlag("cursor-local-resume") === true
|
|
227
|
+
? { resume: true }
|
|
228
|
+
: {}),
|
|
229
|
+
};
|
|
230
|
+
cliCursorSnapshot = {
|
|
231
|
+
config: {
|
|
232
|
+
...(runtime ? { runtime } : {}),
|
|
233
|
+
...(Object.keys(cloud).length ? { cloud } : {}),
|
|
234
|
+
...(Object.keys(local).length ? { local } : {}),
|
|
235
|
+
},
|
|
236
|
+
...(cloudEnvNames ? { cloudEnvNames } : {}),
|
|
237
|
+
};
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
function persistCursorRuntimePreference(
|
|
@@ -265,6 +277,16 @@ function registerCursorRuntimeFlags(pi: Pick<ExtensionAPI, "registerFlag">): voi
|
|
|
265
277
|
type: "boolean",
|
|
266
278
|
default: false,
|
|
267
279
|
});
|
|
280
|
+
pi.registerFlag("cursor-cloud-auto-create-pr", {
|
|
281
|
+
description: `Ask Cursor cloud to create a pull request for this run (or set ${CURSOR_CLOUD_AUTO_CREATE_PR_ENV}=1)`,
|
|
282
|
+
type: "boolean",
|
|
283
|
+
default: false,
|
|
284
|
+
});
|
|
285
|
+
pi.registerFlag("cursor-cloud-skip-reviewer-request", {
|
|
286
|
+
description: `Ask Cursor cloud not to request you as a reviewer for this run (or set ${CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV}=1)`,
|
|
287
|
+
type: "boolean",
|
|
288
|
+
default: false,
|
|
289
|
+
});
|
|
268
290
|
pi.registerFlag("cursor-cloud-allow-local-state", {
|
|
269
291
|
description: `Allow Cursor cloud to proceed with local-only state for this run (or set ${CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV}=1)`,
|
|
270
292
|
type: "boolean",
|
|
@@ -370,6 +392,13 @@ function registerCursorRuntimeCommand(
|
|
|
370
392
|
ctx.ui.notify(`${currentResolution.message} Fix the explicit override before changing the session runtime.`, "error");
|
|
371
393
|
return;
|
|
372
394
|
}
|
|
395
|
+
if (saveProject && (getCursorSessionCwd() !== ctx.cwd || !getCursorSessionProjectTrusted())) {
|
|
396
|
+
ctx.ui.notify(
|
|
397
|
+
"Cannot save Cursor project config without explicit project-trust provenance. Ensure .pi/settings.json or another Pi project resource exists, trust the project, then restart pi; or restart with --approve. Project-local package installs must use --approve on every run that reads or writes .pi/cursor-sdk.json.",
|
|
398
|
+
"error",
|
|
399
|
+
);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
373
402
|
const cloudAcknowledged = raw === "cloud" && await confirmCloudAcknowledgement(
|
|
374
403
|
ctx,
|
|
375
404
|
resolveEffectiveCursorConfigForContext(ctx).cloud.acknowledged.value,
|
|
@@ -378,14 +407,19 @@ function registerCursorRuntimeCommand(
|
|
|
378
407
|
if (saveUser || saveProject) {
|
|
379
408
|
try {
|
|
380
409
|
if (saveUser) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
410
|
+
updateCursorSdkConfig(
|
|
411
|
+
getCursorSdkUserConfigPath(),
|
|
412
|
+
(current) => mergeCursorSdkConfigForUpdate(current, {
|
|
413
|
+
runtime: raw,
|
|
414
|
+
...(cloudAcknowledged ? { cloud: { acknowledged: true } } : {}),
|
|
415
|
+
}),
|
|
416
|
+
{ newFileMode: 0o600 },
|
|
417
|
+
);
|
|
386
418
|
} else {
|
|
387
|
-
|
|
388
|
-
|
|
419
|
+
updateCursorSdkConfig(
|
|
420
|
+
getCursorSdkProjectConfigPath(ctx.cwd),
|
|
421
|
+
(current) => mergeCursorSdkConfigForUpdate(current, { runtime: raw }),
|
|
422
|
+
);
|
|
389
423
|
}
|
|
390
424
|
} catch (error) {
|
|
391
425
|
const effectiveResolution = resolveCursorStatusRuntime(ctx);
|
|
@@ -455,24 +489,9 @@ export function registerCursorCloudRuntimeControls(
|
|
|
455
489
|
}
|
|
456
490
|
|
|
457
491
|
export function resetCursorRuntimeStateForTests(): void {
|
|
458
|
-
|
|
459
|
-
cliSandbox = false;
|
|
460
|
-
cliLocalForce = false;
|
|
492
|
+
cliCursorSnapshot = { config: {} };
|
|
461
493
|
cliLocalForceConsumed = false;
|
|
462
|
-
cliLocalResume = false;
|
|
463
|
-
cliNoLocalResume = false;
|
|
464
494
|
envLocalForceConsumed = false;
|
|
465
|
-
cliCursorRuntime = undefined;
|
|
466
|
-
cliCursorCloudRepo = undefined;
|
|
467
|
-
cliCursorCloudBranch = undefined;
|
|
468
|
-
cliCursorCloudContext = undefined;
|
|
469
|
-
cliCursorCloudDirectPush = false;
|
|
470
|
-
cliCursorCloudAllowLocalState = false;
|
|
471
|
-
cliCursorCloudEnv = undefined;
|
|
472
|
-
cliCursorCloudEnvFromFiles = false;
|
|
473
|
-
cliCursorCloudEnvType = undefined;
|
|
474
|
-
cliCursorCloudEnvName = undefined;
|
|
475
|
-
cliCursorCloudAck = false;
|
|
476
495
|
sessionCursorRuntime = undefined;
|
|
477
496
|
sessionCursorCloudAcknowledged = false;
|
|
478
497
|
}
|
|
@@ -3,12 +3,14 @@ import { classifyCursorConnectError, isCursorSdkAbortConnectError } from "./curs
|
|
|
3
3
|
|
|
4
4
|
interface CursorSdkProcessErrorGuardToken {
|
|
5
5
|
suppressAbortErrors: boolean;
|
|
6
|
+
onLocalTransportClosedPipe?: () => void;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
interface CursorSdkSessionProcessErrorGuardToken {}
|
|
9
10
|
|
|
10
11
|
export interface CursorSdkProcessErrorGuard {
|
|
11
12
|
suppressAbortErrors(): void;
|
|
13
|
+
containLocalTransportClosedPipe(onClosedPipe: () => void): void;
|
|
12
14
|
dispose(): void;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -52,10 +54,66 @@ function isCursorSdkWriteIterableClosedError(error: unknown): boolean {
|
|
|
52
54
|
);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
// The Cursor SDK aborts an in-flight controlled-exec turn via its internal
|
|
58
|
+
// `AbortController.abort()` (user interrupt or stall-detector cancellation),
|
|
59
|
+
// which surfaces as a raw `DOMException [AbortError]` rather than a
|
|
60
|
+
// `ConnectError`. `classifyCursorConnectError` returns undefined for it, so it
|
|
61
|
+
// otherwise falls through the emit patch and terminates the process. A
|
|
62
|
+
// DOMException is not `instanceof Error`, so match structurally on the
|
|
63
|
+
// `AbortError` name plus the same `@cursor/sdk/dist` stack provenance the
|
|
64
|
+
// WriteIterableClosedError recognizer uses, keeping unrelated AbortErrors fatal.
|
|
65
|
+
function isCursorSdkAbortError(error: unknown): boolean {
|
|
66
|
+
if (typeof error !== "object" || error === null) return false;
|
|
67
|
+
const { name, stack } = error as { name?: unknown; stack?: unknown };
|
|
68
|
+
return (
|
|
69
|
+
name === "AbortError" &&
|
|
70
|
+
typeof stack === "string" &&
|
|
71
|
+
/(?:^|[\\/])node_modules[\\/]@cursor[\\/]sdk[\\/]dist[\\/]/.test(stack)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The exact observed incident: the Cursor SDK 1.0.23 local shell executor writes a
|
|
76
|
+
// spawned child's stdin without a stream 'error' listener, so a child exiting while
|
|
77
|
+
// a write is in flight surfaces a raw `write EPIPE` uncaught exception whose stack
|
|
78
|
+
// is exactly the single async pipe-write completion frame. Pi's own piped-stdout or
|
|
79
|
+
// dead-terminal EPIPE normally surfaces through the synchronous write-dispatch path
|
|
80
|
+
// with multiple frames (afterWriteDispatched/Socket._writeGeneric) and must stay
|
|
81
|
+
// fatal per Unix convention, so anything beyond this one-frame contract is rejected.
|
|
82
|
+
const OBSERVED_CLOSED_PIPE_STACK_FRAME =
|
|
83
|
+
/^\s+at WriteWrap\.onWriteComplete \[as oncomplete\] \(node:internal\/stream_base_commons:\d+:\d+\)$/;
|
|
84
|
+
|
|
85
|
+
function isObservedLocalTransportClosedPipeWriteError(error: unknown): boolean {
|
|
86
|
+
if (!(error instanceof Error) || error.name !== "Error") return false;
|
|
87
|
+
const { code, syscall } = error as NodeJS.ErrnoException;
|
|
88
|
+
if (code !== "EPIPE" || syscall !== "write" || !error.message.startsWith("write EPIPE")) return false;
|
|
89
|
+
const frames = (error.stack ?? "").split("\n").filter((line) => /^\s+at /.test(line));
|
|
90
|
+
return frames.length === 1 && OBSERVED_CLOSED_PIPE_STACK_FRAME.test(frames[0] ?? "");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Contained only while a provider turn that declared a local transport is active;
|
|
94
|
+
// each contained turn invalidates its own session-agent scope for recreation.
|
|
95
|
+
function containLocalTransportClosedPipeError(): boolean {
|
|
96
|
+
let contained = false;
|
|
97
|
+
for (const turn of [...activeProviderTurns]) {
|
|
98
|
+
if (!turn.onLocalTransportClosedPipe) continue;
|
|
99
|
+
contained = true;
|
|
100
|
+
try {
|
|
101
|
+
turn.onLocalTransportClosedPipe();
|
|
102
|
+
} catch {
|
|
103
|
+
// stale-agent invalidation must not throw inside process error handling
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return contained;
|
|
107
|
+
}
|
|
108
|
+
|
|
55
109
|
function shouldSuppressProcessError(event: string | symbol, args: readonly unknown[]): boolean {
|
|
56
110
|
if (event !== "uncaughtException" && event !== "unhandledRejection") return false;
|
|
57
111
|
const error = args[0];
|
|
112
|
+
if (isObservedLocalTransportClosedPipeWriteError(error)) {
|
|
113
|
+
return containLocalTransportClosedPipeError();
|
|
114
|
+
}
|
|
58
115
|
if (isCursorSdkWriteIterableClosedError(error)) return activeSessions.size > 0;
|
|
116
|
+
if (isCursorSdkAbortError(error)) return hasActiveAbortSuppression();
|
|
59
117
|
const classification = classifyCursorConnectError(error);
|
|
60
118
|
if (!classification) return false;
|
|
61
119
|
if (classification.kind === "abort") return hasActiveAbortSuppression();
|
|
@@ -137,6 +195,10 @@ export function installCursorSdkProcessErrorGuard(): CursorSdkProcessErrorGuard
|
|
|
137
195
|
if (disposed) return;
|
|
138
196
|
token.suppressAbortErrors = true;
|
|
139
197
|
},
|
|
198
|
+
containLocalTransportClosedPipe(onClosedPipe: () => void): void {
|
|
199
|
+
if (disposed) return;
|
|
200
|
+
token.onLocalTransportClosedPipe = onClosedPipe;
|
|
201
|
+
},
|
|
140
202
|
dispose(): void {
|
|
141
203
|
if (disposed) return;
|
|
142
204
|
disposed = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LocalAgentStore } from "@cursor/sdk";
|
|
1
2
|
import type { ExtensionAPI, ExtensionCommandContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { asRecord, getString } from "./cursor-record-utils.js";
|
|
3
4
|
import { fsyncExistingRegularFile } from "./cursor-durable-fs.js";
|
|
@@ -10,9 +11,15 @@ import {
|
|
|
10
11
|
parseCursorSessionAgentResumeEntryData,
|
|
11
12
|
readResumableCursorSessionAgentIds,
|
|
12
13
|
resolveCursorSessionRepoRoot,
|
|
14
|
+
type CursorSessionAgentCleanupCandidate,
|
|
13
15
|
type CursorSessionAgentResumeEntryData,
|
|
14
16
|
type CursorSessionAgentResumeScope,
|
|
15
17
|
} from "./cursor-session-agent-resume.js";
|
|
18
|
+
import {
|
|
19
|
+
cursorSessionStoreIdentitiesEqual,
|
|
20
|
+
getCursorSessionStoreIdentities,
|
|
21
|
+
openCursorSessionStore,
|
|
22
|
+
} from "./cursor-session-store.js";
|
|
16
23
|
|
|
17
24
|
export const CURSOR_SESSION_AGENT_CLEANUP_ENTRY_TYPE = "cursor-sdk-agent-cleanup";
|
|
18
25
|
|
|
@@ -22,6 +29,7 @@ type CleanupPhase = "intent" | "result";
|
|
|
22
29
|
export interface CursorSessionAgentCleanupFailure {
|
|
23
30
|
agentId: string;
|
|
24
31
|
error: string;
|
|
32
|
+
retryable?: boolean;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export interface CursorSessionAgentCleanupEntryData {
|
|
@@ -48,9 +56,11 @@ type LocalResumeCleanupCommandContext = Pick<ExtensionCommandContext, "cwd"> & {
|
|
|
48
56
|
ui: Pick<ExtensionCommandContext["ui"], "notify">;
|
|
49
57
|
};
|
|
50
58
|
type LocalResumeCleanupSdkOperations = {
|
|
51
|
-
delete(agentId: string, options?: { cwd?: string }): Promise<void>;
|
|
59
|
+
delete(agentId: string, options?: { cwd?: string; store?: LocalAgentStore }): Promise<void>;
|
|
52
60
|
};
|
|
53
61
|
|
|
62
|
+
class InvalidCursorSessionStoreIdentityError extends Error {}
|
|
63
|
+
|
|
54
64
|
// ponytail: grows for the process lifetime, but its ceiling is the exact agent IDs this process
|
|
55
65
|
// attempted to delete (never global) — it only fills the gap until this process exits; the durable
|
|
56
66
|
// intent entry, not this Set, is the authority a restarted process relies on to block retries.
|
|
@@ -106,7 +116,9 @@ function parseCleanupEntryData(value: unknown): CursorSessionAgentCleanupEntryDa
|
|
|
106
116
|
const failedAgentIds = Array.isArray(record.failedAgentIds)
|
|
107
117
|
? record.failedAgentIds.flatMap((item): CursorSessionAgentCleanupFailure[] => {
|
|
108
118
|
const failure = asRecord(item);
|
|
109
|
-
return isCursorLocalAgentId(failure?.agentId) && typeof failure.error === "string"
|
|
119
|
+
return isCursorLocalAgentId(failure?.agentId) && typeof failure.error === "string"
|
|
120
|
+
? [{ agentId: failure.agentId, error: failure.error, ...(failure.retryable === false ? { retryable: false } : {}) }]
|
|
121
|
+
: [];
|
|
110
122
|
})
|
|
111
123
|
: undefined;
|
|
112
124
|
return {
|
|
@@ -124,6 +136,7 @@ function parseCleanupEntryData(value: unknown): CursorSessionAgentCleanupEntryDa
|
|
|
124
136
|
function readUnavailableAgentIds(entries: readonly SessionEntry[]): Set<string> {
|
|
125
137
|
const deleted = new Set<string>();
|
|
126
138
|
const pending = new Set<string>();
|
|
139
|
+
const permanentlyFailed = new Set<string>();
|
|
127
140
|
for (const entry of entries) {
|
|
128
141
|
if (entry.type !== "custom" || entry.customType !== CURSOR_SESSION_AGENT_CLEANUP_ENTRY_TYPE) continue;
|
|
129
142
|
const data = parseCleanupEntryData(entry.data);
|
|
@@ -139,39 +152,57 @@ function readUnavailableAgentIds(entries: readonly SessionEntry[]): Set<string>
|
|
|
139
152
|
pending.delete(agentId);
|
|
140
153
|
}
|
|
141
154
|
if (data.phase === "result") {
|
|
142
|
-
for (const
|
|
155
|
+
for (const failure of data.failedAgentIds ?? []) {
|
|
156
|
+
pending.delete(failure.agentId);
|
|
157
|
+
if (failure.retryable === false) permanentlyFailed.add(failure.agentId);
|
|
158
|
+
}
|
|
143
159
|
}
|
|
144
160
|
}
|
|
145
|
-
return new Set([...deleted, ...pending, ...nondurableCleanupResultAgentIds]);
|
|
161
|
+
return new Set([...deleted, ...pending, ...permanentlyFailed, ...nondurableCleanupResultAgentIds]);
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
function readLatestBranchAgentId(branch: readonly SessionEntry[], scope: CursorSessionAgentCleanupScope): string | undefined {
|
|
149
165
|
return readResumeEntries(branch).filter((entry) => resumeEntryMatchesCleanupScope(entry, scope)).at(-1)?.agentId;
|
|
150
166
|
}
|
|
151
167
|
|
|
152
|
-
|
|
168
|
+
function readCursorSessionAgentCleanupPlanDetails(
|
|
153
169
|
entries: readonly SessionEntry[],
|
|
154
170
|
branch: readonly SessionEntry[],
|
|
155
171
|
scope: CursorSessionAgentCleanupScope,
|
|
156
|
-
): CursorSessionAgentCleanupPlan {
|
|
172
|
+
): CursorSessionAgentCleanupPlan & { candidates: CursorSessionAgentCleanupCandidate[] } {
|
|
157
173
|
const unavailable = readUnavailableAgentIds(entries);
|
|
158
174
|
const latestBranchAgentId = readLatestBranchAgentId(branch, scope);
|
|
159
175
|
const protectedAgentIds = new Set(readResumableCursorSessionAgentIds(entries, scope));
|
|
160
176
|
if (latestBranchAgentId && isCursorLocalAgentId(latestBranchAgentId)) protectedAgentIds.add(latestBranchAgentId);
|
|
161
|
-
const candidates = new
|
|
177
|
+
const candidates = new Map<string, CursorSessionAgentCleanupCandidate>();
|
|
162
178
|
for (const resume of readResumeEntries(entries)) {
|
|
163
179
|
if (!resumeEntryMatchesCleanupScope(resume, scope)) continue;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
const recordedCandidates: CursorSessionAgentCleanupCandidate[] = [
|
|
181
|
+
...(resume.cleanupCandidateAgentIds ?? []).map((agentId) => ({ agentId })),
|
|
182
|
+
...(resume.cleanupCandidates ?? []),
|
|
183
|
+
];
|
|
184
|
+
for (const candidate of recordedCandidates) {
|
|
185
|
+
if (!isCursorLocalAgentId(candidate.agentId) || protectedAgentIds.has(candidate.agentId) || unavailable.has(candidate.agentId)) continue;
|
|
186
|
+
const existing = candidates.get(candidate.agentId);
|
|
187
|
+
if (!existing?.storeIdentity || candidate.storeIdentity) candidates.set(candidate.agentId, candidate);
|
|
167
188
|
}
|
|
168
189
|
}
|
|
169
190
|
return {
|
|
170
|
-
|
|
191
|
+
candidates: [...candidates.values()].sort((left, right) => left.agentId.localeCompare(right.agentId)),
|
|
192
|
+
candidateAgentIds: uniqueSorted([...candidates.values()].map((candidate) => candidate.agentId)),
|
|
171
193
|
protectedAgentIds: uniqueSorted(protectedAgentIds),
|
|
172
194
|
};
|
|
173
195
|
}
|
|
174
196
|
|
|
197
|
+
export function readCursorSessionAgentCleanupPlan(
|
|
198
|
+
entries: readonly SessionEntry[],
|
|
199
|
+
branch: readonly SessionEntry[],
|
|
200
|
+
scope: CursorSessionAgentCleanupScope,
|
|
201
|
+
): CursorSessionAgentCleanupPlan {
|
|
202
|
+
const { candidates: _, ...plan } = readCursorSessionAgentCleanupPlanDetails(entries, branch, scope);
|
|
203
|
+
return plan;
|
|
204
|
+
}
|
|
205
|
+
|
|
175
206
|
function formatCleanupPlan(plan: CursorSessionAgentCleanupPlan): string {
|
|
176
207
|
if (plan.candidateAgentIds.length === 0) return "No recorded superseded local Cursor SDK agents are cleanup-eligible.";
|
|
177
208
|
return [
|
|
@@ -245,7 +276,8 @@ export async function runCursorSessionAgentCleanupCommand(pi: LocalResumeCleanup
|
|
|
245
276
|
|
|
246
277
|
const entries = ctx.sessionManager.getEntries();
|
|
247
278
|
const branch = ctx.sessionManager.getBranch();
|
|
248
|
-
const
|
|
279
|
+
const scope = getCurrentCleanupScope(ctx);
|
|
280
|
+
const plan = readCursorSessionAgentCleanupPlanDetails(entries, branch, scope);
|
|
249
281
|
const baseEntry = {
|
|
250
282
|
runtime: "local" as const,
|
|
251
283
|
timestamp: new Date().toISOString(),
|
|
@@ -274,19 +306,38 @@ export async function runCursorSessionAgentCleanupCommand(pi: LocalResumeCleanup
|
|
|
274
306
|
|
|
275
307
|
const deletedAgentIds: string[] = [];
|
|
276
308
|
const failedAgentIds: CursorSessionAgentCleanupFailure[] = [];
|
|
309
|
+
const openedStores = new Map<string, Awaited<ReturnType<typeof openCursorSessionStore>>>();
|
|
277
310
|
try {
|
|
278
311
|
const operations = await getSdkOperations();
|
|
279
|
-
|
|
312
|
+
const identities = await getCursorSessionStoreIdentities(ctx.cwd, scope.scopeKey, scope.sessionFile !== undefined);
|
|
313
|
+
for (const candidate of plan.candidates) {
|
|
314
|
+
const { agentId } = candidate;
|
|
280
315
|
try {
|
|
281
|
-
|
|
316
|
+
const identity = candidate.storeIdentity ?? identities.defaultStore;
|
|
317
|
+
if (
|
|
318
|
+
!cursorSessionStoreIdentitiesEqual(identity, identities.defaultStore) &&
|
|
319
|
+
!cursorSessionStoreIdentitiesEqual(identity, identities.sessionStore)
|
|
320
|
+
) throw new InvalidCursorSessionStoreIdentityError("Recorded Cursor local store identity is not valid for this pi session");
|
|
321
|
+
let openedStore = openedStores.get(identity.stateRoot);
|
|
322
|
+
if (!openedStore) {
|
|
323
|
+
openedStore = await openCursorSessionStore(ctx.cwd, identity);
|
|
324
|
+
openedStores.set(identity.stateRoot, openedStore);
|
|
325
|
+
}
|
|
326
|
+
await operations.delete(agentId, { cwd: ctx.cwd, store: openedStore.store });
|
|
282
327
|
deletedAgentIds.push(agentId);
|
|
283
328
|
} catch (error) {
|
|
284
|
-
failedAgentIds.push({
|
|
329
|
+
failedAgentIds.push({
|
|
330
|
+
agentId,
|
|
331
|
+
error: scrubSensitiveText(getString(asRecord(error), "message") ?? String(error)),
|
|
332
|
+
...(error instanceof InvalidCursorSessionStoreIdentityError ? { retryable: false } : {}),
|
|
333
|
+
});
|
|
285
334
|
}
|
|
286
335
|
}
|
|
287
336
|
} catch (error) {
|
|
288
337
|
const message = scrubSensitiveText(getString(asRecord(error), "message") ?? String(error));
|
|
289
338
|
failedAgentIds.push(...plan.candidateAgentIds.map((agentId) => ({ agentId, error: message })));
|
|
339
|
+
} finally {
|
|
340
|
+
await Promise.all([...openedStores.values()].map((store) => store.dispose().catch(() => undefined)));
|
|
290
341
|
}
|
|
291
342
|
if (!appendDurableCleanupEntry(pi, ctx, {
|
|
292
343
|
action: "delete",
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
SessionShutdownEvent,
|
|
6
6
|
SessionTreeEvent,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { clearCursorSdkHttp1 } from "./cursor-http1.js";
|
|
8
9
|
import { onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
|
|
9
10
|
|
|
10
11
|
export interface CursorSessionAgentLifecycleExtensionApi {
|
|
@@ -22,11 +23,15 @@ export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecy
|
|
|
22
23
|
});
|
|
23
24
|
pi.on("session_shutdown", async (event) => {
|
|
24
25
|
const { disposeSessionCursorAgent, resetSessionCursorAgent } = await import("./cursor-session-agent.js");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
try {
|
|
27
|
+
if (event.reason === "reload") {
|
|
28
|
+
await resetSessionCursorAgent();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await disposeSessionCursorAgent();
|
|
32
|
+
} finally {
|
|
33
|
+
clearCursorSdkHttp1();
|
|
28
34
|
}
|
|
29
|
-
await disposeSessionCursorAgent();
|
|
30
35
|
});
|
|
31
36
|
pi.on("session_compact", async () => {
|
|
32
37
|
const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
|