@monotykamary/pi-retry 0.3.10 → 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 +52 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-retry",
3
- "version": "0.3.10",
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
@@ -90,7 +90,9 @@ Agent.prototype.continue = function (this: Agent) {
90
90
  lastMsg.stopReason !== 'stop' &&
91
91
  lastMsg.stopReason !== 'aborted') {
92
92
  // Agent was mid-task (toolUse, length, or error) — fall back to prompt([])
93
- if (!_continueInProgress) {
93
+ // Guard: if triggerInvisibleContinue() just completed (within 500ms),
94
+ // the agent already continued — skip to avoid double continuation.
95
+ if (!_continueInProgress && Date.now() - _lastInvisibleContinueTime > 500) {
94
96
  _continueInProgress = true;
95
97
  try {
96
98
  await self.prompt([]);
@@ -121,12 +123,22 @@ const stateOther = new RetryState();
121
123
  // Max_tokens continuation state (indefinite — no cap needed)
122
124
  const stateContinuation = new ContinuationState();
123
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
+
124
131
  // Mutex: only one triggerInvisibleContinue may be in-flight at a time.
125
132
  // Without this, concurrent agent_end events (or a manual /retry during an
126
133
  // automatic retry) race through waitForIdle() and both call prompt([]),
127
134
  // producing "Agent is already processing".
128
135
  let _continueInProgress = false;
129
136
 
137
+ // Timestamp of the last completed triggerInvisibleContinue().
138
+ // Used by the continue() monkey-patch to avoid double continuation when
139
+ // triggerInvisibleContinue just ran and the session's continue() unblocks.
140
+ let _lastInvisibleContinueTime = 0;
141
+
130
142
  // Sleep helper
131
143
  function sleep(ms: number): Promise<void> {
132
144
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -148,6 +160,9 @@ export default function (pi: ExtensionAPI) {
148
160
  // Do NOT reset continuation state — a user abort of a continuation
149
161
  // turn is different from aborting an error retry.
150
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;
151
166
  return;
152
167
  }
153
168
  if (msg.stopReason !== "length") {
@@ -157,6 +172,9 @@ export default function (pi: ExtensionAPI) {
157
172
  stateConnection.succeed();
158
173
  stateOther.succeed();
159
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;
160
178
  }
161
179
  }
162
180
  });
@@ -170,6 +188,9 @@ export default function (pi: ExtensionAPI) {
170
188
  return;
171
189
  }
172
190
 
191
+ // Guard: if the user aborted, don't drive any new prompt([])
192
+ if (_userAborted) return;
193
+
173
194
  // Check for max_tokens stop — auto-continue (invisible to LLM)
174
195
  if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
175
196
  stateContinuation.startContinuation();
@@ -211,6 +232,15 @@ export default function (pi: ExtensionAPI) {
211
232
  const delay = calculateDelay(state.getAttempt());
212
233
 
213
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
+
214
244
  // Must NOT await — see triggerInvisibleContinue() for explanation
215
245
  void triggerInvisibleContinue();
216
246
  state.endRetry();
@@ -298,6 +328,7 @@ export default function (pi: ExtensionAPI) {
298
328
  stateConnection.reset();
299
329
  stateOther.reset();
300
330
  stateContinuation.reset();
331
+ _userAborted = false;
301
332
  ctx.ui.notify("All retry counters reset", "info");
302
333
  return;
303
334
  }
@@ -311,6 +342,10 @@ export default function (pi: ExtensionAPI) {
311
342
  return;
312
343
  }
313
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
+
314
349
  // Auto-detect: max_tokens continuation takes priority
315
350
  if (hasMaxTokensStop(lastAssistant)) {
316
351
  ctx.ui.notify("Manually continuing after max_tokens...", "info");
@@ -361,6 +396,8 @@ export default function (pi: ExtensionAPI) {
361
396
  stateOther.reset();
362
397
  stateContinuation.reset();
363
398
  _continueInProgress = false;
399
+ _lastInvisibleContinueTime = 0;
400
+ _userAborted = false;
364
401
  });
365
402
 
366
403
  // Resume the agent loop invisibly — no message injected into context.
@@ -374,6 +411,12 @@ export default function (pi: ExtensionAPI) {
374
411
  async function triggerInvisibleContinue() {
375
412
  if (!_agent) return;
376
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
+
377
420
  // Guard 1: mutex — if a previous continue is still in-flight, skip
378
421
  if (_continueInProgress) return;
379
422
  _continueInProgress = true;
@@ -383,6 +426,10 @@ export default function (pi: ExtensionAPI) {
383
426
  // finishRun() after agent_end listeners return).
384
427
  await _agent.waitForIdle();
385
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
+
386
433
  try {
387
434
  // Await so _continueInProgress stays true for the full retry.
388
435
  // The session's continue() is blocked (monkey-patch) and the
@@ -395,6 +442,10 @@ export default function (pi: ExtensionAPI) {
395
442
  }
396
443
  } finally {
397
444
  _continueInProgress = false;
445
+ // Record completion time so the continue() monkey-patch can
446
+ // detect that an invisible continue just ran and avoid firing
447
+ // a duplicate prompt([]) (RC7: double continuation guard).
448
+ _lastInvisibleContinueTime = Date.now();
398
449
  }
399
450
  }
400
451
  }