@monotykamary/pi-retry 0.3.4 → 0.3.6
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 +47 -75
package/package.json
CHANGED
package/retry.ts
CHANGED
|
@@ -43,14 +43,48 @@ import {
|
|
|
43
43
|
// Capture the live Agent instance when AgentSession subscribes to it.
|
|
44
44
|
// subscribe() is called during AgentSession construction — fires on both
|
|
45
45
|
// fresh sessions and session resumes.
|
|
46
|
+
//
|
|
47
|
+
// We also monkey-patch continue() so the session's loop can never race
|
|
48
|
+
// our retry. Without this, observing isStreaming is a heuristic that
|
|
49
|
+
// misses the narrow window between our check and the session's call.
|
|
46
50
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
51
|
let _agent: Agent | null = null;
|
|
52
|
+
|
|
48
53
|
const _origSubscribe = Agent.prototype.subscribe as (...args: any[]) => any;
|
|
49
54
|
Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
|
|
50
55
|
_agent = this;
|
|
51
56
|
return _origSubscribe.apply(this, args);
|
|
52
57
|
};
|
|
53
58
|
|
|
59
|
+
// Monkey-patch continue() so the session's built-in retry loop cooperates
|
|
60
|
+
// with our _continueInProgress mutex instead of throwing "Agent is already
|
|
61
|
+
// processing" when we beat it to the punch.
|
|
62
|
+
const _origContinue = Agent.prototype.continue as (this: Agent) => Promise<unknown>;
|
|
63
|
+
Agent.prototype.continue = function (this: Agent) {
|
|
64
|
+
const self = this;
|
|
65
|
+
return (async () => {
|
|
66
|
+
// Wait while pi-retry is driving the agent so we don't double-dip.
|
|
67
|
+
while (_continueInProgress) {
|
|
68
|
+
await new Promise(r => setTimeout(r, 10));
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return await _origContinue.call(self);
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
// After pi-retry finishes, the transcript ends with a fresh
|
|
74
|
+
// assistant message. The session's continue() sees this and
|
|
75
|
+
// would throw "Cannot continue from an assistant message".
|
|
76
|
+
// Catch it — the while-loop will poll _handlePostAgentRun()
|
|
77
|
+
// again, find no error, and exit cleanly.
|
|
78
|
+
const msg = e?.message ?? '';
|
|
79
|
+
if (msg.includes('Cannot continue from an assistant message') ||
|
|
80
|
+
msg.includes('Agent is already processing')) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
};
|
|
87
|
+
|
|
54
88
|
// Per-category retry state (for diagnostics / messaging)
|
|
55
89
|
const state400 = new RetryState();
|
|
56
90
|
const stateCredit = new RetryState();
|
|
@@ -304,22 +338,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
304
338
|
// Resume the agent loop invisibly — no message injected into context.
|
|
305
339
|
// The LLM sees the exact same message list it had before.
|
|
306
340
|
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// the session's
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// them gets "Agent is already processing".
|
|
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):
|
|
319
|
-
// 1. _continueInProgress mutex — prevents concurrent calls from racing
|
|
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
|
|
341
|
+
// The continue() monkey-patch at the top of this file ensures the
|
|
342
|
+
// session's built-in retry loop can never race us. While
|
|
343
|
+
// _continueInProgress is true, the session's continue() waits.
|
|
344
|
+
// When we finish, it wakes, finds the transcript already updated,
|
|
345
|
+
// gracefully no-ops, and the session loop exits.
|
|
323
346
|
async function triggerInvisibleContinue() {
|
|
324
347
|
if (!_agent) return;
|
|
325
348
|
|
|
@@ -328,65 +351,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
328
351
|
_continueInProgress = true;
|
|
329
352
|
|
|
330
353
|
try {
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
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
|
-
}
|
|
361
|
-
|
|
362
|
-
// Guard 3: pre-flight — the user may have sent a message while we
|
|
363
|
-
// waited. agent.state.isStreaming is authoritative.
|
|
364
|
-
if (_agent.state.isStreaming) return;
|
|
365
|
-
|
|
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
|
-
//
|
|
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
|
|
383
|
-
// Promises — a try/catch around an un-awaited call catches nothing.
|
|
384
|
-
// If prompt failed, restore the stripped error message.
|
|
385
|
-
_agent.prompt([]).catch(() => {
|
|
386
|
-
if (hadErrorAssistant) {
|
|
387
|
-
_agent.state.messages = messages;
|
|
388
|
-
}
|
|
389
|
-
});
|
|
354
|
+
// Wait for the current run to finish (activeRun resolves in
|
|
355
|
+
// finishRun() after agent_end listeners return).
|
|
356
|
+
await _agent.waitForIdle();
|
|
357
|
+
|
|
358
|
+
// Fire-and-forget with .catch() as final safety net.
|
|
359
|
+
// The continue() monkey-patch means the session can't collide
|
|
360
|
+
// with us even if it tries — but .catch() is belt-and-suspenders.
|
|
361
|
+
_agent.prompt([]).catch(() => {});
|
|
390
362
|
} finally {
|
|
391
363
|
_continueInProgress = false;
|
|
392
364
|
}
|