@pasko70/pibo 1.7.12 → 1.8.1

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.
@@ -2,6 +2,8 @@ import { SessionManager, shouldCompact } from "@earendil-works/pi-coding-agent";
2
2
  import { getOpenAiCodexProviderUsageForActiveModel } from "../auth/openai-codex-usage.js";
3
3
  import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "./session-errors.js";
4
4
  import { expandInlineSkills } from "./skill-expansion.js";
5
+ import { PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE, PIBO_CONTEXT_GUARD_RESUME_PROMPT, cancelPiboAssistantContextGuardRecovery, claimPiboAssistantContextGuardRecovery, waitForPiboAssistantContextGuardRecovery, } from "./context-guard.js";
6
+ import { PIBO_PROVIDER_RECOVERY_MESSAGE_TYPE, PIBO_PROVIDER_RECOVERY_PROMPT, PiboProviderRecoveryCancelledError, isRetryablePiboAssistantError, isRetryablePiboProviderError, resolvePiboProviderRecoverySettings, waitForPiboProviderRecovery, } from "./provider-recovery.js";
5
7
  const FAST_SERVICE_TIER = "priority";
6
8
  function modelSupportsFastServiceTier(model) {
7
9
  if (!model)
@@ -21,6 +23,9 @@ function withFastServiceTierOption(options) {
21
23
  function errorMessage(error) {
22
24
  return error instanceof Error ? error.message : String(error);
23
25
  }
26
+ function isAssistantMessage(message) {
27
+ return Boolean(message && typeof message === "object" && message.role === "assistant");
28
+ }
24
29
  function numberValue(value) {
25
30
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
26
31
  }
@@ -388,6 +393,7 @@ export class RoutedSession {
388
393
  onSessionOperation;
389
394
  onKillChildren;
390
395
  onStateChange;
396
+ onMessagesInterrupted;
391
397
  queue = [];
392
398
  processing = false;
393
399
  disposed = false;
@@ -398,9 +404,15 @@ export class RoutedSession {
398
404
  nextAssistantIndex = 0;
399
405
  activeThinkingIndex;
400
406
  nextThinkingIndex = 0;
407
+ pendingAssistantError;
408
+ pendingAssistantErrorRetryable = false;
409
+ activeMessageFailed = false;
410
+ providerRecoveryCancelled = false;
411
+ providerRecoveryAbortController;
401
412
  unsubscribe;
413
+ recoverySession;
402
414
  isContinuePatched = false;
403
- constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange) {
415
+ constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange, onMessagesInterrupted) {
404
416
  this.piboSessionId = piboSessionId;
405
417
  this.runtime = runtime;
406
418
  this.emit = emit;
@@ -410,6 +422,7 @@ export class RoutedSession {
410
422
  this.onSessionOperation = onSessionOperation;
411
423
  this.onKillChildren = onKillChildren;
412
424
  this.onStateChange = onStateChange;
425
+ this.onMessagesInterrupted = onMessagesInterrupted;
413
426
  this.fastMode = initialFastMode && this.fastModeSupported();
414
427
  this.bindRuntimeSession();
415
428
  this.patchFastModeProviderRequest();
@@ -485,12 +498,32 @@ export class RoutedSession {
485
498
  }
486
499
  bindRuntimeSession() {
487
500
  this.unsubscribe?.();
488
- this.unsubscribe = this.runtime.session.subscribe((event) => {
501
+ const session = this.runtime.session;
502
+ if (this.recoverySession && this.recoverySession !== session) {
503
+ this.cancelProviderRecovery();
504
+ cancelPiboAssistantContextGuardRecovery(this.recoverySession, new Error("Context guard recovery cancelled because the Pi session changed"));
505
+ }
506
+ this.recoverySession = session;
507
+ claimPiboAssistantContextGuardRecovery(session);
508
+ this.unsubscribe = session.subscribe((event) => {
489
509
  this.onPiEventTelemetry?.(this.piboSessionId, event, { status: this.getStatus(), activeEventId: this.activeMessage?.id });
490
510
  const model = this.runtime.session.model;
491
511
  const normalized = normalizePiEvent(this.piboSessionId, event, { contextWindow: numberValue(model?.contextWindow) });
492
- if (normalized) {
493
- this.emit(this.withActiveMessage(normalized));
512
+ const candidate = event && typeof event === "object" ? event : undefined;
513
+ const assistantMessageEnded = candidate?.type === "message_end" && isAssistantMessage(candidate.message);
514
+ // Pi gets the first chance to recover through its short retry/compaction loop.
515
+ // Keep the final error pending so the routed turn can continue durable recovery.
516
+ if (assistantMessageEnded && normalized?.type === "session_error") {
517
+ this.pendingAssistantError = this.withActiveMessage(normalized);
518
+ this.pendingAssistantErrorRetryable = isRetryablePiboAssistantError(candidate.message);
519
+ }
520
+ else {
521
+ if (assistantMessageEnded) {
522
+ this.pendingAssistantError = undefined;
523
+ this.pendingAssistantErrorRetryable = false;
524
+ }
525
+ if (normalized)
526
+ this.emit(this.withActiveMessage(normalized));
494
527
  }
495
528
  if (this.forwardPiEvents) {
496
529
  this.emit({ type: "pi_event", piboSessionId: this.piboSessionId, event });
@@ -498,6 +531,87 @@ export class RoutedSession {
498
531
  this.handleCompactionEvent(event);
499
532
  });
500
533
  }
534
+ flushPendingAssistantError() {
535
+ if (!this.pendingAssistantError)
536
+ return;
537
+ this.activeMessageFailed = true;
538
+ this.emit(this.pendingAssistantError);
539
+ this.pendingAssistantError = undefined;
540
+ this.pendingAssistantErrorRetryable = false;
541
+ }
542
+ cancelProviderRecovery() {
543
+ this.providerRecoveryCancelled = true;
544
+ this.providerRecoveryAbortController?.abort();
545
+ this.providerRecoveryAbortController = undefined;
546
+ }
547
+ async resumeContextGuardRecovery(session) {
548
+ while (await waitForPiboAssistantContextGuardRecovery(session)) {
549
+ try {
550
+ await session.sendCustomMessage({
551
+ customType: PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE,
552
+ content: [{ type: "text", text: PIBO_CONTEXT_GUARD_RESUME_PROMPT }],
553
+ display: false,
554
+ }, { triggerTurn: true });
555
+ }
556
+ catch (error) {
557
+ const resumeError = error instanceof Error ? error : new Error(String(error));
558
+ cancelPiboAssistantContextGuardRecovery(session, resumeError);
559
+ throw resumeError;
560
+ }
561
+ }
562
+ }
563
+ async recoverTransientProviderErrors(session) {
564
+ let attempt = 0;
565
+ while (this.pendingAssistantError && this.pendingAssistantErrorRetryable) {
566
+ if (this.providerRecoveryCancelled)
567
+ throw new PiboProviderRecoveryCancelledError();
568
+ const settings = resolvePiboProviderRecoverySettings(session.settingsManager);
569
+ if (!settings.enabled)
570
+ return;
571
+ attempt += 1;
572
+ this.pendingAssistantError = undefined;
573
+ this.pendingAssistantErrorRetryable = false;
574
+ const controller = new AbortController();
575
+ this.providerRecoveryAbortController = controller;
576
+ try {
577
+ await waitForPiboProviderRecovery(attempt, settings, controller.signal);
578
+ }
579
+ finally {
580
+ if (this.providerRecoveryAbortController === controller) {
581
+ this.providerRecoveryAbortController = undefined;
582
+ }
583
+ }
584
+ if (this.providerRecoveryCancelled || this.disposed || this.runtime.session !== session || !this.activeMessage) {
585
+ throw new PiboProviderRecoveryCancelledError();
586
+ }
587
+ try {
588
+ await session.sendCustomMessage({
589
+ customType: PIBO_PROVIDER_RECOVERY_MESSAGE_TYPE,
590
+ content: [{ type: "text", text: PIBO_PROVIDER_RECOVERY_PROMPT }],
591
+ display: false,
592
+ details: { attempt },
593
+ }, { triggerTurn: true });
594
+ await this.resumeContextGuardRecovery(session);
595
+ if (this.providerRecoveryCancelled)
596
+ throw new PiboProviderRecoveryCancelledError();
597
+ }
598
+ catch (error) {
599
+ if (error instanceof PiboProviderRecoveryCancelledError)
600
+ throw error;
601
+ if (!isRetryablePiboProviderError(error))
602
+ throw error;
603
+ const message = errorMessage(error);
604
+ this.pendingAssistantError = {
605
+ type: "session_error",
606
+ piboSessionId: this.piboSessionId,
607
+ eventId: this.activeMessage.id,
608
+ error: message,
609
+ errorDetails: runtimeSessionErrorDetails(message),
610
+ };
611
+ this.pendingAssistantErrorRetryable = true;
612
+ }
613
+ }
614
+ }
501
615
  handleCompactionEvent(event) {
502
616
  if (!event || typeof event !== "object")
503
617
  return;
@@ -602,15 +716,17 @@ export class RoutedSession {
602
716
  }
603
717
  removeQueuedMessages(predicate) {
604
718
  this.assertActive();
605
- let removed = 0;
719
+ const removedMessages = [];
606
720
  for (let index = this.queue.length - 1; index >= 0; index -= 1) {
607
721
  const item = this.queue[index];
608
722
  if (item.kind !== "message" || !predicate(item.event))
609
723
  continue;
610
724
  this.queue.splice(index, 1);
611
- removed += 1;
725
+ removedMessages.push(item.event);
612
726
  }
613
- return removed;
727
+ removedMessages.reverse();
728
+ this.notifyMessagesInterrupted(removedMessages, "queued message removed");
729
+ return removedMessages.length;
614
730
  }
615
731
  getCurrentSession() {
616
732
  return this.createSessionSnapshot();
@@ -733,16 +849,25 @@ export class RoutedSession {
733
849
  async dispose() {
734
850
  if (this.disposed)
735
851
  return;
852
+ this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session disposed");
853
+ this.cancelProviderRecovery();
736
854
  this.queue.length = 0;
737
855
  this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: true });
738
856
  this.unsubscribe?.();
739
857
  this.unsubscribe = undefined;
858
+ if (this.recoverySession) {
859
+ this.cancelContextGuardRecovery("Context guard recovery cancelled because the routed session was disposed");
860
+ this.recoverySession = undefined;
861
+ }
740
862
  this.disposed = true;
741
863
  await this.runtime.dispose();
742
864
  }
743
865
  async kill() {
866
+ this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session killed");
867
+ this.cancelProviderRecovery();
744
868
  this.queue.length = 0;
745
869
  this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
870
+ this.cancelContextGuardRecovery("Context guard recovery cancelled because the routed session was killed");
746
871
  await this.runtime.session.abort();
747
872
  return this.piboSessionId;
748
873
  }
@@ -750,11 +875,16 @@ export class RoutedSession {
750
875
  this.assertActive();
751
876
  const queuedIndex = this.queue.findIndex((item) => item.event.id === eventId);
752
877
  if (queuedIndex >= 0) {
753
- this.queue.splice(queuedIndex, 1);
878
+ const [removed] = this.queue.splice(queuedIndex, 1);
879
+ if (removed?.kind === "message")
880
+ this.notifyMessagesInterrupted([removed.event], "message cancelled");
754
881
  this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
755
882
  return true;
756
883
  }
757
884
  if (this.activeMessage?.id === eventId) {
885
+ this.notifyMessagesInterrupted([this.activeMessage], "message cancelled");
886
+ this.cancelProviderRecovery();
887
+ this.cancelContextGuardRecovery("Context guard recovery cancelled with the active message");
758
888
  await this.runtime.session.abort();
759
889
  return true;
760
890
  }
@@ -792,20 +922,32 @@ export class RoutedSession {
792
922
  });
793
923
  try {
794
924
  this.activeMessage = event;
925
+ this.providerRecoveryCancelled = false;
926
+ this.pendingAssistantError = undefined;
927
+ this.pendingAssistantErrorRetryable = false;
928
+ this.activeMessageFailed = false;
795
929
  this.activeAssistantIndex = undefined;
796
930
  this.nextAssistantIndex = 0;
797
931
  this.activeThinkingIndex = undefined;
798
932
  this.nextThinkingIndex = 0;
799
- const expandedText = expandInlineSkills(event.text, this.runtime.session.resourceLoader.getSkills().skills);
800
- await this.runtime.session.prompt(expandedText, { source: promptSource(event.source) });
801
- this.emit({
802
- type: "message_finished",
803
- piboSessionId: this.piboSessionId,
804
- eventId: event.id,
805
- source: event.source,
806
- });
933
+ const session = this.runtime.session;
934
+ const expandedText = expandInlineSkills(event.text, session.resourceLoader.getSkills().skills);
935
+ await session.prompt(expandedText, { source: promptSource(event.source) });
936
+ await this.resumeContextGuardRecovery(session);
937
+ await this.recoverTransientProviderErrors(session);
938
+ this.flushPendingAssistantError();
939
+ if (!this.activeMessageFailed) {
940
+ this.emit({
941
+ type: "message_finished",
942
+ piboSessionId: this.piboSessionId,
943
+ eventId: event.id,
944
+ source: event.source,
945
+ });
946
+ }
807
947
  }
808
948
  catch (error) {
949
+ if (error instanceof PiboProviderRecoveryCancelledError)
950
+ return;
809
951
  const message = errorMessage(error);
810
952
  this.emit({
811
953
  type: "session_error",
@@ -817,6 +959,10 @@ export class RoutedSession {
817
959
  }
818
960
  finally {
819
961
  this.activeMessage = undefined;
962
+ this.providerRecoveryCancelled = false;
963
+ this.pendingAssistantError = undefined;
964
+ this.pendingAssistantErrorRetryable = false;
965
+ this.activeMessageFailed = false;
820
966
  this.activeAssistantIndex = undefined;
821
967
  this.nextAssistantIndex = 0;
822
968
  this.activeThinkingIndex = undefined;
@@ -873,6 +1019,9 @@ export class RoutedSession {
873
1019
  getProviderUsage: () => this.getProviderUsage(),
874
1020
  clearQueue: () => this.clearQueue(),
875
1021
  abort: async () => {
1022
+ if (this.activeMessage)
1023
+ this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
1024
+ this.cancelContextGuardRecovery("Context guard recovery cancelled by abort");
876
1025
  await this.runtime.session.abort();
877
1026
  },
878
1027
  dispose: () => this.dispose(),
@@ -913,6 +1062,11 @@ export class RoutedSession {
913
1062
  },
914
1063
  }, event);
915
1064
  }
1065
+ cancelContextGuardRecovery(message) {
1066
+ const session = this.recoverySession ?? this.runtime.session;
1067
+ cancelPiboAssistantContextGuardRecovery(session, new Error(message));
1068
+ session.abortCompaction?.();
1069
+ }
916
1070
  assertActive() {
917
1071
  if (this.disposed) {
918
1072
  throw new Error(`Session "${this.piboSessionId}" has been disposed`);
@@ -934,10 +1088,21 @@ export class RoutedSession {
934
1088
  }
935
1089
  clearQueue() {
936
1090
  const cleared = this.queue.length;
1091
+ const removedMessages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
937
1092
  this.queue.length = 0;
1093
+ this.notifyMessagesInterrupted(removedMessages, "queue cleared");
938
1094
  this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
939
1095
  return cleared;
940
1096
  }
1097
+ activeAndQueuedMessages() {
1098
+ const messages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
1099
+ return this.activeMessage ? [this.activeMessage, ...messages] : messages;
1100
+ }
1101
+ notifyMessagesInterrupted(messages, reason) {
1102
+ if (messages.length === 0)
1103
+ return;
1104
+ this.onMessagesInterrupted?.(messages, reason);
1105
+ }
941
1106
  createSessionSnapshot() {
942
1107
  const session = this.runtime.session;
943
1108
  const manager = session.sessionManager;