pi-cursor-sdk 0.4.0 → 0.5.0
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 +30 -0
- package/README.md +65 -13
- package/dist/context-window-cache.js +0 -6
- package/dist/context.js +8 -0
- package/dist/cursor-cloud-lifecycle.js +122 -98
- package/dist/cursor-durable-fs.js +97 -3
- package/dist/cursor-fallback-models.generated.js +288 -1
- package/dist/cursor-fallback-warning.js +2 -2
- package/dist/cursor-http1.js +8 -8
- package/dist/cursor-live-run-coordinator.js +9 -13
- package/dist/cursor-native-replay-routing.js +1 -1
- package/dist/cursor-native-tool-display-registration.js +46 -22
- package/dist/cursor-native-tool-display-state.js +57 -22
- package/dist/cursor-native-tool-display-tools.js +5 -6
- package/dist/cursor-pi-tool-bridge-constants.js +23 -2
- package/dist/cursor-pi-tool-bridge-run.js +7 -3
- package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
- package/dist/cursor-pi-tool-bridge.js +31 -9
- package/dist/cursor-provider-binding.js +58 -0
- package/dist/cursor-provider-errors.js +28 -7
- package/dist/cursor-provider-lazy.js +23 -11
- package/dist/cursor-provider-live-run-drain.js +15 -12
- package/dist/cursor-provider-run-finalizer.js +19 -8
- package/dist/cursor-provider-turn-coordinator.js +1 -0
- package/dist/cursor-provider-turn-display-router.js +4 -0
- package/dist/cursor-provider-turn-emit.js +1 -0
- package/dist/cursor-provider-turn-finalize.js +29 -20
- package/dist/cursor-provider-turn-prepare.js +24 -11
- package/dist/cursor-provider-turn-runner.js +37 -11
- package/dist/cursor-provider-turn-send.js +22 -9
- package/dist/cursor-provider-turn-summary.js +89 -0
- package/dist/cursor-provider.js +9 -3
- package/dist/cursor-request-provenance.js +42 -0
- package/dist/cursor-ripgrep-path.js +14 -3
- package/dist/cursor-runtime-state.js +50 -43
- package/dist/cursor-sdk-billed-usage.js +54 -75
- package/dist/cursor-sdk-event-debug-session.js +5 -6
- package/dist/cursor-sdk-event-debug.js +9 -6
- package/dist/cursor-sdk-process-error-guard.js +8 -7
- package/dist/cursor-sdk-runtime.js +13 -0
- package/dist/cursor-session-agent-cleanup.js +2 -2
- package/dist/cursor-session-agent-lifecycle.js +18 -9
- package/dist/cursor-session-agent-lineage.js +28 -12
- package/dist/cursor-session-agent-resume.js +51 -35
- package/dist/cursor-session-agent.js +9 -9
- package/dist/cursor-session-compaction-prep.js +2 -7
- package/dist/cursor-session-scope.js +71 -39
- package/dist/cursor-session-settings.js +61 -0
- package/dist/cursor-skill-tool.js +32 -49
- package/dist/cursor-state.js +72 -62
- package/dist/cursor-usage-accounting.js +13 -70
- package/dist/cursor-usage-command.js +89 -0
- package/dist/cursor-usage-ledger.js +512 -0
- package/dist/index.js +11 -19
- package/dist/model-discovery.js +11 -19
- package/docs/cursor-dogfood-checklist.md +3 -3
- package/docs/cursor-live-smoke-checklist.md +15 -15
- package/docs/cursor-model-ux-spec.md +59 -18
- package/docs/cursor-native-tool-replay.md +5 -3
- package/docs/cursor-native-tool-visual-audit.md +8 -8
- package/docs/cursor-testing-lessons.md +47 -23
- package/docs/cursor-tool-surfaces.md +1 -1
- package/docs/platform-smoke-implementation.md +10 -6
- package/docs/platform-smoke.md +35 -46
- package/package.json +6 -6
- package/scripts/cloud-runtime-smoke.mjs +4 -2
- package/scripts/local-resume-smoke.mjs +1 -1
- package/scripts/platform-smoke/card-detect.mjs +2 -8
- package/scripts/platform-smoke/targets.mjs +8 -15
- package/scripts/platform-smoke/visual-evidence.mjs +7 -2
- package/scripts/tmux-live-smoke.sh +57 -90
- package/src/context-window-cache.ts +0 -6
- package/src/context.ts +8 -0
- package/src/cursor-cloud-lifecycle.ts +124 -98
- package/src/cursor-durable-fs.ts +65 -3
- package/src/cursor-fallback-models.generated.ts +288 -1
- package/src/cursor-fallback-warning.ts +2 -2
- package/src/cursor-http1.ts +8 -8
- package/src/cursor-live-run-coordinator.ts +12 -13
- package/src/cursor-native-replay-routing.ts +2 -1
- package/src/cursor-native-tool-display-registration.ts +48 -24
- package/src/cursor-native-tool-display-state.ts +65 -21
- package/src/cursor-native-tool-display-tools.ts +7 -4
- package/src/cursor-pi-tool-bridge-constants.ts +27 -2
- package/src/cursor-pi-tool-bridge-run.ts +11 -3
- package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
- package/src/cursor-pi-tool-bridge.ts +26 -9
- package/src/cursor-provider-binding.ts +62 -0
- package/src/cursor-provider-errors.ts +27 -7
- package/src/cursor-provider-lazy.ts +21 -15
- package/src/cursor-provider-live-run-drain.ts +20 -13
- package/src/cursor-provider-run-finalizer.ts +24 -15
- package/src/cursor-provider-turn-coordinator.ts +2 -0
- package/src/cursor-provider-turn-display-router.ts +5 -0
- package/src/cursor-provider-turn-emit.ts +3 -1
- package/src/cursor-provider-turn-finalize.ts +38 -23
- package/src/cursor-provider-turn-prepare.ts +23 -14
- package/src/cursor-provider-turn-runner.ts +40 -13
- package/src/cursor-provider-turn-send.ts +23 -15
- package/src/cursor-provider-turn-summary.ts +94 -0
- package/src/cursor-provider-turn-types.ts +37 -4
- package/src/cursor-provider.ts +25 -3
- package/src/cursor-request-provenance.ts +64 -0
- package/src/cursor-ripgrep-path.ts +15 -5
- package/src/cursor-runtime-state.ts +53 -48
- package/src/cursor-sdk-billed-usage.ts +54 -102
- package/src/cursor-sdk-event-debug-session.ts +5 -5
- package/src/cursor-sdk-event-debug.ts +10 -6
- package/src/cursor-sdk-process-error-guard.ts +7 -7
- package/src/cursor-sdk-runtime.ts +14 -0
- package/src/cursor-session-agent-cleanup.ts +2 -2
- package/src/cursor-session-agent-lifecycle.ts +14 -9
- package/src/cursor-session-agent-lineage.ts +26 -12
- package/src/cursor-session-agent-resume.ts +49 -36
- package/src/cursor-session-agent.ts +13 -10
- package/src/cursor-session-compaction-prep.ts +2 -7
- package/src/cursor-session-scope.ts +87 -39
- package/src/cursor-session-settings.ts +72 -0
- package/src/cursor-skill-tool.ts +37 -61
- package/src/cursor-state.ts +74 -67
- package/src/cursor-usage-accounting.ts +17 -82
- package/src/cursor-usage-command.ts +76 -0
- package/src/cursor-usage-ledger.ts +500 -0
- package/src/index.ts +14 -23
- package/src/model-discovery.ts +14 -22
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCursorLocalAgentId } from "./cursor-session-agent-resume.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
|
|
3
3
|
import { asRecord } from "./cursor-record-utils.js";
|
|
4
4
|
export const CURSOR_SESSION_AGENT_LINEAGE_ENTRY_TYPE = "cursor-sdk-agent-lineage";
|
|
5
5
|
const LINEAGE_ENTRY_VERSION = 1;
|
|
@@ -9,9 +9,10 @@ function isIsoTimestamp(value) {
|
|
|
9
9
|
const timestamp = Date.parse(value);
|
|
10
10
|
return !Number.isNaN(timestamp) && new Date(timestamp).toISOString() === value;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
let state = {
|
|
13
13
|
recordedAgentIds: new Set(),
|
|
14
14
|
};
|
|
15
|
+
const statesByScope = new Map();
|
|
15
16
|
export function parseCursorSessionAgentLineageEntryData(value) {
|
|
16
17
|
const record = asRecord(value);
|
|
17
18
|
if (record?.version !== LINEAGE_ENTRY_VERSION ||
|
|
@@ -48,11 +49,16 @@ function readRecordedAgentIds(entries, sessionId) {
|
|
|
48
49
|
}));
|
|
49
50
|
}
|
|
50
51
|
/** Best-effort forensic lineage at the local Agent.send() boundary. Independent of resume. */
|
|
51
|
-
export function recordCursorSessionAgentLineage(agentId) {
|
|
52
|
-
const
|
|
52
|
+
export function recordCursorSessionAgentLineage(agentId, turnScopeKey) {
|
|
53
|
+
const sessionState = turnScopeKey === undefined
|
|
54
|
+
? state
|
|
55
|
+
: statesByScope.get(turnScopeKey) ?? (state.scopeKey === turnScopeKey ? state : undefined);
|
|
56
|
+
if (!sessionState)
|
|
57
|
+
return;
|
|
58
|
+
const { appendEntry, sessionId, sessionFile, scopeKey, cwd } = sessionState;
|
|
53
59
|
if (!appendEntry || !sessionId || !scopeKey || !cwd)
|
|
54
60
|
return;
|
|
55
|
-
if (!isCursorLocalAgentId(agentId) ||
|
|
61
|
+
if (!isCursorLocalAgentId(agentId) || sessionState.recordedAgentIds.has(agentId))
|
|
56
62
|
return;
|
|
57
63
|
const data = {
|
|
58
64
|
version: LINEAGE_ENTRY_VERSION,
|
|
@@ -66,22 +72,31 @@ export function recordCursorSessionAgentLineage(agentId) {
|
|
|
66
72
|
};
|
|
67
73
|
try {
|
|
68
74
|
appendEntry(CURSOR_SESSION_AGENT_LINEAGE_ENTRY_TYPE, data);
|
|
69
|
-
|
|
75
|
+
sessionState.recordedAgentIds.add(agentId);
|
|
70
76
|
}
|
|
71
77
|
catch {
|
|
72
78
|
// Lineage is forensic metadata; a failed stock pi append must not affect the session.
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
export function registerCursorSessionAgentLineage(pi) {
|
|
82
|
+
const sessionState = { recordedAgentIds: new Set() };
|
|
76
83
|
pi.on("session_start", (_event, ctx) => {
|
|
77
|
-
state
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
state = sessionState;
|
|
85
|
+
const record = sessionState;
|
|
86
|
+
if (record.scopeKey && statesByScope.get(record.scopeKey) === record)
|
|
87
|
+
statesByScope.delete(record.scopeKey);
|
|
88
|
+
record.appendEntry = pi.appendEntry;
|
|
89
|
+
record.sessionId = ctx.sessionManager.getSessionId();
|
|
90
|
+
record.sessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
|
|
91
|
+
record.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
|
|
92
|
+
statesByScope.set(record.scopeKey, record);
|
|
93
|
+
record.cwd = ctx.cwd;
|
|
94
|
+
record.recordedAgentIds = readRecordedAgentIds(ctx.sessionManager.getEntries(), record.sessionId);
|
|
83
95
|
});
|
|
84
96
|
pi.on("session_shutdown", () => {
|
|
97
|
+
const state = sessionState;
|
|
98
|
+
if (state.scopeKey && statesByScope.get(state.scopeKey) === state)
|
|
99
|
+
statesByScope.delete(state.scopeKey);
|
|
85
100
|
state.appendEntry = undefined;
|
|
86
101
|
state.sessionId = undefined;
|
|
87
102
|
state.sessionFile = undefined;
|
|
@@ -91,6 +106,7 @@ export function registerCursorSessionAgentLineage(pi) {
|
|
|
91
106
|
});
|
|
92
107
|
}
|
|
93
108
|
function resetStateForTests() {
|
|
109
|
+
statesByScope.clear();
|
|
94
110
|
state.appendEntry = undefined;
|
|
95
111
|
state.sessionId = undefined;
|
|
96
112
|
state.sessionFile = undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { asRecord } from "./cursor-record-utils.js";
|
|
4
|
-
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
4
|
+
import { getCursorSessionScopeSnapshot, getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
5
5
|
export const CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE = "cursor-sdk-agent-resume";
|
|
6
6
|
const LEGACY_RESUME_ENTRY_VERSION = 1;
|
|
7
7
|
const RESUME_ENTRY_VERSION = 2;
|
|
@@ -11,22 +11,19 @@ const EMPTY_BRANCH_HASH = hashParts(["cursor-sdk-agent-resume-branch", "v1"]);
|
|
|
11
11
|
export function isCursorLocalAgentId(value) {
|
|
12
12
|
return typeof value === "string" && value.length <= MAX_LOCAL_AGENT_ID_LENGTH && /^agent-[A-Za-z0-9_-]+$/.test(value);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
let lastBoundState = {
|
|
15
15
|
scopeKey: getCursorSessionScopeKey(),
|
|
16
16
|
cwd: process.cwd(),
|
|
17
17
|
branchPathHash: EMPTY_BRANCH_HASH,
|
|
18
18
|
compactionGeneration: 0,
|
|
19
19
|
unownedUserEntryIds: new Set(),
|
|
20
20
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
export function allowCursorSessionAgentResumeHandlePersist() {
|
|
29
|
-
resumeHandlePersistSuppressed = false;
|
|
21
|
+
const statesByScope = new Map();
|
|
22
|
+
function getResumeState(scopeKey) {
|
|
23
|
+
if (scopeKey !== undefined) {
|
|
24
|
+
return statesByScope.get(scopeKey) ?? lastBoundState;
|
|
25
|
+
}
|
|
26
|
+
return lastBoundState;
|
|
30
27
|
}
|
|
31
28
|
function hashParts(parts) {
|
|
32
29
|
const hash = createHash("sha256");
|
|
@@ -142,7 +139,7 @@ function matchesResumeScope(data, scope) {
|
|
|
142
139
|
data.cwd === scope.cwd &&
|
|
143
140
|
data.repoRoot === scope.repoRoot;
|
|
144
141
|
}
|
|
145
|
-
function matchesCurrentSession(data, branchPathHash, compactionGeneration
|
|
142
|
+
function matchesCurrentSession(data, branchPathHash, compactionGeneration, state) {
|
|
146
143
|
return matchesResumeScope(data, state) &&
|
|
147
144
|
data.compactionGeneration === compactionGeneration &&
|
|
148
145
|
data.branchPathHash === branchPathHash;
|
|
@@ -244,19 +241,19 @@ export function readResumableCursorSessionAgentIds(entries, scope) {
|
|
|
244
241
|
}
|
|
245
242
|
return [...agentIds].sort((a, b) => a.localeCompare(b));
|
|
246
243
|
}
|
|
247
|
-
function canRestoreHandleSpanEntry(entry) {
|
|
244
|
+
function canRestoreHandleSpanEntry(entry, state) {
|
|
248
245
|
if (entry.type === "message" && entry.message.role === "user")
|
|
249
246
|
return !state.unownedUserEntryIds.has(entry.id);
|
|
250
247
|
return canResumeHandleSpanEntry(entry);
|
|
251
248
|
}
|
|
252
|
-
function restoreFromBranch(branch, allEntries
|
|
249
|
+
function restoreFromBranch(branch, allEntries, state) {
|
|
253
250
|
const resumeIndex = indexLatestResumeEntries(allEntries);
|
|
254
251
|
let fold = { branchPathHash: EMPTY_BRANCH_HASH, compactionGeneration: 0 };
|
|
255
252
|
let lastBranchHandle;
|
|
256
253
|
for (const entry of branch) {
|
|
257
254
|
const next = advanceResumeBranchState(entry, fold, resumeIndex, {
|
|
258
|
-
matchesEntry: matchesCurrentSession,
|
|
259
|
-
canSpanEntry: canRestoreHandleSpanEntry,
|
|
255
|
+
matchesEntry: (data, hash, generation) => matchesCurrentSession(data, hash, generation, state),
|
|
256
|
+
canSpanEntry: (entry) => canRestoreHandleSpanEntry(entry, state),
|
|
260
257
|
});
|
|
261
258
|
if (next.activeHandle && next.activeHandle !== fold.activeHandle)
|
|
262
259
|
lastBranchHandle = next.activeHandle;
|
|
@@ -267,7 +264,10 @@ function restoreFromBranch(branch, allEntries = branch) {
|
|
|
267
264
|
state.activeHandle = fold.activeHandle;
|
|
268
265
|
state.lastBranchHandle = lastBranchHandle;
|
|
269
266
|
}
|
|
270
|
-
export function getMatchingCursorSessionAgentResumeHandle(poolKey) {
|
|
267
|
+
export function getMatchingCursorSessionAgentResumeHandle(poolKey, scopeKey) {
|
|
268
|
+
const state = getResumeState(scopeKey);
|
|
269
|
+
if (scopeKey !== undefined && state.scopeKey !== scopeKey)
|
|
270
|
+
return undefined;
|
|
271
271
|
const handle = state.activeHandle;
|
|
272
272
|
if (!handle || !isCursorLocalAgentId(handle.agentId))
|
|
273
273
|
return undefined;
|
|
@@ -290,8 +290,9 @@ export function getMatchingCursorSessionAgentResumeHandle(poolKey) {
|
|
|
290
290
|
sendState: { ...handle.sendState },
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
|
-
export function persistCursorSessionAgentResumeHandle(input) {
|
|
294
|
-
|
|
293
|
+
export function persistCursorSessionAgentResumeHandle(input, scopeKey) {
|
|
294
|
+
const state = getResumeState(scopeKey);
|
|
295
|
+
if (scopeKey !== undefined && state.scopeKey !== scopeKey)
|
|
295
296
|
return;
|
|
296
297
|
if (!isCursorLocalAgentId(input.agentId))
|
|
297
298
|
return;
|
|
@@ -303,13 +304,8 @@ export function persistCursorSessionAgentResumeHandle(input) {
|
|
|
303
304
|
storeIdentity: { ...input.storeIdentity },
|
|
304
305
|
};
|
|
305
306
|
}
|
|
306
|
-
function flushPendingCursorSessionAgentResumeHandle(branch) {
|
|
307
|
-
|
|
308
|
-
state.pendingHandle = undefined;
|
|
309
|
-
restoreFromBranch(branch);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
restoreFromBranch(branch);
|
|
307
|
+
function flushPendingCursorSessionAgentResumeHandle(branch, state) {
|
|
308
|
+
restoreFromBranch(branch, branch, state);
|
|
313
309
|
const pending = state.pendingHandle;
|
|
314
310
|
state.pendingHandle = undefined;
|
|
315
311
|
if (!pending || !state.appendEntry)
|
|
@@ -347,14 +343,23 @@ function flushPendingCursorSessionAgentResumeHandle(branch) {
|
|
|
347
343
|
}
|
|
348
344
|
}
|
|
349
345
|
export function registerCursorSessionAgentResume(pi) {
|
|
350
|
-
|
|
346
|
+
const sessionState = {
|
|
347
|
+
appendEntry: pi.appendEntry, scopeKey: getCursorSessionScopeSnapshot(pi).scopeKey,
|
|
348
|
+
cwd: process.cwd(), branchPathHash: EMPTY_BRANCH_HASH,
|
|
349
|
+
compactionGeneration: 0, unownedUserEntryIds: new Set(),
|
|
350
|
+
};
|
|
351
|
+
const state = sessionState;
|
|
351
352
|
const restoreFromSessionManager = (sessionManager) => {
|
|
352
353
|
const branch = sessionManager.getBranch();
|
|
353
354
|
const entries = sessionManager.getEntries();
|
|
354
|
-
restoreFromBranch(branch, entries.length > 0 ? entries : branch);
|
|
355
|
+
restoreFromBranch(branch, entries.length > 0 ? entries : branch, state);
|
|
355
356
|
};
|
|
356
357
|
pi.on("session_start", (_event, ctx) => {
|
|
357
|
-
state.scopeKey
|
|
358
|
+
if (statesByScope.get(state.scopeKey) === state)
|
|
359
|
+
statesByScope.delete(state.scopeKey);
|
|
360
|
+
state.scopeKey = getCursorSessionScopeSnapshot(pi).scopeKey;
|
|
361
|
+
statesByScope.set(state.scopeKey, state);
|
|
362
|
+
lastBoundState = state;
|
|
358
363
|
state.sessionFile = ctx.sessionManager.getSessionFile?.() ?? undefined;
|
|
359
364
|
state.sessionId = ctx.sessionManager.getSessionId?.() ?? undefined;
|
|
360
365
|
state.cwd = ctx.cwd;
|
|
@@ -366,7 +371,7 @@ export function registerCursorSessionAgentResume(pi) {
|
|
|
366
371
|
restoreFromSessionManager(ctx.sessionManager);
|
|
367
372
|
});
|
|
368
373
|
pi.on("turn_end", (_event, ctx) => {
|
|
369
|
-
flushPendingCursorSessionAgentResumeHandle(ctx.sessionManager.getBranch());
|
|
374
|
+
flushPendingCursorSessionAgentResumeHandle(ctx.sessionManager.getBranch(), state);
|
|
370
375
|
});
|
|
371
376
|
pi.on("session_tree", (_event, ctx) => {
|
|
372
377
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
@@ -375,9 +380,14 @@ export function registerCursorSessionAgentResume(pi) {
|
|
|
375
380
|
}
|
|
376
381
|
restoreFromSessionManager(ctx.sessionManager);
|
|
377
382
|
});
|
|
383
|
+
pi.on("session_before_compact", () => {
|
|
384
|
+
state.pendingHandle = undefined;
|
|
385
|
+
});
|
|
386
|
+
pi.on("session_compact_failed", () => {
|
|
387
|
+
state.pendingHandle = undefined;
|
|
388
|
+
});
|
|
378
389
|
pi.on("session_compact", (event, ctx) => {
|
|
379
390
|
state.pendingHandle = undefined;
|
|
380
|
-
resumeHandlePersistSuppressed = false;
|
|
381
391
|
const branch = ctx.sessionManager.getBranch();
|
|
382
392
|
if (branch.length > 0) {
|
|
383
393
|
restoreFromSessionManager(ctx.sessionManager);
|
|
@@ -388,11 +398,18 @@ export function registerCursorSessionAgentResume(pi) {
|
|
|
388
398
|
state.compactionGeneration += 1;
|
|
389
399
|
state.branchPathHash = hashBranchStep(state.branchPathHash, event.compactionEntry);
|
|
390
400
|
});
|
|
401
|
+
pi.on("session_shutdown", () => {
|
|
402
|
+
if (statesByScope.get(state.scopeKey) === state)
|
|
403
|
+
statesByScope.delete(state.scopeKey);
|
|
404
|
+
state.appendEntry = undefined;
|
|
405
|
+
state.pendingHandle = undefined;
|
|
406
|
+
});
|
|
391
407
|
}
|
|
392
408
|
function setStateForTests(next) {
|
|
393
|
-
Object.assign(
|
|
409
|
+
Object.assign(getResumeState(), next);
|
|
394
410
|
}
|
|
395
411
|
function resetStateForTests() {
|
|
412
|
+
const state = lastBoundState;
|
|
396
413
|
state.appendEntry = undefined;
|
|
397
414
|
state.scopeKey = getCursorSessionScopeKey();
|
|
398
415
|
state.sessionFile = undefined;
|
|
@@ -405,13 +422,12 @@ function resetStateForTests() {
|
|
|
405
422
|
state.lastBranchHandle = undefined;
|
|
406
423
|
state.pendingHandle = undefined;
|
|
407
424
|
state.unownedUserEntryIds = new Set();
|
|
408
|
-
|
|
425
|
+
statesByScope.clear();
|
|
409
426
|
}
|
|
410
427
|
export const __testUtils = {
|
|
411
428
|
EMPTY_BRANCH_HASH,
|
|
412
429
|
hashBranchStep,
|
|
413
430
|
reset: resetStateForTests,
|
|
414
431
|
set: setStateForTests,
|
|
415
|
-
state,
|
|
416
|
-
isResumeHandlePersistSuppressed: () => resumeHandlePersistSuppressed,
|
|
432
|
+
get state() { return getResumeState(); },
|
|
417
433
|
};
|
|
@@ -82,8 +82,7 @@ function buildLocalSafetyPoolKey(localSafety) {
|
|
|
82
82
|
function buildApiKeyPoolKeyFingerprint(apiKey) {
|
|
83
83
|
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
84
84
|
}
|
|
85
|
-
function buildBridgePoolKeySuffix() {
|
|
86
|
-
const registeredBridge = getRegisteredCursorPiToolBridge();
|
|
85
|
+
function buildBridgePoolKeySuffix(registeredBridge) {
|
|
87
86
|
if (!registeredBridge)
|
|
88
87
|
return "bridge:absent";
|
|
89
88
|
return registeredBridge.getToolSurfaceSignature();
|
|
@@ -101,7 +100,7 @@ function buildSessionAgentPoolKey(scopeKey, params) {
|
|
|
101
100
|
? "http1:on"
|
|
102
101
|
: "http1:off",
|
|
103
102
|
buildApiKeyPoolKeyFingerprint(params.apiKey),
|
|
104
|
-
buildBridgePoolKeySuffix(),
|
|
103
|
+
buildBridgePoolKeySuffix("bridge" in params ? params.bridge : getRegisteredCursorPiToolBridge()),
|
|
105
104
|
].join("\0");
|
|
106
105
|
}
|
|
107
106
|
async function disposePoolEntry(entry, options) {
|
|
@@ -179,7 +178,7 @@ function commitSessionAgentSendForLease(scopeKey, poolKey, instanceId, context,
|
|
|
179
178
|
poolKey: entry.poolKey,
|
|
180
179
|
sendState: entry.sendState,
|
|
181
180
|
storeIdentity: entry.sessionStore.identity,
|
|
182
|
-
});
|
|
181
|
+
}, scopeKey);
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
184
|
function normalizeRunCompletion(completion) {
|
|
@@ -279,7 +278,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
279
278
|
let bridgeRun;
|
|
280
279
|
let sessionStore;
|
|
281
280
|
try {
|
|
282
|
-
const registeredBridge =
|
|
281
|
+
const registeredBridge = params.bridge;
|
|
283
282
|
if (registeredBridge) {
|
|
284
283
|
bridgeRun = await registeredBridge.createRun({
|
|
285
284
|
onToolRequest: params.onBridgeToolRequest,
|
|
@@ -299,7 +298,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
299
298
|
createAgent ??= sdk.Agent.create;
|
|
300
299
|
resumeAgent ??= sdk.Agent.resume;
|
|
301
300
|
}
|
|
302
|
-
const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey) : undefined;
|
|
301
|
+
const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey, scopeKey) : undefined;
|
|
303
302
|
const storeSelection = await openCursorSessionStoreForScope({
|
|
304
303
|
cwd: params.cwd,
|
|
305
304
|
scopeKey,
|
|
@@ -373,9 +372,10 @@ export function invalidateSessionAgent(scopeKey = getCursorSessionScopeKey(), op
|
|
|
373
372
|
if (options?.deadTransport)
|
|
374
373
|
deadTransportScopeKeys.add(scopeKey);
|
|
375
374
|
}
|
|
376
|
-
export async function acquireSessionCursorAgent(
|
|
377
|
-
const
|
|
378
|
-
const
|
|
375
|
+
export async function acquireSessionCursorAgent(input) {
|
|
376
|
+
const params = { ...input, bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
|
|
377
|
+
const scopeKey = params.scope?.scopeKey ?? getCursorSessionScopeKey();
|
|
378
|
+
const persistentStore = (params.scope ? params.scope.sessionFile : getCursorSessionFile()) !== undefined;
|
|
379
379
|
while (true) {
|
|
380
380
|
assertScopeAcceptsAcquire(scopeKey);
|
|
381
381
|
if (invalidatedScopeKeys.has(scopeKey)) {
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
2
2
|
import { resetSessionCursorAgent } from "./cursor-session-agent.js";
|
|
3
|
-
import { suppressCursorSessionAgentResumeHandlePersist } from "./cursor-session-agent-resume.js";
|
|
4
3
|
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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).
|
|
5
|
+
* Release scoped conversation runs and reset the pool before compaction.
|
|
6
|
+
* Summary execution owns a separate temporary agent, never this pool.
|
|
11
7
|
*/
|
|
12
8
|
export async function prepareCursorSessionForCompaction(scopeKey = getCursorSessionScopeKey()) {
|
|
13
|
-
suppressCursorSessionAgentResumeHandlePersist();
|
|
14
9
|
while (true) {
|
|
15
10
|
const run = cursorLiveRuns.getActiveForScope(scopeKey);
|
|
16
11
|
if (!run || run.disposed)
|
|
@@ -12,40 +12,58 @@ const state = {
|
|
|
12
12
|
projectTrusted: false,
|
|
13
13
|
sessionGeneration: 0,
|
|
14
14
|
};
|
|
15
|
-
const
|
|
15
|
+
const scopes = new Map();
|
|
16
|
+
const scopeChangeHandlers = new Map();
|
|
17
|
+
const scopeGenerations = new Map([[ANONYMOUS_SESSION_SCOPE_KEY, 0]]);
|
|
16
18
|
const projectTrustResolutionCwds = new Set();
|
|
17
19
|
let nextSessionGeneration = 1;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
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;
|
|
26
|
+
}
|
|
27
|
+
export function cursorSessionScopeKeyForManager(manager) {
|
|
28
|
+
return manager.getSessionFile?.() ?? (manager.getSessionId?.()
|
|
29
|
+
? `${EPHEMERAL_SESSION_SCOPE_PREFIX}${manager.getSessionId()}`
|
|
30
|
+
: ANONYMOUS_SESSION_SCOPE_KEY);
|
|
31
|
+
}
|
|
32
|
+
export function getCursorSessionScopeSnapshot(pi) {
|
|
33
|
+
if (!pi && scopes.size > 1)
|
|
34
|
+
throw new Error("Cursor provider ownership requires a session-bound provider closure when multiple extensions are loaded.");
|
|
35
|
+
const record = pi ? scopes.get(pi) : state;
|
|
36
|
+
if (!record)
|
|
37
|
+
throw new Error("Cursor session binding is no longer active.");
|
|
38
|
+
const scopeKey = scopeKeyFor(record);
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
cwd: record.sessionCwd,
|
|
41
|
+
scopeKey,
|
|
42
|
+
sessionFile: record.sessionFile,
|
|
43
|
+
sessionId: record.sessionId,
|
|
44
|
+
sessionName: record.sessionName,
|
|
45
|
+
projectTrusted: record.projectTrusted,
|
|
46
|
+
generation: getCursorSessionScopeGeneration(scopeKey),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function hasLiveCursorSessions() {
|
|
50
|
+
return scopes.size > 0;
|
|
51
|
+
}
|
|
22
52
|
export function getCursorSessionFile() {
|
|
23
53
|
return state.sessionFile;
|
|
24
54
|
}
|
|
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
|
-
*/
|
|
55
|
+
/** Stable pool key, with process-local anonymous fallback before session_start. */
|
|
29
56
|
export function getCursorSessionScopeKey() {
|
|
30
|
-
|
|
31
|
-
return state.sessionFile;
|
|
32
|
-
if (state.sessionId)
|
|
33
|
-
return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${state.sessionId}`;
|
|
34
|
-
return ANONYMOUS_SESSION_SCOPE_KEY;
|
|
57
|
+
return scopeKeyFor(state);
|
|
35
58
|
}
|
|
36
59
|
export function getCursorSessionScopeGeneration(scopeKey = getCursorSessionScopeKey()) {
|
|
37
60
|
return scopeGenerations.get(scopeKey) ?? 0;
|
|
38
61
|
}
|
|
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
62
|
export function getCursorSessionCwd() {
|
|
45
63
|
return state.sessionCwd;
|
|
46
64
|
}
|
|
47
|
-
export function getCursorSessionProjectTrusted() {
|
|
48
|
-
return state.projectTrusted;
|
|
65
|
+
export function getCursorSessionProjectTrusted(pi) {
|
|
66
|
+
return pi ? scopes.get(pi)?.projectTrusted === true : state.projectTrusted;
|
|
49
67
|
}
|
|
50
68
|
export function getCursorSessionName() {
|
|
51
69
|
return state.sessionName;
|
|
@@ -61,9 +79,8 @@ function setCursorSessionScope(cwd, sessionFile, sessionId, projectTrusted = fal
|
|
|
61
79
|
state.sessionId = sessionId;
|
|
62
80
|
state.sessionName = normalizeCursorSessionName(sessionName);
|
|
63
81
|
state.projectTrusted = projectTrusted;
|
|
64
|
-
state.sessionGeneration = nextSessionGeneration
|
|
65
|
-
|
|
66
|
-
scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration);
|
|
82
|
+
state.sessionGeneration = nextSessionGeneration++;
|
|
83
|
+
scopeGenerations.set(scopeKeyFor(state), state.sessionGeneration);
|
|
67
84
|
}
|
|
68
85
|
function recordProjectTrustResolution(cwd) {
|
|
69
86
|
projectTrustResolutionCwds.add(resolve(cwd));
|
|
@@ -72,35 +89,50 @@ function isCliProjectTrustApproved(args = process.argv.slice(2)) {
|
|
|
72
89
|
return parseArgs(args).projectTrustOverride === true;
|
|
73
90
|
}
|
|
74
91
|
function resetCursorSessionScope() {
|
|
75
|
-
state
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
state.projectTrusted = false;
|
|
80
|
-
state.sessionGeneration = 0;
|
|
92
|
+
Object.assign(state, {
|
|
93
|
+
sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
|
|
94
|
+
sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
|
|
95
|
+
});
|
|
81
96
|
nextSessionGeneration = 1;
|
|
82
97
|
scopeGenerations.clear();
|
|
83
|
-
scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY,
|
|
98
|
+
scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY, 0);
|
|
84
99
|
projectTrustResolutionCwds.clear();
|
|
100
|
+
scopes.clear();
|
|
101
|
+
scopeChangeHandlers.clear();
|
|
85
102
|
}
|
|
86
|
-
export function onCursorSessionScopeKeyChange(handler) {
|
|
87
|
-
|
|
103
|
+
export function onCursorSessionScopeKeyChange(pi, handler) {
|
|
104
|
+
scopeChangeHandlers.set(pi, handler);
|
|
88
105
|
}
|
|
89
106
|
export function registerCursorSessionScope(pi) {
|
|
107
|
+
// A fresh binding has no previous pooled scope to steal from the last-bound mirror.
|
|
108
|
+
const record = {
|
|
109
|
+
sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
|
|
110
|
+
sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
|
|
111
|
+
};
|
|
112
|
+
scopes.set(pi, record);
|
|
113
|
+
const trustedCwds = new Set();
|
|
90
114
|
pi.on("project_trust", (event) => {
|
|
91
|
-
|
|
115
|
+
trustedCwds.add(resolve(event.cwd));
|
|
92
116
|
return { trusted: "undecided" };
|
|
93
117
|
});
|
|
94
118
|
pi.on("session_start", async (_event, ctx) => {
|
|
95
|
-
const previousScopeKey =
|
|
119
|
+
const previousScopeKey = scopeKeyFor(record);
|
|
120
|
+
const wasBound = record.sessionGeneration > 0;
|
|
96
121
|
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
|
-
|
|
99
|
-
|
|
122
|
+
&& (trustedCwds.has(resolve(ctx.cwd)) || projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined);
|
|
123
|
+
Object.assign(record, state);
|
|
124
|
+
scopes.set(pi, record);
|
|
125
|
+
if (wasBound && previousScopeKey !== scopeKeyFor(record)) {
|
|
126
|
+
await scopeChangeHandlers.get(pi)?.(previousScopeKey);
|
|
100
127
|
}
|
|
101
128
|
});
|
|
102
129
|
pi.on("session_info_changed", (event) => {
|
|
103
|
-
|
|
130
|
+
record.sessionName = normalizeCursorSessionName(event.name);
|
|
131
|
+
Object.assign(state, record);
|
|
132
|
+
});
|
|
133
|
+
pi.on("session_shutdown", () => {
|
|
134
|
+
scopes.delete(pi);
|
|
135
|
+
scopeChangeHandlers.delete(pi);
|
|
104
136
|
});
|
|
105
137
|
}
|
|
106
138
|
export const __testUtils = {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { cursorSessionScopeKeyForManager, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
|
|
2
|
+
function createCliSettings() {
|
|
3
|
+
return {
|
|
4
|
+
forceFast: false, forceNoFast: false,
|
|
5
|
+
mode: { kind: "unset" },
|
|
6
|
+
config: {},
|
|
7
|
+
cloudEnvNames: undefined,
|
|
8
|
+
localForceConsumed: false,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function createSettings() {
|
|
12
|
+
return {
|
|
13
|
+
fast: new Map(),
|
|
14
|
+
mode: undefined,
|
|
15
|
+
runtime: undefined,
|
|
16
|
+
cloudAcknowledged: false,
|
|
17
|
+
http1: undefined,
|
|
18
|
+
projectTrusted: false,
|
|
19
|
+
cli: createCliSettings(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const settings = new Map();
|
|
23
|
+
let cliOwners = new WeakMap();
|
|
24
|
+
let legacySettings = createSettings();
|
|
25
|
+
export function getCursorSessionSettings(scopeKey) {
|
|
26
|
+
if (scopeKey === undefined)
|
|
27
|
+
return legacySettings;
|
|
28
|
+
let value = settings.get(scopeKey);
|
|
29
|
+
if (!value) {
|
|
30
|
+
value = createSettings();
|
|
31
|
+
settings.set(scopeKey, value);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export function cursorSettingsScopeForContext(ctx) {
|
|
36
|
+
const manager = ctx.sessionManager;
|
|
37
|
+
return manager ? cursorSessionScopeKeyForManager(manager) : undefined;
|
|
38
|
+
}
|
|
39
|
+
export function registerCursorSessionSettings(pi) {
|
|
40
|
+
let scopeKey;
|
|
41
|
+
let cli = createCliSettings();
|
|
42
|
+
pi.on("session_start", (_event, ctx) => {
|
|
43
|
+
if (scopeKey)
|
|
44
|
+
settings.delete(scopeKey);
|
|
45
|
+
scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager);
|
|
46
|
+
legacySettings = getCursorSessionSettings(scopeKey);
|
|
47
|
+
// Reload creates a new ExtensionAPI but retains the same public manager.
|
|
48
|
+
// Keep the run's one-shot CLI consumption without retaining dead managers.
|
|
49
|
+
cli = cliOwners.get(ctx.sessionManager) ?? cli;
|
|
50
|
+
cliOwners.set(ctx.sessionManager, cli);
|
|
51
|
+
legacySettings.cli = cli;
|
|
52
|
+
legacySettings.projectTrusted = getCursorSessionProjectTrusted(pi);
|
|
53
|
+
});
|
|
54
|
+
pi.on("session_shutdown", () => { if (scopeKey)
|
|
55
|
+
settings.delete(scopeKey); });
|
|
56
|
+
}
|
|
57
|
+
export function resetCursorSessionSettingsForTests() {
|
|
58
|
+
settings.clear();
|
|
59
|
+
cliOwners = new WeakMap();
|
|
60
|
+
legacySettings = createSettings();
|
|
61
|
+
}
|