@monotykamary/pi-retry 0.3.12 → 0.4.0
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.
- package/package.json +1 -1
- package/retry.ts +185 -76
package/package.json
CHANGED
package/retry.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import {
|
|
4
5
|
has400or413Error,
|
|
5
6
|
hasCreditError,
|
|
@@ -39,15 +40,26 @@ import {
|
|
|
39
40
|
* - agent.prompt([]) starts a fresh agent loop with an empty prompt array
|
|
40
41
|
* - No message injected into context — LLM sees the exact same message list
|
|
41
42
|
* - No convertToLlm involvement, no filter needed, no session artifact
|
|
43
|
+
*
|
|
44
|
+
* Retry loop design:
|
|
45
|
+
* - The agent_end handler detects retryable errors but does NOT sleep.
|
|
46
|
+
* It fires triggerInvisibleContinue() immediately, keeping processEvents
|
|
47
|
+
* unblocked so the agent can finish its run and become idle.
|
|
48
|
+
* - triggerInvisibleContinue() owns the retry loop: it waits for idle,
|
|
49
|
+
* removes error assistant messages from agent state, calls prompt([])
|
|
50
|
+
* and checks the result. On error it sleeps (outside processEvents)
|
|
51
|
+
* and retries. On success or user abort the loop exits.
|
|
52
|
+
* - The continue() monkey-patch cooperates: while _continueInProgress is
|
|
53
|
+
* true, the session's continue() spins. After the loop finishes, it
|
|
54
|
+
* calls _origContinue which checks the now-updated agent state. For
|
|
55
|
+
* stopReason "error" it no longer falls back to prompt([]) (the loop
|
|
56
|
+
* already handled it). For toolUse/length (compaction mid-task) it
|
|
57
|
+
* still falls back to prompt([]).
|
|
42
58
|
*/
|
|
43
59
|
|
|
44
60
|
// Capture the live Agent instance when AgentSession subscribes to it.
|
|
45
61
|
// subscribe() is called during AgentSession construction — fires on both
|
|
46
62
|
// fresh sessions and session resumes.
|
|
47
|
-
//
|
|
48
|
-
// We also monkey-patch continue() so the session's loop can never race
|
|
49
|
-
// our retry. Without this, observing isStreaming is a heuristic that
|
|
50
|
-
// misses the narrow window between our check and the session's call.
|
|
51
63
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
64
|
let _agent: Agent | null = null;
|
|
53
65
|
|
|
@@ -59,18 +71,9 @@ Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
|
|
|
59
71
|
|
|
60
72
|
// Monkey-patch continue() so the session's built-in retry loop cooperates
|
|
61
73
|
// with our _continueInProgress mutex AND can convert the "Cannot continue
|
|
62
|
-
// from assistant" error into a prompt([]) call when the agent was mid-task
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
// - stopReason "stop" → agent finished cleanly, don't continue
|
|
66
|
-
// - stopReason "aborted" → user cancelled, don't continue
|
|
67
|
-
// - stopReason "error" → pi-retry IS the error handler, fall back to prompt([])
|
|
68
|
-
// so the retry actually happens (rather than swallowing and stalling)
|
|
69
|
-
// - stopReason "toolUse" or "length" → mid-task, fall back to prompt([])
|
|
70
|
-
//
|
|
71
|
-
// This ensures the agent loop actually continues after compaction instead
|
|
72
|
-
// of swallowing the error and letting the while-loop die.
|
|
73
|
-
const _origContinue = Agent.prototype.continue as (this: Agent) => Promise<unknown>;
|
|
74
|
+
// from assistant" error into a prompt([]) call when the agent was mid-task
|
|
75
|
+
// (compaction, toolUse, length — but NOT error, which the loop handles).
|
|
76
|
+
const _origContinue = Agent.prototype.continue as (this: Agent) => Promise<void>;
|
|
74
77
|
Agent.prototype.continue = function (this: Agent) {
|
|
75
78
|
const self = this;
|
|
76
79
|
return (async () => {
|
|
@@ -84,15 +87,22 @@ Agent.prototype.continue = function (this: Agent) {
|
|
|
84
87
|
const msg = e?.message ?? '';
|
|
85
88
|
if (msg.includes('Cannot continue from message role') ||
|
|
86
89
|
msg.includes('Cannot continue from an assistant message')) {
|
|
87
|
-
// Check stopReason — only continue if the agent was mid-task
|
|
88
90
|
const lastMsg = self.state.messages[self.state.messages.length - 1];
|
|
89
|
-
if (lastMsg?.role === 'assistant'
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
if (lastMsg?.role === 'assistant') {
|
|
92
|
+
// stopReason "error": pi-retry's loop is the error handler.
|
|
93
|
+
// It will have already retried or the user aborted — don't
|
|
94
|
+
// start a second retry path via prompt([]).
|
|
95
|
+
if (lastMsg.stopReason === 'error') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// stopReason "stop" / "aborted": agent finished or user cancelled.
|
|
99
|
+
// Don't continue.
|
|
100
|
+
if (lastMsg.stopReason === 'stop' || lastMsg.stopReason === 'aborted') {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// stopReason "toolUse" or "length": agent was mid-task (e.g.
|
|
104
|
+
// compaction broke the message ordering). Fall back to prompt([]).
|
|
105
|
+
if (!_continueInProgress) {
|
|
96
106
|
_continueInProgress = true;
|
|
97
107
|
try {
|
|
98
108
|
await self.prompt([]);
|
|
@@ -103,7 +113,6 @@ Agent.prototype.continue = function (this: Agent) {
|
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
|
-
// For stop/aborted: return void, the session loop exits naturally
|
|
107
116
|
return;
|
|
108
117
|
}
|
|
109
118
|
if (msg.includes('Agent is already processing')) {
|
|
@@ -114,6 +123,27 @@ Agent.prototype.continue = function (this: Agent) {
|
|
|
114
123
|
})();
|
|
115
124
|
};
|
|
116
125
|
|
|
126
|
+
// Monkey-patch AgentSession._prepareRetry to suppress the built-in retry
|
|
127
|
+
// when pi-retry's loop is driving. Without this, both the built-in retry
|
|
128
|
+
// and pi-retry race to handle the same error: the built-in retry counts
|
|
129
|
+
// 3 failed attempts and shows "Retry failed after 3 attempts: ...",
|
|
130
|
+
// while pi-retry is still looping indefinitely in the background.
|
|
131
|
+
//
|
|
132
|
+
// When _continueInProgress is true (pi-retry is running), _prepareRetry
|
|
133
|
+
// returns false immediately, so _handlePostAgentRun falls through to
|
|
134
|
+
// the compaction check and the while loop in _runAgentPrompt exits
|
|
135
|
+
// cleanly. No auto_retry_start/end events, no "Retry failed" message.
|
|
136
|
+
//
|
|
137
|
+
// When _continueInProgress is false (pi-retry is not active), the
|
|
138
|
+
// built-in retry works normally as a fallback.
|
|
139
|
+
const _origPrepareRetry = (AgentSession.prototype as any)._prepareRetry;
|
|
140
|
+
(AgentSession.prototype as any)._prepareRetry = function(this: any, message: any) {
|
|
141
|
+
if (_continueInProgress) {
|
|
142
|
+
return Promise.resolve(false);
|
|
143
|
+
}
|
|
144
|
+
return _origPrepareRetry.call(this, message);
|
|
145
|
+
};
|
|
146
|
+
|
|
117
147
|
// Per-category retry state (for diagnostics / messaging)
|
|
118
148
|
const state400 = new RetryState();
|
|
119
149
|
const stateCredit = new RetryState();
|
|
@@ -139,11 +169,33 @@ let _continueInProgress = false;
|
|
|
139
169
|
// triggerInvisibleContinue just ran and the session's continue() unblocks.
|
|
140
170
|
let _lastInvisibleContinueTime = 0;
|
|
141
171
|
|
|
142
|
-
// Sleep helper
|
|
172
|
+
// Sleep helper (non-abortable — used inside the retry loop outside
|
|
173
|
+
// processEvents where no abort signal is available)
|
|
143
174
|
function sleep(ms: number): Promise<void> {
|
|
144
175
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
145
176
|
}
|
|
146
177
|
|
|
178
|
+
// Remove the error assistant message at the end of agent state, if present.
|
|
179
|
+
// Same technique used by the built-in retry in _prepareRetry — the error
|
|
180
|
+
// message stays in the session journal for history but is removed from the
|
|
181
|
+
// agent's live transcript so the LLM receives a clean context on retry.
|
|
182
|
+
function removeErrorFromAgentState(): void {
|
|
183
|
+
if (!_agent) return;
|
|
184
|
+
const messages = _agent.state.messages;
|
|
185
|
+
const lastMsg = messages[messages.length - 1];
|
|
186
|
+
if (lastMsg?.role === 'assistant' && lastMsg.stopReason === 'error') {
|
|
187
|
+
_agent.state.messages = messages.slice(0, -1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if the agent's last message indicates a retryable error.
|
|
192
|
+
function lastMessageIsRetryableError(): boolean {
|
|
193
|
+
if (!_agent) return false;
|
|
194
|
+
const messages = _agent.state.messages;
|
|
195
|
+
const lastMsg = messages[messages.length - 1];
|
|
196
|
+
return lastMsg?.role === 'assistant' && lastMsg.stopReason === 'error';
|
|
197
|
+
}
|
|
198
|
+
|
|
147
199
|
export default function (pi: ExtensionAPI) {
|
|
148
200
|
|
|
149
201
|
// Reset retry counters on successful completion (not max_tokens, not error)
|
|
@@ -157,8 +209,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
209
|
stateCredit.reset();
|
|
158
210
|
stateConnection.reset();
|
|
159
211
|
stateOther.reset();
|
|
160
|
-
// Do NOT reset continuation state — a user abort of a continuation
|
|
161
|
-
// turn is different from aborting an error retry.
|
|
162
212
|
stateContinuation.endContinuation();
|
|
163
213
|
// Signal to any in-flight triggerInvisibleContinue or pending retry
|
|
164
214
|
// that the user has cancelled — don't drive a new prompt([]).
|
|
@@ -179,11 +229,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
179
229
|
}
|
|
180
230
|
});
|
|
181
231
|
|
|
182
|
-
// Handle errors and max_tokens on agent_end
|
|
232
|
+
// Handle errors and max_tokens on agent_end.
|
|
233
|
+
//
|
|
234
|
+
// IMPORTANT: this handler must return quickly and NOT await sleep().
|
|
235
|
+
// The handler is invoked inside processEvents(), which blocks finishRun()
|
|
236
|
+
// until all listeners settle. A sleep here freezes the entire agent —
|
|
237
|
+
// no UI updates, no abort handling, no event processing.
|
|
238
|
+
//
|
|
239
|
+
// Instead, the handler detects errors and kicks off
|
|
240
|
+
// triggerInvisibleContinue(), which owns the retry loop with backoff
|
|
241
|
+
// sleeps that happen AFTER processEvents returns (outside the agent run).
|
|
183
242
|
pi.on("agent_end", async (event, ctx) => {
|
|
184
243
|
const entries = ctx.sessionManager.getEntries();
|
|
185
244
|
const lastAssistant = getLastAssistantMessage(entries);
|
|
186
|
-
|
|
245
|
+
|
|
187
246
|
if (!lastAssistant || !isAssistantMessage(lastAssistant)) {
|
|
188
247
|
return;
|
|
189
248
|
}
|
|
@@ -191,6 +250,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
191
250
|
// Guard: if the user aborted, don't drive any new prompt([])
|
|
192
251
|
if (_userAborted) return;
|
|
193
252
|
|
|
253
|
+
// If the retry loop is already driving, don't interfere — it will
|
|
254
|
+
// see the new error on its next loop iteration.
|
|
255
|
+
if (_continueInProgress) return;
|
|
256
|
+
|
|
194
257
|
// Check for max_tokens stop — auto-continue (invisible to LLM)
|
|
195
258
|
if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
|
|
196
259
|
stateContinuation.startContinuation();
|
|
@@ -198,7 +261,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
198
261
|
`Max tokens reached — auto-continuing (continuation ${stateContinuation.getCount()})...`,
|
|
199
262
|
"info",
|
|
200
263
|
);
|
|
201
|
-
// Must NOT await — see triggerInvisibleContinue() for explanation
|
|
202
264
|
void triggerInvisibleContinue();
|
|
203
265
|
stateContinuation.endContinuation();
|
|
204
266
|
return;
|
|
@@ -228,22 +290,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
228
290
|
|
|
229
291
|
if (state.getIsRetrying()) return;
|
|
230
292
|
|
|
293
|
+
// Record the error for diagnostics but do NOT sleep here.
|
|
294
|
+
// The retry loop in triggerInvisibleContinue handles backoff.
|
|
231
295
|
state.startRetry(errorMsg);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
await sleep(delay);
|
|
235
|
-
|
|
236
|
-
// Re-check: user may have aborted during the backoff sleep.
|
|
237
|
-
// turn_end with stopReason "aborted" sets _userAborted, but this
|
|
238
|
-
// handler was already past the initial check.
|
|
239
|
-
if (_userAborted) {
|
|
240
|
-
state.endRetry();
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
296
|
+
state.endRetry();
|
|
243
297
|
|
|
244
|
-
// Must NOT await — see triggerInvisibleContinue() for explanation
|
|
245
298
|
void triggerInvisibleContinue();
|
|
246
|
-
state.endRetry();
|
|
247
299
|
return;
|
|
248
300
|
}
|
|
249
301
|
|
|
@@ -267,9 +319,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
267
319
|
if (subcommand === "status") {
|
|
268
320
|
const entries = ctx.sessionManager.getEntries();
|
|
269
321
|
const lastAssistant = getLastAssistantMessage(entries);
|
|
270
|
-
|
|
322
|
+
|
|
271
323
|
let status = "=== Retry Status ===\n\n";
|
|
272
|
-
|
|
324
|
+
|
|
273
325
|
// 400/413 state
|
|
274
326
|
status += "400/413 Errors:\n";
|
|
275
327
|
status += ` Current attempt: ${state400.getAttempt()}\n`;
|
|
@@ -293,20 +345,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
345
|
status += ` Current attempt: ${stateOther.getAttempt()}\n`;
|
|
294
346
|
status += ` Is retrying: ${stateOther.getIsRetrying()}\n`;
|
|
295
347
|
status += ` Last error: ${stateOther.getLastErrorMessage().substring(0, 100) || "None"}\n\n`;
|
|
296
|
-
|
|
348
|
+
|
|
297
349
|
// Continuation state
|
|
298
350
|
status += "Max Tokens Continuation:\n";
|
|
299
351
|
status += ` Continuations used: ${stateContinuation.getCount()}\n`;
|
|
300
352
|
status += ` Is continuing: ${stateContinuation.getIsContinuing()}\n`;
|
|
301
353
|
status += ` Trigger: invisible (agent.prompt([]), LLM never sees a prompt)\n\n`;
|
|
302
|
-
|
|
354
|
+
|
|
303
355
|
// Config
|
|
304
356
|
status += "Configuration:\n";
|
|
305
357
|
status += ` Base delay: 2000ms\n`;
|
|
306
358
|
status += ` Max delay: 60000ms\n`;
|
|
307
359
|
status += ` Backoff multiplier: 2\n`;
|
|
308
|
-
status += `
|
|
309
|
-
|
|
360
|
+
status += ` Retry loop: infinite (triggerInvisibleContinue loops until success or abort)\n\n`;
|
|
361
|
+
|
|
310
362
|
// Last assistant info
|
|
311
363
|
if (lastAssistant && isAssistantMessage(lastAssistant)) {
|
|
312
364
|
status += "Last Assistant Message:\n";
|
|
@@ -316,7 +368,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
316
368
|
status += ` Error category: ${getErrorCategory(lastAssistant.errorMessage)}`;
|
|
317
369
|
}
|
|
318
370
|
}
|
|
319
|
-
|
|
371
|
+
|
|
320
372
|
ctx.ui.notify(status, "info");
|
|
321
373
|
return;
|
|
322
374
|
}
|
|
@@ -336,7 +388,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
336
388
|
// /retry (no args) - Manual trigger with auto-detection
|
|
337
389
|
const entries = ctx.sessionManager.getEntries();
|
|
338
390
|
const lastAssistant = getLastAssistantMessage(entries);
|
|
339
|
-
|
|
391
|
+
|
|
340
392
|
if (!lastAssistant || !isAssistantMessage(lastAssistant)) {
|
|
341
393
|
ctx.ui.notify("No assistant message found to retry", "warning");
|
|
342
394
|
return;
|
|
@@ -400,24 +452,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
400
452
|
_userAborted = false;
|
|
401
453
|
});
|
|
402
454
|
|
|
403
|
-
//
|
|
404
|
-
//
|
|
455
|
+
// Retry loop driver — the core of pi-retry.
|
|
456
|
+
//
|
|
457
|
+
// Unlike the original one-shot design, this function loops. After each
|
|
458
|
+
// prompt([]) call it checks the result:
|
|
459
|
+
// - Success (stopReason !== "error"): loop exits, agent is done.
|
|
460
|
+
// - Error (stopReason === "error"): sleep with backoff, then retry.
|
|
461
|
+
// - User abort (stopReason "aborted"): loop exits immediately.
|
|
405
462
|
//
|
|
406
|
-
// The
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
463
|
+
// The backoff sleep happens AFTER prompt([]) returns and processEvents
|
|
464
|
+
// has settled, so it does NOT block the agent. The agent is idle during
|
|
465
|
+
// the sleep and can respond to user input (e.g. Escape to abort).
|
|
466
|
+
//
|
|
467
|
+
// Before each retry, the error assistant message is removed from
|
|
468
|
+
// agent.state.messages so the LLM receives a clean context (same
|
|
469
|
+
// technique as the built-in retry's _prepareRetry).
|
|
411
470
|
async function triggerInvisibleContinue() {
|
|
412
471
|
if (!_agent) return;
|
|
413
472
|
|
|
414
|
-
// Guard
|
|
415
|
-
// This catches aborts that happened between agent_end firing and
|
|
416
|
-
// this async function actually executing (e.g. during backoff sleep
|
|
417
|
-
// or while waitForIdle was pending).
|
|
473
|
+
// Guard: if the user aborted, don't drive a new prompt([]).
|
|
418
474
|
if (_userAborted) return;
|
|
419
475
|
|
|
420
|
-
// Guard
|
|
476
|
+
// Guard: mutex — if a previous continue is still in-flight, skip
|
|
421
477
|
if (_continueInProgress) return;
|
|
422
478
|
_continueInProgress = true;
|
|
423
479
|
|
|
@@ -430,22 +486,75 @@ export default function (pi: ExtensionAPI) {
|
|
|
430
486
|
// we were waiting for the agent to become idle.
|
|
431
487
|
if (_userAborted) return;
|
|
432
488
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
//
|
|
441
|
-
|
|
489
|
+
let attempt = 0;
|
|
490
|
+
|
|
491
|
+
// Loop until success or abort.
|
|
492
|
+
while (true) {
|
|
493
|
+
if (_userAborted) return;
|
|
494
|
+
|
|
495
|
+
// Remove the error assistant message from agent state so
|
|
496
|
+
// prompt([]) sends a clean context to the LLM.
|
|
497
|
+
removeErrorFromAgentState();
|
|
498
|
+
|
|
499
|
+
attempt++;
|
|
500
|
+
const delay = calculateDelay(attempt);
|
|
501
|
+
|
|
502
|
+
// Notify the user about the upcoming retry attempt.
|
|
503
|
+
_notifyRetryAttempt(attempt, delay);
|
|
504
|
+
|
|
505
|
+
// Sleep with backoff BEFORE the retry attempt.
|
|
506
|
+
// This matches the built-in retry's UX: "Retrying (attempt N) in Xs..."
|
|
507
|
+
// The sleep is safe: we are outside processEvents, the agent is
|
|
508
|
+
// idle, and the user can press Escape to abort.
|
|
509
|
+
await sleep(delay);
|
|
510
|
+
|
|
511
|
+
// Re-check after sleep — user may have aborted during backoff.
|
|
512
|
+
if (_userAborted) return;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
await _agent.prompt([]);
|
|
516
|
+
} catch {
|
|
517
|
+
// "Agent is already processing" or other transient error —
|
|
518
|
+
// the session or another driver is handling it.
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// prompt([]) completed. Check the result.
|
|
523
|
+
if (!lastMessageIsRetryableError()) {
|
|
524
|
+
// Success or non-error terminal state — exit the loop.
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Error again — loop back for another attempt.
|
|
442
529
|
}
|
|
443
530
|
} finally {
|
|
444
531
|
_continueInProgress = false;
|
|
445
|
-
// Record completion time so the continue() monkey-patch can
|
|
446
|
-
// detect that an invisible continue just ran and avoid firing
|
|
447
|
-
// a duplicate prompt([]) (RC7: double continuation guard).
|
|
448
532
|
_lastInvisibleContinueTime = Date.now();
|
|
449
533
|
}
|
|
450
534
|
}
|
|
535
|
+
|
|
536
|
+
// Notify the user about a retry attempt via the extension API.
|
|
537
|
+
// ctx.ui.notify is only available inside event handlers, not inside
|
|
538
|
+
// triggerInvisibleContinue. We capture a fresh reference from the
|
|
539
|
+
// most recent handler invocation so it's always current.
|
|
540
|
+
let _notifyFn: ((message: string, level: "info" | "warning" | "error") => void) | null = null;
|
|
541
|
+
|
|
542
|
+
// Refresh on every handler that carries a ctx — stale references
|
|
543
|
+
// break after session switches (the old ctx becomes invalid).
|
|
544
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
545
|
+
_notifyFn = (message, level) => ctx.ui.notify(message, level);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
549
|
+
if (!_notifyFn) {
|
|
550
|
+
_notifyFn = (message, level) => ctx.ui.notify(message, level);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
function _notifyRetryAttempt(attempt: number, delayMs: number) {
|
|
555
|
+
if (_notifyFn) {
|
|
556
|
+
const duration = formatDuration(delayMs);
|
|
557
|
+
_notifyFn(`Retry attempt ${attempt} (backoff ${duration})...`, "info");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
451
560
|
}
|