pi-cursor-sdk 0.1.17 → 0.1.19

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 (51) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +38 -1
  3. package/docs/cursor-live-smoke-checklist.md +22 -2
  4. package/docs/cursor-model-ux-spec.md +5 -4
  5. package/docs/cursor-native-tool-replay.md +96 -2
  6. package/docs/cursor-testing-lessons.md +428 -0
  7. package/package.json +11 -2
  8. package/scripts/debug-provider-events.mjs +403 -0
  9. package/scripts/debug-sdk-events.mjs +413 -0
  10. package/scripts/isolated-cursor-smoke.sh +226 -0
  11. package/scripts/lib/cursor-probe-utils.mjs +52 -0
  12. package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
  13. package/scripts/validate-smoke-jsonl.mjs +86 -7
  14. package/src/context.ts +45 -32
  15. package/src/cursor-agent-message-web-tools.ts +172 -0
  16. package/src/cursor-agents-context.ts +176 -0
  17. package/src/cursor-context-tools.ts +6 -0
  18. package/src/cursor-display-text.ts +10 -0
  19. package/src/cursor-incomplete-tool-visibility.ts +118 -0
  20. package/src/cursor-live-run-coordinator.ts +18 -7
  21. package/src/cursor-model.ts +12 -0
  22. package/src/cursor-native-replay-routing.ts +48 -0
  23. package/src/cursor-native-replay-trace.ts +29 -0
  24. package/src/cursor-native-tool-display-registration.ts +14 -7
  25. package/src/cursor-native-tool-display-replay.ts +63 -5
  26. package/src/cursor-native-tool-display-tools.ts +20 -0
  27. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  28. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  29. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  30. package/src/cursor-provider-errors.ts +96 -0
  31. package/src/cursor-provider-live-run-drain.ts +208 -63
  32. package/src/cursor-provider-turn-coordinator.ts +217 -47
  33. package/src/cursor-provider.ts +275 -83
  34. package/src/cursor-question-tool.ts +10 -5
  35. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  36. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  37. package/src/cursor-sdk-event-debug-session.ts +163 -0
  38. package/src/cursor-sdk-event-debug.ts +597 -0
  39. package/src/cursor-sensitive-text.ts +27 -7
  40. package/src/cursor-session-agent.ts +25 -3
  41. package/src/cursor-session-send-policy.ts +43 -0
  42. package/src/cursor-setting-sources.ts +29 -0
  43. package/src/cursor-state.ts +1 -5
  44. package/src/cursor-tool-lifecycle.ts +111 -0
  45. package/src/cursor-tool-names.ts +12 -0
  46. package/src/cursor-tool-transcript.ts +4 -2
  47. package/src/cursor-transcript-tool-formatters.ts +228 -5
  48. package/src/cursor-transcript-tool-specs.ts +113 -14
  49. package/src/cursor-transcript-utils.ts +12 -0
  50. package/src/cursor-web-tool-activity.ts +84 -0
  51. package/src/index.ts +4 -1
@@ -2,24 +2,32 @@ import type { AssistantMessage, AssistantMessageEventStream } from "@earendil-wo
2
2
  import type { InteractionUpdate } from "@cursor/sdk";
3
3
  import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
4
4
  import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
5
- import { canRenderCursorToolNatively } from "./cursor-native-tool-display.js";
5
+ import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
6
+ import { resolveNativeReplayDisposition } from "./cursor-native-replay-routing.js";
7
+ import { truncateCursorDisplayLine } from "./cursor-display-text.js";
6
8
  import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
7
9
  import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
8
10
  import { scrubPiToolDisplay, scrubSensitiveText } from "./cursor-sensitive-text.js";
9
11
  import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
10
- import { getString, getToolArgs, getToolName } from "./cursor-transcript-utils.js";
11
-
12
- function sanitizeSingleLine(value: string): string {
13
- return value.replace(/\s+/g, " ").trim();
14
- }
15
-
16
- function truncateSingleLine(value: string, maxLength = 240): string {
17
- const sanitized = sanitizeSingleLine(value);
18
- return sanitized.length > maxLength ? `${sanitized.slice(0, maxLength - 1)}…` : sanitized;
19
- }
12
+ import {
13
+ recordDiscardedIncompleteStartedToolCall,
14
+ type CursorSdkEventDebugRecorder,
15
+ DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
16
+ } from "./cursor-sdk-event-debug.js";
17
+ import {
18
+ buildIncompleteCursorToolDisplay,
19
+ formatIncompleteCursorToolTrace,
20
+ type IncompleteCursorToolDiscardReason,
21
+ } from "./cursor-incomplete-tool-visibility.js";
22
+ import { getToolName, normalizeToolName } from "./cursor-transcript-utils.js";
23
+ import {
24
+ CURSOR_TOOL_LIFECYCLE_DEFER_MS,
25
+ formatCursorToolLifecycleProgressText,
26
+ isCursorToolLifecycleEligible,
27
+ } from "./cursor-tool-lifecycle.js";
20
28
 
21
29
  function formatCursorToolName(toolCall: unknown): string {
22
- return truncateSingleLine(getToolName(toolCall), 80) || "unknown";
30
+ return truncateCursorDisplayLine(getToolName(toolCall), 80) || "unknown";
23
31
  }
24
32
 
25
33
  interface CursorShellOutputDelta {
@@ -37,17 +45,6 @@ function isCursorShellToolCall(toolCall: unknown): boolean {
37
45
  return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
38
46
  }
39
47
 
40
- function isCursorTaskToolCall(toolCall: unknown): boolean {
41
- return getToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase() === "task";
42
- }
43
-
44
- function extractCursorTaskProgressLabel(toolCall: unknown, apiKey?: string): string | undefined {
45
- if (!isCursorTaskToolCall(toolCall)) return undefined;
46
- const description = getString(getToolArgs(toolCall), "description");
47
- if (!description?.trim()) return undefined;
48
- return truncateSingleLine(scrubSensitiveText(description, apiKey));
49
- }
50
-
51
48
  function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
52
49
  if (update.type !== "shell-output-delta") return undefined;
53
50
  const event = getField(update, "event");
@@ -90,13 +87,15 @@ function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: Cur
90
87
  };
91
88
  }
92
89
 
90
+ type CursorToolDisplaySource = "started" | "fallback" | "transcript";
91
+
93
92
  type ToolCompletionResolution =
94
93
  | { action: "ignore-bridge"; identity?: string }
95
94
  | {
96
95
  action: "handle";
97
96
  toolCall: unknown;
98
97
  identity?: string;
99
- source?: "started" | "fallback";
98
+ source?: CursorToolDisplaySource;
100
99
  matchedStartedCallId?: string;
101
100
  };
102
101
 
@@ -107,8 +106,10 @@ export interface CursorSdkTurnCoordinatorOptions {
107
106
  resolvedApiKey?: string;
108
107
  liveRun?: CursorLiveRun;
109
108
  useNativeToolReplay: boolean;
109
+ activeToolNames?: ReadonlySet<string>;
110
110
  nativeReplayId: string;
111
111
  textDeltas: string[];
112
+ debugRecorder?: CursorSdkEventDebugRecorder;
112
113
  }
113
114
 
114
115
  export class CursorSdkTurnCoordinator {
@@ -118,10 +119,12 @@ export class CursorSdkTurnCoordinator {
118
119
  readonly resolvedApiKey?: string;
119
120
  readonly liveRun?: CursorLiveRun;
120
121
  readonly useNativeToolReplay: boolean;
122
+ readonly activeToolNames?: ReadonlySet<string>;
121
123
  readonly nativeReplayId: string;
122
124
  readonly textDeltas: string[];
123
125
 
124
126
  private readonly contentEmitter: CursorPartialContentEmitter;
127
+ private readonly debugRecorder?: CursorSdkEventDebugRecorder;
125
128
  private nativeToolDisplayCounter = 0;
126
129
  private nativeToolReplayStarted = false;
127
130
  private cursorPlanTextCandidate: string | undefined;
@@ -133,7 +136,8 @@ export class CursorSdkTurnCoordinator {
133
136
  private readonly completedToolIdentities = new Set<string>();
134
137
  private readonly completedStartedToolFingerprints = new Set<string>();
135
138
  private readonly completedFallbackToolFingerprints = new Set<string>();
136
- private readonly emittedTaskProgressCallIds = new Set<string>();
139
+ private readonly emittedLifecycleCallIds = new Set<string>();
140
+ private readonly lifecycleTimers = new Map<string, ReturnType<typeof setTimeout>>();
137
141
 
138
142
  constructor(options: CursorSdkTurnCoordinatorOptions) {
139
143
  this.stream = options.stream;
@@ -142,8 +146,10 @@ export class CursorSdkTurnCoordinator {
142
146
  this.resolvedApiKey = options.resolvedApiKey;
143
147
  this.liveRun = options.liveRun;
144
148
  this.useNativeToolReplay = options.useNativeToolReplay;
149
+ this.activeToolNames = options.activeToolNames;
145
150
  this.nativeReplayId = options.nativeReplayId;
146
151
  this.textDeltas = options.textDeltas;
152
+ this.debugRecorder = options.debugRecorder;
147
153
  this.contentEmitter = new CursorPartialContentEmitter(options.stream, options.partial, undefined, false);
148
154
  }
149
155
 
@@ -155,13 +161,26 @@ export class CursorSdkTurnCoordinator {
155
161
  return this.nativeToolReplayStarted;
156
162
  }
157
163
 
158
- discardIncompleteStartedToolCalls(): void {
164
+ discardIncompleteStartedToolCalls(
165
+ reason: IncompleteCursorToolDiscardReason = DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
166
+ ): void {
167
+ for (const [callId, toolCall] of this.startedToolCalls) {
168
+ if (typeof callId !== "string") continue;
169
+ recordDiscardedIncompleteStartedToolCall(this.debugRecorder, process.env, {
170
+ toolName: normalizeToolName(getToolName(toolCall)),
171
+ callId,
172
+ reason,
173
+ });
174
+ this.emitIncompleteStartedToolCall(toolCall, reason);
175
+ }
159
176
  this.startedToolCalls.clear();
160
177
  this.bridgeStartedToolCallIds.clear();
161
178
  this.activeShellCallIds.clear();
162
179
  this.ambiguousShellOutputCallIds.clear();
163
180
  this.shellOutputDeltasByCallId.clear();
164
- this.emittedTaskProgressCallIds.clear();
181
+ this.emittedLifecycleCallIds.clear();
182
+ for (const timer of this.lifecycleTimers.values()) clearTimeout(timer);
183
+ this.lifecycleTimers.clear();
165
184
  }
166
185
 
167
186
  closeTraceBlock(): void {
@@ -199,14 +218,14 @@ export class CursorSdkTurnCoordinator {
199
218
  return;
200
219
  }
201
220
  if (update.type === "partial-tool-call") {
202
- this.maybeEmitCursorTaskProgress(update.callId, update.toolCall);
221
+ this.maybeScheduleCursorToolLifecycle(update.callId, update.toolCall);
203
222
  return;
204
223
  }
205
224
  if (update.type === "tool-call-started") {
206
225
  if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
207
226
  if (typeof update.callId === "string") this.bridgeStartedToolCallIds.add(update.callId);
208
227
  } else {
209
- this.maybeEmitCursorTaskProgress(update.callId, update.toolCall);
228
+ this.maybeScheduleCursorToolLifecycle(update.callId, update.toolCall);
210
229
  this.startedToolCalls.set(update.callId, update.toolCall);
211
230
  if (isCursorShellToolCall(update.toolCall)) this.activeShellCallIds.add(update.callId);
212
231
  }
@@ -219,7 +238,10 @@ export class CursorSdkTurnCoordinator {
219
238
  toolCall: update.toolCall,
220
239
  startedToolCall: this.startedToolCalls.get(update.callId),
221
240
  });
222
- if (resolution.action === "ignore-bridge") return;
241
+ if (resolution.action === "ignore-bridge") {
242
+ this.recordIgnoreBridgeDecision(resolution.identity, getToolName(update.toolCall), "delta");
243
+ return;
244
+ }
223
245
  this.handleCompletedToolCall(resolution.toolCall, {
224
246
  identity: resolution.identity,
225
247
  source: resolution.source,
@@ -232,7 +254,7 @@ export class CursorSdkTurnCoordinator {
232
254
  return;
233
255
  }
234
256
  if (update.type === "summary") {
235
- const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
257
+ const summary = `Cursor summary: ${truncateCursorDisplayLine(update.summary)}\n`;
236
258
  if (this.liveRun) {
237
259
  cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: summary });
238
260
  } else {
@@ -255,7 +277,10 @@ export class CursorSdkTurnCoordinator {
255
277
  callId: stepId,
256
278
  toolCall,
257
279
  });
258
- if (resolution.action === "ignore-bridge") return;
280
+ if (resolution.action === "ignore-bridge") {
281
+ this.recordIgnoreBridgeDecision(resolution.identity, getToolName(toolCall), "step");
282
+ return;
283
+ }
259
284
  this.handleCompletedToolCall(resolution.toolCall, {
260
285
  identity: resolution.identity,
261
286
  source: resolution.source,
@@ -323,20 +348,52 @@ export class CursorSdkTurnCoordinator {
323
348
  };
324
349
  }
325
350
 
351
+ handleTranscriptCompletedToolCalls(toolCalls: readonly { identity: string; toolCall: unknown }[]): void {
352
+ for (const { identity, toolCall } of toolCalls) {
353
+ this.handleCompletedToolCall(toolCall, { identity, source: "transcript" });
354
+ }
355
+ }
356
+
326
357
  private handleCompletedToolCall(
327
358
  toolCall: unknown,
328
- options: { identity?: string; source?: "started" | "fallback" } = {},
359
+ options: { identity?: string; source?: CursorToolDisplaySource } = {},
329
360
  ): void {
330
361
  const planText = getCursorCreatePlanText(toolCall);
331
362
  if (planText) this.cursorPlanTextCandidate = scrubSensitiveText(planText, this.resolvedApiKey);
332
363
 
333
364
  const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd: this.cwd }), this.resolvedApiKey);
334
365
  const display = buildCursorPiToolDisplay(toolCall, { cwd: this.cwd });
366
+ const toolName = display.toolName;
335
367
  const fingerprint = this.getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
336
- if (options.identity && this.completedToolIdentities.has(options.identity)) return;
368
+ if (options.identity && this.completedToolIdentities.has(options.identity)) {
369
+ this.recordDisplayDecision({
370
+ action: "skip-duplicate",
371
+ toolName,
372
+ identity: options.identity,
373
+ source: options.source,
374
+ reason: "identity-already-completed",
375
+ });
376
+ return;
377
+ }
337
378
  if (options.source === "started") {
338
- if (this.completedFallbackToolFingerprints.has(fingerprint)) return;
379
+ if (this.completedFallbackToolFingerprints.has(fingerprint)) {
380
+ this.recordDisplayDecision({
381
+ action: "skip-duplicate",
382
+ toolName,
383
+ identity: options.identity,
384
+ source: options.source,
385
+ reason: "fallback-fingerprint-already-completed",
386
+ });
387
+ return;
388
+ }
339
389
  } else if (this.completedStartedToolFingerprints.has(fingerprint) || this.completedFallbackToolFingerprints.has(fingerprint)) {
390
+ this.recordDisplayDecision({
391
+ action: "skip-duplicate",
392
+ toolName,
393
+ identity: options.identity,
394
+ source: options.source,
395
+ reason: "fingerprint-already-completed",
396
+ });
340
397
  return;
341
398
  }
342
399
  if (options.identity) this.completedToolIdentities.add(options.identity);
@@ -346,13 +403,26 @@ export class CursorSdkTurnCoordinator {
346
403
  this.completedFallbackToolFingerprints.add(fingerprint);
347
404
  }
348
405
 
349
- const nativeRenderable = canRenderCursorToolNatively(display.toolName);
350
- const route = this.useNativeToolReplay && nativeRenderable && this.liveRun ? "native_replay" : "trace";
406
+ const disposition = resolveNativeReplayDisposition({
407
+ toolName: display.toolName,
408
+ useNativeToolReplay: this.useNativeToolReplay,
409
+ activeToolNames: this.activeToolNames,
410
+ hasLiveRun: this.liveRun !== undefined,
411
+ });
351
412
 
352
- if (route === "native_replay" && this.liveRun) {
413
+ if (disposition === "queue_replay" && this.liveRun) {
353
414
  this.nativeToolReplayStarted = true;
354
415
  const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
355
416
  const scrubbedDisplay = scrubPiToolDisplay(display, this.resolvedApiKey);
417
+ this.recordDisplayDecision({
418
+ action: "queue_replay",
419
+ disposition,
420
+ toolName,
421
+ identity: options.identity,
422
+ source: options.source,
423
+ transcript,
424
+ replayToolId: id,
425
+ });
356
426
  cursorLiveRuns.queueEvent(this.liveRun, {
357
427
  type: "tool",
358
428
  tool: { ...scrubbedDisplay, id },
@@ -360,7 +430,84 @@ export class CursorSdkTurnCoordinator {
360
430
  return;
361
431
  }
362
432
 
363
- this.emitCursorToolTrace(transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`);
433
+ const traceText =
434
+ disposition === "inactive_trace"
435
+ ? formatInactiveCursorReplayTrace(scrubPiToolDisplay(display, this.resolvedApiKey))
436
+ : transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`;
437
+ this.recordDisplayDecision({
438
+ action: "emit_trace",
439
+ disposition,
440
+ toolName,
441
+ identity: options.identity,
442
+ source: options.source,
443
+ transcript,
444
+ traceText,
445
+ });
446
+ this.emitCursorToolTrace(traceText);
447
+ }
448
+
449
+ private emitIncompleteStartedToolCall(toolCall: unknown, reason: IncompleteCursorToolDiscardReason): void {
450
+ const display = scrubPiToolDisplay(
451
+ buildIncompleteCursorToolDisplay(toolCall, reason, { apiKey: this.resolvedApiKey }),
452
+ this.resolvedApiKey,
453
+ );
454
+ const toolName = display.toolName;
455
+ const disposition = resolveNativeReplayDisposition({
456
+ toolName,
457
+ useNativeToolReplay: this.useNativeToolReplay,
458
+ activeToolNames: this.activeToolNames,
459
+ hasLiveRun: this.liveRun !== undefined,
460
+ });
461
+
462
+ // Aborted live runs emit trace visibility only; do not synthesize a toolUse replay turn.
463
+ if (disposition === "queue_replay" && this.liveRun && reason !== "abort") {
464
+ this.nativeToolReplayStarted = true;
465
+ const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
466
+ this.recordDisplayDecision({
467
+ action: "queue_replay",
468
+ disposition,
469
+ toolName,
470
+ source: "started",
471
+ reason: "incomplete-started-tool-call",
472
+ replayToolId: id,
473
+ });
474
+ cursorLiveRuns.queueEvent(this.liveRun, {
475
+ type: "tool",
476
+ tool: { ...display, id },
477
+ });
478
+ return;
479
+ }
480
+
481
+ const traceText =
482
+ disposition === "inactive_trace"
483
+ ? formatInactiveCursorReplayTrace(display)
484
+ : formatIncompleteCursorToolTrace(display);
485
+ this.recordDisplayDecision({
486
+ action: "emit_trace",
487
+ disposition,
488
+ toolName,
489
+ source: "started",
490
+ reason: "incomplete-started-tool-call",
491
+ traceText,
492
+ });
493
+ this.emitCursorToolTrace(traceText);
494
+ }
495
+
496
+ private recordIgnoreBridgeDecision(
497
+ identity: string | undefined,
498
+ toolName: string,
499
+ source: "delta" | "step",
500
+ ): void {
501
+ this.debugRecorder?.recordDisplayDecision({
502
+ action: "ignore-bridge",
503
+ toolName,
504
+ identity,
505
+ source,
506
+ });
507
+ }
508
+
509
+ private recordDisplayDecision(decision: Parameters<CursorSdkEventDebugRecorder["recordDisplayDecision"]>[0]): void {
510
+ this.debugRecorder?.recordDisplayDecision(decision);
364
511
  }
365
512
 
366
513
  private emitCursorToolTrace(text: string): void {
@@ -373,17 +520,39 @@ export class CursorSdkTurnCoordinator {
373
520
  this.contentEmitter.appendThinkingBlock(traceText);
374
521
  }
375
522
 
376
- private maybeEmitCursorTaskProgress(callId: unknown, toolCall: unknown): void {
377
- if (typeof callId !== "string" || this.emittedTaskProgressCallIds.has(callId)) return;
523
+ private maybeScheduleCursorToolLifecycle(callId: unknown, toolCall: unknown): void {
524
+ if (typeof callId !== "string" || this.emittedLifecycleCallIds.has(callId)) return;
378
525
  if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) return;
379
- const label = extractCursorTaskProgressLabel(toolCall, this.resolvedApiKey);
380
- if (!label) return;
381
- this.emittedTaskProgressCallIds.add(callId);
382
- this.emitCursorTaskProgress(label);
526
+ if (!isCursorToolLifecycleEligible(toolCall)) return;
527
+
528
+ this.cancelCursorToolLifecycleTimer(callId);
529
+ const timer = setTimeout(() => {
530
+ this.lifecycleTimers.delete(callId);
531
+ if (!this.startedToolCalls.has(callId)) return;
532
+ if (this.emittedLifecycleCallIds.has(callId)) return;
533
+ this.emitCursorToolLifecycle(callId, toolCall);
534
+ }, CURSOR_TOOL_LIFECYCLE_DEFER_MS);
535
+ timer.unref?.();
536
+ this.lifecycleTimers.set(callId, timer);
383
537
  }
384
538
 
385
- private emitCursorTaskProgress(label: string): void {
386
- const progressText = `Cursor task: ${label}\n`;
539
+ private cancelCursorToolLifecycleTimer(callId: string): void {
540
+ const timer = this.lifecycleTimers.get(callId);
541
+ if (!timer) return;
542
+ clearTimeout(timer);
543
+ this.lifecycleTimers.delete(callId);
544
+ }
545
+
546
+ private emitCursorToolLifecycle(callId: string, toolCall: unknown): void {
547
+ const progressText = formatCursorToolLifecycleProgressText(toolCall, this.resolvedApiKey);
548
+ if (!progressText) return;
549
+ this.emittedLifecycleCallIds.add(callId);
550
+ this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle", {
551
+ callId,
552
+ toolName: normalizeToolName(getToolName(toolCall)),
553
+ progressText,
554
+ liveRun: this.liveRun !== undefined,
555
+ });
387
556
  if (this.liveRun) {
388
557
  cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: progressText });
389
558
  return;
@@ -404,6 +573,7 @@ export class CursorSdkTurnCoordinator {
404
573
  }
405
574
 
406
575
  private clearStartedToolCall(callId: string): void {
576
+ this.cancelCursorToolLifecycleTimer(callId);
407
577
  this.startedToolCalls.delete(callId);
408
578
  this.bridgeStartedToolCallIds.delete(callId);
409
579
  this.activeShellCallIds.delete(callId);