@monotykamary/pi-retry 0.3.5 → 0.3.7
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 +52 -54
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,45 +351,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
328
351
|
_continueInProgress = true;
|
|
329
352
|
|
|
330
353
|
try {
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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.
|
|
354
|
+
// Wait for the current run to finish (activeRun resolves in
|
|
355
|
+
// finishRun() after agent_end listeners return).
|
|
356
|
+
await _agent.waitForIdle();
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
// Await so _continueInProgress stays true for the full retry.
|
|
360
|
+
// The session's continue() is blocked (monkey-patch) and the
|
|
361
|
+
// session's _runAgentPrompt stays alive, keeping the UI
|
|
362
|
+
// "Working…" until the agent is actually done.
|
|
363
|
+
await _agent.prompt([]);
|
|
364
|
+
} catch {
|
|
365
|
+
// Ignore — if prompt throws, something else is driving.
|
|
366
|
+
// The session will handle it or report the error.
|
|
349
367
|
}
|
|
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
|
-
// Guard 4: .catch() swallows the "already processing" error as a
|
|
367
|
-
// last resort. agent.prompt() is async, so errors become rejected
|
|
368
|
-
// Promises — a try/catch around an un-awaited call catches nothing.
|
|
369
|
-
_agent.prompt([]).catch(() => {});
|
|
370
368
|
} finally {
|
|
371
369
|
_continueInProgress = false;
|
|
372
370
|
}
|