@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.
Files changed (3) hide show
  1. package/README.md +8 -8
  2. package/package.json +1 -1
  3. 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 (invisible no prompt pollution) | Model hits output token limit mid-generation |
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, **invisible** to the LLM
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
- - **ALL triggers are invisible** — custom messages with `display: false`, stripped by context handler (no TUI clutter, no conversation pollution)
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
- // Continuation is now invisible no CONTINUATION_PROMPT needed
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 (both invisible)** — Wait (exponential backoff for errors), then trigger a new turn via `pi.sendMessage()` with `customType`, `display: false`, and `triggerTurn: true`
139
- 6. **Context cleanup** — The `context` event strips all custom-type triggers before the LLM sees them (insurance against custom `convertToLlm` overrides)
140
- 7. **Indefinite continuation** — Max_tokens auto-continues are uncapped; each continuation produces valid output and the model naturally terminates when done
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 an invisible custom message no visible "Continue" prompt in the conversation
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-retry",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
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",
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
- * - ALL triggers are invisible hidden AgentSession turns are filtered before the LLM call
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
- * - A context hook removes that marker before provider serialization
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
- // Check if the agent's last message indicates a retryable error.
170
- function lastMessageIsRetryableError(): boolean {
171
- if (!_agent) return false;
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
- return lastMsg?.role === 'assistant' && lastMsg.stopReason === 'error';
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
- const markRealPromptStart = () => {
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 (invisible to LLM)
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 (filtered before provider call)\n\n`;
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 (stopReason !== "error"): loop exits, agent is done.
509
- // - Error (stopReason === "error"): sleep with backoff, then retry.
510
- // - User abort (stopReason "aborted"): loop exits immediately.
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. The error remains in the session journal for history.
564
- const isErrorRetry = lastMessageIsRetryableError();
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: isErrorRetry
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. Check the result.
615
- if (!lastMessageIsRetryableError()) {
616
- // Success or non-error terminal state — exit the loop.
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.