File size: 3,980 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { isResponsesOutputLimitToolCallError } from "@openclaw/ai/diagnostics";
import { isContextOverflow } from "@openclaw/ai/internal/runtime";
import type { AssistantMessage } from "../../llm/types.js";
import { isRetryableAssistantError } from "../../llm/utils/retry.js";
import { sleep } from "../../utils/sleep.js";
import { classifyRateLimitWindow } from "../failover/retry-evidence.js";
import { AgentSessionExtensions } from "./agent-session-extensions.js";

export abstract class AgentSessionExecution extends AgentSessionExtensions {
  // =========================================================================
  // Auto-Retry
  // =========================================================================

  /**
   * Check if an error is retryable (overloaded, rate limit, server errors).
   * Context overflow errors are NOT retryable (handled by compaction instead).
   */
  protected isRetryableError(message: AssistantMessage): boolean {
    if (message.stopReason !== "error" || !message.errorMessage) {
      return false;
    }

    // Context overflow is handled by compaction, not retry
    const contextWindow = this.model?.contextWindow ?? 0;
    if (isContextOverflow(message, contextWindow)) {
      return false;
    }

    return isResponsesOutputLimitToolCallError(message) || isRetryableAssistantError(message);
  }

  /**
   * Prepare a retryable error for continuation with exponential backoff.
   * @returns true if the caller should continue the agent, false otherwise
   */
  protected async prepareRetry(message: AssistantMessage): Promise<boolean> {
    const settings = this.settingsManager.getRetrySettings();
    if (!settings.enabled) {
      return false;
    }

    this.retryCount++;

    if (this.retryCount > settings.maxRetries) {
      // Preserve the completed attempt count so post-run handling can emit the final failure.
      this.retryCount--;
      return false;
    }

    const backoffDelayMs = settings.baseDelayMs * 2 ** (this.retryCount - 1);
    const rateLimitWindow = classifyRateLimitWindow(message.errorMessage);
    const retryAfterDelayMs =
      rateLimitWindow.kind === "short" && rateLimitWindow.retryAfterSeconds !== undefined
        ? Math.ceil(rateLimitWindow.retryAfterSeconds * 1000)
        : 0;
    const delayMs = Math.max(backoffDelayMs, retryAfterDelayMs);

    this.emit({
      type: "auto_retry_start",
      attempt: this.retryCount,
      maxAttempts: settings.maxRetries,
      delayMs,
      errorMessage: message.errorMessage || "Unknown error",
    });

    // Async tool results can settle after the error. Keep them and the durable transcript.
    const messages = this.agent.state.messages;
    const failedIndex = messages.findLastIndex((candidate) => candidate === message);
    if (failedIndex >= 0) {
      this.agent.state.messages = messages.toSpliced(failedIndex, 1);
    }

    // Wait with exponential backoff (abortable)
    this.retryAbortController = new AbortController();
    try {
      await sleep(delayMs, this.retryAbortController.signal);
    } catch {
      // Aborted during sleep - emit end event so UI can clean up
      const attempt = this.retryCount;
      this.retryCount = 0;
      this.emit({
        type: "auto_retry_end",
        success: false,
        attempt,
        finalError: "Retry cancelled",
      });
      return false;
    } finally {
      this.retryAbortController = undefined;
    }

    return true;
  }

  /**
   * Cancel in-progress retry.
   */
  abortRetry(): void {
    this.retryAbortController?.abort();
  }

  /** Whether auto-retry is currently in progress */
  get isRetrying(): boolean {
    return this.retryAbortController !== undefined;
  }

  /** Whether auto-retry is enabled */
  get autoRetryEnabled(): boolean {
    return this.settingsManager.getRetryEnabled();
  }

  /**
   * Toggle auto-retry setting.
   */
  setAutoRetryEnabled(enabled: boolean): void {
    this.settingsManager.setRetryEnabled(enabled);
  }
}