@monotykamary/pi-retry 0.3.2 → 0.3.4

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 +68 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-retry",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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
@@ -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
- // 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.
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
- // GUARDS (three layers):
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. isStreaming pre-flightdetects user-initiated runs before prompt()
316
- // 3. .catch() on prompt() final safety net, swallows rejected promises
320
+ // 2. waitForIdle + settle loop waits for session to fully finish
321
+ // 3. isStreaming pre-flightdetects 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,21 +328,65 @@ export default function (pi: ExtensionAPI) {
322
328
  _continueInProgress = true;
323
329
 
324
330
  try {
325
- await _agent.waitForIdle();
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 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).
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
- // Guard 3: .catch() swallows the "already processing" error as a
333
- // last resort. This handles the remaining microtask TOCTOU gap
334
- // between the isStreaming check and prompt() acquiring the lock.
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.)
335
369
  //
336
- // IMPORTANT: agent.prompt() is async, so errors become rejected
370
+ // IMPORTANT: We must strip BEFORE calling prompt([]) if prompt
371
+ // starts successfully, the LLM sees the context without the error
372
+ // and generates a fresh response. If prompt fails (swallowed by
373
+ // .catch()), we restore the message so the agent state stays
374
+ // consistent.
375
+ const messages = _agent.state.messages;
376
+ const hadErrorAssistant = messages.length > 0 && messages[messages.length - 1].role === "assistant";
377
+ if (hadErrorAssistant) {
378
+ _agent.state.messages = messages.slice(0, -1);
379
+ }
380
+
381
+ // Guard 4: .catch() swallows the "already processing" error as a
382
+ // last resort. agent.prompt() is async, so errors become rejected
337
383
  // Promises — a try/catch around an un-awaited call catches nothing.
338
- // Must use .catch() on the returned Promise instead.
339
- _agent.prompt([]).catch(() => {});
384
+ // If prompt failed, restore the stripped error message.
385
+ _agent.prompt([]).catch(() => {
386
+ if (hadErrorAssistant) {
387
+ _agent.state.messages = messages;
388
+ }
389
+ });
340
390
  } finally {
341
391
  _continueInProgress = false;
342
392
  }