@monotykamary/pi-retry 0.6.6 → 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 +8 -8
- package/package.json +1 -1
- package/retry.ts +44 -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,9 @@ 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
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
|
|
142
142
|
|
|
143
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.
|
|
@@ -161,7 +161,7 @@ These are explicitly **not** retried:
|
|
|
161
161
|
### Max Tokens (stopReason: "length")
|
|
162
162
|
- The model hit its `max_tokens` / output token limit
|
|
163
163
|
- The model's response was truncated mid-generation
|
|
164
|
-
- Auto-continuation sends
|
|
164
|
+
- Auto-continuation sends a provider-valid custom message hidden from the TUI
|
|
165
165
|
|
|
166
166
|
### 400/413 Errors
|
|
167
167
|
- HTTP 400 Bad Request
|
package/package.json
CHANGED
package/retry.ts
CHANGED
|
@@ -39,12 +39,12 @@ const RETRY_CANCELLED_EVENT = "pi-retry:cancelled";
|
|
|
39
39
|
* - Automatic detection and retry for ALL errors (catch-all)
|
|
40
40
|
* - Indefinite retry with exponential backoff (capped at 60s)
|
|
41
41
|
* - Auto-continuation when model hits max output tokens (stopReason "length")
|
|
42
|
-
* -
|
|
42
|
+
* - Retry triggers are hidden in the TUI and serialized as provider-valid user turns
|
|
43
43
|
* - Unified manual controls via /retry command
|
|
44
44
|
*
|
|
45
45
|
* Continuation mechanism:
|
|
46
46
|
* - A hidden custom message starts or joins a canonical AgentSession turn
|
|
47
|
-
* -
|
|
47
|
+
* - The message remains in context as a provider-valid user turn
|
|
48
48
|
* - AgentSession remains authoritative for busy state and queued messages
|
|
49
49
|
*
|
|
50
50
|
* Retry loop design:
|
|
@@ -166,33 +166,26 @@ function removeErrorFromAgentState(): void {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
type HiddenTurnKind = "retry" | "continue";
|
|
170
|
+
|
|
171
|
+
function getHiddenTurnKind(): HiddenTurnKind | null {
|
|
172
|
+
if (!_agent) return null;
|
|
172
173
|
const messages = _agent.state.messages;
|
|
173
174
|
const lastMsg = messages[messages.length - 1];
|
|
174
|
-
|
|
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";
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
export default function (pi: ExtensionAPI) {
|
|
178
186
|
|
|
179
|
-
|
|
187
|
+
pi.on("input", () => {
|
|
180
188
|
_inputGeneration++;
|
|
181
|
-
};
|
|
182
|
-
pi.on("input", (event) => {
|
|
183
|
-
if (event.source === "interactive" || event.source === "rpc") {
|
|
184
|
-
markRealPromptStart();
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
pi.on("before_agent_start", markRealPromptStart);
|
|
188
|
-
|
|
189
|
-
pi.on("context", (event) => {
|
|
190
|
-
const messages = event.messages.filter((message: any) => !(
|
|
191
|
-
message.role === "custom" &&
|
|
192
|
-
(message.customType === RETRY_TRIGGER_CUSTOM_TYPE ||
|
|
193
|
-
message.customType === CONTINUATION_CUSTOM_TYPE)
|
|
194
|
-
));
|
|
195
|
-
if (messages.length !== event.messages.length) return { messages };
|
|
196
189
|
});
|
|
197
190
|
|
|
198
191
|
// Reset retry counters on successful completion (not max_tokens, not error)
|
|
@@ -254,14 +247,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
254
247
|
_continueInputGeneration === _inputGeneration
|
|
255
248
|
) return;
|
|
256
249
|
|
|
257
|
-
// Check for max_tokens stop — auto-continue
|
|
250
|
+
// Check for max_tokens stop — auto-continue with a hidden TUI message
|
|
258
251
|
if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
|
|
259
252
|
stateContinuation.startContinuation();
|
|
260
253
|
ctx.ui.notify(
|
|
261
254
|
`Max tokens reached — auto-continuing (continuation ${stateContinuation.getCount()})...`,
|
|
262
255
|
"info",
|
|
263
256
|
);
|
|
264
|
-
void triggerInvisibleContinue();
|
|
257
|
+
void triggerInvisibleContinue("continue");
|
|
265
258
|
stateContinuation.endContinuation();
|
|
266
259
|
return;
|
|
267
260
|
}
|
|
@@ -308,7 +301,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
308
301
|
state.startRetry(errorMsg);
|
|
309
302
|
state.endRetry();
|
|
310
303
|
|
|
311
|
-
void triggerInvisibleContinue();
|
|
304
|
+
void triggerInvisibleContinue("retry");
|
|
312
305
|
return;
|
|
313
306
|
}
|
|
314
307
|
|
|
@@ -363,7 +356,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
363
356
|
status += "Max Tokens Continuation:\n";
|
|
364
357
|
status += ` Continuations used: ${stateContinuation.getCount()}\n`;
|
|
365
358
|
status += ` Is continuing: ${stateContinuation.getIsContinuing()}\n`;
|
|
366
|
-
status += ` Trigger: hidden AgentSession turn
|
|
359
|
+
status += ` Trigger: hidden provider-valid AgentSession turn\n\n`;
|
|
367
360
|
|
|
368
361
|
// Config
|
|
369
362
|
status += "Configuration:\n";
|
|
@@ -414,7 +407,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
414
407
|
// Auto-detect: max_tokens continuation takes priority
|
|
415
408
|
if (hasMaxTokensStop(lastAssistant)) {
|
|
416
409
|
ctx.ui.notify("Manually continuing after max_tokens...", "info");
|
|
417
|
-
void triggerInvisibleContinue();
|
|
410
|
+
void triggerInvisibleContinue("continue");
|
|
418
411
|
return;
|
|
419
412
|
}
|
|
420
413
|
|
|
@@ -433,21 +426,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
433
426
|
if (has400or413Error(lastAssistant)) {
|
|
434
427
|
ctx.ui.notify("Manually retrying 400/413 error...", "info");
|
|
435
428
|
state400.reset();
|
|
436
|
-
void triggerInvisibleContinue();
|
|
429
|
+
void triggerInvisibleContinue("retry");
|
|
437
430
|
return;
|
|
438
431
|
}
|
|
439
432
|
|
|
440
433
|
if (hasCreditError(lastAssistant)) {
|
|
441
434
|
ctx.ui.notify("Manually retrying credit error...", "info");
|
|
442
435
|
stateCredit.reset();
|
|
443
|
-
void triggerInvisibleContinue();
|
|
436
|
+
void triggerInvisibleContinue("retry");
|
|
444
437
|
return;
|
|
445
438
|
}
|
|
446
439
|
|
|
447
440
|
if (hasConnectionError(lastAssistant)) {
|
|
448
441
|
ctx.ui.notify("Manually retrying connection error...", "info");
|
|
449
442
|
stateConnection.reset();
|
|
450
|
-
void triggerInvisibleContinue();
|
|
443
|
+
void triggerInvisibleContinue("retry");
|
|
451
444
|
return;
|
|
452
445
|
}
|
|
453
446
|
|
|
@@ -455,7 +448,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
455
448
|
if (hasRetryableError(lastAssistant)) {
|
|
456
449
|
ctx.ui.notify("Manually retrying error...", "info");
|
|
457
450
|
stateOther.reset();
|
|
458
|
-
void triggerInvisibleContinue();
|
|
451
|
+
void triggerInvisibleContinue("retry");
|
|
459
452
|
return;
|
|
460
453
|
}
|
|
461
454
|
|
|
@@ -505,9 +498,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
505
498
|
//
|
|
506
499
|
// Unlike the original one-shot design, this function loops. After each
|
|
507
500
|
// hidden AgentSession turn it checks the result:
|
|
508
|
-
// - Success
|
|
509
|
-
// - Error
|
|
510
|
-
// -
|
|
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.
|
|
511
505
|
//
|
|
512
506
|
// The backoff sleep happens AFTER the hidden turn settles and processEvents
|
|
513
507
|
// has settled, so it does NOT block the agent. The agent is idle during
|
|
@@ -516,7 +510,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
516
510
|
// Before each retry, the error assistant message is removed from
|
|
517
511
|
// agent.state.messages so the LLM receives a clean context (same
|
|
518
512
|
// technique as the built-in retry's _prepareRetry).
|
|
519
|
-
async function triggerInvisibleContinue() {
|
|
513
|
+
async function triggerInvisibleContinue(initialKind: HiddenTurnKind) {
|
|
520
514
|
if (!_agent) return;
|
|
521
515
|
|
|
522
516
|
// Guard: if the user aborted, do not queue another retry turn.
|
|
@@ -550,6 +544,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
550
544
|
) return;
|
|
551
545
|
|
|
552
546
|
let attempt = 0;
|
|
547
|
+
let hiddenTurnKind: HiddenTurnKind | null = initialKind;
|
|
553
548
|
|
|
554
549
|
// Loop until success, abort, or session change.
|
|
555
550
|
while (true) {
|
|
@@ -560,8 +555,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
560
555
|
) return;
|
|
561
556
|
|
|
562
557
|
// Preserve the trigger kind before removing a trailing error from
|
|
563
|
-
// live state.
|
|
564
|
-
|
|
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
|
+
}
|
|
565
564
|
removeErrorFromAgentState();
|
|
566
565
|
|
|
567
566
|
attempt++;
|
|
@@ -584,10 +583,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
584
583
|
try {
|
|
585
584
|
pi.sendMessage(
|
|
586
585
|
{
|
|
587
|
-
customType:
|
|
586
|
+
customType: hiddenTurnKind === "retry"
|
|
588
587
|
? RETRY_TRIGGER_CUSTOM_TYPE
|
|
589
588
|
: CONTINUATION_CUSTOM_TYPE,
|
|
590
|
-
content:
|
|
589
|
+
content: hiddenTurnKind === "retry"
|
|
590
|
+
? "Retry the previous request."
|
|
591
|
+
: "Continue exactly where you left off without repeating content.",
|
|
591
592
|
display: false,
|
|
592
593
|
details: undefined,
|
|
593
594
|
},
|
|
@@ -611,14 +612,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
611
612
|
_inputGeneration !== myInputGeneration
|
|
612
613
|
) return;
|
|
613
614
|
|
|
614
|
-
// The hidden AgentSession turn completed.
|
|
615
|
-
|
|
616
|
-
|
|
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) {
|
|
617
619
|
didRetryComplete = true;
|
|
618
620
|
return;
|
|
619
621
|
}
|
|
620
|
-
|
|
621
|
-
// Error again — loop back for another attempt.
|
|
622
622
|
}
|
|
623
623
|
} finally {
|
|
624
624
|
// Release the mutex only if this loop still owns it.
|