@monotykamary/pi-retry 0.3.11 → 0.3.12

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 (2) hide show
  1. package/package.json +1 -1
  2. package/retry.ts +39 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-retry",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
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
@@ -123,6 +123,11 @@ const stateOther = new RetryState();
123
123
  // Max_tokens continuation state (indefinite — no cap needed)
124
124
  const stateContinuation = new ContinuationState();
125
125
 
126
+ // Abort flag: set when turn_end reports stopReason "aborted", cleared on
127
+ // session_start and on fresh user activity. Prevents triggerInvisibleContinue()
128
+ // from driving a new prompt([]) after the user explicitly cancelled.
129
+ let _userAborted = false;
130
+
126
131
  // Mutex: only one triggerInvisibleContinue may be in-flight at a time.
127
132
  // Without this, concurrent agent_end events (or a manual /retry during an
128
133
  // automatic retry) race through waitForIdle() and both call prompt([]),
@@ -155,6 +160,9 @@ export default function (pi: ExtensionAPI) {
155
160
  // Do NOT reset continuation state — a user abort of a continuation
156
161
  // turn is different from aborting an error retry.
157
162
  stateContinuation.endContinuation();
163
+ // Signal to any in-flight triggerInvisibleContinue or pending retry
164
+ // that the user has cancelled — don't drive a new prompt([]).
165
+ _userAborted = true;
158
166
  return;
159
167
  }
160
168
  if (msg.stopReason !== "length") {
@@ -164,6 +172,9 @@ export default function (pi: ExtensionAPI) {
164
172
  stateConnection.succeed();
165
173
  stateOther.succeed();
166
174
  stateContinuation.complete();
175
+ // Clear abort flag — this is a fresh successful turn, so any
176
+ // previous abort is stale and shouldn't block future retries.
177
+ _userAborted = false;
167
178
  }
168
179
  }
169
180
  });
@@ -177,6 +188,9 @@ export default function (pi: ExtensionAPI) {
177
188
  return;
178
189
  }
179
190
 
191
+ // Guard: if the user aborted, don't drive any new prompt([])
192
+ if (_userAborted) return;
193
+
180
194
  // Check for max_tokens stop — auto-continue (invisible to LLM)
181
195
  if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
182
196
  stateContinuation.startContinuation();
@@ -218,6 +232,15 @@ export default function (pi: ExtensionAPI) {
218
232
  const delay = calculateDelay(state.getAttempt());
219
233
 
220
234
  await sleep(delay);
235
+
236
+ // Re-check: user may have aborted during the backoff sleep.
237
+ // turn_end with stopReason "aborted" sets _userAborted, but this
238
+ // handler was already past the initial check.
239
+ if (_userAborted) {
240
+ state.endRetry();
241
+ return;
242
+ }
243
+
221
244
  // Must NOT await — see triggerInvisibleContinue() for explanation
222
245
  void triggerInvisibleContinue();
223
246
  state.endRetry();
@@ -305,6 +328,7 @@ export default function (pi: ExtensionAPI) {
305
328
  stateConnection.reset();
306
329
  stateOther.reset();
307
330
  stateContinuation.reset();
331
+ _userAborted = false;
308
332
  ctx.ui.notify("All retry counters reset", "info");
309
333
  return;
310
334
  }
@@ -318,6 +342,10 @@ export default function (pi: ExtensionAPI) {
318
342
  return;
319
343
  }
320
344
 
345
+ // Manual /retry overrides any previous abort — the user is
346
+ // explicitly requesting a retry, so clear the abort flag.
347
+ _userAborted = false;
348
+
321
349
  // Auto-detect: max_tokens continuation takes priority
322
350
  if (hasMaxTokensStop(lastAssistant)) {
323
351
  ctx.ui.notify("Manually continuing after max_tokens...", "info");
@@ -369,6 +397,7 @@ export default function (pi: ExtensionAPI) {
369
397
  stateContinuation.reset();
370
398
  _continueInProgress = false;
371
399
  _lastInvisibleContinueTime = 0;
400
+ _userAborted = false;
372
401
  });
373
402
 
374
403
  // Resume the agent loop invisibly — no message injected into context.
@@ -382,6 +411,12 @@ export default function (pi: ExtensionAPI) {
382
411
  async function triggerInvisibleContinue() {
383
412
  if (!_agent) return;
384
413
 
414
+ // Guard 0: if the user aborted, don't drive a new prompt([]).
415
+ // This catches aborts that happened between agent_end firing and
416
+ // this async function actually executing (e.g. during backoff sleep
417
+ // or while waitForIdle was pending).
418
+ if (_userAborted) return;
419
+
385
420
  // Guard 1: mutex — if a previous continue is still in-flight, skip
386
421
  if (_continueInProgress) return;
387
422
  _continueInProgress = true;
@@ -391,6 +426,10 @@ export default function (pi: ExtensionAPI) {
391
426
  // finishRun() after agent_end listeners return).
392
427
  await _agent.waitForIdle();
393
428
 
429
+ // Re-check after waitForIdle: the user may have aborted while
430
+ // we were waiting for the agent to become idle.
431
+ if (_userAborted) return;
432
+
394
433
  try {
395
434
  // Await so _continueInProgress stays true for the full retry.
396
435
  // The session's continue() is blocked (monkey-patch) and the