pi-cursor-sdk 0.2.0 → 0.3.6
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 +62 -0
- package/README.md +54 -50
- package/dist/bundled-context-windows.js +39 -0
- package/dist/context-window-cache.js +92 -0
- package/dist/context.js +338 -0
- package/dist/cursor-active-tools.js +3 -0
- package/dist/cursor-agent-message-web-tools.js +165 -0
- package/dist/cursor-agents-context-registration.js +17 -0
- package/dist/cursor-agents-context.js +113 -0
- package/dist/cursor-api-key.js +34 -0
- package/dist/cursor-bridge-contract.js +27 -0
- package/dist/cursor-cloud-lifecycle.js +650 -0
- package/dist/cursor-cloud-local-state.js +460 -0
- package/dist/cursor-cloud-options.js +145 -0
- package/dist/cursor-cloud-reporting.js +222 -0
- package/dist/cursor-compact-tool-summary.js +75 -0
- package/dist/cursor-config.js +613 -0
- package/dist/cursor-context-tools.js +4 -0
- package/dist/cursor-display-only-trace.js +11 -0
- package/dist/cursor-display-text.js +18 -0
- package/dist/cursor-durable-fs.js +53 -0
- package/dist/cursor-edit-diff.js +13 -0
- package/dist/cursor-env-boolean.js +19 -0
- package/dist/cursor-fallback-models.generated.js +7760 -0
- package/dist/cursor-fallback-warning.js +15 -0
- package/dist/cursor-http1.js +45 -0
- package/dist/cursor-incomplete-tool-visibility.js +83 -0
- package/dist/cursor-live-run-accounting.js +55 -0
- package/dist/cursor-live-run-coordinator.js +406 -0
- package/dist/cursor-mcp-timeout-override.js +116 -0
- package/dist/cursor-model-lifecycle.js +32 -0
- package/dist/cursor-model.js +5 -0
- package/dist/cursor-native-replay-routing.js +35 -0
- package/dist/cursor-native-replay-trace.js +28 -0
- package/dist/cursor-native-tool-display-registration.js +102 -0
- package/dist/cursor-native-tool-display-replay.js +428 -0
- package/dist/cursor-native-tool-display-state.js +70 -0
- package/dist/cursor-native-tool-display-tools.js +123 -0
- package/dist/cursor-native-tool-names.js +11 -0
- package/dist/cursor-partial-content-emitter.js +118 -0
- package/dist/cursor-pi-tool-bridge-abort.js +114 -0
- package/dist/cursor-pi-tool-bridge-constants.js +6 -0
- package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
- package/dist/cursor-pi-tool-bridge-env.js +18 -0
- package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
- package/dist/cursor-pi-tool-bridge-run.js +355 -0
- package/dist/cursor-pi-tool-bridge-server.js +163 -0
- package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
- package/dist/cursor-pi-tool-bridge-types.js +1 -0
- package/dist/cursor-pi-tool-bridge.js +123 -0
- package/dist/cursor-provider-errors.js +311 -0
- package/dist/cursor-provider-lazy.js +39 -0
- package/dist/cursor-provider-live-run-drain.js +412 -0
- package/dist/cursor-provider-overflow.js +90 -0
- package/dist/cursor-provider-run-finalizer.js +176 -0
- package/dist/cursor-provider-run-outcome.js +76 -0
- package/dist/cursor-provider-turn-coordinator.js +275 -0
- package/dist/cursor-provider-turn-display-router.js +158 -0
- package/dist/cursor-provider-turn-emit.js +35 -0
- package/dist/cursor-provider-turn-finalize.js +154 -0
- package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
- package/dist/cursor-provider-turn-prepare.js +374 -0
- package/dist/cursor-provider-turn-runner.js +146 -0
- package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
- package/dist/cursor-provider-turn-send.js +138 -0
- package/dist/cursor-provider-turn-shell-output.js +117 -0
- package/dist/cursor-provider-turn-tool-ledger.js +99 -0
- package/dist/cursor-provider-turn-types.js +1 -0
- package/dist/cursor-provider.js +72 -0
- package/dist/cursor-question-tool.js +194 -0
- package/dist/cursor-record-utils.js +61 -0
- package/dist/cursor-replay-activity-builders.js +114 -0
- package/dist/cursor-replay-source-names.js +27 -0
- package/dist/cursor-replay-summary-args.js +105 -0
- package/dist/cursor-replay-tool-details.js +167 -0
- package/dist/cursor-ripgrep-path.js +27 -0
- package/dist/cursor-run-final-text.js +63 -0
- package/dist/cursor-runtime-state.js +380 -0
- package/dist/cursor-sdk-billed-usage.js +86 -0
- package/dist/cursor-sdk-event-debug-constants.js +33 -0
- package/dist/cursor-sdk-event-debug-session.js +107 -0
- package/dist/cursor-sdk-event-debug.js +461 -0
- package/dist/cursor-sdk-output-filter.js +1 -0
- package/dist/cursor-sdk-process-error-guard.js +221 -0
- package/dist/cursor-sdk-runtime.js +3 -0
- package/dist/cursor-sensitive-text.js +23 -0
- package/dist/cursor-session-agent-cleanup.js +311 -0
- package/dist/cursor-session-agent-lifecycle.js +32 -0
- package/dist/cursor-session-agent-lineage.js +103 -0
- package/dist/cursor-session-agent-resume.js +417 -0
- package/dist/cursor-session-agent.js +507 -0
- package/dist/cursor-session-compaction-prep.js +21 -0
- package/dist/cursor-session-scope.js +113 -0
- package/dist/cursor-session-send-policy.js +18 -0
- package/dist/cursor-session-store.js +105 -0
- package/dist/cursor-session-turn-queue.js +50 -0
- package/dist/cursor-setting-sources.js +15 -0
- package/dist/cursor-skill-tool.js +230 -0
- package/dist/cursor-state.js +519 -0
- package/dist/cursor-task-presentation.js +59 -0
- package/dist/cursor-tool-lifecycle.js +94 -0
- package/dist/cursor-tool-manifest.js +34 -0
- package/dist/cursor-tool-presentation-registry.js +305 -0
- package/dist/cursor-tool-result-display-readers.js +158 -0
- package/dist/cursor-tool-transcript.js +32 -0
- package/dist/cursor-tool-visibility.js +28 -0
- package/dist/cursor-transcript-tool-formatters.js +618 -0
- package/dist/cursor-transcript-tool-specs.js +284 -0
- package/dist/cursor-transcript-utils.js +170 -0
- package/dist/cursor-usage-accounting.js +166 -0
- package/dist/cursor-web-tool-activity.js +23 -0
- package/dist/cursor-web-tool-args.js +12 -0
- package/dist/index.js +82 -0
- package/dist/model-discovery.js +305 -0
- package/dist/model-list-cache.js +156 -0
- package/docs/cursor-dogfood-checklist.md +2 -1
- package/docs/cursor-live-smoke-checklist.md +13 -12
- package/docs/cursor-model-ux-spec.md +25 -6
- package/docs/cursor-native-tool-replay.md +13 -13
- package/docs/cursor-native-tool-visual-audit.md +6 -3
- package/docs/cursor-testing-lessons.md +7 -7
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +3 -3
- package/package.json +19 -7
- package/platform-smoke.config.mjs +1 -1
- package/scripts/build.mjs +126 -0
- package/scripts/cloud-runtime-smoke.mjs +3 -1
- package/scripts/debug-provider-events.mjs +3 -1
- package/scripts/debug-sdk-events.mjs +1 -1
- package/scripts/isolated-cursor-smoke.sh +5 -4
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
- package/scripts/lib/cursor-visual-render.mjs +1 -1
- package/scripts/lib/ensure-built.mjs +16 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
- package/scripts/local-resume-smoke.mjs +3 -1
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/render-ansi.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/prepare.mjs +102 -0
- package/scripts/steering-rpc-smoke.mjs +4 -2
- package/scripts/tmux-live-smoke.sh +11 -11
- package/scripts/visual-tui-smoke-self-test.mjs +4 -1
- package/scripts/visual-tui-smoke.mjs +51 -7
- package/src/bundled-context-windows.ts +4 -1
- package/src/cursor-agents-context-registration.ts +2 -2
- package/src/cursor-agents-context.ts +0 -2
- package/src/cursor-api-key.ts +5 -4
- package/src/cursor-cloud-reporting.ts +23 -2
- package/src/cursor-fallback-models.generated.ts +592 -204
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-native-tool-display-registration.ts +10 -11
- package/src/cursor-provider-errors.ts +1 -1
- package/src/cursor-provider-lazy.ts +5 -4
- package/src/cursor-provider-live-run-drain.ts +1 -0
- package/src/cursor-provider-run-finalizer.ts +1 -0
- package/src/cursor-provider-turn-finalize.ts +12 -0
- package/src/cursor-provider-turn-types.ts +2 -0
- package/src/cursor-sdk-billed-usage.ts +107 -0
- package/src/cursor-sdk-process-error-guard.ts +6 -4
- package/src/cursor-session-agent-lifecycle.ts +8 -9
- package/src/cursor-session-agent-resume.ts +23 -0
- package/src/cursor-session-compaction-prep.ts +4 -0
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-usage-accounting.ts +70 -9
- package/src/index.ts +1 -1
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { isCursorLocalAgentId } from "./cursor-session-agent-resume.js";
|
|
2
|
+
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
3
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
4
|
+
export const CURSOR_SESSION_AGENT_LINEAGE_ENTRY_TYPE = "cursor-sdk-agent-lineage";
|
|
5
|
+
const LINEAGE_ENTRY_VERSION = 1;
|
|
6
|
+
function isIsoTimestamp(value) {
|
|
7
|
+
if (typeof value !== "string" || !value)
|
|
8
|
+
return false;
|
|
9
|
+
const timestamp = Date.parse(value);
|
|
10
|
+
return !Number.isNaN(timestamp) && new Date(timestamp).toISOString() === value;
|
|
11
|
+
}
|
|
12
|
+
const state = {
|
|
13
|
+
recordedAgentIds: new Set(),
|
|
14
|
+
};
|
|
15
|
+
export function parseCursorSessionAgentLineageEntryData(value) {
|
|
16
|
+
const record = asRecord(value);
|
|
17
|
+
if (record?.version !== LINEAGE_ENTRY_VERSION ||
|
|
18
|
+
record.runtime !== "local" ||
|
|
19
|
+
!isCursorLocalAgentId(record.agentId) ||
|
|
20
|
+
typeof record.sessionId !== "string" ||
|
|
21
|
+
!record.sessionId ||
|
|
22
|
+
typeof record.scopeKey !== "string" ||
|
|
23
|
+
!record.scopeKey ||
|
|
24
|
+
typeof record.cwd !== "string" ||
|
|
25
|
+
!record.cwd ||
|
|
26
|
+
!isIsoTimestamp(record.timestamp)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (record.sessionFile !== undefined && (typeof record.sessionFile !== "string" || !record.sessionFile))
|
|
30
|
+
return undefined;
|
|
31
|
+
return {
|
|
32
|
+
version: LINEAGE_ENTRY_VERSION,
|
|
33
|
+
runtime: "local",
|
|
34
|
+
agentId: record.agentId,
|
|
35
|
+
sessionId: record.sessionId,
|
|
36
|
+
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
37
|
+
scopeKey: record.scopeKey,
|
|
38
|
+
cwd: record.cwd,
|
|
39
|
+
timestamp: record.timestamp,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function readRecordedAgentIds(entries, sessionId) {
|
|
43
|
+
return new Set(entries.flatMap((entry) => {
|
|
44
|
+
if (entry.type !== "custom" || entry.customType !== CURSOR_SESSION_AGENT_LINEAGE_ENTRY_TYPE)
|
|
45
|
+
return [];
|
|
46
|
+
const data = parseCursorSessionAgentLineageEntryData(entry.data);
|
|
47
|
+
return data?.sessionId === sessionId ? [data.agentId] : [];
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
/** Best-effort forensic lineage at the local Agent.send() boundary. Independent of resume. */
|
|
51
|
+
export function recordCursorSessionAgentLineage(agentId) {
|
|
52
|
+
const { appendEntry, sessionId, sessionFile, scopeKey, cwd } = state;
|
|
53
|
+
if (!appendEntry || !sessionId || !scopeKey || !cwd)
|
|
54
|
+
return;
|
|
55
|
+
if (!isCursorLocalAgentId(agentId) || state.recordedAgentIds.has(agentId))
|
|
56
|
+
return;
|
|
57
|
+
const data = {
|
|
58
|
+
version: LINEAGE_ENTRY_VERSION,
|
|
59
|
+
runtime: "local",
|
|
60
|
+
agentId,
|
|
61
|
+
sessionId,
|
|
62
|
+
...(sessionFile ? { sessionFile } : {}),
|
|
63
|
+
scopeKey,
|
|
64
|
+
cwd,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
appendEntry(CURSOR_SESSION_AGENT_LINEAGE_ENTRY_TYPE, data);
|
|
69
|
+
state.recordedAgentIds.add(agentId);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Lineage is forensic metadata; a failed stock pi append must not affect the session.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function registerCursorSessionAgentLineage(pi) {
|
|
76
|
+
pi.on("session_start", (_event, ctx) => {
|
|
77
|
+
state.appendEntry = pi.appendEntry;
|
|
78
|
+
state.sessionId = ctx.sessionManager.getSessionId();
|
|
79
|
+
state.sessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
|
|
80
|
+
state.scopeKey = getCursorSessionScopeKey();
|
|
81
|
+
state.cwd = ctx.cwd;
|
|
82
|
+
state.recordedAgentIds = readRecordedAgentIds(ctx.sessionManager.getEntries(), state.sessionId);
|
|
83
|
+
});
|
|
84
|
+
pi.on("session_shutdown", () => {
|
|
85
|
+
state.appendEntry = undefined;
|
|
86
|
+
state.sessionId = undefined;
|
|
87
|
+
state.sessionFile = undefined;
|
|
88
|
+
state.scopeKey = undefined;
|
|
89
|
+
state.cwd = undefined;
|
|
90
|
+
state.recordedAgentIds = new Set();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function resetStateForTests() {
|
|
94
|
+
state.appendEntry = undefined;
|
|
95
|
+
state.sessionId = undefined;
|
|
96
|
+
state.sessionFile = undefined;
|
|
97
|
+
state.scopeKey = undefined;
|
|
98
|
+
state.cwd = undefined;
|
|
99
|
+
state.recordedAgentIds = new Set();
|
|
100
|
+
}
|
|
101
|
+
export const __testUtils = {
|
|
102
|
+
reset: resetStateForTests,
|
|
103
|
+
};
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
4
|
+
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
5
|
+
export const CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE = "cursor-sdk-agent-resume";
|
|
6
|
+
const LEGACY_RESUME_ENTRY_VERSION = 1;
|
|
7
|
+
const RESUME_ENTRY_VERSION = 2;
|
|
8
|
+
const MAX_LOCAL_AGENT_ID_LENGTH = 256;
|
|
9
|
+
const EMPTY_BRANCH_HASH = hashParts(["cursor-sdk-agent-resume-branch", "v1"]);
|
|
10
|
+
// @cursor/sdk AgentOptions.agentId is a public custom string, so local resume narrows it without assuming UUIDs.
|
|
11
|
+
export function isCursorLocalAgentId(value) {
|
|
12
|
+
return typeof value === "string" && value.length <= MAX_LOCAL_AGENT_ID_LENGTH && /^agent-[A-Za-z0-9_-]+$/.test(value);
|
|
13
|
+
}
|
|
14
|
+
const state = {
|
|
15
|
+
scopeKey: getCursorSessionScopeKey(),
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
branchPathHash: EMPTY_BRANCH_HASH,
|
|
18
|
+
compactionGeneration: 0,
|
|
19
|
+
unownedUserEntryIds: new Set(),
|
|
20
|
+
};
|
|
21
|
+
// Compaction summarizer sends commit a one-message resume handle. Drop it so the
|
|
22
|
+
// next normal turn_end cannot persist that lineage (#223).
|
|
23
|
+
let resumeHandlePersistSuppressed = false;
|
|
24
|
+
export function suppressCursorSessionAgentResumeHandlePersist() {
|
|
25
|
+
resumeHandlePersistSuppressed = true;
|
|
26
|
+
state.pendingHandle = undefined;
|
|
27
|
+
}
|
|
28
|
+
export function allowCursorSessionAgentResumeHandlePersist() {
|
|
29
|
+
resumeHandlePersistSuppressed = false;
|
|
30
|
+
}
|
|
31
|
+
function hashParts(parts) {
|
|
32
|
+
const hash = createHash("sha256");
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
hash.update(part);
|
|
35
|
+
hash.update("\0");
|
|
36
|
+
}
|
|
37
|
+
return hash.digest("hex").slice(0, 32);
|
|
38
|
+
}
|
|
39
|
+
function hashBranchStep(previous, entry) {
|
|
40
|
+
return hashParts([
|
|
41
|
+
previous,
|
|
42
|
+
entry.type,
|
|
43
|
+
entry.id,
|
|
44
|
+
entry.parentId ?? "",
|
|
45
|
+
entry.type === "custom" ? entry.customType : "",
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
export function resolveCursorSessionRepoRoot(cwd) {
|
|
49
|
+
try {
|
|
50
|
+
return execFileSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
53
|
+
timeout: 2_000,
|
|
54
|
+
}).trim() || undefined;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isSendState(value) {
|
|
61
|
+
const record = asRecord(value);
|
|
62
|
+
return typeof record?.bootstrapped === "boolean" &&
|
|
63
|
+
typeof record.contextFingerprint === "string" &&
|
|
64
|
+
typeof record.incrementalSendCount === "number";
|
|
65
|
+
}
|
|
66
|
+
function parseStoreIdentity(value) {
|
|
67
|
+
const record = asRecord(value);
|
|
68
|
+
if (record?.version !== 1 || typeof record.stateRoot !== "string" || !record.stateRoot)
|
|
69
|
+
return undefined;
|
|
70
|
+
return { version: 1, stateRoot: record.stateRoot };
|
|
71
|
+
}
|
|
72
|
+
function parseCleanupCandidates(value) {
|
|
73
|
+
if (!Array.isArray(value))
|
|
74
|
+
return undefined;
|
|
75
|
+
const candidates = value.flatMap((item) => {
|
|
76
|
+
const record = asRecord(item);
|
|
77
|
+
if (!isCursorLocalAgentId(record?.agentId))
|
|
78
|
+
return [];
|
|
79
|
+
const storeIdentity = record.storeIdentity === undefined ? undefined : parseStoreIdentity(record.storeIdentity);
|
|
80
|
+
if (record.storeIdentity !== undefined && !storeIdentity)
|
|
81
|
+
return [];
|
|
82
|
+
return [{ agentId: record.agentId, ...(storeIdentity ? { storeIdentity } : {}) }];
|
|
83
|
+
});
|
|
84
|
+
return candidates.length ? candidates : undefined;
|
|
85
|
+
}
|
|
86
|
+
export function parseCursorSessionAgentResumeEntryData(value) {
|
|
87
|
+
const record = asRecord(value);
|
|
88
|
+
if (!record)
|
|
89
|
+
return undefined;
|
|
90
|
+
if ((record.version !== LEGACY_RESUME_ENTRY_VERSION && record.version !== RESUME_ENTRY_VERSION) ||
|
|
91
|
+
record.runtime !== "local")
|
|
92
|
+
return undefined;
|
|
93
|
+
if (!isCursorLocalAgentId(record.agentId) ||
|
|
94
|
+
typeof record.scopeKey !== "string" ||
|
|
95
|
+
typeof record.cwd !== "string" ||
|
|
96
|
+
typeof record.poolKey !== "string" ||
|
|
97
|
+
typeof record.branchPathHash !== "string" ||
|
|
98
|
+
typeof record.compactionGeneration !== "number" ||
|
|
99
|
+
typeof record.createdAt !== "string" ||
|
|
100
|
+
!isSendState(record.sendState))
|
|
101
|
+
return undefined;
|
|
102
|
+
if (record.sessionFile !== undefined && typeof record.sessionFile !== "string")
|
|
103
|
+
return undefined;
|
|
104
|
+
if (record.sessionId !== undefined && typeof record.sessionId !== "string")
|
|
105
|
+
return undefined;
|
|
106
|
+
if (record.repoRoot !== undefined && typeof record.repoRoot !== "string")
|
|
107
|
+
return undefined;
|
|
108
|
+
const storeIdentity = parseStoreIdentity(record.storeIdentity);
|
|
109
|
+
if (record.version === RESUME_ENTRY_VERSION && !storeIdentity)
|
|
110
|
+
return undefined;
|
|
111
|
+
const cleanupCandidateAgentIds = Array.isArray(record.cleanupCandidateAgentIds)
|
|
112
|
+
? record.cleanupCandidateAgentIds.filter(isCursorLocalAgentId)
|
|
113
|
+
: undefined;
|
|
114
|
+
const cleanupCandidates = parseCleanupCandidates(record.cleanupCandidates);
|
|
115
|
+
return {
|
|
116
|
+
version: record.version,
|
|
117
|
+
runtime: "local",
|
|
118
|
+
agentId: record.agentId,
|
|
119
|
+
scopeKey: record.scopeKey,
|
|
120
|
+
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
121
|
+
...(record.sessionId ? { sessionId: record.sessionId } : {}),
|
|
122
|
+
cwd: record.cwd,
|
|
123
|
+
...(record.repoRoot ? { repoRoot: record.repoRoot } : {}),
|
|
124
|
+
poolKey: record.poolKey,
|
|
125
|
+
branchPathHash: record.branchPathHash,
|
|
126
|
+
compactionGeneration: record.compactionGeneration,
|
|
127
|
+
sendState: {
|
|
128
|
+
bootstrapped: record.sendState.bootstrapped,
|
|
129
|
+
contextFingerprint: record.sendState.contextFingerprint,
|
|
130
|
+
incrementalSendCount: record.sendState.incrementalSendCount,
|
|
131
|
+
},
|
|
132
|
+
createdAt: record.createdAt,
|
|
133
|
+
...(storeIdentity ? { storeIdentity } : {}),
|
|
134
|
+
...(cleanupCandidateAgentIds?.length ? { cleanupCandidateAgentIds: [...new Set(cleanupCandidateAgentIds)] } : {}),
|
|
135
|
+
...(cleanupCandidates ? { cleanupCandidates } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function matchesResumeScope(data, scope) {
|
|
139
|
+
return data.scopeKey === scope.scopeKey &&
|
|
140
|
+
data.sessionFile === scope.sessionFile &&
|
|
141
|
+
data.sessionId === scope.sessionId &&
|
|
142
|
+
data.cwd === scope.cwd &&
|
|
143
|
+
data.repoRoot === scope.repoRoot;
|
|
144
|
+
}
|
|
145
|
+
function matchesCurrentSession(data, branchPathHash, compactionGeneration = state.compactionGeneration) {
|
|
146
|
+
return matchesResumeScope(data, state) &&
|
|
147
|
+
data.compactionGeneration === compactionGeneration &&
|
|
148
|
+
data.branchPathHash === branchPathHash;
|
|
149
|
+
}
|
|
150
|
+
function canResumeHandleSpanEntry(entry) {
|
|
151
|
+
if (entry.type === "custom" || entry.type === "label" || entry.type === "session_info")
|
|
152
|
+
return true;
|
|
153
|
+
return entry.type === "message" && entry.message.role === "user";
|
|
154
|
+
}
|
|
155
|
+
function resumeAgentLineageKey(data) {
|
|
156
|
+
return JSON.stringify([
|
|
157
|
+
data.agentId,
|
|
158
|
+
data.scopeKey,
|
|
159
|
+
data.sessionFile,
|
|
160
|
+
data.sessionId,
|
|
161
|
+
data.cwd,
|
|
162
|
+
data.repoRoot,
|
|
163
|
+
data.poolKey,
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
function indexLatestResumeEntries(entries) {
|
|
167
|
+
const entryIds = new Set();
|
|
168
|
+
const latestEntryIdByLineage = new Map();
|
|
169
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
170
|
+
const entry = entries[index];
|
|
171
|
+
entryIds.add(entry.id);
|
|
172
|
+
if (entry.type !== "custom" || entry.customType !== CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE)
|
|
173
|
+
continue;
|
|
174
|
+
const data = parseCursorSessionAgentResumeEntryData(entry.data);
|
|
175
|
+
if (!data)
|
|
176
|
+
continue;
|
|
177
|
+
const lineage = resumeAgentLineageKey(data);
|
|
178
|
+
if (!latestEntryIdByLineage.has(lineage))
|
|
179
|
+
latestEntryIdByLineage.set(lineage, entry.id);
|
|
180
|
+
}
|
|
181
|
+
return { entryIds, latestEntryIdByLineage };
|
|
182
|
+
}
|
|
183
|
+
/** One fold step shared by the tree-wide and single-branch resume-handle walks: advances
|
|
184
|
+
* branchPathHash/compactionGeneration and adopts a matching, non-superseded resume handle. */
|
|
185
|
+
function advanceResumeBranchState(entry, previous, resumeIndex, params) {
|
|
186
|
+
if (entry.type === "custom" && entry.customType === CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE) {
|
|
187
|
+
const data = parseCursorSessionAgentResumeEntryData(entry.data);
|
|
188
|
+
const latestEntryId = data ? resumeIndex.latestEntryIdByLineage.get(resumeAgentLineageKey(data)) : undefined;
|
|
189
|
+
const superseded = resumeIndex.entryIds.has(entry.id) && latestEntryId !== entry.id;
|
|
190
|
+
if (data && params.matchesEntry(data, previous.branchPathHash, previous.compactionGeneration) && !superseded) {
|
|
191
|
+
return { ...previous, activeHandle: data };
|
|
192
|
+
}
|
|
193
|
+
return previous;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
branchPathHash: hashBranchStep(previous.branchPathHash, entry),
|
|
197
|
+
compactionGeneration: entry.type === "compaction" ? previous.compactionGeneration + 1 : previous.compactionGeneration,
|
|
198
|
+
activeHandle: previous.activeHandle && !params.canSpanEntry(entry) ? undefined : previous.activeHandle,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function readResumableCursorSessionAgentIds(entries, scope) {
|
|
202
|
+
const resumeIndex = indexLatestResumeEntries(entries);
|
|
203
|
+
const states = new Map();
|
|
204
|
+
const parentIds = new Set();
|
|
205
|
+
let rootCount = 0;
|
|
206
|
+
let completeTree = true;
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (states.has(entry.id))
|
|
209
|
+
completeTree = false;
|
|
210
|
+
if (entry.parentId === null)
|
|
211
|
+
rootCount += 1;
|
|
212
|
+
const parent = entry.parentId ? states.get(entry.parentId) : undefined;
|
|
213
|
+
if (entry.parentId) {
|
|
214
|
+
parentIds.add(entry.parentId);
|
|
215
|
+
if (!parent)
|
|
216
|
+
completeTree = false;
|
|
217
|
+
}
|
|
218
|
+
const previous = {
|
|
219
|
+
branchPathHash: parent?.branchPathHash ?? EMPTY_BRANCH_HASH,
|
|
220
|
+
compactionGeneration: parent?.compactionGeneration ?? 0,
|
|
221
|
+
activeHandle: parent?.activeHandle,
|
|
222
|
+
};
|
|
223
|
+
states.set(entry.id, advanceResumeBranchState(entry, previous, resumeIndex, {
|
|
224
|
+
matchesEntry: (data, branchPathHash, compactionGeneration) => matchesResumeScope(data, scope) &&
|
|
225
|
+
data.compactionGeneration === compactionGeneration &&
|
|
226
|
+
data.branchPathHash === branchPathHash,
|
|
227
|
+
canSpanEntry: canResumeHandleSpanEntry,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
if (entries.length > 0 && rootCount !== 1)
|
|
231
|
+
completeTree = false;
|
|
232
|
+
if (!completeTree) {
|
|
233
|
+
return [...new Set(entries.flatMap((entry) => {
|
|
234
|
+
if (entry.type !== "custom" || entry.customType !== CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE)
|
|
235
|
+
return [];
|
|
236
|
+
const data = parseCursorSessionAgentResumeEntryData(entry.data);
|
|
237
|
+
return data && matchesResumeScope(data, scope) ? [data.agentId] : [];
|
|
238
|
+
}))].sort((a, b) => a.localeCompare(b));
|
|
239
|
+
}
|
|
240
|
+
const agentIds = new Set();
|
|
241
|
+
for (const [entryId, branchState] of states) {
|
|
242
|
+
if (!parentIds.has(entryId) && branchState.activeHandle)
|
|
243
|
+
agentIds.add(branchState.activeHandle.agentId);
|
|
244
|
+
}
|
|
245
|
+
return [...agentIds].sort((a, b) => a.localeCompare(b));
|
|
246
|
+
}
|
|
247
|
+
function canRestoreHandleSpanEntry(entry) {
|
|
248
|
+
if (entry.type === "message" && entry.message.role === "user")
|
|
249
|
+
return !state.unownedUserEntryIds.has(entry.id);
|
|
250
|
+
return canResumeHandleSpanEntry(entry);
|
|
251
|
+
}
|
|
252
|
+
function restoreFromBranch(branch, allEntries = branch) {
|
|
253
|
+
const resumeIndex = indexLatestResumeEntries(allEntries);
|
|
254
|
+
let fold = { branchPathHash: EMPTY_BRANCH_HASH, compactionGeneration: 0 };
|
|
255
|
+
let lastBranchHandle;
|
|
256
|
+
for (const entry of branch) {
|
|
257
|
+
const next = advanceResumeBranchState(entry, fold, resumeIndex, {
|
|
258
|
+
matchesEntry: matchesCurrentSession,
|
|
259
|
+
canSpanEntry: canRestoreHandleSpanEntry,
|
|
260
|
+
});
|
|
261
|
+
if (next.activeHandle && next.activeHandle !== fold.activeHandle)
|
|
262
|
+
lastBranchHandle = next.activeHandle;
|
|
263
|
+
fold = next;
|
|
264
|
+
}
|
|
265
|
+
state.branchPathHash = fold.branchPathHash;
|
|
266
|
+
state.compactionGeneration = fold.compactionGeneration;
|
|
267
|
+
state.activeHandle = fold.activeHandle;
|
|
268
|
+
state.lastBranchHandle = lastBranchHandle;
|
|
269
|
+
}
|
|
270
|
+
export function getMatchingCursorSessionAgentResumeHandle(poolKey) {
|
|
271
|
+
const handle = state.activeHandle;
|
|
272
|
+
if (!handle || !isCursorLocalAgentId(handle.agentId))
|
|
273
|
+
return undefined;
|
|
274
|
+
if (handle.poolKey !== poolKey)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (handle.scopeKey !== state.scopeKey)
|
|
277
|
+
return undefined;
|
|
278
|
+
if (handle.sessionFile !== state.sessionFile)
|
|
279
|
+
return undefined;
|
|
280
|
+
if (handle.sessionId !== state.sessionId)
|
|
281
|
+
return undefined;
|
|
282
|
+
if (handle.cwd !== state.cwd)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (handle.repoRoot !== state.repoRoot)
|
|
285
|
+
return undefined;
|
|
286
|
+
if (handle.compactionGeneration !== state.compactionGeneration)
|
|
287
|
+
return undefined;
|
|
288
|
+
return {
|
|
289
|
+
...handle,
|
|
290
|
+
sendState: { ...handle.sendState },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
export function persistCursorSessionAgentResumeHandle(input) {
|
|
294
|
+
if (resumeHandlePersistSuppressed)
|
|
295
|
+
return;
|
|
296
|
+
if (!isCursorLocalAgentId(input.agentId))
|
|
297
|
+
return;
|
|
298
|
+
state.pendingHandle = {
|
|
299
|
+
runtime: input.runtime,
|
|
300
|
+
agentId: input.agentId,
|
|
301
|
+
poolKey: input.poolKey,
|
|
302
|
+
sendState: { ...input.sendState },
|
|
303
|
+
storeIdentity: { ...input.storeIdentity },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function flushPendingCursorSessionAgentResumeHandle(branch) {
|
|
307
|
+
if (resumeHandlePersistSuppressed) {
|
|
308
|
+
state.pendingHandle = undefined;
|
|
309
|
+
restoreFromBranch(branch);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
restoreFromBranch(branch);
|
|
313
|
+
const pending = state.pendingHandle;
|
|
314
|
+
state.pendingHandle = undefined;
|
|
315
|
+
if (!pending || !state.appendEntry)
|
|
316
|
+
return;
|
|
317
|
+
const previousHandle = state.activeHandle ?? state.lastBranchHandle;
|
|
318
|
+
const cleanupCandidates = state.sessionFile && previousHandle && previousHandle.agentId !== pending.agentId
|
|
319
|
+
? [{
|
|
320
|
+
agentId: previousHandle.agentId,
|
|
321
|
+
...(previousHandle.storeIdentity ? { storeIdentity: { ...previousHandle.storeIdentity } } : {}),
|
|
322
|
+
}]
|
|
323
|
+
: undefined;
|
|
324
|
+
const data = {
|
|
325
|
+
version: RESUME_ENTRY_VERSION,
|
|
326
|
+
runtime: pending.runtime,
|
|
327
|
+
agentId: pending.agentId,
|
|
328
|
+
scopeKey: state.scopeKey,
|
|
329
|
+
...(state.sessionFile ? { sessionFile: state.sessionFile } : {}),
|
|
330
|
+
...(state.sessionId ? { sessionId: state.sessionId } : {}),
|
|
331
|
+
cwd: state.cwd,
|
|
332
|
+
...(state.repoRoot ? { repoRoot: state.repoRoot } : {}),
|
|
333
|
+
poolKey: pending.poolKey,
|
|
334
|
+
branchPathHash: state.branchPathHash,
|
|
335
|
+
compactionGeneration: state.compactionGeneration,
|
|
336
|
+
sendState: { ...pending.sendState },
|
|
337
|
+
createdAt: new Date().toISOString(),
|
|
338
|
+
storeIdentity: { ...pending.storeIdentity },
|
|
339
|
+
...(cleanupCandidates ? { cleanupCandidates } : {}),
|
|
340
|
+
};
|
|
341
|
+
try {
|
|
342
|
+
state.appendEntry(CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE, data);
|
|
343
|
+
state.activeHandle = data;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Resume persistence is an optimization; a failed custom-entry append must not fail the completed turn.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
export function registerCursorSessionAgentResume(pi) {
|
|
350
|
+
state.appendEntry = pi.appendEntry;
|
|
351
|
+
const restoreFromSessionManager = (sessionManager) => {
|
|
352
|
+
const branch = sessionManager.getBranch();
|
|
353
|
+
const entries = sessionManager.getEntries();
|
|
354
|
+
restoreFromBranch(branch, entries.length > 0 ? entries : branch);
|
|
355
|
+
};
|
|
356
|
+
pi.on("session_start", (_event, ctx) => {
|
|
357
|
+
state.scopeKey = getCursorSessionScopeKey();
|
|
358
|
+
state.sessionFile = ctx.sessionManager.getSessionFile?.() ?? undefined;
|
|
359
|
+
state.sessionId = ctx.sessionManager.getSessionId?.() ?? undefined;
|
|
360
|
+
state.cwd = ctx.cwd;
|
|
361
|
+
state.repoRoot = resolveCursorSessionRepoRoot(ctx.cwd);
|
|
362
|
+
state.unownedUserEntryIds = new Set(ctx.sessionManager.getBranch().flatMap((entry) => entry.type === "message" && entry.message.role === "user" ? [entry.id] : []));
|
|
363
|
+
restoreFromSessionManager(ctx.sessionManager);
|
|
364
|
+
});
|
|
365
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
366
|
+
restoreFromSessionManager(ctx.sessionManager);
|
|
367
|
+
});
|
|
368
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
369
|
+
flushPendingCursorSessionAgentResumeHandle(ctx.sessionManager.getBranch());
|
|
370
|
+
});
|
|
371
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
372
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
373
|
+
if (entry.type === "message" && entry.message.role === "user")
|
|
374
|
+
state.unownedUserEntryIds.add(entry.id);
|
|
375
|
+
}
|
|
376
|
+
restoreFromSessionManager(ctx.sessionManager);
|
|
377
|
+
});
|
|
378
|
+
pi.on("session_compact", (event, ctx) => {
|
|
379
|
+
state.pendingHandle = undefined;
|
|
380
|
+
resumeHandlePersistSuppressed = false;
|
|
381
|
+
const branch = ctx.sessionManager.getBranch();
|
|
382
|
+
if (branch.length > 0) {
|
|
383
|
+
restoreFromSessionManager(ctx.sessionManager);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
state.activeHandle = undefined;
|
|
387
|
+
state.lastBranchHandle = undefined;
|
|
388
|
+
state.compactionGeneration += 1;
|
|
389
|
+
state.branchPathHash = hashBranchStep(state.branchPathHash, event.compactionEntry);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function setStateForTests(next) {
|
|
393
|
+
Object.assign(state, next);
|
|
394
|
+
}
|
|
395
|
+
function resetStateForTests() {
|
|
396
|
+
state.appendEntry = undefined;
|
|
397
|
+
state.scopeKey = getCursorSessionScopeKey();
|
|
398
|
+
state.sessionFile = undefined;
|
|
399
|
+
state.sessionId = undefined;
|
|
400
|
+
state.cwd = process.cwd();
|
|
401
|
+
state.repoRoot = undefined;
|
|
402
|
+
state.branchPathHash = EMPTY_BRANCH_HASH;
|
|
403
|
+
state.compactionGeneration = 0;
|
|
404
|
+
state.activeHandle = undefined;
|
|
405
|
+
state.lastBranchHandle = undefined;
|
|
406
|
+
state.pendingHandle = undefined;
|
|
407
|
+
state.unownedUserEntryIds = new Set();
|
|
408
|
+
resumeHandlePersistSuppressed = false;
|
|
409
|
+
}
|
|
410
|
+
export const __testUtils = {
|
|
411
|
+
EMPTY_BRANCH_HASH,
|
|
412
|
+
hashBranchStep,
|
|
413
|
+
reset: resetStateForTests,
|
|
414
|
+
set: setStateForTests,
|
|
415
|
+
state,
|
|
416
|
+
isResumeHandlePersistSuppressed: () => resumeHandlePersistSuppressed,
|
|
417
|
+
};
|