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.
Files changed (125) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +65 -13
  3. package/dist/context-window-cache.js +0 -6
  4. package/dist/context.js +8 -0
  5. package/dist/cursor-cloud-lifecycle.js +122 -98
  6. package/dist/cursor-durable-fs.js +97 -3
  7. package/dist/cursor-fallback-models.generated.js +288 -1
  8. package/dist/cursor-fallback-warning.js +2 -2
  9. package/dist/cursor-http1.js +8 -8
  10. package/dist/cursor-live-run-coordinator.js +9 -13
  11. package/dist/cursor-native-replay-routing.js +1 -1
  12. package/dist/cursor-native-tool-display-registration.js +46 -22
  13. package/dist/cursor-native-tool-display-state.js +57 -22
  14. package/dist/cursor-native-tool-display-tools.js +5 -6
  15. package/dist/cursor-pi-tool-bridge-constants.js +23 -2
  16. package/dist/cursor-pi-tool-bridge-run.js +7 -3
  17. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  18. package/dist/cursor-pi-tool-bridge.js +31 -9
  19. package/dist/cursor-provider-binding.js +58 -0
  20. package/dist/cursor-provider-errors.js +28 -7
  21. package/dist/cursor-provider-lazy.js +23 -11
  22. package/dist/cursor-provider-live-run-drain.js +15 -12
  23. package/dist/cursor-provider-run-finalizer.js +19 -8
  24. package/dist/cursor-provider-turn-coordinator.js +1 -0
  25. package/dist/cursor-provider-turn-display-router.js +4 -0
  26. package/dist/cursor-provider-turn-emit.js +1 -0
  27. package/dist/cursor-provider-turn-finalize.js +29 -20
  28. package/dist/cursor-provider-turn-prepare.js +24 -11
  29. package/dist/cursor-provider-turn-runner.js +37 -11
  30. package/dist/cursor-provider-turn-send.js +22 -9
  31. package/dist/cursor-provider-turn-summary.js +89 -0
  32. package/dist/cursor-provider.js +9 -3
  33. package/dist/cursor-request-provenance.js +42 -0
  34. package/dist/cursor-ripgrep-path.js +14 -3
  35. package/dist/cursor-runtime-state.js +50 -43
  36. package/dist/cursor-sdk-billed-usage.js +54 -75
  37. package/dist/cursor-sdk-event-debug-session.js +5 -6
  38. package/dist/cursor-sdk-event-debug.js +9 -6
  39. package/dist/cursor-sdk-process-error-guard.js +8 -7
  40. package/dist/cursor-sdk-runtime.js +13 -0
  41. package/dist/cursor-session-agent-cleanup.js +2 -2
  42. package/dist/cursor-session-agent-lifecycle.js +18 -9
  43. package/dist/cursor-session-agent-lineage.js +28 -12
  44. package/dist/cursor-session-agent-resume.js +51 -35
  45. package/dist/cursor-session-agent.js +9 -9
  46. package/dist/cursor-session-compaction-prep.js +2 -7
  47. package/dist/cursor-session-scope.js +71 -39
  48. package/dist/cursor-session-settings.js +61 -0
  49. package/dist/cursor-skill-tool.js +32 -49
  50. package/dist/cursor-state.js +72 -62
  51. package/dist/cursor-usage-accounting.js +13 -70
  52. package/dist/cursor-usage-command.js +89 -0
  53. package/dist/cursor-usage-ledger.js +512 -0
  54. package/dist/index.js +11 -19
  55. package/dist/model-discovery.js +11 -19
  56. package/docs/cursor-dogfood-checklist.md +3 -3
  57. package/docs/cursor-live-smoke-checklist.md +15 -15
  58. package/docs/cursor-model-ux-spec.md +59 -18
  59. package/docs/cursor-native-tool-replay.md +5 -3
  60. package/docs/cursor-native-tool-visual-audit.md +8 -8
  61. package/docs/cursor-testing-lessons.md +47 -23
  62. package/docs/cursor-tool-surfaces.md +1 -1
  63. package/docs/platform-smoke-implementation.md +10 -6
  64. package/docs/platform-smoke.md +35 -46
  65. package/package.json +6 -6
  66. package/scripts/cloud-runtime-smoke.mjs +4 -2
  67. package/scripts/local-resume-smoke.mjs +1 -1
  68. package/scripts/platform-smoke/card-detect.mjs +2 -8
  69. package/scripts/platform-smoke/targets.mjs +8 -15
  70. package/scripts/platform-smoke/visual-evidence.mjs +7 -2
  71. package/scripts/tmux-live-smoke.sh +57 -90
  72. package/src/context-window-cache.ts +0 -6
  73. package/src/context.ts +8 -0
  74. package/src/cursor-cloud-lifecycle.ts +124 -98
  75. package/src/cursor-durable-fs.ts +65 -3
  76. package/src/cursor-fallback-models.generated.ts +288 -1
  77. package/src/cursor-fallback-warning.ts +2 -2
  78. package/src/cursor-http1.ts +8 -8
  79. package/src/cursor-live-run-coordinator.ts +12 -13
  80. package/src/cursor-native-replay-routing.ts +2 -1
  81. package/src/cursor-native-tool-display-registration.ts +48 -24
  82. package/src/cursor-native-tool-display-state.ts +65 -21
  83. package/src/cursor-native-tool-display-tools.ts +7 -4
  84. package/src/cursor-pi-tool-bridge-constants.ts +27 -2
  85. package/src/cursor-pi-tool-bridge-run.ts +11 -3
  86. package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
  87. package/src/cursor-pi-tool-bridge.ts +26 -9
  88. package/src/cursor-provider-binding.ts +62 -0
  89. package/src/cursor-provider-errors.ts +27 -7
  90. package/src/cursor-provider-lazy.ts +21 -15
  91. package/src/cursor-provider-live-run-drain.ts +20 -13
  92. package/src/cursor-provider-run-finalizer.ts +24 -15
  93. package/src/cursor-provider-turn-coordinator.ts +2 -0
  94. package/src/cursor-provider-turn-display-router.ts +5 -0
  95. package/src/cursor-provider-turn-emit.ts +3 -1
  96. package/src/cursor-provider-turn-finalize.ts +38 -23
  97. package/src/cursor-provider-turn-prepare.ts +23 -14
  98. package/src/cursor-provider-turn-runner.ts +40 -13
  99. package/src/cursor-provider-turn-send.ts +23 -15
  100. package/src/cursor-provider-turn-summary.ts +94 -0
  101. package/src/cursor-provider-turn-types.ts +37 -4
  102. package/src/cursor-provider.ts +25 -3
  103. package/src/cursor-request-provenance.ts +64 -0
  104. package/src/cursor-ripgrep-path.ts +15 -5
  105. package/src/cursor-runtime-state.ts +53 -48
  106. package/src/cursor-sdk-billed-usage.ts +54 -102
  107. package/src/cursor-sdk-event-debug-session.ts +5 -5
  108. package/src/cursor-sdk-event-debug.ts +10 -6
  109. package/src/cursor-sdk-process-error-guard.ts +7 -7
  110. package/src/cursor-sdk-runtime.ts +14 -0
  111. package/src/cursor-session-agent-cleanup.ts +2 -2
  112. package/src/cursor-session-agent-lifecycle.ts +14 -9
  113. package/src/cursor-session-agent-lineage.ts +26 -12
  114. package/src/cursor-session-agent-resume.ts +49 -36
  115. package/src/cursor-session-agent.ts +13 -10
  116. package/src/cursor-session-compaction-prep.ts +2 -7
  117. package/src/cursor-session-scope.ts +87 -39
  118. package/src/cursor-session-settings.ts +72 -0
  119. package/src/cursor-skill-tool.ts +37 -61
  120. package/src/cursor-state.ts +74 -67
  121. package/src/cursor-usage-accounting.ts +17 -82
  122. package/src/cursor-usage-command.ts +76 -0
  123. package/src/cursor-usage-ledger.ts +500 -0
  124. package/src/index.ts +14 -23
  125. package/src/model-discovery.ts +14 -22
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { closeSync, constants, existsSync, fchmodSync, fstatSync, fsyncSync, lstatSync, openSync, readFileSync, readSync, writeFileSync } from "node:fs";
2
+ import { closeSync, constants, existsSync, fchmodSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { resolveCursorApiKey } from "./cursor-api-key.js";
5
5
  import { MAX_CLOUD_REPORT_BRANCHES, } from "./cursor-cloud-reporting.js";
6
6
  import { CLOUD_AGENT_ID_PATTERN, CLOUD_LIFECYCLE_ENTRY_TYPE, CLOUD_LIFECYCLE_JOURNAL_PREFIX, } from "../shared/cursor-cloud-lifecycle-constants.mjs";
7
- import { fsyncExistingRegularFile, noFollowFlag, openExistingRegularFileNoFollow } from "./cursor-durable-fs.js";
7
+ import { createRegularFileExclusive, fsyncExistingRegularFile, openExistingRegularFileNoFollow } from "./cursor-durable-fs.js";
8
8
  import { truncateCursorDisplayLine } from "./cursor-display-text.js";
9
9
  import { asRecord, getString } from "./cursor-record-utils.js";
10
10
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
@@ -19,6 +19,7 @@ const MAX_LIST_RECORDS = 25;
19
19
  const MAX_CLOUD_AGENT_ID_ERROR_LENGTH = 256;
20
20
  const DURABLE_LEDGER_VERSION = 1;
21
21
  const DURABLE_LEDGER_PREFIX = CLOUD_LIFECYCLE_JOURNAL_PREFIX;
22
+ const cloudBindings = new Map();
22
23
  let cloudLifecycleApi;
23
24
  let cloudLifecycleSession = {};
24
25
  let durableWriterForTests;
@@ -111,52 +112,29 @@ function buildBaseEntry(agentId, action) {
111
112
  };
112
113
  }
113
114
  function captureCloudLifecycleSession(ctx) {
114
- cloudLifecycleSession = {
115
- sessionFile: ctx.sessionManager.getSessionFile?.() ?? undefined,
116
- sessionId: ctx.sessionManager.getSessionId?.() ?? undefined,
117
- getBranch: () => ctx.sessionManager.getBranch(),
115
+ const manager = ctx.sessionManager;
116
+ return {
117
+ sessionFile: manager.getSessionFile() ?? undefined,
118
+ sessionId: manager.getSessionId(),
119
+ getBranch: () => manager.getBranch(),
118
120
  };
119
121
  }
120
122
  function durableLedgerPath(sessionFile, sessionId) {
121
123
  const sessionHash = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
122
124
  return join(dirname(sessionFile), `${DURABLE_LEDGER_PREFIX}-${sessionHash}.journal`);
123
125
  }
124
- function sameFileIdentity(left, right) {
125
- return left.dev === right.dev && left.ino === right.ino;
126
- }
127
- function createRegularFileExclusive(path, flags, mode) {
128
- let fd;
129
- try {
130
- fd = openSync(path, flags | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), mode);
131
- const opened = fstatSync(fd);
132
- const after = lstatSync(path);
133
- if (!opened.isFile() || !after.isFile() || !sameFileIdentity(opened, after)) {
134
- throw new Error("created journal path changed while opening");
135
- }
136
- return fd;
137
- }
138
- catch (error) {
139
- if (fd !== undefined) {
140
- try {
141
- closeSync(fd);
142
- }
143
- catch { }
144
- }
145
- throw error;
146
- }
147
- }
148
- function fsyncCloudLifecycleSessionFile() {
126
+ function fsyncCloudLifecycleSessionFile(session) {
149
127
  if (sessionFsyncForTests)
150
128
  return sessionFsyncForTests();
151
- const sessionFile = cloudLifecycleSession.sessionFile;
129
+ const sessionFile = session.sessionFile;
152
130
  if (!sessionFile || !existsSync(sessionFile))
153
131
  return true;
154
132
  return fsyncExistingRegularFile(sessionFile);
155
133
  }
156
- function appendDurableCloudLifecycleEntry(data, anchorEntryId) {
134
+ function appendDurableCloudLifecycleEntry(data, anchorEntryId, session) {
157
135
  if (durableWriterForTests)
158
136
  return durableWriterForTests(data);
159
- const { sessionFile, sessionId } = cloudLifecycleSession;
137
+ const { sessionFile, sessionId } = session;
160
138
  if (!sessionFile || !sessionId)
161
139
  return false;
162
140
  const entry = {
@@ -210,14 +188,14 @@ function appendDurableCloudLifecycleEntry(data, anchorEntryId) {
210
188
  }
211
189
  }
212
190
  }
213
- function appendCloudLifecycleEntry(pi, data) {
214
- if (!data || (!durableWriterForTests && (!cloudLifecycleSession.sessionFile || !cloudLifecycleSession.sessionId)))
191
+ function appendCloudLifecycleEntry(pi, data, session) {
192
+ if (!data || (!durableWriterForTests && (!session.sessionFile || !session.sessionId)))
215
193
  return false;
216
194
  let anchorEntryId;
217
195
  try {
218
- const previousEntryId = durableWriterForTests ? undefined : cloudLifecycleSession.getBranch?.().at(-1)?.id;
196
+ const previousEntryId = durableWriterForTests ? undefined : session.getBranch?.().at(-1)?.id;
219
197
  pi.appendEntry(CLOUD_LIFECYCLE_ENTRY_TYPE, data);
220
- const anchor = cloudLifecycleSession.getBranch?.().at(-1);
198
+ const anchor = session.getBranch?.().at(-1);
221
199
  anchorEntryId = durableWriterForTests
222
200
  ? "test-cloud-lifecycle-entry"
223
201
  : anchor?.type === "custom" &&
@@ -230,15 +208,14 @@ function appendCloudLifecycleEntry(pi, data) {
230
208
  return false;
231
209
  }
232
210
  return anchorEntryId !== undefined &&
233
- fsyncCloudLifecycleSessionFile() &&
234
- appendDurableCloudLifecycleEntry(data, anchorEntryId);
211
+ fsyncCloudLifecycleSessionFile(session) &&
212
+ appendDurableCloudLifecycleEntry(data, anchorEntryId, session);
235
213
  }
236
- function appendCloudLifecycleMutationEntry(pi, ctx, data) {
237
- const anchorEntryId = ctx.sessionManager.getBranch().at(-1)?.id;
238
- if (!data || !anchorEntryId || !fsyncCloudLifecycleSessionFile() || !appendDurableCloudLifecycleEntry(data, anchorEntryId))
214
+ function appendCloudLifecycleMutationEntry(pi, data, session, anchorEntryId) {
215
+ if (!data || !anchorEntryId || !fsyncCloudLifecycleSessionFile(session) || !appendDurableCloudLifecycleEntry(data, anchorEntryId, session))
239
216
  return false;
240
217
  try {
241
- pi.appendEntry(CLOUD_LIFECYCLE_ENTRY_TYPE, data);
218
+ pi?.appendEntry(CLOUD_LIFECYCLE_ENTRY_TYPE, data);
242
219
  }
243
220
  catch {
244
221
  // The fsynced mutation journal is authoritative.
@@ -247,12 +224,20 @@ function appendCloudLifecycleMutationEntry(pi, ctx, data) {
247
224
  }
248
225
  export function registerCursorCloudLifecycleLedger(pi) {
249
226
  cloudLifecycleApi = pi;
250
- pi.on("session_start", (_event, ctx) => captureCloudLifecycleSession(ctx));
251
- pi.on("before_agent_start", (_event, ctx) => captureCloudLifecycleSession(ctx));
252
- pi.on("session_tree", (_event, ctx) => captureCloudLifecycleSession(ctx));
227
+ const binding = { api: pi, session: {} };
228
+ cloudBindings.set(pi, binding);
229
+ const capture = (ctx) => {
230
+ binding.session = captureCloudLifecycleSession(ctx);
231
+ cloudLifecycleSession = binding.session;
232
+ };
233
+ pi.on("session_start", (_event, ctx) => capture(ctx));
234
+ pi.on("before_agent_start", (_event, ctx) => capture(ctx));
235
+ pi.on("session_tree", (_event, ctx) => capture(ctx));
236
+ pi.on("session_shutdown", () => { cloudBindings.delete(pi); });
253
237
  }
254
238
  export function recordCursorCloudLifecycleRun(report, options = {}) {
255
- if (!cloudLifecycleApi)
239
+ const api = options.binding?.api ?? cloudLifecycleApi;
240
+ if (!api)
256
241
  return false;
257
242
  const baseEntry = buildBaseEntry(report.agentId, "record");
258
243
  if (!baseEntry)
@@ -261,19 +246,26 @@ export function recordCursorCloudLifecycleRun(report, options = {}) {
261
246
  const runId = typeof report.runId === "string"
262
247
  ? sanitizeLedgerString(report.runId, MAX_LEDGER_RUN_ID_LENGTH, options.apiKey)
263
248
  : "";
264
- return appendCloudLifecycleEntry(cloudLifecycleApi, {
249
+ return appendCloudLifecycleEntry(api, {
265
250
  ...baseEntry,
266
251
  ...(runId ? { runId } : {}),
267
252
  ...(branches ? { branches } : {}),
268
- });
269
- }
270
- export function recordCursorCloudLifecycleSafely(report, apiKey) {
271
- try {
272
- return recordCursorCloudLifecycleRun({ ...report, branches: [] }, { apiKey });
273
- }
274
- catch {
275
- return false;
276
- }
253
+ }, options.binding?.session ?? cloudLifecycleSession);
254
+ }
255
+ export function captureCursorCloudLifecycleRecorder(owner) {
256
+ const current = owner ? cloudBindings.get(owner) : undefined;
257
+ const api = current?.api ?? (owner ? undefined : cloudLifecycleApi);
258
+ const session = { ...(current?.session ?? (owner ? {} : cloudLifecycleSession)) };
259
+ return (report, apiKey) => {
260
+ if (!api)
261
+ return false;
262
+ try {
263
+ return recordCursorCloudLifecycleRun({ ...report, branches: report.branches ?? [] }, { apiKey, binding: { api, session } });
264
+ }
265
+ catch {
266
+ return false;
267
+ }
268
+ };
277
269
  }
278
270
  export function createCursorCloudLifecyclePersistenceError(agentId, phase, cancellationConfirmed, apiKey) {
279
271
  const boundedAgentId = truncateCursorDisplayLine(scrubSensitiveText(agentId, apiKey), MAX_CLOUD_AGENT_ID_ERROR_LENGTH);
@@ -357,9 +349,8 @@ function readCursorCloudLifecycleEntries(entries) {
357
349
  export function readCursorCloudLifecycleAgents(entries) {
358
350
  return reduceCloudLifecycleEntries(readCursorCloudLifecycleEntries(entries));
359
351
  }
360
- function readDurableCloudLifecycleJournal(ctx) {
361
- const sessionFile = ctx.sessionManager.getSessionFile?.();
362
- const sessionId = ctx.sessionManager.getSessionId?.();
352
+ function readDurableCloudLifecycleJournal(session) {
353
+ const { sessionFile, sessionId } = session;
363
354
  if (!sessionFile || !sessionId)
364
355
  return [];
365
356
  let fd;
@@ -403,13 +394,13 @@ function readDurableCloudLifecycleJournal(ctx) {
403
394
  }
404
395
  return entries;
405
396
  }
406
- function readDurableCloudLifecycleEntries(ctx) {
407
- const sessionFile = ctx.sessionManager.getSessionFile?.();
397
+ function readDurableCloudLifecycleEntries(session) {
398
+ const { sessionFile } = session;
408
399
  if (!sessionFile)
409
400
  return [];
410
401
  const fileless = !existsSync(sessionFile);
411
- const branchIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
412
- return readDurableCloudLifecycleJournal(ctx).flatMap((entry) => {
402
+ const branchIds = new Set((session.getBranch?.() ?? []).map((entry) => entry.id));
403
+ return readDurableCloudLifecycleJournal(session).flatMap((entry) => {
413
404
  if (entry.data.action !== "record")
414
405
  return [entry.data];
415
406
  if (fileless)
@@ -420,18 +411,18 @@ function readDurableCloudLifecycleEntries(ctx) {
420
411
  function cloudLifecycleEntryKey(data) {
421
412
  return JSON.stringify([data.action, data.agentId, data.runId, data.timestamp, data.branches]);
422
413
  }
423
- function reconcileDurableCloudLifecycleOrphans(pi, ctx) {
424
- const sessionFile = ctx.sessionManager.getSessionFile?.();
414
+ function reconcileDurableCloudLifecycleOrphans(pi, session) {
415
+ const { sessionFile } = session;
425
416
  if (!sessionFile || !existsSync(sessionFile))
426
417
  return true;
427
- const journal = readDurableCloudLifecycleJournal(ctx);
418
+ const journal = readDurableCloudLifecycleJournal(session);
428
419
  const claimedAgentIds = new Set(journal.flatMap((entry) => entry.data.action === "record" && entry.anchorEntryId !== null ? [entry.data.agentId] : []));
429
420
  const latestOrphanByAgent = new Map();
430
421
  for (const entry of journal) {
431
422
  if (entry.data.action === "record" && entry.anchorEntryId === null)
432
423
  latestOrphanByAgent.set(entry.data.agentId, entry);
433
424
  }
434
- const branch = ctx.sessionManager.getBranch();
425
+ const branch = session.getBranch?.() ?? [];
435
426
  for (const [agentId, orphan] of latestOrphanByAgent) {
436
427
  if (claimedAgentIds.has(agentId))
437
428
  continue;
@@ -447,25 +438,25 @@ function reconcileDurableCloudLifecycleOrphans(pi, ctx) {
447
438
  }
448
439
  }
449
440
  if (matchingEntry) {
450
- if (!fsyncCloudLifecycleSessionFile() || !appendDurableCloudLifecycleEntry(orphan.data, matchingEntry.id))
441
+ if (!fsyncCloudLifecycleSessionFile(session) || !appendDurableCloudLifecycleEntry(orphan.data, matchingEntry.id, session))
451
442
  return false;
452
443
  continue;
453
444
  }
454
- if (orphan.sessionFile !== sessionFile && !appendCloudLifecycleEntry(pi, orphan.data))
445
+ if (orphan.sessionFile !== sessionFile && !appendCloudLifecycleEntry(pi, orphan.data, session))
455
446
  return false;
456
447
  }
457
448
  return true;
458
449
  }
459
- function reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx) {
460
- if (reconcileDurableCloudLifecycleOrphans(pi, ctx))
450
+ function reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx, session) {
451
+ if (reconcileDurableCloudLifecycleOrphans(pi, session))
461
452
  return true;
462
453
  ctx.ui.notify("Unable to reconcile the durable Cursor cloud lifecycle journal with this session branch.", "error");
463
454
  return false;
464
455
  }
465
- function readRecordedCloudAgents(ctx) {
466
- const durableEntries = readDurableCloudLifecycleEntries(ctx);
456
+ function readRecordedCloudAgents(session) {
457
+ const durableEntries = readDurableCloudLifecycleEntries(session);
467
458
  const durableKeys = new Set(durableEntries.map(cloudLifecycleEntryKey));
468
- const branchOnlyEntries = readCursorCloudLifecycleEntries(ctx.sessionManager.getBranch())
459
+ const branchOnlyEntries = readCursorCloudLifecycleEntries(session.getBranch?.() ?? [])
469
460
  .filter((entry) => !durableKeys.has(cloudLifecycleEntryKey(entry)));
470
461
  return reduceCloudLifecycleEntries([...durableEntries, ...branchOnlyEntries]);
471
462
  }
@@ -544,40 +535,78 @@ async function resolveCloudLifecycleMutationApiKey(ctx) {
544
535
  return undefined;
545
536
  }
546
537
  async function mutateRecordedCloudAgent(params) {
547
- const records = readRecordedCloudAgents(params.ctx);
548
- const validationError = validateRecordedCloudAgentId(params.agentId, records, params.action);
538
+ const manager = params.ctx.sessionManager;
539
+ const ui = params.ctx.ui;
540
+ const modelRegistry = params.ctx.modelRegistry;
541
+ const binding = cloudBindings.get(params.pi);
542
+ let anchorEntryId = manager.getBranch().at(-1)?.id;
543
+ // Include recoverable fileless records before auth, without writing anything
544
+ // until credentials are available. General provider recording stays live.
545
+ const readAuthority = () => JSON.stringify([readRecordedCloudAgents(params.session), readDurableCloudLifecycleJournal(params.session)]);
546
+ let authority = readAuthority();
547
+ const isOriginCurrent = () => {
548
+ try {
549
+ return cloudBindings.get(params.pi) === binding &&
550
+ params.ctx.sessionManager === manager &&
551
+ manager.getSessionFile() === params.session.sessionFile &&
552
+ manager.getSessionId() === params.session.sessionId &&
553
+ manager.getBranch().at(-1)?.id === anchorEntryId;
554
+ }
555
+ catch {
556
+ return false; // Pi invalidates the command context on owning reload.
557
+ }
558
+ };
559
+ const checkAuthority = () => {
560
+ if (isOriginCurrent() && readAuthority() === authority)
561
+ return true;
562
+ ui.notify(`Cursor cloud ${params.action} was not started because its originating session branch or authority changed. Retry from the intended branch.`, "error");
563
+ return false;
564
+ };
565
+ const apiKey = await resolveCloudLifecycleMutationApiKey({ modelRegistry, ui, sessionManager: manager });
566
+ if (!apiKey || !checkAuthority())
567
+ return;
568
+ if (!reconcileDurableCloudLifecycleOrphansForCommand(params.pi, { modelRegistry, ui, sessionManager: manager }, params.session))
569
+ return;
570
+ // Reconciliation may append only the unchanged, captured orphan authority.
571
+ anchorEntryId = manager.getBranch().at(-1)?.id;
572
+ authority = readAuthority();
573
+ const validationError = validateRecordedCloudAgentId(params.agentId, readRecordedCloudAgents(params.session), params.action);
549
574
  if (validationError) {
550
- params.ctx.ui.notify(validationError, "error");
575
+ ui.notify(validationError, "error");
551
576
  return;
552
577
  }
553
- const apiKey = params.apiKey;
554
578
  let operations;
555
579
  try {
556
580
  operations = await getSdkOperations();
557
581
  }
558
582
  catch (error) {
559
- params.ctx.ui.notify(`Failed to prepare Cursor cloud ${params.action}: ${formatCloudLifecycleError(error, apiKey)}`, "error");
583
+ ui.notify(`Failed to prepare Cursor cloud ${params.action}: ${formatCloudLifecycleError(error, apiKey)}`, "error");
560
584
  return;
561
585
  }
562
- if (!appendCloudLifecycleMutationEntry(params.pi, params.ctx, buildBaseEntry(params.agentId, params.action === "archive" ? "archive_intent" : "delete_intent"))) {
563
- params.ctx.ui.notify(`Cursor cloud ${params.action} was not started because its durable intent could not be recorded.`, "error");
586
+ if (!checkAuthority())
587
+ return;
588
+ if (!appendCloudLifecycleMutationEntry(params.pi, buildBaseEntry(params.agentId, params.action === "archive" ? "archive_intent" : "delete_intent"), params.session, anchorEntryId)) {
589
+ ui.notify(`Cursor cloud ${params.action} was not started because its durable intent could not be recorded.`, "error");
564
590
  return;
565
591
  }
592
+ // After SDK start the actual result belongs to this journal and post-intent
593
+ // anchor, even if Pi replaces the context or selects another branch.
594
+ anchorEntryId = manager.getBranch().at(-1)?.id;
566
595
  try {
567
596
  await operations[params.action](params.agentId, { apiKey });
568
597
  }
569
598
  catch (error) {
570
- params.ctx.ui.notify(`Cursor cloud ${params.action} for agent ${params.agentId} is unresolved: ${formatCloudLifecycleError(error, apiKey)} Inspect it in the Cursor Cloud dashboard before retrying.`, "error");
599
+ ui.notify(`Cursor cloud ${params.action} for agent ${params.agentId} is unresolved: ${formatCloudLifecycleError(error, apiKey)} Inspect it in the Cursor Cloud dashboard before retrying.`, "error");
571
600
  return;
572
601
  }
573
- if (!appendCloudLifecycleMutationEntry(params.pi, params.ctx, buildBaseEntry(params.agentId, params.action))) {
574
- params.ctx.ui.notify(`Cursor cloud agent ${params.agentId} ${params.action === "archive" ? "archived" : "deleted"}, but its durable result could not be recorded. Inspect it in the Cursor Cloud dashboard before retrying.`, "error");
602
+ if (!appendCloudLifecycleMutationEntry(isOriginCurrent() ? params.pi : undefined, buildBaseEntry(params.agentId, params.action), params.session, anchorEntryId)) {
603
+ ui.notify(`Cursor cloud agent ${params.agentId} ${params.action === "archive" ? "archived" : "deleted"}, but its durable result could not be recorded. Inspect it in the Cursor Cloud dashboard before retrying.`, "error");
575
604
  return;
576
605
  }
577
- params.ctx.ui.notify(`Cursor cloud agent ${params.agentId} ${params.action === "archive" ? "archived" : "deleted"}.`, "info");
606
+ ui.notify(`Cursor cloud agent ${params.agentId} ${params.action === "archive" ? "archived" : "deleted"}.`, "info");
578
607
  }
579
608
  export async function runCursorCloudLifecycleCommand(pi, args, ctx) {
580
- captureCloudLifecycleSession(ctx);
609
+ const session = captureCloudLifecycleSession(ctx);
581
610
  const usage = "Usage: /cursor-cloud list | archive <bc-agentId> | delete <bc-agentId> --yes";
582
611
  const tokens = tokenizeArgs(args);
583
612
  const [subcommand, agentId, ...rest] = tokens;
@@ -586,9 +615,9 @@ export async function runCursorCloudLifecycleCommand(pi, args, ctx) {
586
615
  ctx.ui.notify(`Invalid Cursor cloud arguments. ${usage}`, "error");
587
616
  return;
588
617
  }
589
- if (!reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx))
618
+ if (!reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx, session))
590
619
  return;
591
- ctx.ui.notify(formatCloudLifecycleRecords(readRecordedCloudAgents(ctx)), "info");
620
+ ctx.ui.notify(formatCloudLifecycleRecords(readRecordedCloudAgents(session)), "info");
592
621
  return;
593
622
  }
594
623
  if (subcommand === "archive") {
@@ -601,10 +630,7 @@ export async function runCursorCloudLifecycleCommand(pi, args, ctx) {
601
630
  ctx.ui.notify(validationError, "error");
602
631
  return;
603
632
  }
604
- const apiKey = await resolveCloudLifecycleMutationApiKey(ctx);
605
- if (!apiKey || !reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx))
606
- return;
607
- await mutateRecordedCloudAgent({ pi, ctx, agentId, action: "archive", apiKey });
633
+ await mutateRecordedCloudAgent({ pi, ctx, agentId, action: "archive", session });
608
634
  return;
609
635
  }
610
636
  if (subcommand === "delete") {
@@ -617,16 +643,14 @@ export async function runCursorCloudLifecycleCommand(pi, args, ctx) {
617
643
  ctx.ui.notify(validationError, "error");
618
644
  return;
619
645
  }
620
- const apiKey = await resolveCloudLifecycleMutationApiKey(ctx);
621
- if (!apiKey || !reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx))
622
- return;
623
- await mutateRecordedCloudAgent({ pi, ctx, agentId, action: "delete", apiKey });
646
+ await mutateRecordedCloudAgent({ pi, ctx, agentId, action: "delete", session });
624
647
  return;
625
648
  }
626
649
  ctx.ui.notify(`Invalid Cursor cloud command. ${usage}`, "error");
627
650
  }
628
651
  export const __testUtils = {
629
652
  reset: () => {
653
+ cloudBindings.clear();
630
654
  cloudLifecycleApi = undefined;
631
655
  cloudLifecycleSession = {};
632
656
  durableWriterForTests = undefined;
@@ -1,5 +1,6 @@
1
- import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync } from "node:fs";
2
- function sameFileIdentity(left, right) {
1
+ import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, openSync, readSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ export function sameFileIdentity(left, right) {
3
4
  return left.dev === right.dev && left.ino === right.ino;
4
5
  }
5
6
  export function noFollowFlag() {
@@ -29,7 +30,100 @@ export function openExistingRegularFileNoFollow(path, flags) {
29
30
  throw error;
30
31
  }
31
32
  }
32
- /** Opens a read-write descriptor (required for Windows FlushFileBuffers) so fsync applies to the real file, not a swapped-in symlink target. */
33
+ /** Creates a private regular file without replacing or following an existing path. */
34
+ export function createRegularFileExclusive(path, flags, mode) {
35
+ const fd = openSync(path, flags | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), mode);
36
+ try {
37
+ const opened = fstatSync(fd);
38
+ const after = lstatSync(path);
39
+ if (!opened.isFile() || !after.isFile() || !sameFileIdentity(opened, after))
40
+ throw new Error("created file changed while opening");
41
+ return fd;
42
+ }
43
+ catch (error) {
44
+ closeSync(fd);
45
+ throw error;
46
+ }
47
+ }
48
+ /** Bounded, fail-closed framed journals; an incomplete last frame is not recognized. */
49
+ export function readDurableFramePrefix(path, maxBytes = 16 * 1024 * 1024) {
50
+ let fd;
51
+ try {
52
+ fd = openExistingRegularFileNoFollow(path, constants.O_RDONLY);
53
+ }
54
+ catch (error) {
55
+ if (error.code === "ENOENT")
56
+ return { frames: [], incompleteTail: false };
57
+ throw error;
58
+ }
59
+ try {
60
+ if (fstatSync(fd).size > maxBytes)
61
+ throw new Error("journal exceeds size limit");
62
+ const buffer = Buffer.alloc(Math.min(fstatSync(fd).size + 1, maxBytes + 1));
63
+ let length = 0;
64
+ while (length < buffer.length) {
65
+ const count = readSync(fd, buffer, length, buffer.length - length, length);
66
+ if (!count)
67
+ break;
68
+ length += count;
69
+ }
70
+ if (length === buffer.length)
71
+ throw new Error("journal changed or exceeds size limit");
72
+ const text = buffer.subarray(0, length).toString("utf8");
73
+ const end = text.lastIndexOf("\n");
74
+ return {
75
+ frames: end < 0 ? [] : text.slice(0, end).split("\n").map(line => JSON.parse(line)),
76
+ incompleteTail: !!text && !text.endsWith("\n"),
77
+ };
78
+ }
79
+ finally {
80
+ closeSync(fd);
81
+ }
82
+ }
83
+ export function readDurableFrames(path, maxBytes = 16 * 1024 * 1024) {
84
+ const result = readDurableFramePrefix(path, maxBytes);
85
+ if (result.incompleteTail)
86
+ throw new Error("incomplete journal frame");
87
+ return result.frames;
88
+ }
89
+ export function appendDurableFrame(path, data, validate) {
90
+ // Validate framing and the owner's complete candidate history once, before recognition.
91
+ const frames = readDurableFrames(path);
92
+ validate?.([...frames, data]);
93
+ let created = false;
94
+ let fd;
95
+ try {
96
+ fd = createRegularFileExclusive(path, constants.O_RDWR | constants.O_APPEND, 0o600);
97
+ created = true;
98
+ }
99
+ catch (error) {
100
+ if (error.code !== "EEXIST")
101
+ throw error;
102
+ fd = openExistingRegularFileNoFollow(path, constants.O_RDWR | constants.O_APPEND);
103
+ }
104
+ try {
105
+ if (process.platform !== "win32")
106
+ fchmodSync(fd, 0o600);
107
+ const frame = `${JSON.stringify(data)}\n`;
108
+ if (fstatSync(fd).size + Buffer.byteLength(frame) > 16 * 1024 * 1024)
109
+ throw new Error("journal exceeds size limit");
110
+ writeFileSync(fd, frame);
111
+ fsyncSync(fd);
112
+ if (created && process.platform !== "win32") {
113
+ const directoryFd = openSync(dirname(path), constants.O_RDONLY);
114
+ try {
115
+ fsyncSync(directoryFd);
116
+ }
117
+ finally {
118
+ closeSync(directoryFd);
119
+ }
120
+ }
121
+ }
122
+ finally {
123
+ closeSync(fd);
124
+ }
125
+ }
126
+ /** Opens a read-write descriptor (required for Windows FlushFileBuffers) without following a swapped-in symlink. */
33
127
  export function fsyncExistingRegularFile(path) {
34
128
  let fd;
35
129
  try {