@monotykamary/pi-retry 0.5.0 → 0.6.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 +2 -2
- package/retry.ts +58 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-retry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Extension suite for pi coding agent that handles 400/413 errors and connection errors with automatic retry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tom X Nguyen",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"lint:dead": "knip --no-gitignore"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
43
|
+
"@earendil-works/pi-agent-core": "^0.79.1",
|
|
44
44
|
"@earendil-works/pi-coding-agent": "0.75.4",
|
|
45
45
|
"@types/node": "25.9.1",
|
|
46
46
|
"@vitest/coverage-v8": "4.1.7",
|
package/retry.ts
CHANGED
|
@@ -169,10 +169,30 @@ let _continueInProgress = false;
|
|
|
169
169
|
// triggerInvisibleContinue just ran and the session's continue() unblocks.
|
|
170
170
|
let _lastInvisibleContinueTime = 0;
|
|
171
171
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
// Session generation counter: incremented on every session_start.
|
|
173
|
+
// The retry loop captures the current generation when it starts and exits
|
|
174
|
+
// when it changes — this handles /new and other session switches.
|
|
175
|
+
let _sessionGeneration = 0;
|
|
176
|
+
|
|
177
|
+
// Interruptible sleep: polls _userAborted and _sessionGeneration every
|
|
178
|
+
// 100ms. Returns true if interrupted (abort or session change), false if
|
|
179
|
+
// the full delay elapsed normally.
|
|
180
|
+
function interruptibleSleep(ms: number, generation: number): Promise<boolean> {
|
|
181
|
+
if (ms <= 0) return Promise.resolve(false);
|
|
182
|
+
return new Promise(resolve => {
|
|
183
|
+
const checkInterval = 100;
|
|
184
|
+
let elapsed = 0;
|
|
185
|
+
const timer = setInterval(() => {
|
|
186
|
+
elapsed += checkInterval;
|
|
187
|
+
if (_userAborted || _sessionGeneration !== generation) {
|
|
188
|
+
clearInterval(timer);
|
|
189
|
+
resolve(true);
|
|
190
|
+
} else if (elapsed >= ms) {
|
|
191
|
+
clearInterval(timer);
|
|
192
|
+
resolve(false);
|
|
193
|
+
}
|
|
194
|
+
}, checkInterval);
|
|
195
|
+
});
|
|
176
196
|
}
|
|
177
197
|
|
|
178
198
|
// Remove the error assistant message at the end of agent state, if present.
|
|
@@ -442,12 +462,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
442
462
|
|
|
443
463
|
// Initialize
|
|
444
464
|
pi.on("session_start", async () => {
|
|
465
|
+
// Bump the generation counter so any in-flight retry loop from a
|
|
466
|
+
// previous session exits on its next checkpoint (within 100ms during
|
|
467
|
+
// backoff sleep, or immediately after prompt([]) returns).
|
|
468
|
+
_sessionGeneration++;
|
|
469
|
+
|
|
445
470
|
state400.reset();
|
|
446
471
|
stateCredit.reset();
|
|
447
472
|
stateConnection.reset();
|
|
448
473
|
stateOther.reset();
|
|
449
474
|
stateContinuation.reset();
|
|
450
|
-
_continueInProgress
|
|
475
|
+
// Do NOT reset _continueInProgress here — the in-flight loop's
|
|
476
|
+
// finally block handles it conditionally (only if the generation
|
|
477
|
+
// hasn't changed since the loop started). Resetting it here would
|
|
478
|
+
// break the mutex invariant and could allow a second loop to start.
|
|
451
479
|
_lastInvisibleContinueTime = 0;
|
|
452
480
|
_userAborted = false;
|
|
453
481
|
});
|
|
@@ -477,20 +505,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
477
505
|
if (_continueInProgress) return;
|
|
478
506
|
_continueInProgress = true;
|
|
479
507
|
|
|
508
|
+
// Capture the current session generation. If /new fires while we're
|
|
509
|
+
// looping, _sessionGeneration will increment and the loop will exit.
|
|
510
|
+
const myGeneration = _sessionGeneration;
|
|
511
|
+
|
|
480
512
|
try {
|
|
481
513
|
// Wait for the current run to finish (activeRun resolves in
|
|
482
514
|
// finishRun() after agent_end listeners return).
|
|
483
515
|
await _agent.waitForIdle();
|
|
484
516
|
|
|
485
|
-
// Re-check after waitForIdle: the user may have aborted
|
|
486
|
-
//
|
|
487
|
-
if (_userAborted) return;
|
|
517
|
+
// Re-check after waitForIdle: the user may have aborted or the
|
|
518
|
+
// session may have changed while we were waiting.
|
|
519
|
+
if (_userAborted || _sessionGeneration !== myGeneration) return;
|
|
488
520
|
|
|
489
521
|
let attempt = 0;
|
|
490
522
|
|
|
491
|
-
// Loop until success or
|
|
523
|
+
// Loop until success, abort, or session change.
|
|
492
524
|
while (true) {
|
|
493
|
-
if (_userAborted) return;
|
|
525
|
+
if (_userAborted || _sessionGeneration !== myGeneration) return;
|
|
494
526
|
|
|
495
527
|
// Remove the error assistant message from agent state so
|
|
496
528
|
// prompt([]) sends a clean context to the LLM.
|
|
@@ -502,14 +534,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
502
534
|
// Notify the user about the upcoming retry attempt.
|
|
503
535
|
_notifyRetryAttempt(attempt, delay);
|
|
504
536
|
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
await
|
|
510
|
-
|
|
511
|
-
// Re-check after sleep — user may have aborted during backoff.
|
|
512
|
-
if (_userAborted) return;
|
|
537
|
+
// Interruptible sleep with backoff BEFORE the retry attempt.
|
|
538
|
+
// Polls _userAborted and _sessionGeneration every 100ms so ESC
|
|
539
|
+
// and /new take effect within 100ms instead of waiting for the
|
|
540
|
+
// full backoff (up to 60s).
|
|
541
|
+
const interrupted = await interruptibleSleep(delay, myGeneration);
|
|
542
|
+
if (interrupted) return;
|
|
513
543
|
|
|
514
544
|
try {
|
|
515
545
|
await _agent.prompt([]);
|
|
@@ -519,6 +549,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
519
549
|
return;
|
|
520
550
|
}
|
|
521
551
|
|
|
552
|
+
// Re-check after prompt: the user may have hit ESC during the
|
|
553
|
+
// prompt, or /new may have fired — don't keep retrying.
|
|
554
|
+
if (_userAborted || _sessionGeneration !== myGeneration) return;
|
|
555
|
+
|
|
522
556
|
// prompt([]) completed. Check the result.
|
|
523
557
|
if (!lastMessageIsRetryableError()) {
|
|
524
558
|
// Success or non-error terminal state — exit the loop.
|
|
@@ -528,7 +562,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
528
562
|
// Error again — loop back for another attempt.
|
|
529
563
|
}
|
|
530
564
|
} finally {
|
|
531
|
-
|
|
565
|
+
// Only reset the mutex if the session hasn't changed since we
|
|
566
|
+
// started. If /new fired, a new retry loop may already own the
|
|
567
|
+
// mutex — resetting it here would clobber that.
|
|
568
|
+
if (_sessionGeneration === myGeneration) {
|
|
569
|
+
_continueInProgress = false;
|
|
570
|
+
}
|
|
532
571
|
_lastInvisibleContinueTime = Date.now();
|
|
533
572
|
}
|
|
534
573
|
}
|