@monotykamary/pi-retry 0.6.5 → 0.6.7
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/README.md +9 -8
- package/package.json +1 -1
- package/retry.ts +56 -44
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ This extension automatically detects and retries **all** errors by default, with
|
|
|
25
25
|
| HTTP 400/413 | **Indefinite** with capped backoff, NO compaction | Transient context overflow that might resolve |
|
|
26
26
|
| Credit / payment errors | **Indefinite** with capped backoff | "Not Enough Credits", insufficient balance, 402 |
|
|
27
27
|
| Connection errors | **Indefinite** with capped backoff | Network hiccups, connection drops, socket errors, stream exhaustion |
|
|
28
|
-
| Max tokens (`stopReason: "length"`) | **Auto-continue** indefinitely
|
|
28
|
+
| Max tokens (`stopReason: "length"`) | **Auto-continue** indefinitely with hidden continuation turns | Model hits output token limit mid-generation |
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -48,10 +48,10 @@ This extension provides **automatic** infinite retry with sensible exponential b
|
|
|
48
48
|
**Features:**
|
|
49
49
|
- **Catch-all retry** — Any `stopReason: "error"` is retried, regardless of error message
|
|
50
50
|
- Automatic detection of 400/413, connection, credit, and stream exhaustion errors
|
|
51
|
-
- **Auto-continuation** when the model hits its max output tokens (`stopReason: "length"`) — indefinite, no cap,
|
|
51
|
+
- **Auto-continuation** when the model hits its max output tokens (`stopReason: "length"`) — indefinite, no cap, hidden from the TUI
|
|
52
52
|
- **Indefinite retry** — Keeps retrying until success
|
|
53
53
|
- Exponential backoff with cap: max 60s between retries
|
|
54
|
-
- **
|
|
54
|
+
- **Hidden triggers** — provider-valid custom messages use `display: false`, so retries do not add TUI clutter
|
|
55
55
|
- Manual controls via unified `/retry` command
|
|
56
56
|
- Non-retryable errors are explicitly logged so you know why we didn't retry
|
|
57
57
|
|
|
@@ -124,7 +124,7 @@ Edit the constants at the top of `retry.ts`:
|
|
|
124
124
|
const BASE_DELAY_MS = 2000; // Start with 2 seconds
|
|
125
125
|
const MAX_DELAY_MS = 60000; // Cap at 60 seconds
|
|
126
126
|
const BACKOFF_MULTIPLIER = 2; // Double each time
|
|
127
|
-
//
|
|
127
|
+
// Continuations use a hidden provider-valid custom message
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
---
|
|
@@ -135,9 +135,10 @@ const BACKOFF_MULTIPLIER = 2; // Double each time
|
|
|
135
135
|
2. **Check for any error** — Examine the last assistant message for `stopReason === "error"`
|
|
136
136
|
3. **Blacklist check** — Skip known permanent failures (invalid API key, model not found, etc.)
|
|
137
137
|
4. **Categorize for messaging** — Classify into 400/413, credit, connection, or other for nice UI notifications
|
|
138
|
-
5. **Retry or continue
|
|
139
|
-
6. **
|
|
140
|
-
7. **Indefinite continuation** — Max_tokens auto-continues are uncapped;
|
|
138
|
+
5. **Retry or continue with hidden turns** — Wait with exponential backoff, then trigger a provider-valid custom user turn via `pi.sendMessage()` with `display: false` and `triggerTurn: true`
|
|
139
|
+
6. **Valid provider context** — Hidden retry and continuation messages remain in context so providers never receive a trailing assistant message
|
|
140
|
+
7. **Indefinite continuation** — Max_tokens auto-continues are uncapped; repeated `length` stops keep producing continuation turns until the model terminates normally
|
|
141
|
+
8. **Lifecycle exposure** — Emits `pi-retry:started`, `pi-retry:completed`, and `pi-retry:cancelled` on Pi's shared extension event bus with a matching `retryId`, allowing status integrations to suppress intermediate completion signals
|
|
141
142
|
|
|
142
143
|
The pi's built-in `transform-messages` already strips aborted/errored assistant messages from the LLM context, so the model never sees the failed attempts.
|
|
143
144
|
|
|
@@ -160,7 +161,7 @@ These are explicitly **not** retried:
|
|
|
160
161
|
### Max Tokens (stopReason: "length")
|
|
161
162
|
- The model hit its `max_tokens` / output token limit
|
|
162
163
|
- The model's response was truncated mid-generation
|
|
163
|
-
- Auto-continuation sends
|
|
164
|
+
- Auto-continuation sends a provider-valid custom message hidden from the TUI
|
|
164
165
|
|
|
165
166
|
### 400/413 Errors
|
|
166
167
|
- HTTP 400 Bad Request
|
package/package.json
CHANGED
package/retry.ts
CHANGED
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
CONTINUATION_CUSTOM_TYPE,
|
|
22
22
|
} from "./src/index.js";
|
|
23
23
|
|
|
24
|
+
const RETRY_STARTED_EVENT = "pi-retry:started";
|
|
25
|
+
const RETRY_COMPLETED_EVENT = "pi-retry:completed";
|
|
26
|
+
const RETRY_CANCELLED_EVENT = "pi-retry:cancelled";
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
29
|
* Unified retry extension — retries EVERY error by default.
|
|
26
30
|
*
|
|
@@ -35,12 +39,12 @@ import {
|
|
|
35
39
|
* - Automatic detection and retry for ALL errors (catch-all)
|
|
36
40
|
* - Indefinite retry with exponential backoff (capped at 60s)
|
|
37
41
|
* - Auto-continuation when model hits max output tokens (stopReason "length")
|
|
38
|
-
* -
|
|
42
|
+
* - Retry triggers are hidden in the TUI and serialized as provider-valid user turns
|
|
39
43
|
* - Unified manual controls via /retry command
|
|
40
44
|
*
|
|
41
45
|
* Continuation mechanism:
|
|
42
46
|
* - A hidden custom message starts or joins a canonical AgentSession turn
|
|
43
|
-
* -
|
|
47
|
+
* - The message remains in context as a provider-valid user turn
|
|
44
48
|
* - AgentSession remains authoritative for busy state and queued messages
|
|
45
49
|
*
|
|
46
50
|
* Retry loop design:
|
|
@@ -111,6 +115,7 @@ let _continueInProgress = false;
|
|
|
111
115
|
let _continueGeneration: number | null = null;
|
|
112
116
|
let _continueInputGeneration: number | null = null;
|
|
113
117
|
let _inputGeneration = 0;
|
|
118
|
+
let _retryLifecycleId = 0;
|
|
114
119
|
|
|
115
120
|
// Session generation counter: incremented on every session_start.
|
|
116
121
|
// The retry loop captures the current generation when it starts and exits
|
|
@@ -161,33 +166,26 @@ function removeErrorFromAgentState(): void {
|
|
|
161
166
|
}
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
type HiddenTurnKind = "retry" | "continue";
|
|
170
|
+
|
|
171
|
+
function getHiddenTurnKind(): HiddenTurnKind | null {
|
|
172
|
+
if (!_agent) return null;
|
|
167
173
|
const messages = _agent.state.messages;
|
|
168
174
|
const lastMsg = messages[messages.length - 1];
|
|
169
|
-
|
|
175
|
+
if (lastMsg?.role !== "assistant") return null;
|
|
176
|
+
if (lastMsg.stopReason === "error") return "retry";
|
|
177
|
+
if (lastMsg.stopReason === "length") return "continue";
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function lastMessageIsRetryableError(): boolean {
|
|
182
|
+
return getHiddenTurnKind() === "retry";
|
|
170
183
|
}
|
|
171
184
|
|
|
172
185
|
export default function (pi: ExtensionAPI) {
|
|
173
186
|
|
|
174
|
-
|
|
187
|
+
pi.on("input", () => {
|
|
175
188
|
_inputGeneration++;
|
|
176
|
-
};
|
|
177
|
-
pi.on("input", (event) => {
|
|
178
|
-
if (event.source === "interactive" || event.source === "rpc") {
|
|
179
|
-
markRealPromptStart();
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
pi.on("before_agent_start", markRealPromptStart);
|
|
183
|
-
|
|
184
|
-
pi.on("context", (event) => {
|
|
185
|
-
const messages = event.messages.filter((message: any) => !(
|
|
186
|
-
message.role === "custom" &&
|
|
187
|
-
(message.customType === RETRY_TRIGGER_CUSTOM_TYPE ||
|
|
188
|
-
message.customType === CONTINUATION_CUSTOM_TYPE)
|
|
189
|
-
));
|
|
190
|
-
if (messages.length !== event.messages.length) return { messages };
|
|
191
189
|
});
|
|
192
190
|
|
|
193
191
|
// Reset retry counters on successful completion (not max_tokens, not error)
|
|
@@ -249,14 +247,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
249
247
|
_continueInputGeneration === _inputGeneration
|
|
250
248
|
) return;
|
|
251
249
|
|
|
252
|
-
// Check for max_tokens stop — auto-continue
|
|
250
|
+
// Check for max_tokens stop — auto-continue with a hidden TUI message
|
|
253
251
|
if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
|
|
254
252
|
stateContinuation.startContinuation();
|
|
255
253
|
ctx.ui.notify(
|
|
256
254
|
`Max tokens reached — auto-continuing (continuation ${stateContinuation.getCount()})...`,
|
|
257
255
|
"info",
|
|
258
256
|
);
|
|
259
|
-
void triggerInvisibleContinue();
|
|
257
|
+
void triggerInvisibleContinue("continue");
|
|
260
258
|
stateContinuation.endContinuation();
|
|
261
259
|
return;
|
|
262
260
|
}
|
|
@@ -303,7 +301,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
303
301
|
state.startRetry(errorMsg);
|
|
304
302
|
state.endRetry();
|
|
305
303
|
|
|
306
|
-
void triggerInvisibleContinue();
|
|
304
|
+
void triggerInvisibleContinue("retry");
|
|
307
305
|
return;
|
|
308
306
|
}
|
|
309
307
|
|
|
@@ -358,7 +356,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
358
356
|
status += "Max Tokens Continuation:\n";
|
|
359
357
|
status += ` Continuations used: ${stateContinuation.getCount()}\n`;
|
|
360
358
|
status += ` Is continuing: ${stateContinuation.getIsContinuing()}\n`;
|
|
361
|
-
status += ` Trigger: hidden AgentSession turn
|
|
359
|
+
status += ` Trigger: hidden provider-valid AgentSession turn\n\n`;
|
|
362
360
|
|
|
363
361
|
// Config
|
|
364
362
|
status += "Configuration:\n";
|
|
@@ -409,7 +407,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
409
407
|
// Auto-detect: max_tokens continuation takes priority
|
|
410
408
|
if (hasMaxTokensStop(lastAssistant)) {
|
|
411
409
|
ctx.ui.notify("Manually continuing after max_tokens...", "info");
|
|
412
|
-
void triggerInvisibleContinue();
|
|
410
|
+
void triggerInvisibleContinue("continue");
|
|
413
411
|
return;
|
|
414
412
|
}
|
|
415
413
|
|
|
@@ -428,21 +426,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
428
426
|
if (has400or413Error(lastAssistant)) {
|
|
429
427
|
ctx.ui.notify("Manually retrying 400/413 error...", "info");
|
|
430
428
|
state400.reset();
|
|
431
|
-
void triggerInvisibleContinue();
|
|
429
|
+
void triggerInvisibleContinue("retry");
|
|
432
430
|
return;
|
|
433
431
|
}
|
|
434
432
|
|
|
435
433
|
if (hasCreditError(lastAssistant)) {
|
|
436
434
|
ctx.ui.notify("Manually retrying credit error...", "info");
|
|
437
435
|
stateCredit.reset();
|
|
438
|
-
void triggerInvisibleContinue();
|
|
436
|
+
void triggerInvisibleContinue("retry");
|
|
439
437
|
return;
|
|
440
438
|
}
|
|
441
439
|
|
|
442
440
|
if (hasConnectionError(lastAssistant)) {
|
|
443
441
|
ctx.ui.notify("Manually retrying connection error...", "info");
|
|
444
442
|
stateConnection.reset();
|
|
445
|
-
void triggerInvisibleContinue();
|
|
443
|
+
void triggerInvisibleContinue("retry");
|
|
446
444
|
return;
|
|
447
445
|
}
|
|
448
446
|
|
|
@@ -450,7 +448,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
450
448
|
if (hasRetryableError(lastAssistant)) {
|
|
451
449
|
ctx.ui.notify("Manually retrying error...", "info");
|
|
452
450
|
stateOther.reset();
|
|
453
|
-
void triggerInvisibleContinue();
|
|
451
|
+
void triggerInvisibleContinue("retry");
|
|
454
452
|
return;
|
|
455
453
|
}
|
|
456
454
|
|
|
@@ -500,9 +498,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
500
498
|
//
|
|
501
499
|
// Unlike the original one-shot design, this function loops. After each
|
|
502
500
|
// hidden AgentSession turn it checks the result:
|
|
503
|
-
// - Success
|
|
504
|
-
// - Error
|
|
505
|
-
// -
|
|
501
|
+
// - Success: loop exits when the stop reason is neither error nor length.
|
|
502
|
+
// - Error: sleep with backoff, then retry the request.
|
|
503
|
+
// - Length: sleep with backoff, then continue the response.
|
|
504
|
+
// - User abort: loop exits immediately.
|
|
506
505
|
//
|
|
507
506
|
// The backoff sleep happens AFTER the hidden turn settles and processEvents
|
|
508
507
|
// has settled, so it does NOT block the agent. The agent is idle during
|
|
@@ -511,7 +510,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
511
510
|
// Before each retry, the error assistant message is removed from
|
|
512
511
|
// agent.state.messages so the LLM receives a clean context (same
|
|
513
512
|
// technique as the built-in retry's _prepareRetry).
|
|
514
|
-
async function triggerInvisibleContinue() {
|
|
513
|
+
async function triggerInvisibleContinue(initialKind: HiddenTurnKind) {
|
|
515
514
|
if (!_agent) return;
|
|
516
515
|
|
|
517
516
|
// Guard: if the user aborted, do not queue another retry turn.
|
|
@@ -520,6 +519,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
520
519
|
// Guard: mutex — if a previous continue is still in-flight, skip
|
|
521
520
|
if (_continueInProgress) return;
|
|
522
521
|
_continueInProgress = true;
|
|
522
|
+
const retryLifecycleId = ++_retryLifecycleId;
|
|
523
|
+
let didRetryComplete = false;
|
|
524
|
+
pi.events.emit(RETRY_STARTED_EVENT, { retryId: retryLifecycleId });
|
|
523
525
|
|
|
524
526
|
// Capture the current session generation. If /new fires while we're
|
|
525
527
|
// looping, _sessionGeneration will increment and the loop will exit.
|
|
@@ -542,6 +544,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
542
544
|
) return;
|
|
543
545
|
|
|
544
546
|
let attempt = 0;
|
|
547
|
+
let hiddenTurnKind: HiddenTurnKind | null = initialKind;
|
|
545
548
|
|
|
546
549
|
// Loop until success, abort, or session change.
|
|
547
550
|
while (true) {
|
|
@@ -552,8 +555,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
552
555
|
) return;
|
|
553
556
|
|
|
554
557
|
// Preserve the trigger kind before removing a trailing error from
|
|
555
|
-
// live state.
|
|
556
|
-
|
|
558
|
+
// live state. Length-stopped output stays in context so the model can
|
|
559
|
+
// continue from it; error messages remain only in the session journal.
|
|
560
|
+
if (!hiddenTurnKind) {
|
|
561
|
+
didRetryComplete = true;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
557
564
|
removeErrorFromAgentState();
|
|
558
565
|
|
|
559
566
|
attempt++;
|
|
@@ -576,10 +583,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
576
583
|
try {
|
|
577
584
|
pi.sendMessage(
|
|
578
585
|
{
|
|
579
|
-
customType:
|
|
586
|
+
customType: hiddenTurnKind === "retry"
|
|
580
587
|
? RETRY_TRIGGER_CUSTOM_TYPE
|
|
581
588
|
: CONTINUATION_CUSTOM_TYPE,
|
|
582
|
-
content:
|
|
589
|
+
content: hiddenTurnKind === "retry"
|
|
590
|
+
? "Retry the previous request."
|
|
591
|
+
: "Continue exactly where you left off without repeating content.",
|
|
583
592
|
display: false,
|
|
584
593
|
details: undefined,
|
|
585
594
|
},
|
|
@@ -603,17 +612,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
603
612
|
_inputGeneration !== myInputGeneration
|
|
604
613
|
) return;
|
|
605
614
|
|
|
606
|
-
// The hidden AgentSession turn completed.
|
|
607
|
-
|
|
608
|
-
|
|
615
|
+
// The hidden AgentSession turn completed. Both errors and output
|
|
616
|
+
// length stops need another turn; all other terminal states are done.
|
|
617
|
+
hiddenTurnKind = getHiddenTurnKind();
|
|
618
|
+
if (!hiddenTurnKind) {
|
|
619
|
+
didRetryComplete = true;
|
|
609
620
|
return;
|
|
610
621
|
}
|
|
611
|
-
|
|
612
|
-
// Error again — loop back for another attempt.
|
|
613
622
|
}
|
|
614
623
|
} finally {
|
|
615
624
|
// Release the mutex only if this loop still owns it.
|
|
616
625
|
if (_continueGeneration === myGeneration) {
|
|
626
|
+
pi.events.emit(didRetryComplete ? RETRY_COMPLETED_EVENT : RETRY_CANCELLED_EVENT, {
|
|
627
|
+
retryId: retryLifecycleId,
|
|
628
|
+
});
|
|
617
629
|
_continueInProgress = false;
|
|
618
630
|
_continueGeneration = null;
|
|
619
631
|
_continueInputGeneration = null;
|