@monotykamary/pi-retry 0.3.0 → 0.3.1
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/package.json +1 -1
- package/retry.ts +51 -9
package/package.json
CHANGED
package/retry.ts
CHANGED
|
@@ -60,6 +60,12 @@ const stateOther = new RetryState();
|
|
|
60
60
|
// Max_tokens continuation state (indefinite — no cap needed)
|
|
61
61
|
const stateContinuation = new ContinuationState();
|
|
62
62
|
|
|
63
|
+
// Mutex: only one triggerInvisibleContinue may be in-flight at a time.
|
|
64
|
+
// Without this, concurrent agent_end events (or a manual /retry during an
|
|
65
|
+
// automatic retry) race through waitForIdle() and both call prompt([]),
|
|
66
|
+
// producing "Agent is already processing".
|
|
67
|
+
let _continueInProgress = false;
|
|
68
|
+
|
|
63
69
|
// Sleep helper
|
|
64
70
|
function sleep(ms: number): Promise<void> {
|
|
65
71
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -110,7 +116,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
110
116
|
`Max tokens reached — auto-continuing (continuation ${stateContinuation.getCount()})...`,
|
|
111
117
|
"info",
|
|
112
118
|
);
|
|
113
|
-
triggerInvisibleContinue()
|
|
119
|
+
// Must NOT await — see triggerInvisibleContinue() for explanation
|
|
120
|
+
void triggerInvisibleContinue();
|
|
114
121
|
stateContinuation.endContinuation();
|
|
115
122
|
return;
|
|
116
123
|
}
|
|
@@ -143,7 +150,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
150
|
const delay = calculateDelay(state.getAttempt());
|
|
144
151
|
|
|
145
152
|
await sleep(delay);
|
|
146
|
-
triggerInvisibleContinue()
|
|
153
|
+
// Must NOT await — see triggerInvisibleContinue() for explanation
|
|
154
|
+
void triggerInvisibleContinue();
|
|
147
155
|
state.endRetry();
|
|
148
156
|
return;
|
|
149
157
|
}
|
|
@@ -244,7 +252,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
244
252
|
// Auto-detect: max_tokens continuation takes priority
|
|
245
253
|
if (hasMaxTokensStop(lastAssistant)) {
|
|
246
254
|
ctx.ui.notify("Manually continuing after max_tokens...", "info");
|
|
247
|
-
triggerInvisibleContinue();
|
|
255
|
+
void triggerInvisibleContinue();
|
|
248
256
|
return;
|
|
249
257
|
}
|
|
250
258
|
|
|
@@ -252,21 +260,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
252
260
|
if (has400or413Error(lastAssistant)) {
|
|
253
261
|
ctx.ui.notify("Manually retrying 400/413 error...", "info");
|
|
254
262
|
state400.reset();
|
|
255
|
-
triggerInvisibleContinue();
|
|
263
|
+
void triggerInvisibleContinue();
|
|
256
264
|
return;
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
if (hasCreditError(lastAssistant)) {
|
|
260
268
|
ctx.ui.notify("Manually retrying credit error...", "info");
|
|
261
269
|
stateCredit.reset();
|
|
262
|
-
triggerInvisibleContinue();
|
|
270
|
+
void triggerInvisibleContinue();
|
|
263
271
|
return;
|
|
264
272
|
}
|
|
265
273
|
|
|
266
274
|
if (hasConnectionError(lastAssistant)) {
|
|
267
275
|
ctx.ui.notify("Manually retrying connection error...", "info");
|
|
268
276
|
stateConnection.reset();
|
|
269
|
-
triggerInvisibleContinue();
|
|
277
|
+
void triggerInvisibleContinue();
|
|
270
278
|
return;
|
|
271
279
|
}
|
|
272
280
|
|
|
@@ -274,7 +282,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
282
|
if (hasRetryableError(lastAssistant)) {
|
|
275
283
|
ctx.ui.notify("Manually retrying error...", "info");
|
|
276
284
|
stateOther.reset();
|
|
277
|
-
triggerInvisibleContinue();
|
|
285
|
+
void triggerInvisibleContinue();
|
|
278
286
|
return;
|
|
279
287
|
}
|
|
280
288
|
|
|
@@ -290,12 +298,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
290
298
|
stateConnection.reset();
|
|
291
299
|
stateOther.reset();
|
|
292
300
|
stateContinuation.reset();
|
|
301
|
+
_continueInProgress = false;
|
|
293
302
|
});
|
|
294
303
|
|
|
295
304
|
// Resume the agent loop invisibly — no message injected into context.
|
|
296
305
|
// The LLM sees the exact same message list it had before.
|
|
297
|
-
|
|
306
|
+
//
|
|
307
|
+
// IMPORTANT: We must wait for the agent to become idle before calling
|
|
308
|
+
// prompt(). The agent_end event fires while the active run is still
|
|
309
|
+
// set (it's cleared in the finally block, after all listeners settle).
|
|
310
|
+
// Calling prompt() inside an agent_end listener would otherwise throw
|
|
311
|
+
// "Agent is already processing a prompt" because activeRun is truthy.
|
|
312
|
+
//
|
|
313
|
+
// GUARDS (three layers):
|
|
314
|
+
// 1. _continueInProgress mutex — prevents concurrent calls from racing
|
|
315
|
+
// 2. isStreaming pre-flight — detects user-initiated runs before prompt()
|
|
316
|
+
// 3. try/catch — final safety net, swallows the error gracefully
|
|
317
|
+
async function triggerInvisibleContinue() {
|
|
298
318
|
if (!_agent) return;
|
|
299
|
-
|
|
319
|
+
|
|
320
|
+
// Guard 1: mutex — if a previous continue is still in-flight, skip
|
|
321
|
+
if (_continueInProgress) return;
|
|
322
|
+
_continueInProgress = true;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
await _agent.waitForIdle();
|
|
326
|
+
|
|
327
|
+
// Guard 2: pre-flight — the user may have sent a message while we
|
|
328
|
+
// waited for idle. agent.state.isStreaming is authoritative (read
|
|
329
|
+
// directly from the activeRun field, no TOCTOU beyond the next line).
|
|
330
|
+
if (_agent.state.isStreaming) return;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
_agent.prompt([]);
|
|
334
|
+
} catch {
|
|
335
|
+
// Guard 3: catch the "already processing" error as a last resort.
|
|
336
|
+
// This handles the remaining microtask TOCTOU gap between the
|
|
337
|
+
// isStreaming check and prompt() acquiring the lock.
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
_continueInProgress = false;
|
|
341
|
+
}
|
|
300
342
|
}
|
|
301
343
|
}
|