condukt 0.6.18 → 0.6.20

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.
@@ -50,6 +50,18 @@ const cp = __importStar(require("child_process"));
50
50
  const fs = __importStar(require("fs"));
51
51
  const path = __importStar(require("path"));
52
52
  const lifecycle_events_1 = require("./lifecycle-events");
53
+ const NAMED_SDK_EVENTS = new Set([
54
+ 'assistant.message', 'assistant.message_delta',
55
+ 'assistant.reasoning', 'assistant.reasoning_delta',
56
+ 'assistant.intent', 'assistant.usage',
57
+ 'tool.execution_start', 'tool.execution_complete',
58
+ 'tool.execution_partial_result',
59
+ 'session.idle', 'session.task_complete', 'session.error',
60
+ 'model.call_failure', 'abort',
61
+ 'session.compaction_start', 'session.compaction_complete',
62
+ 'subagent.started', 'subagent.completed', 'subagent.failed',
63
+ 'permission.requested',
64
+ ]);
53
65
  // ---------------------------------------------------------------------------
54
66
  // PATH hardening: shared logic with SubprocessBackend
55
67
  // ---------------------------------------------------------------------------
@@ -197,8 +209,13 @@ class SdkSession {
197
209
  timeoutTimer = null;
198
210
  heartbeatTimer = null;
199
211
  compactionTimer = null;
212
+ abortGraceTimer = null;
200
213
  compactionInProgress = false;
214
+ turnSettled = false;
215
+ compactionRecoveryInProgress = false;
201
216
  aborted = false;
217
+ _turnText = new Map();
218
+ intentionalAborts = new WeakSet();
202
219
  /**
203
220
  * Maps toolCallId -> toolName for attributing tool_complete events.
204
221
  * Populated from assistant.message toolRequests and tool.execution_start.
@@ -231,10 +248,21 @@ class SdkSession {
231
248
  * Matches SubprocessBackend.send() contract: fire-and-forget, events stream via on().
232
249
  */
233
250
  send(prompt) {
251
+ if (this.aborted) {
252
+ this.emitError(new Error('Session is aborted'));
253
+ return;
254
+ }
255
+ // Detach the preceding SDK handle synchronously before resetting shared turn
256
+ // state. Any late events from it then fail the per-handler isActive() guard.
257
+ this.clearTimers();
258
+ void this._cleanup();
259
+ this._turnText.clear();
260
+ this._toolCallNames.clear();
261
+ this._callIdToParent.clear();
262
+ this._pendingPartials.clear();
263
+ this.turnSettled = false;
234
264
  this._run(prompt).catch((err) => {
235
- if (!this.aborted) {
236
- this.emitError(err instanceof Error ? err : new Error(String(err)));
237
- }
265
+ this.fail(err instanceof Error ? err : new Error(String(err)), 'session.run');
238
266
  });
239
267
  }
240
268
  /**
@@ -286,7 +314,15 @@ class SdkSession {
286
314
  onPermissionRequest: approveAll,
287
315
  workingDirectory: this.config.cwd,
288
316
  reasoningEffort: this.config.thinkingBudget,
317
+ // Registered before createSession issues its RPC, closing the early-event
318
+ // gap for session.start and *_loaded events. Named payload handlers are
319
+ // wired after the handle exists; this hook handles class-wide liveness and
320
+ // future failure-shaped events without duplicating named event output.
321
+ onEvent: (e) => this.handleEarlyEvent(e),
289
322
  };
323
+ if (this.config.contextTier) {
324
+ sessionConfig.contextTier = this.config.contextTier;
325
+ }
290
326
  // CLI 1.0.11+ discovers MCP servers, skills, and custom instructions from
291
327
  // configDir. Without this, the CLI searches workingDirectory (which is often
292
328
  // a temp execution dir) and fails to find the project's .copilot/ config.
@@ -317,6 +353,13 @@ class SdkSession {
317
353
  bufferExhaustionThreshold: 0.90,
318
354
  };
319
355
  const sdkSession = await client.createSession(sessionConfig);
356
+ if (this.aborted) {
357
+ try {
358
+ await sdkSession.disconnect();
359
+ }
360
+ catch { /* Ignore inert early-abort handle */ }
361
+ return;
362
+ }
320
363
  this._sdkSession = sdkSession;
321
364
  // Set autopilot mode explicitly (matches SubprocessBackend's --autopilot flag)
322
365
  try {
@@ -326,19 +369,21 @@ class SdkSession {
326
369
  // SDK may not support mode.set — continue without it
327
370
  }
328
371
  // ---------------------------------------------------------------
329
- // Set up hard timeout
372
+ // Wire SDK events -> CopilotSession events
373
+ // ---------------------------------------------------------------
374
+ this._wireEvents(sdkSession);
330
375
  // ---------------------------------------------------------------
376
+ // Set up turn timers only while this handle is still active
377
+ // ---------------------------------------------------------------
378
+ if (this.aborted || this._sdkSession !== sdkSession)
379
+ return;
331
380
  this.timeoutTimer = setTimeout(() => {
332
- this.emitError(new Error(`Session timed out after ${this.config.timeout}s`));
333
- this.abort();
381
+ if (this.aborted || this._sdkSession !== sdkSession)
382
+ return;
383
+ this.fail(new Error(`Session timed out after ${this.config.timeout}s`), 'timeout');
334
384
  }, this.config.timeout * 1000);
335
- // Set up heartbeat timeout
336
385
  this.resetHeartbeat();
337
386
  // ---------------------------------------------------------------
338
- // Wire SDK events -> CopilotSession events
339
- // ---------------------------------------------------------------
340
- this._wireEvents(sdkSession);
341
- // ---------------------------------------------------------------
342
387
  // Send the prompt (fire-and-forget; events stream via handlers)
343
388
  // ---------------------------------------------------------------
344
389
  await sdkSession.send({ prompt });
@@ -347,16 +392,25 @@ class SdkSession {
347
392
  * Wire all SDK session events to CopilotSession event emissions.
348
393
  */
349
394
  _wireEvents(sdkSession) {
395
+ const isActive = () => this._sdkSession === sdkSession && !this.aborted;
350
396
  // --- Assistant text response ---
351
397
  sdkSession.on('assistant.message', (e) => {
352
- if (this.aborted)
398
+ if (!isActive())
353
399
  return;
354
400
  this.resetHeartbeat();
355
401
  const data = e.data;
356
402
  const content = typeof data?.content === 'string' ? data.content : '';
357
403
  const parentToolCallId = typeof data?.parentToolCallId === 'string' ? data.parentToolCallId : undefined;
358
- if (content)
359
- this.emit('text', content, parentToolCallId);
404
+ const streamKey = parentToolCallId ?? '__root__';
405
+ if (content) {
406
+ const streamed = this._turnText.get(streamKey) ?? '';
407
+ const remainder = content === streamed
408
+ ? ''
409
+ : content.startsWith(streamed) ? content.slice(streamed.length) : content;
410
+ if (remainder)
411
+ this.emit('text', remainder, parentToolCallId);
412
+ }
413
+ this._turnText.delete(streamKey);
360
414
  // Pre-seed _toolCallNames from tool requests so tool.execution_complete
361
415
  // can resolve names even if tool.execution_start lacks a toolCallId.
362
416
  const toolRequests = Array.isArray(data?.toolRequests) ? data.toolRequests : [];
@@ -369,18 +423,21 @@ class SdkSession {
369
423
  });
370
424
  // --- Assistant text delta (streaming) ---
371
425
  sdkSession.on('assistant.message_delta', (e) => {
372
- if (this.aborted)
426
+ if (!isActive())
373
427
  return;
374
428
  this.resetHeartbeat();
375
429
  const data = e.data;
376
430
  const delta = typeof data?.deltaContent === 'string' ? data.deltaContent : '';
377
431
  const parentToolCallId = typeof data?.parentToolCallId === 'string' ? data.parentToolCallId : undefined;
378
- if (delta)
432
+ if (delta) {
433
+ const streamKey = parentToolCallId ?? '__root__';
434
+ this._turnText.set(streamKey, (this._turnText.get(streamKey) ?? '') + delta);
379
435
  this.emit('text', delta, parentToolCallId);
436
+ }
380
437
  });
381
438
  // --- Reasoning ---
382
439
  sdkSession.on('assistant.reasoning', (e) => {
383
- if (this.aborted)
440
+ if (!isActive())
384
441
  return;
385
442
  this.resetHeartbeat();
386
443
  const content = typeof e.data?.content === 'string' ? e.data.content : '';
@@ -388,7 +445,7 @@ class SdkSession {
388
445
  this.emit('reasoning', content);
389
446
  });
390
447
  sdkSession.on('assistant.reasoning_delta', (e) => {
391
- if (this.aborted)
448
+ if (!isActive())
392
449
  return;
393
450
  this.resetHeartbeat();
394
451
  const delta = typeof e.data?.deltaContent === 'string' ? e.data.deltaContent : '';
@@ -397,7 +454,7 @@ class SdkSession {
397
454
  });
398
455
  // --- Tool execution start ---
399
456
  sdkSession.on('tool.execution_start', (e) => {
400
- if (this.aborted)
457
+ if (!isActive())
401
458
  return;
402
459
  this.resetHeartbeat();
403
460
  const data = e.data;
@@ -426,7 +483,7 @@ class SdkSession {
426
483
  });
427
484
  // --- Tool execution complete ---
428
485
  sdkSession.on('tool.execution_complete', (e) => {
429
- if (this.aborted)
486
+ if (!isActive())
430
487
  return;
431
488
  this.resetHeartbeat();
432
489
  const data = e.data;
@@ -457,7 +514,7 @@ class SdkSession {
457
514
  // the SDK. We look it up from the _callIdToParent map populated by
458
515
  // tool.execution_start.
459
516
  sdkSession.on('tool.execution_partial_result', (e) => {
460
- if (this.aborted)
517
+ if (!isActive())
461
518
  return;
462
519
  this.resetHeartbeat();
463
520
  const data = e.data;
@@ -481,37 +538,68 @@ class SdkSession {
481
538
  });
482
539
  // --- Session idle (agent finished all work) ---
483
540
  sdkSession.on('session.idle', () => {
484
- if (this.aborted)
541
+ if (!isActive() || this.compactionRecoveryInProgress)
485
542
  return;
486
- this.clearTimers();
487
- this.emit('idle');
488
- this._cleanup();
543
+ this.settleIdle();
489
544
  });
490
545
  // --- task_complete → idle (some models fire this instead of session.idle) ---
491
546
  sdkSession.on('session.task_complete', () => {
492
- if (this.aborted)
547
+ if (!isActive() || this.compactionRecoveryInProgress)
493
548
  return;
494
- this.clearTimers();
495
- this.emit('idle');
496
- this._cleanup();
549
+ this.settleIdle();
497
550
  });
498
- // --- Session error ---
551
+ // The CLI normally follows an abort event with session.idle. If that
552
+ // terminal event is lost, fail quickly rather than waiting for heartbeat.
553
+ sdkSession.on('abort', (e) => {
554
+ if (!isActive() || this.compactionRecoveryInProgress)
555
+ return;
556
+ if (this.intentionalAborts.delete(sdkSession))
557
+ return;
558
+ if (this.abortGraceTimer)
559
+ clearTimeout(this.abortGraceTimer);
560
+ this.abortGraceTimer = setTimeout(() => {
561
+ this.abortGraceTimer = null;
562
+ if (!isActive())
563
+ return;
564
+ const reason = typeof e.data?.reason === 'string' ? `: ${e.data.reason}` : '';
565
+ this.fail(new Error(`SDK turn aborted${reason}`), 'abort', e.data);
566
+ }, 1000);
567
+ });
568
+ // --- Session/model errors ---
499
569
  sdkSession.on('session.error', (e) => {
500
- if (this.aborted)
570
+ if (!isActive())
571
+ return;
572
+ // A rate-limit error eligible for automatic model switching is followed by
573
+ // auto_mode_switch.requested. Let the headless policy resolve that request
574
+ // instead of tearing down the session before it can recover.
575
+ if (e.data?.eligibleForAutoSwitch === true) {
576
+ this.resetHeartbeat();
501
577
  return;
502
- this.clearTimers();
503
- const msg = typeof e.data?.message === 'string' ? e.data.message : 'Unknown session error';
504
- try {
505
- process.stderr.write(`[SdkBackend] session.error: ${msg}\n`);
506
578
  }
507
- catch { /* */ }
508
- this.emitError(new Error(msg));
579
+ const msg = typeof e.data?.message === 'string' ? e.data.message : 'Unknown session error';
580
+ this.fail(new Error(msg), 'session.error', e.data);
581
+ });
582
+ // model.call_failure is telemetry for a failed LLM request, but the SDK has
583
+ // no API to resume/retry that in-flight turn. A second send() would append a
584
+ // duplicate user message rather than replaying the failed call, so fail the
585
+ // condukt session immediately instead of waiting for its heartbeat timeout.
586
+ sdkSession.on('model.call_failure', (e) => {
587
+ if (!isActive())
588
+ return;
589
+ const data = e.data;
590
+ const detail = typeof data?.errorMessage === 'string'
591
+ ? data.errorMessage
592
+ : typeof data?.errorCode === 'string'
593
+ ? data.errorCode
594
+ : 'Unknown model call failure';
595
+ const status = typeof data?.statusCode === 'number' ? ` (HTTP ${data.statusCode})` : '';
596
+ this.fail(new Error(`Model call failed${status}: ${detail}`), 'model.call_failure', data);
509
597
  });
510
598
  // --- Context compaction (infinite sessions) ---
511
599
  // During compaction the model goes silent. SUSPEND the heartbeat entirely
512
600
  // (not reset) to prevent killing the session. Hard timeout remains as safety net.
513
601
  sdkSession.on('session.compaction_start', () => {
514
- if (this.aborted)
602
+ if (!isActive())
515
603
  return;
516
604
  this.compactionInProgress = true;
517
605
  // SUSPEND heartbeat — compaction silence is expected
@@ -528,8 +616,10 @@ class SdkSession {
528
616
  // Recovery: if compaction doesn't complete within 3 min, escalate.
529
617
  // Capture session to local to avoid TOCTOU null dereference after await.
530
618
  const session = this._sdkSession;
619
+ if (!session || !isActive())
620
+ return;
531
621
  this.compactionTimer = setTimeout(async () => {
532
- if (!this.compactionInProgress || this.aborted || !session)
622
+ if (!this.compactionInProgress || !isActive() || !session)
533
623
  return;
534
624
  try {
535
625
  try {
@@ -538,13 +628,13 @@ class SdkSession {
538
628
  catch { /* */ }
539
629
  await session.rpc.compaction.compact();
540
630
  // Re-check guards after await — session may have been torn down
541
- if (!this.compactionInProgress || this.aborted)
631
+ if (!this.compactionInProgress || !isActive())
542
632
  return;
543
633
  // If force-compact works, compaction_complete will fire naturally
544
634
  }
545
635
  catch {
546
636
  // Re-check guards after await
547
- if (this.aborted)
637
+ if (!isActive())
548
638
  return;
549
639
  try {
550
640
  process.stderr.write('[SdkBackend] Force compact failed — aborting + re-sending\n');
@@ -555,17 +645,30 @@ class SdkSession {
555
645
  // SDK docs: "The session remains valid and can continue to be used for new messages."
556
646
  // Wait for idle after abort before sending, to avoid racing with in-flight processing.
557
647
  this.compactionInProgress = false;
648
+ this.compactionRecoveryInProgress = true;
649
+ this._turnText.clear();
558
650
  try {
651
+ this.intentionalAborts.add(session);
559
652
  await session.abort();
653
+ // Some SDK versions emit abort synchronously, others just after the
654
+ // request resolves. Keep the marker through the settling window.
655
+ if (this.abortGraceTimer) {
656
+ clearTimeout(this.abortGraceTimer);
657
+ this.abortGraceTimer = null;
658
+ }
659
+ // WeakSet entries are removed when the delayed intentional abort event
660
+ // arrives. The handle itself becomes collectible after session cleanup.
560
661
  // Wait briefly for the abort to settle before re-sending.
561
662
  // The SDK resolves abort() on acknowledgement, not on idle.
562
663
  await new Promise(resolve => setTimeout(resolve, 2000));
563
- if (this.aborted)
664
+ if (!isActive())
564
665
  return;
565
666
  await session.send({ prompt: 'Continue from where you left off.' });
667
+ this.compactionRecoveryInProgress = false;
566
668
  this.resetHeartbeat();
567
669
  }
568
670
  catch {
671
+ this.compactionRecoveryInProgress = false;
569
672
  try {
570
673
  process.stderr.write('[SdkBackend] Recovery failed — restarting heartbeat as safety net\n');
571
674
  }
@@ -576,7 +679,7 @@ class SdkSession {
576
679
  }, 3 * 60 * 1000);
577
680
  });
578
681
  sdkSession.on('session.compaction_complete', (e) => {
579
- if (this.aborted)
682
+ if (!isActive())
580
683
  return;
581
684
  this.compactionInProgress = false;
582
685
  if (this.compactionTimer) {
@@ -607,7 +710,7 @@ class SdkSession {
607
710
  // The synthetic tool_start/tool_complete dual-emit is removed — sub-agent
608
711
  // grouping is handled by SubagentSectionPart in the UI layer.
609
712
  sdkSession.on('subagent.started', (e) => {
610
- if (this.aborted)
713
+ if (!isActive())
611
714
  return;
612
715
  this.resetHeartbeat();
613
716
  const data = e.data;
@@ -616,7 +719,7 @@ class SdkSession {
616
719
  this.emit('subagent_start', name, { ...data, toolCallId });
617
720
  });
618
721
  sdkSession.on('subagent.completed', (e) => {
619
- if (this.aborted)
722
+ if (!isActive())
620
723
  return;
621
724
  this.resetHeartbeat();
622
725
  const data = e.data;
@@ -625,7 +728,7 @@ class SdkSession {
625
728
  this.emit('subagent_end', name, { ...data, toolCallId });
626
729
  });
627
730
  sdkSession.on('subagent.failed', (e) => {
628
- if (this.aborted)
731
+ if (!isActive())
629
732
  return;
630
733
  this.resetHeartbeat();
631
734
  const data = e.data;
@@ -636,7 +739,7 @@ class SdkSession {
636
739
  });
637
740
  // --- Rich events (optional; consumers can subscribe or ignore) ---
638
741
  sdkSession.on('assistant.intent', (e) => {
639
- if (this.aborted)
742
+ if (!isActive())
640
743
  return;
641
744
  this.resetHeartbeat();
642
745
  const intent = typeof e.data?.intent === 'string' ? e.data.intent : '';
@@ -644,37 +747,24 @@ class SdkSession {
644
747
  this.emit('intent', intent);
645
748
  });
646
749
  sdkSession.on('assistant.usage', (e) => {
647
- if (this.aborted)
750
+ if (!isActive())
648
751
  return;
649
752
  this.resetHeartbeat();
650
753
  this.emit('usage', e.data ?? {});
651
754
  });
652
755
  sdkSession.on('permission.requested', (e) => {
653
- if (this.aborted)
756
+ if (!isActive())
654
757
  return;
655
758
  this.resetHeartbeat();
656
759
  this.emit('permission', e.data ?? {});
657
760
  });
658
- // Named event types handled by typed handlers above
659
- const HANDLED_EVENT_TYPES = new Set([
660
- 'assistant.message', 'assistant.message_delta',
661
- 'assistant.reasoning', 'assistant.reasoning_delta',
662
- 'assistant.intent', 'assistant.usage',
663
- 'tool.execution_start', 'tool.execution_complete',
664
- 'tool.execution_partial_result',
665
- 'session.idle', 'session.task_complete', 'session.error',
666
- 'session.compaction_start', 'session.compaction_complete',
667
- 'subagent.started', 'subagent.completed', 'subagent.failed',
668
- 'permission.requested',
669
- ]);
761
+ // Class-level handling for the full SDK event surface. Named handlers above
762
+ // retain payload-specific mapping; this dispatcher supplies liveness,
763
+ // terminal fallbacks, pending-request policies, and future-event safety.
670
764
  sdkSession.on((e) => {
671
- if (this.aborted)
765
+ if (!isActive())
672
766
  return;
673
- if (e && typeof e.type === 'string'
674
- && !HANDLED_EVENT_TYPES.has(e.type)
675
- && !lifecycle_events_1.LIFECYCLE_EVENT_TYPES.has(e.type)) {
676
- process.stderr.write(`[SdkBackend] Unhandled event: ${e.type}\n`);
677
- }
767
+ this.dispatchClassEvent(sdkSession, e);
678
768
  });
679
769
  }
680
770
  on(event, handler) {
@@ -741,13 +831,158 @@ class SdkSession {
741
831
  catch { /* closed stream */ }
742
832
  this.emit('error', err);
743
833
  }
834
+ settleIdle() {
835
+ if (this.aborted || this.turnSettled)
836
+ return;
837
+ this.turnSettled = true;
838
+ this._turnText.clear();
839
+ this.clearTimers();
840
+ this.emit('idle');
841
+ void this._cleanup();
842
+ }
843
+ fail(err, eventType, data) {
844
+ if (this.aborted || this.turnSettled)
845
+ return;
846
+ this.turnSettled = true;
847
+ this.clearTimers();
848
+ try {
849
+ process.stderr.write(`[SdkBackend] ${eventType}: ${this.serializeEventDataCompact(eventType, data)}\n`);
850
+ }
851
+ catch { /* closed stream */ }
852
+ this.emitError(err);
853
+ this.aborted = true;
854
+ void this._cleanup();
855
+ }
856
+ handleEarlyEvent(e) {
857
+ if (this.aborted || !e || typeof e.type !== 'string')
858
+ return;
859
+ // Once the active handle exists, the session catch-all is authoritative.
860
+ // Only the pre-handle creation window is handled here.
861
+ if (this._sdkSession)
862
+ return;
863
+ const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
864
+ if (eventClass === 'informational')
865
+ return;
866
+ if (this.isFailureShapedEvent(e)) {
867
+ const payload = this.serializeEventDataCompact(e.type, e.data);
868
+ try {
869
+ process.stderr.write(`[SdkBackend] Early failure event: ${e.type} data=${payload}\n`);
870
+ }
871
+ catch { /* */ }
872
+ this.fail(new Error(`Early SDK failure event: ${e.type}`), e.type, e.data);
873
+ }
874
+ }
875
+ dispatchClassEvent(sdkSession, e) {
876
+ if (!e || typeof e.type !== 'string' || NAMED_SDK_EVENTS.has(e.type))
877
+ return;
878
+ const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
879
+ if (e.type === 'session.shutdown' && e.data?.shutdownType === 'error') {
880
+ const reason = typeof e.data.errorReason === 'string'
881
+ ? e.data.errorReason
882
+ : 'SDK session shut down abnormally';
883
+ this.fail(new Error(reason), 'session.shutdown', e.data);
884
+ return;
885
+ }
886
+ if (eventClass === 'terminal-success') {
887
+ this.settleIdle();
888
+ return;
889
+ }
890
+ if (eventClass === 'terminal-failure') {
891
+ this.fail(new Error(`SDK terminal failure: ${e.type}`), e.type, e.data);
892
+ return;
893
+ }
894
+ if (eventClass === 'streaming-liveness') {
895
+ this.resetHeartbeat();
896
+ return;
897
+ }
898
+ if (eventClass === 'pending-request') {
899
+ this.resetHeartbeat();
900
+ void this.resolvePendingRequest(sdkSession, e);
901
+ return;
902
+ }
903
+ if (eventClass === 'informational')
904
+ return;
905
+ const payload = this.serializeEventDataCompact(e.type, e.data);
906
+ try {
907
+ process.stderr.write(`[SdkBackend] Unknown event: ${e.type} data=${payload}\n`);
908
+ }
909
+ catch { /* */ }
910
+ if (this.isFailureShapedEvent(e)) {
911
+ this.fail(new Error(`Unknown SDK failure event: ${e.type}`), e.type, e.data);
912
+ }
913
+ }
914
+ serializeEventData(data) {
915
+ try {
916
+ return JSON.stringify(data ?? {});
917
+ }
918
+ catch {
919
+ return '(unstringifiable payload)';
920
+ }
921
+ }
922
+ serializeEventDataCompact(type, data) {
923
+ if (type === 'session.binary_asset')
924
+ return '(binary payload omitted)';
925
+ const serialized = this.serializeEventData(data);
926
+ return serialized.length > 2000 ? `${serialized.slice(0, 2000)}…` : serialized;
927
+ }
928
+ isFailureShapedEvent(e) {
929
+ const type = e.type ?? '';
930
+ if (/(?:^|[._-])(error|failure|fatal)(?:$|[._-])/i.test(type))
931
+ return true;
932
+ const data = e.data;
933
+ if (!data)
934
+ return false;
935
+ return data.failed === true
936
+ || typeof data.error === 'string'
937
+ || (data.error != null && typeof data.error === 'object');
938
+ }
939
+ async resolvePendingRequest(sdkSession, e) {
940
+ if (this.aborted || this._sdkSession !== sdkSession)
941
+ return;
942
+ const requestId = typeof e.data?.requestId === 'string' ? e.data.requestId : '';
943
+ if (!requestId) {
944
+ this.fail(new Error(`Pending SDK request missing requestId: ${e.type}`), e.type ?? 'pending-request', e.data);
945
+ return;
946
+ }
947
+ try {
948
+ switch (e.type) {
949
+ case 'sampling.requested':
950
+ await sdkSession.rpc.mcp.cancelSamplingExecution({ requestId });
951
+ break;
952
+ case 'auto_mode_switch.requested':
953
+ // Approve this turn only. Do not persist a user preference from a
954
+ // headless execution engine.
955
+ await sdkSession.rpc.ui.handlePendingAutoModeSwitch({ requestId, response: 'yes' });
956
+ break;
957
+ case 'session_limits_exhausted.requested':
958
+ await sdkSession.rpc.ui.handlePendingSessionLimitsExhausted({
959
+ requestId,
960
+ response: { action: 'cancel' },
961
+ });
962
+ break;
963
+ case 'mcp.headers_refresh_required':
964
+ // The runtime has its own timeout fallback; registering no response
965
+ // avoids inventing credentials in a headless process.
966
+ break;
967
+ }
968
+ }
969
+ catch (err) {
970
+ if (this.aborted || this._sdkSession !== sdkSession)
971
+ return;
972
+ this.fail(err instanceof Error ? err : new Error(String(err)), `${e.type}.policy`, e.data);
973
+ }
974
+ }
744
975
  resetHeartbeat() {
976
+ if (this.aborted || this.turnSettled || !this._sdkSession)
977
+ return;
978
+ const sdkSession = this._sdkSession;
745
979
  if (this.heartbeatTimer) {
746
980
  clearTimeout(this.heartbeatTimer);
747
981
  }
748
982
  this.heartbeatTimer = setTimeout(() => {
749
- this.emitError(new Error(`No output for ${this.config.heartbeatTimeout}s (heartbeat timeout)`));
750
- this.abort();
983
+ if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
984
+ return;
985
+ this.fail(new Error(`No output for ${this.config.heartbeatTimeout}s (heartbeat timeout)`), 'heartbeat');
751
986
  }, this.config.heartbeatTimeout * 1000);
752
987
  }
753
988
  clearTimers() {
@@ -763,7 +998,12 @@ class SdkSession {
763
998
  clearTimeout(this.compactionTimer);
764
999
  this.compactionTimer = null;
765
1000
  }
1001
+ if (this.abortGraceTimer) {
1002
+ clearTimeout(this.abortGraceTimer);
1003
+ this.abortGraceTimer = null;
1004
+ }
766
1005
  this.compactionInProgress = false;
1006
+ this.compactionRecoveryInProgress = false;
767
1007
  }
768
1008
  }
769
1009
  //# sourceMappingURL=sdk-backend.js.map