@monotykamary/pi-retry 0.3.2 → 0.3.3
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 +56 -18
package/package.json
CHANGED
package/retry.ts
CHANGED
|
@@ -304,16 +304,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
304
304
|
// Resume the agent loop invisibly — no message injected into context.
|
|
305
305
|
// The LLM sees the exact same message list it had before.
|
|
306
306
|
//
|
|
307
|
-
//
|
|
308
|
-
// prompt(). The agent_end event fires
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
307
|
+
// CRITICAL: We must wait until the SESSION has fully finished before we
|
|
308
|
+
// call prompt(). The agent_end event fires inside the current run, and
|
|
309
|
+
// the session's _runAgentPrompt loop may still call continue() after the
|
|
310
|
+
// run resolves (for built-in retries, compaction, etc.). If we call
|
|
311
|
+
// prompt([]) while the session's continue() is about to run, one of
|
|
312
|
+
// them gets "Agent is already processing".
|
|
312
313
|
//
|
|
313
|
-
//
|
|
314
|
+
// The solution: wait for isStreaming to be false AND stay false across
|
|
315
|
+
// a microtask yield. This ensures the session's loop has fully exited
|
|
316
|
+
// and won't call continue() under our feet.
|
|
317
|
+
//
|
|
318
|
+
// GUARDS (four layers):
|
|
314
319
|
// 1. _continueInProgress mutex — prevents concurrent calls from racing
|
|
315
|
-
// 2.
|
|
316
|
-
// 3.
|
|
320
|
+
// 2. waitForIdle + settle loop — waits for session to fully finish
|
|
321
|
+
// 3. isStreaming pre-flight — detects user-initiated runs before prompt()
|
|
322
|
+
// 4. .catch() on prompt() — final safety net, swallows rejected promises
|
|
317
323
|
async function triggerInvisibleContinue() {
|
|
318
324
|
if (!_agent) return;
|
|
319
325
|
|
|
@@ -322,20 +328,52 @@ export default function (pi: ExtensionAPI) {
|
|
|
322
328
|
_continueInProgress = true;
|
|
323
329
|
|
|
324
330
|
try {
|
|
325
|
-
|
|
331
|
+
// Guard 2: wait for the current run to finish, then verify the
|
|
332
|
+
// session has no further continue() calls pending.
|
|
333
|
+
//
|
|
334
|
+
// The session's _runAgentPrompt loop:
|
|
335
|
+
// await agent.prompt(messages);
|
|
336
|
+
// while (await _handlePostAgentRun()) { await agent.continue(); }
|
|
337
|
+
//
|
|
338
|
+
// waitForIdle() resolves when the FIRST run's activeRun is cleared.
|
|
339
|
+
// But the session may immediately start a new run via continue()
|
|
340
|
+
// (built-in retry, compaction). We must keep waiting until
|
|
341
|
+
// isStreaming stays false across a microtask yield — that proves
|
|
342
|
+
// the session's loop has exited.
|
|
343
|
+
const MAX_SETTLE_ATTEMPTS = 50;
|
|
344
|
+
for (let i = 0; i < MAX_SETTLE_ATTEMPTS; i++) {
|
|
345
|
+
await _agent.waitForIdle();
|
|
346
|
+
if (!_agent.state.isStreaming) break;
|
|
347
|
+
// Session started another run (built-in retry, compaction) — wait
|
|
348
|
+
// for it to finish before checking again.
|
|
349
|
+
}
|
|
350
|
+
// Yield once more: if the session's _handlePostAgentRun returns true
|
|
351
|
+
// synchronously, the while loop will call continue() on the next
|
|
352
|
+
// microtask. By yielding, we give it a chance to start that run
|
|
353
|
+
// so our next isStreaming check catches it.
|
|
354
|
+
await new Promise(r => setTimeout(r, 0));
|
|
355
|
+
if (_agent.state.isStreaming) {
|
|
356
|
+
// Session is handling the retry/compaction itself. We don't need
|
|
357
|
+
// to do anything — it will emit its own agent_end when done, which
|
|
358
|
+
// may trigger this handler again for further retries.
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
326
361
|
|
|
327
|
-
// Guard
|
|
328
|
-
// waited
|
|
329
|
-
// directly from the activeRun field, no TOCTOU beyond the next line).
|
|
362
|
+
// Guard 3: pre-flight — the user may have sent a message while we
|
|
363
|
+
// waited. agent.state.isStreaming is authoritative.
|
|
330
364
|
if (_agent.state.isStreaming) return;
|
|
331
365
|
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
366
|
+
// Remove the error assistant message from the transcript so the LLM
|
|
367
|
+
// can retry from the same context. (The session's _prepareRetry does
|
|
368
|
+
// the same thing for built-in retries.)
|
|
369
|
+
const messages = _agent.state.messages;
|
|
370
|
+
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
371
|
+
_agent.state.messages = messages.slice(0, -1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Guard 4: .catch() swallows the "already processing" error as a
|
|
375
|
+
// last resort. agent.prompt() is async, so errors become rejected
|
|
337
376
|
// Promises — a try/catch around an un-awaited call catches nothing.
|
|
338
|
-
// Must use .catch() on the returned Promise instead.
|
|
339
377
|
_agent.prompt([]).catch(() => {});
|
|
340
378
|
} finally {
|
|
341
379
|
_continueInProgress = false;
|