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.
Files changed (168) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +54 -50
  3. package/dist/bundled-context-windows.js +39 -0
  4. package/dist/context-window-cache.js +92 -0
  5. package/dist/context.js +338 -0
  6. package/dist/cursor-active-tools.js +3 -0
  7. package/dist/cursor-agent-message-web-tools.js +165 -0
  8. package/dist/cursor-agents-context-registration.js +17 -0
  9. package/dist/cursor-agents-context.js +113 -0
  10. package/dist/cursor-api-key.js +34 -0
  11. package/dist/cursor-bridge-contract.js +27 -0
  12. package/dist/cursor-cloud-lifecycle.js +650 -0
  13. package/dist/cursor-cloud-local-state.js +460 -0
  14. package/dist/cursor-cloud-options.js +145 -0
  15. package/dist/cursor-cloud-reporting.js +222 -0
  16. package/dist/cursor-compact-tool-summary.js +75 -0
  17. package/dist/cursor-config.js +613 -0
  18. package/dist/cursor-context-tools.js +4 -0
  19. package/dist/cursor-display-only-trace.js +11 -0
  20. package/dist/cursor-display-text.js +18 -0
  21. package/dist/cursor-durable-fs.js +53 -0
  22. package/dist/cursor-edit-diff.js +13 -0
  23. package/dist/cursor-env-boolean.js +19 -0
  24. package/dist/cursor-fallback-models.generated.js +7760 -0
  25. package/dist/cursor-fallback-warning.js +15 -0
  26. package/dist/cursor-http1.js +45 -0
  27. package/dist/cursor-incomplete-tool-visibility.js +83 -0
  28. package/dist/cursor-live-run-accounting.js +55 -0
  29. package/dist/cursor-live-run-coordinator.js +406 -0
  30. package/dist/cursor-mcp-timeout-override.js +116 -0
  31. package/dist/cursor-model-lifecycle.js +32 -0
  32. package/dist/cursor-model.js +5 -0
  33. package/dist/cursor-native-replay-routing.js +35 -0
  34. package/dist/cursor-native-replay-trace.js +28 -0
  35. package/dist/cursor-native-tool-display-registration.js +102 -0
  36. package/dist/cursor-native-tool-display-replay.js +428 -0
  37. package/dist/cursor-native-tool-display-state.js +70 -0
  38. package/dist/cursor-native-tool-display-tools.js +123 -0
  39. package/dist/cursor-native-tool-names.js +11 -0
  40. package/dist/cursor-partial-content-emitter.js +118 -0
  41. package/dist/cursor-pi-tool-bridge-abort.js +114 -0
  42. package/dist/cursor-pi-tool-bridge-constants.js +6 -0
  43. package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
  44. package/dist/cursor-pi-tool-bridge-env.js +18 -0
  45. package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
  46. package/dist/cursor-pi-tool-bridge-run.js +355 -0
  47. package/dist/cursor-pi-tool-bridge-server.js +163 -0
  48. package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
  49. package/dist/cursor-pi-tool-bridge-types.js +1 -0
  50. package/dist/cursor-pi-tool-bridge.js +123 -0
  51. package/dist/cursor-provider-errors.js +311 -0
  52. package/dist/cursor-provider-lazy.js +39 -0
  53. package/dist/cursor-provider-live-run-drain.js +412 -0
  54. package/dist/cursor-provider-overflow.js +90 -0
  55. package/dist/cursor-provider-run-finalizer.js +176 -0
  56. package/dist/cursor-provider-run-outcome.js +76 -0
  57. package/dist/cursor-provider-turn-coordinator.js +275 -0
  58. package/dist/cursor-provider-turn-display-router.js +158 -0
  59. package/dist/cursor-provider-turn-emit.js +35 -0
  60. package/dist/cursor-provider-turn-finalize.js +154 -0
  61. package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
  62. package/dist/cursor-provider-turn-prepare.js +374 -0
  63. package/dist/cursor-provider-turn-runner.js +146 -0
  64. package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
  65. package/dist/cursor-provider-turn-send.js +138 -0
  66. package/dist/cursor-provider-turn-shell-output.js +117 -0
  67. package/dist/cursor-provider-turn-tool-ledger.js +99 -0
  68. package/dist/cursor-provider-turn-types.js +1 -0
  69. package/dist/cursor-provider.js +72 -0
  70. package/dist/cursor-question-tool.js +194 -0
  71. package/dist/cursor-record-utils.js +61 -0
  72. package/dist/cursor-replay-activity-builders.js +114 -0
  73. package/dist/cursor-replay-source-names.js +27 -0
  74. package/dist/cursor-replay-summary-args.js +105 -0
  75. package/dist/cursor-replay-tool-details.js +167 -0
  76. package/dist/cursor-ripgrep-path.js +27 -0
  77. package/dist/cursor-run-final-text.js +63 -0
  78. package/dist/cursor-runtime-state.js +380 -0
  79. package/dist/cursor-sdk-billed-usage.js +86 -0
  80. package/dist/cursor-sdk-event-debug-constants.js +33 -0
  81. package/dist/cursor-sdk-event-debug-session.js +107 -0
  82. package/dist/cursor-sdk-event-debug.js +461 -0
  83. package/dist/cursor-sdk-output-filter.js +1 -0
  84. package/dist/cursor-sdk-process-error-guard.js +221 -0
  85. package/dist/cursor-sdk-runtime.js +3 -0
  86. package/dist/cursor-sensitive-text.js +23 -0
  87. package/dist/cursor-session-agent-cleanup.js +311 -0
  88. package/dist/cursor-session-agent-lifecycle.js +32 -0
  89. package/dist/cursor-session-agent-lineage.js +103 -0
  90. package/dist/cursor-session-agent-resume.js +417 -0
  91. package/dist/cursor-session-agent.js +507 -0
  92. package/dist/cursor-session-compaction-prep.js +21 -0
  93. package/dist/cursor-session-scope.js +113 -0
  94. package/dist/cursor-session-send-policy.js +18 -0
  95. package/dist/cursor-session-store.js +105 -0
  96. package/dist/cursor-session-turn-queue.js +50 -0
  97. package/dist/cursor-setting-sources.js +15 -0
  98. package/dist/cursor-skill-tool.js +230 -0
  99. package/dist/cursor-state.js +519 -0
  100. package/dist/cursor-task-presentation.js +59 -0
  101. package/dist/cursor-tool-lifecycle.js +94 -0
  102. package/dist/cursor-tool-manifest.js +34 -0
  103. package/dist/cursor-tool-presentation-registry.js +305 -0
  104. package/dist/cursor-tool-result-display-readers.js +158 -0
  105. package/dist/cursor-tool-transcript.js +32 -0
  106. package/dist/cursor-tool-visibility.js +28 -0
  107. package/dist/cursor-transcript-tool-formatters.js +618 -0
  108. package/dist/cursor-transcript-tool-specs.js +284 -0
  109. package/dist/cursor-transcript-utils.js +170 -0
  110. package/dist/cursor-usage-accounting.js +166 -0
  111. package/dist/cursor-web-tool-activity.js +23 -0
  112. package/dist/cursor-web-tool-args.js +12 -0
  113. package/dist/index.js +82 -0
  114. package/dist/model-discovery.js +305 -0
  115. package/dist/model-list-cache.js +156 -0
  116. package/docs/cursor-dogfood-checklist.md +2 -1
  117. package/docs/cursor-live-smoke-checklist.md +13 -12
  118. package/docs/cursor-model-ux-spec.md +25 -6
  119. package/docs/cursor-native-tool-replay.md +13 -13
  120. package/docs/cursor-native-tool-visual-audit.md +6 -3
  121. package/docs/cursor-testing-lessons.md +7 -7
  122. package/docs/cursor-tool-surfaces.md +5 -5
  123. package/docs/platform-smoke.md +3 -3
  124. package/package.json +19 -7
  125. package/platform-smoke.config.mjs +1 -1
  126. package/scripts/build.mjs +126 -0
  127. package/scripts/cloud-runtime-smoke.mjs +3 -1
  128. package/scripts/debug-provider-events.mjs +3 -1
  129. package/scripts/debug-sdk-events.mjs +1 -1
  130. package/scripts/isolated-cursor-smoke.sh +5 -4
  131. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
  132. package/scripts/lib/cursor-visual-render.mjs +1 -1
  133. package/scripts/lib/ensure-built.mjs +16 -0
  134. package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
  135. package/scripts/local-resume-smoke.mjs +3 -1
  136. package/scripts/platform-smoke/card-detect.mjs +1 -1
  137. package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
  138. package/scripts/platform-smoke/render-ansi.mjs +1 -1
  139. package/scripts/platform-smoke/targets.mjs +2 -2
  140. package/scripts/prepare.mjs +102 -0
  141. package/scripts/steering-rpc-smoke.mjs +4 -2
  142. package/scripts/tmux-live-smoke.sh +11 -11
  143. package/scripts/visual-tui-smoke-self-test.mjs +4 -1
  144. package/scripts/visual-tui-smoke.mjs +51 -7
  145. package/src/bundled-context-windows.ts +4 -1
  146. package/src/cursor-agents-context-registration.ts +2 -2
  147. package/src/cursor-agents-context.ts +0 -2
  148. package/src/cursor-api-key.ts +5 -4
  149. package/src/cursor-cloud-reporting.ts +23 -2
  150. package/src/cursor-fallback-models.generated.ts +592 -204
  151. package/src/cursor-live-run-coordinator.ts +1 -0
  152. package/src/cursor-native-tool-display-registration.ts +10 -11
  153. package/src/cursor-provider-errors.ts +1 -1
  154. package/src/cursor-provider-lazy.ts +5 -4
  155. package/src/cursor-provider-live-run-drain.ts +1 -0
  156. package/src/cursor-provider-run-finalizer.ts +1 -0
  157. package/src/cursor-provider-turn-finalize.ts +12 -0
  158. package/src/cursor-provider-turn-types.ts +2 -0
  159. package/src/cursor-sdk-billed-usage.ts +107 -0
  160. package/src/cursor-sdk-process-error-guard.ts +6 -4
  161. package/src/cursor-session-agent-lifecycle.ts +8 -9
  162. package/src/cursor-session-agent-resume.ts +23 -0
  163. package/src/cursor-session-compaction-prep.ts +4 -0
  164. package/src/cursor-tool-manifest.ts +1 -1
  165. package/src/cursor-usage-accounting.ts +70 -9
  166. package/src/index.ts +1 -1
  167. package/tsconfig.build.json +8 -0
  168. package/tsconfig.json +16 -0
@@ -43,6 +43,7 @@ export interface CursorLiveRun {
43
43
  sdkRun?: CursorLiveSdkRun;
44
44
  ignoreFutureSdkTurnUsage?: boolean;
45
45
  accounting: CursorLiveRunAccountingState;
46
+ billedTurnUsage?: CursorSdkTurnUsage;
46
47
  pendingEvents: CursorLiveQueuedEvent[];
47
48
  textDeltas: string[];
48
49
  emittedText: string;
@@ -18,6 +18,7 @@ import {
18
18
  skippedNativeToolNames,
19
19
  } from "./cursor-native-tool-display-state.js";
20
20
  import { isCursorReplayToolName } from "./cursor-tool-presentation-registry.js";
21
+ import { registerNativeCursorTool } from "./cursor-native-tool-display-tools.js";
21
22
 
22
23
  export const CURSOR_CORE_PI_REPLAY_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
23
24
  const CORE_PI_TOOL_NAMES = new Set<string>(CURSOR_CORE_PI_REPLAY_TOOL_NAMES);
@@ -40,12 +41,11 @@ type NativeRegistrationContext = Pick<ExtensionContext, "mode" | "model"> & {
40
41
  ui: Pick<ExtensionContext["ui"], "notify">;
41
42
  };
42
43
 
43
- async function registerNativeCursorToolsFromSet(
44
+ function registerNativeCursorToolsFromSet(
44
45
  pi: CursorNativeToolRegistryApi,
45
46
  toolNames: readonly NativeCursorToolName[],
46
- ): Promise<NativeCursorToolName[]> {
47
+ ): NativeCursorToolName[] {
47
48
  const newlySkippedToolNames: NativeCursorToolName[] = [];
48
- let registerNativeCursorTool: ((pi: CursorNativeToolRegistryApi, toolName: NativeCursorToolName) => void) | undefined;
49
49
  for (const toolName of toolNames) {
50
50
  if (registeredNativeToolNames.has(toolName) || skippedNativeToolNames.has(toolName)) continue;
51
51
  if (hasNonBuiltinTool(pi, toolName)) {
@@ -53,7 +53,6 @@ async function registerNativeCursorToolsFromSet(
53
53
  newlySkippedToolNames.push(toolName);
54
54
  continue;
55
55
  }
56
- registerNativeCursorTool ??= (await import("./cursor-native-tool-display-tools.js")).registerNativeCursorTool;
57
56
  registerNativeCursorTool(pi, toolName);
58
57
  registeredNativeToolNames.add(toolName);
59
58
  }
@@ -105,31 +104,31 @@ export function syncRegisteredNativeCursorToolsForModel(
105
104
  if (changed) pi.setActiveTools([...activeToolNames]);
106
105
  }
107
106
 
108
- async function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
107
+ function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
109
108
  if (!isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
110
109
 
111
110
  const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
112
111
  const skippedToolNames = [
113
- ...(await registerNativeCursorToolsFromSet(pi, nonCoreToolNames)),
114
- ...(await registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES)),
112
+ ...registerNativeCursorToolsFromSet(pi, nonCoreToolNames),
113
+ ...registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES),
115
114
  ];
116
115
  notifySkippedNativeCursorToolsIfNeeded(ctx, skippedToolNames);
117
116
  }
118
117
 
119
- async function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): Promise<void> {
118
+ function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
120
119
  const requested = isCursorNativeToolRegistrationRequested(ctx.mode);
121
120
  setCursorNativeToolDisplayRuntimeRequested(requested);
122
121
  if (!requested) {
123
122
  removeRegisteredNonCoreNativeCursorTools(pi);
124
123
  return;
125
124
  }
126
- await ensureNativeCursorToolsRegisteredForModel(pi, ctx);
125
+ ensureNativeCursorToolsRegisteredForModel(pi, ctx);
127
126
  syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
128
127
  }
129
128
 
130
129
  export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExtensionApi): void {
131
- registerCursorModelLifecycle(pi, async (ctx) => {
132
- await ensureThenSyncNativeCursorToolsForModel(pi, ctx);
130
+ registerCursorModelLifecycle(pi, (ctx) => {
131
+ ensureThenSyncNativeCursorToolsForModel(pi, ctx);
133
132
  });
134
133
  }
135
134
 
@@ -166,7 +166,7 @@ function isCursorSdkStallAbortNetworkError(code: unknown, evidence: string, stac
166
166
  }
167
167
 
168
168
  /**
169
- * @cursor/sdk@1.0.23 throws internal RetriableError (name/kind "RetriableError") with message
169
+ * @cursor/sdk@1.0.27 throws internal RetriableError (name/kind "RetriableError") with message
170
170
  * "Connection stalled" or "Connection stalled repeatedly" after fetchWithRetry exhausts stalls.
171
171
  * The ConnectError is only the cause; the top-level error is not a ConnectError.
172
172
  */
@@ -7,8 +7,10 @@ import {
7
7
  type Model,
8
8
  type SimpleStreamOptions,
9
9
  } from "@earendil-works/pi-ai";
10
+ import { streamCursor } from "./cursor-provider.js";
11
+ import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
10
12
 
11
- function makeProviderLoadErrorMessage(model: Model<Api>, error: unknown): AssistantMessage {
13
+ function makeProviderRuntimeErrorMessage(model: Model<Api>, error: unknown, apiKey?: string): AssistantMessage {
12
14
  return {
13
15
  role: "assistant",
14
16
  content: [],
@@ -25,7 +27,7 @@ function makeProviderLoadErrorMessage(model: Model<Api>, error: unknown): Assist
25
27
  },
26
28
  stopReason: "error",
27
29
  timestamp: Date.now(),
28
- errorMessage: `Failed to load Cursor provider runtime: ${error instanceof Error ? error.message : String(error)}`,
30
+ errorMessage: `Cursor provider runtime failed: ${sanitizeCursorProviderError(error, apiKey)}`,
29
31
  };
30
32
  }
31
33
 
@@ -37,12 +39,11 @@ export function streamCursorLazy(
37
39
  const outer = createAssistantMessageEventStream();
38
40
  queueMicrotask(async () => {
39
41
  try {
40
- const { streamCursor } = await import("./cursor-provider.js");
41
42
  for await (const event of streamCursor(model, context, options)) {
42
43
  outer.push(event);
43
44
  }
44
45
  } catch (error) {
45
- const message = makeProviderLoadErrorMessage(model, error);
46
+ const message = makeProviderRuntimeErrorMessage(model, error, options?.apiKey);
46
47
  outer.push({ type: "error", reason: "error", error: message });
47
48
  outer.end(message);
48
49
  }
@@ -383,6 +383,7 @@ export async function drainCursorLiveRunTurn(
383
383
  applyCursorUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens), {
384
384
  runtime: "local",
385
385
  turn: cursorLiveRuns.takeSdkTurnUsage(run),
386
+ billed: run.billedTurnUsage,
386
387
  });
387
388
  if (run.resumeNotice) {
388
389
  emitDisplayOnlyTraceBlock(stream, partial, run.resumeNotice);
@@ -186,6 +186,7 @@ export class CursorRunFinalizer {
186
186
  applyCursorUsage(partial, model, context, prepared.meta.promptInputTokens, {
187
187
  runtime: prepared.runtimeTarget,
188
188
  turn: prepared.runtime.turnCoordinator.lastSdkTurnUsage,
189
+ billed: prepared.runtime.billedTurnUsage,
189
190
  });
190
191
  if (prepared.meta.resumeNotice) emitDisplayOnlyTraceBlock(stream, partial, prepared.meta.resumeNotice);
191
192
  if (displayOnlyTraceBlock) emitDisplayOnlyTraceBlock(stream, partial, displayOnlyTraceBlock);
@@ -17,6 +17,7 @@ import {
17
17
  } from "./cursor-provider-run-outcome.js";
18
18
  import type { CursorProviderTurnPrepareResult } from "./cursor-provider-turn-types.js";
19
19
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
20
+ import { attachCursorSdkBilledTurnUsage } from "./cursor-sdk-billed-usage.js";
20
21
 
21
22
  export async function cacheSdkContextWindow(
22
23
  agentId: string,
@@ -149,6 +150,16 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
149
150
  resolvedApiKey: params.resolvedApiKey,
150
151
  optionsApiKey: params.optionsApiKey,
151
152
  });
153
+ const billed = await attachCursorSdkBilledTurnUsage({
154
+ agent: params.prepared.agent,
155
+ agentId: params.run.agentId,
156
+ runtime: params.prepared.runtimeTarget,
157
+ runId: params.run.id,
158
+ });
159
+ params.prepared.runtime.billedTurnUsage = billed.turn;
160
+ if (params.prepared.runtime.liveRun) {
161
+ params.prepared.runtime.liveRun.billedTurnUsage = billed.turn;
162
+ }
152
163
  let displayOnlyTraceBlock: string | undefined;
153
164
  if (params.prepared.runtimeTarget === "cloud" && isCursorRunFinishedSuccessfully(outcome)) {
154
165
  let report: CursorCloudRunReport = { agentId: params.run.agentId, runId: params.run.id, branches: [] };
@@ -158,6 +169,7 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
158
169
  run: params.run,
159
170
  waitResult,
160
171
  apiKey,
172
+ agentUsage: billed.agentUsage,
161
173
  });
162
174
  } catch (error) {
163
175
  recordCursorCloudReportingError(params.sdkEventDebug, error, apiKey);
@@ -14,6 +14,7 @@ import type { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
14
14
  import type { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
15
15
  import type { CursorPrompt } from "./context.js";
16
16
  import type { CursorResolvedSetting } from "./cursor-config.js";
17
+ import type { CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
17
18
 
18
19
  export interface CursorProviderTurnRunnerParams {
19
20
  model: Model<Api>;
@@ -44,6 +45,7 @@ export interface CursorProviderTurnSendMeta {
44
45
 
45
46
  interface CursorProviderTurnRuntimeBase {
46
47
  turnCoordinator: CursorSdkTurnCoordinator;
48
+ billedTurnUsage?: CursorSdkTurnUsage;
47
49
  }
48
50
 
49
51
  /**
@@ -0,0 +1,107 @@
1
+ import type { SDKAgent } from "@cursor/sdk";
2
+ import type { CursorRuntime } from "./cursor-config.js";
3
+ import { asRecord, getArray, getString } from "./cursor-record-utils.js";
4
+ import { readCursorSdkTurnUsage, type CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
5
+
6
+ const BILLED_USAGE_TIMEOUT_MS = 5000;
7
+
8
+ // ponytail: process-lifetime watermark of billed usage UUIDs per agentId; reset on process exit.
9
+ // Upgrade to session-scoped storage if multi-day processes retain enough agent IDs to matter.
10
+ const seenBilledRunIdsByAgent = new Map<string, Set<string>>();
11
+
12
+ export function isCursorSdkClientMintedRunId(runId: string): boolean {
13
+ return runId.startsWith("run-");
14
+ }
15
+
16
+ export function sumCursorSdkTurnUsage(usages: readonly CursorSdkTurnUsage[]): CursorSdkTurnUsage | undefined {
17
+ if (usages.length === 0) return undefined;
18
+ return usages.reduce(
19
+ (total, usage) => ({
20
+ inputTokens: total.inputTokens + usage.inputTokens,
21
+ outputTokens: total.outputTokens + usage.outputTokens,
22
+ cacheReadTokens: total.cacheReadTokens + usage.cacheReadTokens,
23
+ cacheWriteTokens: total.cacheWriteTokens + usage.cacheWriteTokens,
24
+ }),
25
+ );
26
+ }
27
+
28
+ export function peekCursorBilledUsageRunIds(agentId: string): ReadonlySet<string> {
29
+ return seenBilledRunIdsByAgent.get(agentId) ?? new Set();
30
+ }
31
+
32
+ export function rememberCursorBilledUsageRunIds(agentId: string, runIds: readonly string[]): void {
33
+ if (runIds.length === 0) return;
34
+ let seen = seenBilledRunIdsByAgent.get(agentId);
35
+ if (!seen) {
36
+ seen = new Set();
37
+ seenBilledRunIdsByAgent.set(agentId, seen);
38
+ }
39
+ for (const runId of runIds) seen.add(runId);
40
+ }
41
+
42
+ function withTimeout<T>(promise: Promise<T>, timeoutMs = BILLED_USAGE_TIMEOUT_MS): Promise<T | undefined> {
43
+ let timer: ReturnType<typeof setTimeout> | undefined;
44
+ const timeout = new Promise<undefined>((resolve) => {
45
+ timer = setTimeout(() => resolve(undefined), timeoutMs);
46
+ timer.unref?.();
47
+ });
48
+ return Promise.race([promise.catch(() => undefined), timeout]).finally(() => {
49
+ if (timer) clearTimeout(timer);
50
+ });
51
+ }
52
+
53
+ export function selectCursorBilledTurnUsage(
54
+ agentUsage: unknown,
55
+ options: { runtime: CursorRuntime; runId?: string; seenRunIds?: ReadonlySet<string> },
56
+ ): { turn?: CursorSdkTurnUsage; runIds: string[] } {
57
+ const runs = (getArray(asRecord(agentUsage), "runs") ?? []).flatMap((item) => {
58
+ const record = asRecord(item);
59
+ const runId = getString(record, "runId");
60
+ const usage = readCursorSdkTurnUsage(record?.usage);
61
+ return runId && usage ? [{ runId, usage }] : [];
62
+ });
63
+ if (options.runtime === "cloud" && options.runId) {
64
+ const match = runs.find((run) => run.runId === options.runId);
65
+ return match ? { turn: match.usage, runIds: [match.runId] } : { runIds: [] };
66
+ }
67
+ const unseen = runs.filter((run) => !options.seenRunIds?.has(run.runId));
68
+ return { turn: sumCursorSdkTurnUsage(unseen.map((run) => run.usage)), runIds: unseen.map((run) => run.runId) };
69
+ }
70
+
71
+ export async function fetchCursorSdkAgentUsage(
72
+ agent: SDKAgent,
73
+ options: { runtime: CursorRuntime; runId?: string },
74
+ ): Promise<unknown | undefined> {
75
+ if (typeof agent.getUsage !== "function") return undefined;
76
+ const query =
77
+ options.runtime === "cloud" && options.runId && isCursorSdkClientMintedRunId(options.runId)
78
+ ? { runId: options.runId }
79
+ : undefined;
80
+ return withTimeout(Promise.resolve(agent.getUsage(query)));
81
+ }
82
+
83
+ export async function attachCursorSdkBilledTurnUsage(options: {
84
+ agent: SDKAgent;
85
+ agentId: string;
86
+ runtime: CursorRuntime;
87
+ runId?: string;
88
+ }): Promise<{ agentUsage?: unknown; turn?: CursorSdkTurnUsage }> {
89
+ const agentUsage = await fetchCursorSdkAgentUsage(options.agent, {
90
+ runtime: options.runtime,
91
+ runId: options.runId,
92
+ });
93
+ if (!agentUsage) return {};
94
+ const selected = selectCursorBilledTurnUsage(agentUsage, {
95
+ runtime: options.runtime,
96
+ runId: options.runtime === "cloud" ? options.runId : undefined,
97
+ seenRunIds: peekCursorBilledUsageRunIds(options.agentId),
98
+ });
99
+ rememberCursorBilledUsageRunIds(options.agentId, selected.runIds);
100
+ return { agentUsage, turn: selected.turn };
101
+ }
102
+
103
+ export const __testUtils = {
104
+ reset(): void {
105
+ seenBilledRunIdsByAgent.clear();
106
+ },
107
+ };
@@ -75,10 +75,12 @@ function isCursorSdkAbortError(error: unknown): boolean {
75
75
  // The exact observed incident: the Cursor SDK 1.0.23 local shell executor writes a
76
76
  // spawned child's stdin without a stream 'error' listener, so a child exiting while
77
77
  // a write is in flight surfaces a raw `write EPIPE` uncaught exception whose stack
78
- // is exactly the single async pipe-write completion frame. Pi's own piped-stdout or
79
- // dead-terminal EPIPE normally surfaces through the synchronous write-dispatch path
80
- // with multiple frames (afterWriteDispatched/Socket._writeGeneric) and must stay
81
- // fatal per Unix convention, so anything beyond this one-frame contract is rejected.
78
+ // is exactly the single async pipe-write completion frame. Installed 1.0.27 attaches
79
+ // a no-op `error` listener before that write; keep this guard as defense in depth.
80
+ // Pi's own piped-stdout or dead-terminal EPIPE normally surfaces through the
81
+ // synchronous write-dispatch path with multiple frames (afterWriteDispatched /
82
+ // Socket._writeGeneric) and must stay fatal per Unix convention, so anything beyond
83
+ // this one-frame contract is rejected.
82
84
  const OBSERVED_CLOSED_PIPE_STACK_FRAME =
83
85
  /^\s+at WriteWrap\.onWriteComplete \[as oncomplete\] \(node:internal\/stream_base_commons:\d+:\d+\)$/;
84
86
 
@@ -7,6 +7,11 @@ import type {
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { clearCursorSdkHttp1 } from "./cursor-http1.js";
9
9
  import { onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
10
+ import {
11
+ disposeSessionCursorAgent,
12
+ invalidateSessionAgent,
13
+ resetSessionCursorAgent,
14
+ } from "./cursor-session-agent.js";
10
15
 
11
16
  export interface CursorSessionAgentLifecycleExtensionApi {
12
17
  on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
@@ -18,11 +23,9 @@ export interface CursorSessionAgentLifecycleExtensionApi {
18
23
 
19
24
  export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecycleExtensionApi): void {
20
25
  onCursorSessionScopeKeyChange(async (previousScopeKey) => {
21
- const { disposeSessionCursorAgent } = await import("./cursor-session-agent.js");
22
26
  await disposeSessionCursorAgent(previousScopeKey);
23
27
  });
24
28
  pi.on("session_shutdown", async (event) => {
25
- const { disposeSessionCursorAgent, resetSessionCursorAgent } = await import("./cursor-session-agent.js");
26
29
  try {
27
30
  if (event.reason === "reload") {
28
31
  await resetSessionCursorAgent();
@@ -33,20 +36,16 @@ export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecy
33
36
  clearCursorSdkHttp1();
34
37
  }
35
38
  });
36
- pi.on("session_compact", async () => {
37
- const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
39
+ pi.on("session_compact", () => {
38
40
  invalidateSessionAgent();
39
41
  });
40
- pi.on("session_before_tree", async () => {
41
- const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
42
+ pi.on("session_before_tree", () => {
42
43
  invalidateSessionAgent();
43
44
  });
44
45
  pi.on("session_tree", async () => {
45
- const { resetSessionCursorAgent } = await import("./cursor-session-agent.js");
46
46
  await resetSessionCursorAgent();
47
47
  });
48
- pi.on("model_select", async () => {
49
- const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
48
+ pi.on("model_select", () => {
50
49
  invalidateSessionAgent();
51
50
  });
52
51
  }
@@ -81,6 +81,19 @@ const state: CursorSessionResumeState = {
81
81
  unownedUserEntryIds: new Set(),
82
82
  };
83
83
 
84
+ // Compaction summarizer sends commit a one-message resume handle. Drop it so the
85
+ // next normal turn_end cannot persist that lineage (#223).
86
+ let resumeHandlePersistSuppressed = false;
87
+
88
+ export function suppressCursorSessionAgentResumeHandlePersist(): void {
89
+ resumeHandlePersistSuppressed = true;
90
+ state.pendingHandle = undefined;
91
+ }
92
+
93
+ export function allowCursorSessionAgentResumeHandlePersist(): void {
94
+ resumeHandlePersistSuppressed = false;
95
+ }
96
+
84
97
  function hashParts(parts: readonly string[]): string {
85
98
  const hash = createHash("sha256");
86
99
  for (const part of parts) {
@@ -360,6 +373,7 @@ export function getMatchingCursorSessionAgentResumeHandle(poolKey: string): Curs
360
373
  }
361
374
 
362
375
  export function persistCursorSessionAgentResumeHandle(input: PendingCursorSessionAgentResumeHandle): void {
376
+ if (resumeHandlePersistSuppressed) return;
363
377
  if (!isCursorLocalAgentId(input.agentId)) return;
364
378
  state.pendingHandle = {
365
379
  runtime: input.runtime,
@@ -371,6 +385,11 @@ export function persistCursorSessionAgentResumeHandle(input: PendingCursorSessio
371
385
  }
372
386
 
373
387
  function flushPendingCursorSessionAgentResumeHandle(branch: readonly SessionEntry[]): void {
388
+ if (resumeHandlePersistSuppressed) {
389
+ state.pendingHandle = undefined;
390
+ restoreFromBranch(branch);
391
+ return;
392
+ }
374
393
  restoreFromBranch(branch);
375
394
  const pending = state.pendingHandle;
376
395
  state.pendingHandle = undefined;
@@ -442,6 +461,8 @@ export function registerCursorSessionAgentResume(pi: CursorSessionAgentResumeExt
442
461
  restoreFromSessionManager(ctx.sessionManager);
443
462
  });
444
463
  pi.on("session_compact", (event, ctx) => {
464
+ state.pendingHandle = undefined;
465
+ resumeHandlePersistSuppressed = false;
445
466
  const branch = ctx.sessionManager.getBranch();
446
467
  if (branch.length > 0) {
447
468
  restoreFromSessionManager(ctx.sessionManager);
@@ -471,6 +492,7 @@ function resetStateForTests(): void {
471
492
  state.lastBranchHandle = undefined;
472
493
  state.pendingHandle = undefined;
473
494
  state.unownedUserEntryIds = new Set();
495
+ resumeHandlePersistSuppressed = false;
474
496
  }
475
497
 
476
498
  export const __testUtils = {
@@ -479,4 +501,5 @@ export const __testUtils = {
479
501
  reset: resetStateForTests,
480
502
  set: setStateForTests,
481
503
  state,
504
+ isResumeHandlePersistSuppressed: () => resumeHandlePersistSuppressed,
482
505
  };
@@ -1,15 +1,19 @@
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";
3
4
  import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
4
5
 
5
6
  /**
6
7
  * Prepare the pooled Cursor session agent for pi compaction summarization.
7
8
  * Releases any scoped live-run drain state still tied to the pooled agent, then
8
9
  * disposes the pool entry so summarization acquires a clean SDK agent.
10
+ * Suppresses local-resume handle persist so the summarizer send cannot leak
11
+ * a one-message handle into the next normal turn (#223).
9
12
  */
10
13
  export async function prepareCursorSessionForCompaction(
11
14
  scopeKey: string = getCursorSessionScopeKey(),
12
15
  ): Promise<void> {
16
+ suppressCursorSessionAgentResumeHandlePersist();
13
17
  while (true) {
14
18
  const run = cursorLiveRuns.getActiveForScope(scopeKey);
15
19
  if (!run || run.disposed) break;
@@ -4,7 +4,7 @@ import type { CursorPiToolBridgeSnapshot } from "./cursor-pi-tool-bridge-types.j
4
4
  export const CURSOR_TOOL_MANIFEST_ENV = "PI_CURSOR_TOOL_MANIFEST";
5
5
 
6
6
  /**
7
- * Representative @cursor/sdk@1.0.23 local-agent ToolType values; actual exposure can vary by run.
7
+ * Representative @cursor/sdk@1.0.27 local-agent ToolType values; actual exposure can vary by run.
8
8
  * See docs/cursor-native-tool-replay.md#sdk-tooltype-replay-matrix.
9
9
  */
10
10
  export const CURSOR_HOST_TOOL_MANIFEST_SUMMARY =
@@ -91,18 +91,30 @@ function getCursorSdkUncachedInputTokens(turnUsage: CursorSdkTurnUsage): number
91
91
  return turnUsage.inputTokens - turnUsage.cacheReadTokens - turnUsage.cacheWriteTokens;
92
92
  }
93
93
 
94
- export function isCursorSdkUsageSafeForPiMessage(turnUsage: CursorSdkTurnUsage, model: Model<Api>): boolean {
94
+ export function isCursorSdkUsagePartitionSafe(turnUsage: CursorSdkTurnUsage, model: Model<Api>): boolean {
95
95
  const counts = [turnUsage.inputTokens, turnUsage.outputTokens, turnUsage.cacheReadTokens, turnUsage.cacheWriteTokens];
96
96
  const uncachedInput = getCursorSdkUncachedInputTokens(turnUsage);
97
97
  return (
98
98
  counts.every((count) => Number.isFinite(count) && count >= 0) &&
99
99
  Number.isFinite(uncachedInput) &&
100
100
  uncachedInput >= 0 &&
101
- turnUsage.outputTokens <= model.maxTokens &&
101
+ turnUsage.outputTokens <= model.maxTokens
102
+ );
103
+ }
104
+
105
+ export function isCursorSdkUsageSafeForPiMessage(turnUsage: CursorSdkTurnUsage, model: Model<Api>): boolean {
106
+ return (
107
+ isCursorSdkUsagePartitionSafe(turnUsage, model) &&
102
108
  turnUsage.inputTokens + turnUsage.outputTokens <= model.contextWindow
103
109
  );
104
110
  }
105
111
 
112
+ export interface CursorSdkUsageApplyOptions {
113
+ runtime: CursorRuntime;
114
+ turn?: CursorSdkTurnUsage;
115
+ billed?: CursorSdkTurnUsage;
116
+ }
117
+
106
118
  export function applyCursorSdkUsage(partial: AssistantMessage, turnUsage: CursorSdkTurnUsage): void {
107
119
  // Pi treats input/cacheRead/cacheWrite as disjoint additive prompt components.
108
120
  partial.usage.input = getCursorSdkUncachedInputTokens(turnUsage);
@@ -117,8 +129,23 @@ function isCompatibleCursorAssistantMeasurement(assistant: AssistantMessage, mod
117
129
  return assistant.api === model.api && assistant.provider === model.provider && assistant.model === model.id;
118
130
  }
119
131
 
132
+ function getLatestCompactionBoundary(context: Context): { index: number; tokensBefore?: number } | undefined {
133
+ for (let index = context.messages.length - 1; index >= 0; index -= 1) {
134
+ const message = context.messages[index] as { role?: string; tokensBefore?: number };
135
+ if (message.role !== "compactionSummary") continue;
136
+ const tokensBefore = message.tokensBefore;
137
+ return {
138
+ index,
139
+ tokensBefore: Number.isFinite(tokensBefore) && tokensBefore !== undefined && tokensBefore > 0 ? Math.floor(tokensBefore) : undefined,
140
+ };
141
+ }
142
+ return undefined;
143
+ }
144
+
120
145
  function getLastAcceptedContextOccupancy(context: Context, model: Model<Api>): number {
146
+ const boundary = getLatestCompactionBoundary(context);
121
147
  for (let index = context.messages.length - 1; index >= 0; index -= 1) {
148
+ if (boundary && index < boundary.index) break;
122
149
  const message = context.messages[index];
123
150
  if (message.role !== "assistant" || !("usage" in message)) continue;
124
151
  const assistant = message as AssistantMessage;
@@ -127,7 +154,9 @@ function getLastAcceptedContextOccupancy(context: Context, model: Model<Api>): n
127
154
  const { usage } = assistant;
128
155
  const total =
129
156
  usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
130
- if (Number.isFinite(total) && total > 0 && total <= model.contextWindow) return total;
157
+ if (!Number.isFinite(total) || total <= 0 || total > model.contextWindow) continue;
158
+ if (boundary?.tokensBefore !== undefined && total >= boundary.tokensBefore) continue;
159
+ return total;
131
160
  }
132
161
  return 0;
133
162
  }
@@ -146,18 +175,50 @@ export function applyCursorApproximateUsage(partial: AssistantMessage, model: Mo
146
175
  );
147
176
  }
148
177
 
178
+ function applyCursorOccupancyEstimate(partial: AssistantMessage, model: Model<Api>, context: Context): void {
179
+ partial.usage.totalTokens = Math.max(
180
+ estimateCursorContextTotalTokens(partial, model, context),
181
+ getLastAcceptedContextOccupancy(context, model),
182
+ );
183
+ }
184
+
185
+ function isCurrentLocalOccupancy(turn: CursorSdkTurnUsage, model: Model<Api>, context: Context): boolean {
186
+ if (!isCursorSdkUsageSafeForPiMessage(turn, model)) return false;
187
+ const tokensBefore = getLatestCompactionBoundary(context)?.tokensBefore;
188
+ return tokensBefore === undefined || turn.inputTokens + turn.outputTokens < tokensBefore;
189
+ }
190
+
191
+ function applyResolvedCursorOccupancy(
192
+ partial: AssistantMessage,
193
+ model: Model<Api>,
194
+ context: Context,
195
+ localTurn: CursorSdkTurnUsage | undefined,
196
+ ): void {
197
+ if (localTurn && isCurrentLocalOccupancy(localTurn, model, context)) {
198
+ partial.usage.totalTokens = localTurn.inputTokens + localTurn.outputTokens;
199
+ return;
200
+ }
201
+ applyCursorOccupancyEstimate(partial, model, context);
202
+ }
203
+
149
204
  export function applyCursorUsage(
150
205
  partial: AssistantMessage,
151
206
  model: Model<Api>,
152
207
  context: Context,
153
208
  sessionInputTokens: number,
154
- sdkUsage?: { runtime: CursorRuntime; turn?: CursorSdkTurnUsage },
209
+ sdkUsage?: CursorSdkUsageApplyOptions,
155
210
  ): void {
156
- // Only local raw turn-ended usage has a captured full-prompt/cache-partition contract.
157
- // Cloud raw usage remains display-only until its field semantics are independently observed.
158
- const usage = sdkUsage?.runtime === "local" ? sdkUsage.turn : undefined;
159
- if (usage && isCursorSdkUsageSafeForPiMessage(usage, model)) {
160
- applyCursorSdkUsage(partial, usage);
211
+ const billed = sdkUsage?.billed;
212
+ const localTurn = sdkUsage?.runtime === "local" ? sdkUsage.turn : undefined;
213
+ if (billed && isCursorSdkUsagePartitionSafe(billed, model)) {
214
+ applyCursorSdkUsage(partial, billed);
215
+ applyResolvedCursorOccupancy(partial, model, context, localTurn);
216
+ return;
217
+ }
218
+ // Only local raw turn-ended usage has a captured full-prompt/cache-partition occupancy contract.
219
+ if (localTurn && isCursorSdkUsageSafeForPiMessage(localTurn, model)) {
220
+ applyCursorSdkUsage(partial, localTurn);
221
+ applyResolvedCursorOccupancy(partial, model, context, localTurn);
161
222
  return;
162
223
  }
163
224
  applyCursorApproximateUsage(partial, model, context, sessionInputTokens);
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import { registerCursorFallbackIssueWarning } from "./cursor-fallback-warning.js
15
15
  import { registerCursorAgentsContextDedup } from "./cursor-agents-context-registration.js";
16
16
  import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
17
17
  import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
18
+ import { prepareCursorSessionForCompaction } from "./cursor-session-compaction-prep.js";
18
19
 
19
20
  type CursorExtensionApi =
20
21
  & Pick<ExtensionAPI, "registerProvider" | "registerCommand" | "on">
@@ -54,7 +55,6 @@ export default async function (pi: CursorExtensionApi) {
54
55
  registerCursorSessionAgentLifecycle(pi);
55
56
  registerCursorSessionAgentResume(pi);
56
57
  pi.on("session_before_compact", async () => {
57
- const { prepareCursorSessionForCompaction } = await import("./cursor-session-compaction-prep.js");
58
58
  await prepareCursorSessionForCompaction();
59
59
  });
60
60
  registerCursorRuntimeControls(pi);
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "declaration": false,
6
+ "sourceMap": false
7
+ }
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true,
12
+ "noEmit": true
13
+ },
14
+ "include": ["src"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }