@monotykamary/pi-retry 0.3.11 → 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 +211 -63
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();
|
|
@@ -123,6 +153,11 @@ const stateOther = new RetryState();
|
|
|
123
153
|
// Max_tokens continuation state (indefinite — no cap needed)
|
|
124
154
|
const stateContinuation = new ContinuationState();
|
|
125
155
|
|
|
156
|
+
// Abort flag: set when turn_end reports stopReason "aborted", cleared on
|
|
157
|
+
// session_start and on fresh user activity. Prevents triggerInvisibleContinue()
|
|
158
|
+
// from driving a new prompt([]) after the user explicitly cancelled.
|
|
159
|
+
let _userAborted = false;
|
|
160
|
+
|
|
126
161
|
// Mutex: only one triggerInvisibleContinue may be in-flight at a time.
|
|
127
162
|
// Without this, concurrent agent_end events (or a manual /retry during an
|
|
128
163
|
// automatic retry) race through waitForIdle() and both call prompt([]),
|
|
@@ -134,11 +169,33 @@ let _continueInProgress = false;
|
|
|
134
169
|
// triggerInvisibleContinue just ran and the session's continue() unblocks.
|
|
135
170
|
let _lastInvisibleContinueTime = 0;
|
|
136
171
|
|
|
137
|
-
// Sleep helper
|
|
172
|
+
// Sleep helper (non-abortable — used inside the retry loop outside
|
|
173
|
+
// processEvents where no abort signal is available)
|
|
138
174
|
function sleep(ms: number): Promise<void> {
|
|
139
175
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
140
176
|
}
|
|
141
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
|
+
|
|
142
199
|
export default function (pi: ExtensionAPI) {
|
|
143
200
|
|
|
144
201
|
// Reset retry counters on successful completion (not max_tokens, not error)
|
|
@@ -152,9 +209,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
152
209
|
stateCredit.reset();
|
|
153
210
|
stateConnection.reset();
|
|
154
211
|
stateOther.reset();
|
|
155
|
-
// Do NOT reset continuation state — a user abort of a continuation
|
|
156
|
-
// turn is different from aborting an error retry.
|
|
157
212
|
stateContinuation.endContinuation();
|
|
213
|
+
// Signal to any in-flight triggerInvisibleContinue or pending retry
|
|
214
|
+
// that the user has cancelled — don't drive a new prompt([]).
|
|
215
|
+
_userAborted = true;
|
|
158
216
|
return;
|
|
159
217
|
}
|
|
160
218
|
if (msg.stopReason !== "length") {
|
|
@@ -164,19 +222,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
164
222
|
stateConnection.succeed();
|
|
165
223
|
stateOther.succeed();
|
|
166
224
|
stateContinuation.complete();
|
|
225
|
+
// Clear abort flag — this is a fresh successful turn, so any
|
|
226
|
+
// previous abort is stale and shouldn't block future retries.
|
|
227
|
+
_userAborted = false;
|
|
167
228
|
}
|
|
168
229
|
}
|
|
169
230
|
});
|
|
170
231
|
|
|
171
|
-
// 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).
|
|
172
242
|
pi.on("agent_end", async (event, ctx) => {
|
|
173
243
|
const entries = ctx.sessionManager.getEntries();
|
|
174
244
|
const lastAssistant = getLastAssistantMessage(entries);
|
|
175
|
-
|
|
245
|
+
|
|
176
246
|
if (!lastAssistant || !isAssistantMessage(lastAssistant)) {
|
|
177
247
|
return;
|
|
178
248
|
}
|
|
179
249
|
|
|
250
|
+
// Guard: if the user aborted, don't drive any new prompt([])
|
|
251
|
+
if (_userAborted) return;
|
|
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
|
+
|
|
180
257
|
// Check for max_tokens stop — auto-continue (invisible to LLM)
|
|
181
258
|
if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
|
|
182
259
|
stateContinuation.startContinuation();
|
|
@@ -184,7 +261,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
184
261
|
`Max tokens reached — auto-continuing (continuation ${stateContinuation.getCount()})...`,
|
|
185
262
|
"info",
|
|
186
263
|
);
|
|
187
|
-
// Must NOT await — see triggerInvisibleContinue() for explanation
|
|
188
264
|
void triggerInvisibleContinue();
|
|
189
265
|
stateContinuation.endContinuation();
|
|
190
266
|
return;
|
|
@@ -214,13 +290,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
214
290
|
|
|
215
291
|
if (state.getIsRetrying()) return;
|
|
216
292
|
|
|
293
|
+
// Record the error for diagnostics but do NOT sleep here.
|
|
294
|
+
// The retry loop in triggerInvisibleContinue handles backoff.
|
|
217
295
|
state.startRetry(errorMsg);
|
|
218
|
-
|
|
296
|
+
state.endRetry();
|
|
219
297
|
|
|
220
|
-
await sleep(delay);
|
|
221
|
-
// Must NOT await — see triggerInvisibleContinue() for explanation
|
|
222
298
|
void triggerInvisibleContinue();
|
|
223
|
-
state.endRetry();
|
|
224
299
|
return;
|
|
225
300
|
}
|
|
226
301
|
|
|
@@ -244,9 +319,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
244
319
|
if (subcommand === "status") {
|
|
245
320
|
const entries = ctx.sessionManager.getEntries();
|
|
246
321
|
const lastAssistant = getLastAssistantMessage(entries);
|
|
247
|
-
|
|
322
|
+
|
|
248
323
|
let status = "=== Retry Status ===\n\n";
|
|
249
|
-
|
|
324
|
+
|
|
250
325
|
// 400/413 state
|
|
251
326
|
status += "400/413 Errors:\n";
|
|
252
327
|
status += ` Current attempt: ${state400.getAttempt()}\n`;
|
|
@@ -270,20 +345,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
270
345
|
status += ` Current attempt: ${stateOther.getAttempt()}\n`;
|
|
271
346
|
status += ` Is retrying: ${stateOther.getIsRetrying()}\n`;
|
|
272
347
|
status += ` Last error: ${stateOther.getLastErrorMessage().substring(0, 100) || "None"}\n\n`;
|
|
273
|
-
|
|
348
|
+
|
|
274
349
|
// Continuation state
|
|
275
350
|
status += "Max Tokens Continuation:\n";
|
|
276
351
|
status += ` Continuations used: ${stateContinuation.getCount()}\n`;
|
|
277
352
|
status += ` Is continuing: ${stateContinuation.getIsContinuing()}\n`;
|
|
278
353
|
status += ` Trigger: invisible (agent.prompt([]), LLM never sees a prompt)\n\n`;
|
|
279
|
-
|
|
354
|
+
|
|
280
355
|
// Config
|
|
281
356
|
status += "Configuration:\n";
|
|
282
357
|
status += ` Base delay: 2000ms\n`;
|
|
283
358
|
status += ` Max delay: 60000ms\n`;
|
|
284
359
|
status += ` Backoff multiplier: 2\n`;
|
|
285
|
-
status += `
|
|
286
|
-
|
|
360
|
+
status += ` Retry loop: infinite (triggerInvisibleContinue loops until success or abort)\n\n`;
|
|
361
|
+
|
|
287
362
|
// Last assistant info
|
|
288
363
|
if (lastAssistant && isAssistantMessage(lastAssistant)) {
|
|
289
364
|
status += "Last Assistant Message:\n";
|
|
@@ -293,7 +368,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
368
|
status += ` Error category: ${getErrorCategory(lastAssistant.errorMessage)}`;
|
|
294
369
|
}
|
|
295
370
|
}
|
|
296
|
-
|
|
371
|
+
|
|
297
372
|
ctx.ui.notify(status, "info");
|
|
298
373
|
return;
|
|
299
374
|
}
|
|
@@ -305,6 +380,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
305
380
|
stateConnection.reset();
|
|
306
381
|
stateOther.reset();
|
|
307
382
|
stateContinuation.reset();
|
|
383
|
+
_userAborted = false;
|
|
308
384
|
ctx.ui.notify("All retry counters reset", "info");
|
|
309
385
|
return;
|
|
310
386
|
}
|
|
@@ -312,12 +388,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
312
388
|
// /retry (no args) - Manual trigger with auto-detection
|
|
313
389
|
const entries = ctx.sessionManager.getEntries();
|
|
314
390
|
const lastAssistant = getLastAssistantMessage(entries);
|
|
315
|
-
|
|
391
|
+
|
|
316
392
|
if (!lastAssistant || !isAssistantMessage(lastAssistant)) {
|
|
317
393
|
ctx.ui.notify("No assistant message found to retry", "warning");
|
|
318
394
|
return;
|
|
319
395
|
}
|
|
320
396
|
|
|
397
|
+
// Manual /retry overrides any previous abort — the user is
|
|
398
|
+
// explicitly requesting a retry, so clear the abort flag.
|
|
399
|
+
_userAborted = false;
|
|
400
|
+
|
|
321
401
|
// Auto-detect: max_tokens continuation takes priority
|
|
322
402
|
if (hasMaxTokensStop(lastAssistant)) {
|
|
323
403
|
ctx.ui.notify("Manually continuing after max_tokens...", "info");
|
|
@@ -369,20 +449,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
369
449
|
stateContinuation.reset();
|
|
370
450
|
_continueInProgress = false;
|
|
371
451
|
_lastInvisibleContinueTime = 0;
|
|
452
|
+
_userAborted = false;
|
|
372
453
|
});
|
|
373
454
|
|
|
374
|
-
//
|
|
375
|
-
// The LLM sees the exact same message list it had before.
|
|
455
|
+
// Retry loop driver — the core of pi-retry.
|
|
376
456
|
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
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.
|
|
462
|
+
//
|
|
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).
|
|
382
470
|
async function triggerInvisibleContinue() {
|
|
383
471
|
if (!_agent) return;
|
|
384
472
|
|
|
385
|
-
// Guard
|
|
473
|
+
// Guard: if the user aborted, don't drive a new prompt([]).
|
|
474
|
+
if (_userAborted) return;
|
|
475
|
+
|
|
476
|
+
// Guard: mutex — if a previous continue is still in-flight, skip
|
|
386
477
|
if (_continueInProgress) return;
|
|
387
478
|
_continueInProgress = true;
|
|
388
479
|
|
|
@@ -391,22 +482,79 @@ export default function (pi: ExtensionAPI) {
|
|
|
391
482
|
// finishRun() after agent_end listeners return).
|
|
392
483
|
await _agent.waitForIdle();
|
|
393
484
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
485
|
+
// Re-check after waitForIdle: the user may have aborted while
|
|
486
|
+
// we were waiting for the agent to become idle.
|
|
487
|
+
if (_userAborted) return;
|
|
488
|
+
|
|
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.
|
|
403
529
|
}
|
|
404
530
|
} finally {
|
|
405
531
|
_continueInProgress = false;
|
|
406
|
-
// Record completion time so the continue() monkey-patch can
|
|
407
|
-
// detect that an invisible continue just ran and avoid firing
|
|
408
|
-
// a duplicate prompt([]) (RC7: double continuation guard).
|
|
409
532
|
_lastInvisibleContinueTime = Date.now();
|
|
410
533
|
}
|
|
411
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
|
+
}
|
|
412
560
|
}
|