@monotykamary/pi-retry 0.6.3 → 0.6.5
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 +3 -3
- package/retry.ts +137 -128
- package/src/error-patterns.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-retry",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
37
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
36
|
+
"@earendil-works/pi-agent-core": "^0.80.7",
|
|
37
|
+
"@earendil-works/pi-coding-agent": "^0.80.7",
|
|
38
38
|
"@types/node": "25.9.1",
|
|
39
39
|
"@vitest/coverage-v8": "4.1.7",
|
|
40
40
|
"knip": "6.14.1",
|
package/retry.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
getErrorCategory,
|
|
18
18
|
RetryState,
|
|
19
19
|
ContinuationState,
|
|
20
|
+
RETRY_TRIGGER_CUSTOM_TYPE,
|
|
21
|
+
CONTINUATION_CUSTOM_TYPE,
|
|
20
22
|
} from "./src/index.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -33,29 +35,22 @@ import {
|
|
|
33
35
|
* - Automatic detection and retry for ALL errors (catch-all)
|
|
34
36
|
* - Indefinite retry with exponential backoff (capped at 60s)
|
|
35
37
|
* - Auto-continuation when model hits max output tokens (stopReason "length")
|
|
36
|
-
* - ALL triggers are invisible —
|
|
38
|
+
* - ALL triggers are invisible — hidden AgentSession turns are filtered before the LLM call
|
|
37
39
|
* - Unified manual controls via /retry command
|
|
38
40
|
*
|
|
39
|
-
*
|
|
40
|
-
* -
|
|
41
|
-
* -
|
|
42
|
-
* -
|
|
43
|
-
* - No convertToLlm involvement, no filter needed, no session artifact
|
|
41
|
+
* Continuation mechanism:
|
|
42
|
+
* - A hidden custom message starts or joins a canonical AgentSession turn
|
|
43
|
+
* - A context hook removes that marker before provider serialization
|
|
44
|
+
* - AgentSession remains authoritative for busy state and queued messages
|
|
44
45
|
*
|
|
45
46
|
* Retry loop design:
|
|
46
47
|
* - The agent_end handler detects retryable errors but does NOT sleep.
|
|
47
48
|
* It fires triggerInvisibleContinue() immediately, keeping processEvents
|
|
48
49
|
* unblocked so the agent can finish its run and become idle.
|
|
49
50
|
* - triggerInvisibleContinue() owns the retry loop: it waits for idle,
|
|
50
|
-
* removes error assistant messages from
|
|
51
|
-
* and checks the result. On error it sleeps
|
|
52
|
-
* and retries. On success or user abort the loop exits.
|
|
53
|
-
* - The continue() monkey-patch cooperates: while _continueInProgress is
|
|
54
|
-
* true, the session's continue() spins. After the loop finishes, it
|
|
55
|
-
* calls _origContinue which checks the now-updated agent state. For
|
|
56
|
-
* stopReason "error" it no longer falls back to prompt([]) (the loop
|
|
57
|
-
* already handled it). For toolUse/length (compaction mid-task) it
|
|
58
|
-
* still falls back to prompt([]).
|
|
51
|
+
* removes error assistant messages from live state, queues a hidden
|
|
52
|
+
* AgentSession turn, and checks the result. On error it sleeps outside
|
|
53
|
+
* processEvents and retries. On success or user abort the loop exits.
|
|
59
54
|
*/
|
|
60
55
|
|
|
61
56
|
// Capture the live Agent instance when AgentSession subscribes to it.
|
|
@@ -70,64 +65,6 @@ Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
|
|
|
70
65
|
return _origSubscribe.apply(this, args);
|
|
71
66
|
};
|
|
72
67
|
|
|
73
|
-
// Monkey-patch continue() so the session's built-in retry loop cooperates
|
|
74
|
-
// with our _continueInProgress mutex AND can convert the "Cannot continue
|
|
75
|
-
// from assistant" error into a prompt([]) call when the agent was mid-task
|
|
76
|
-
// (compaction, toolUse, length — but NOT error, which the loop handles).
|
|
77
|
-
//
|
|
78
|
-
// Note (pi 0.79+): Agent.continue() now drains queued steering/follow-up
|
|
79
|
-
// messages before throwing, so this throw path only fires when there are
|
|
80
|
-
// genuinely no queued messages — the prompt([]) fallback is still correct.
|
|
81
|
-
const _origContinue = Agent.prototype.continue as (this: Agent) => Promise<void>;
|
|
82
|
-
Agent.prototype.continue = function (this: Agent) {
|
|
83
|
-
const self = this;
|
|
84
|
-
return (async () => {
|
|
85
|
-
// Wait while pi-retry is driving the agent so we don't double-dip.
|
|
86
|
-
while (_continueInProgress) {
|
|
87
|
-
await new Promise(r => setTimeout(r, 10));
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
return await _origContinue.call(self);
|
|
91
|
-
} catch (e: any) {
|
|
92
|
-
const msg = e?.message ?? '';
|
|
93
|
-
if (msg.includes('Cannot continue from message role') ||
|
|
94
|
-
msg.includes('Cannot continue from an assistant message')) {
|
|
95
|
-
const lastMsg = self.state.messages[self.state.messages.length - 1];
|
|
96
|
-
if (lastMsg?.role === 'assistant') {
|
|
97
|
-
// stopReason "error": pi-retry's loop is the error handler.
|
|
98
|
-
// It will have already retried or the user aborted — don't
|
|
99
|
-
// start a second retry path via prompt([]).
|
|
100
|
-
if (lastMsg.stopReason === 'error') {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
// stopReason "stop" / "aborted": agent finished or user cancelled.
|
|
104
|
-
// Don't continue.
|
|
105
|
-
if (lastMsg.stopReason === 'stop' || lastMsg.stopReason === 'aborted') {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// stopReason "toolUse" or "length": agent was mid-task (e.g.
|
|
109
|
-
// compaction broke the message ordering). Fall back to prompt([]).
|
|
110
|
-
if (!_continueInProgress) {
|
|
111
|
-
_continueInProgress = true;
|
|
112
|
-
try {
|
|
113
|
-
await self.prompt([]);
|
|
114
|
-
} catch {
|
|
115
|
-
// Agent already processing or other transient error
|
|
116
|
-
} finally {
|
|
117
|
-
_continueInProgress = false;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (msg.includes('Agent is already processing')) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
throw e;
|
|
127
|
-
}
|
|
128
|
-
})();
|
|
129
|
-
};
|
|
130
|
-
|
|
131
68
|
// Monkey-patch AgentSession._prepareRetry to suppress the built-in retry
|
|
132
69
|
// when pi-retry's loop is driving. Without this, both the built-in retry
|
|
133
70
|
// and pi-retry race to handle the same error: the built-in retry counts
|
|
@@ -143,7 +80,10 @@ Agent.prototype.continue = function (this: Agent) {
|
|
|
143
80
|
// built-in retry works normally as a fallback.
|
|
144
81
|
const _origPrepareRetry = (AgentSession.prototype as any)._prepareRetry;
|
|
145
82
|
(AgentSession.prototype as any)._prepareRetry = function(this: any, message: any) {
|
|
146
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
_continueInProgress &&
|
|
85
|
+
_continueInputGeneration === _inputGeneration
|
|
86
|
+
) {
|
|
147
87
|
return Promise.resolve(false);
|
|
148
88
|
}
|
|
149
89
|
return _origPrepareRetry.call(this, message);
|
|
@@ -160,36 +100,44 @@ const stateContinuation = new ContinuationState();
|
|
|
160
100
|
|
|
161
101
|
// Abort flag: set when turn_end reports stopReason "aborted", cleared on
|
|
162
102
|
// session_start and on fresh user activity. Prevents triggerInvisibleContinue()
|
|
163
|
-
// from
|
|
103
|
+
// from starting a hidden retry turn after the user explicitly cancelled.
|
|
164
104
|
let _userAborted = false;
|
|
165
105
|
|
|
166
106
|
// Mutex: only one triggerInvisibleContinue may be in-flight at a time.
|
|
167
107
|
// Without this, concurrent agent_end events (or a manual /retry during an
|
|
168
|
-
// automatic retry)
|
|
169
|
-
// producing "Agent is already processing".
|
|
108
|
+
// automatic retry) could queue duplicate turns for the same failure.
|
|
170
109
|
let _continueInProgress = false;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let _lastInvisibleContinueTime = 0;
|
|
110
|
+
// Session generation that owns the retry mutex and its Escape handler.
|
|
111
|
+
let _continueGeneration: number | null = null;
|
|
112
|
+
let _continueInputGeneration: number | null = null;
|
|
113
|
+
let _inputGeneration = 0;
|
|
176
114
|
|
|
177
115
|
// Session generation counter: incremented on every session_start.
|
|
178
116
|
// The retry loop captures the current generation when it starts and exits
|
|
179
117
|
// when it changes — this handles /new and other session switches.
|
|
180
118
|
let _sessionGeneration = 0;
|
|
181
119
|
|
|
120
|
+
let _terminalInputUnsubscribe: (() => void) | null = null;
|
|
121
|
+
|
|
182
122
|
// Interruptible sleep: polls _userAborted and _sessionGeneration every
|
|
183
123
|
// 100ms. Returns true if interrupted (abort or session change), false if
|
|
184
124
|
// the full delay elapsed normally.
|
|
185
|
-
function interruptibleSleep(
|
|
125
|
+
function interruptibleSleep(
|
|
126
|
+
ms: number,
|
|
127
|
+
generation: number,
|
|
128
|
+
inputGeneration: number,
|
|
129
|
+
): Promise<boolean> {
|
|
186
130
|
if (ms <= 0) return Promise.resolve(false);
|
|
187
131
|
return new Promise(resolve => {
|
|
188
132
|
const checkInterval = 100;
|
|
189
133
|
let elapsed = 0;
|
|
190
134
|
const timer = setInterval(() => {
|
|
191
135
|
elapsed += checkInterval;
|
|
192
|
-
if (
|
|
136
|
+
if (
|
|
137
|
+
_userAborted ||
|
|
138
|
+
_sessionGeneration !== generation ||
|
|
139
|
+
_inputGeneration !== inputGeneration
|
|
140
|
+
) {
|
|
193
141
|
clearInterval(timer);
|
|
194
142
|
resolve(true);
|
|
195
143
|
} else if (elapsed >= ms) {
|
|
@@ -223,6 +171,25 @@ function lastMessageIsRetryableError(): boolean {
|
|
|
223
171
|
|
|
224
172
|
export default function (pi: ExtensionAPI) {
|
|
225
173
|
|
|
174
|
+
const markRealPromptStart = () => {
|
|
175
|
+
_inputGeneration++;
|
|
176
|
+
};
|
|
177
|
+
pi.on("input", (event) => {
|
|
178
|
+
if (event.source === "interactive" || event.source === "rpc") {
|
|
179
|
+
markRealPromptStart();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
pi.on("before_agent_start", markRealPromptStart);
|
|
183
|
+
|
|
184
|
+
pi.on("context", (event) => {
|
|
185
|
+
const messages = event.messages.filter((message: any) => !(
|
|
186
|
+
message.role === "custom" &&
|
|
187
|
+
(message.customType === RETRY_TRIGGER_CUSTOM_TYPE ||
|
|
188
|
+
message.customType === CONTINUATION_CUSTOM_TYPE)
|
|
189
|
+
));
|
|
190
|
+
if (messages.length !== event.messages.length) return { messages };
|
|
191
|
+
});
|
|
192
|
+
|
|
226
193
|
// Reset retry counters on successful completion (not max_tokens, not error)
|
|
227
194
|
pi.on("turn_end", async (event, ctx) => {
|
|
228
195
|
const msg = event.message as any;
|
|
@@ -236,7 +203,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
203
|
stateOther.reset();
|
|
237
204
|
stateContinuation.endContinuation();
|
|
238
205
|
// Signal to any in-flight triggerInvisibleContinue or pending retry
|
|
239
|
-
// that the user has cancelled —
|
|
206
|
+
// that the user has cancelled — do not queue another retry turn.
|
|
240
207
|
_userAborted = true;
|
|
241
208
|
return;
|
|
242
209
|
}
|
|
@@ -272,12 +239,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
272
239
|
return;
|
|
273
240
|
}
|
|
274
241
|
|
|
275
|
-
// Guard: if the user aborted,
|
|
242
|
+
// Guard: if the user aborted, do not queue another retry turn.
|
|
276
243
|
if (_userAborted) return;
|
|
277
244
|
|
|
278
245
|
// If the retry loop is already driving, don't interfere — it will
|
|
279
246
|
// see the new error on its next loop iteration.
|
|
280
|
-
if (
|
|
247
|
+
if (
|
|
248
|
+
_continueInProgress &&
|
|
249
|
+
_continueInputGeneration === _inputGeneration
|
|
250
|
+
) return;
|
|
281
251
|
|
|
282
252
|
// Check for max_tokens stop — auto-continue (invisible to LLM)
|
|
283
253
|
if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
|
|
@@ -293,20 +263,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
263
|
|
|
294
264
|
// Context overflow: defer to compaction. Do NOT retry here.
|
|
295
265
|
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
// re-send the same oversized context → overflow again → infinite loop
|
|
300
|
-
// (pi-retry's error loop is uncapped). Meanwhile pi-retry's _continueInProgress
|
|
301
|
-
// mutex would block pi-core's own compaction-retry (agent.continue()), so
|
|
302
|
-
// pi-core never gets to compact either.
|
|
303
|
-
//
|
|
304
|
-
// Instead, return without firing triggerInvisibleContinue. pi-core's
|
|
305
|
-
// _checkCompaction (which runs in _handlePostAgentRun regardless of
|
|
306
|
-
// extensions) detects the same overflow, compacts (statically via pi-vcc
|
|
307
|
-
// when installed), and retries once via agent.continue() with the reduced
|
|
308
|
-
// context. _continueInProgress stays false, so pi-core's continue is
|
|
309
|
-
// unblocked.
|
|
266
|
+
// Retrying the same oversized context before compaction would produce an
|
|
267
|
+
// uncapped overflow loop. Leave _continueInProgress false so Pi can run
|
|
268
|
+
// its normal compaction and retry path with the reduced context.
|
|
310
269
|
if (isContextOverflowError(lastAssistant)) {
|
|
311
270
|
ctx.ui.notify(
|
|
312
271
|
"Context overflow — deferring to compaction (auto-retry after compact).",
|
|
@@ -399,7 +358,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
399
358
|
status += "Max Tokens Continuation:\n";
|
|
400
359
|
status += ` Continuations used: ${stateContinuation.getCount()}\n`;
|
|
401
360
|
status += ` Is continuing: ${stateContinuation.getIsContinuing()}\n`;
|
|
402
|
-
status += ` Trigger:
|
|
361
|
+
status += ` Trigger: hidden AgentSession turn (filtered before provider call)\n\n`;
|
|
403
362
|
|
|
404
363
|
// Config
|
|
405
364
|
status += "Configuration:\n";
|
|
@@ -501,10 +460,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
501
460
|
});
|
|
502
461
|
|
|
503
462
|
// Initialize
|
|
504
|
-
pi.on("session_start", async () => {
|
|
463
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
505
464
|
// Bump the generation counter so any in-flight retry loop from a
|
|
506
465
|
// previous session exits on its next checkpoint (within 100ms during
|
|
507
|
-
// backoff sleep, or immediately after
|
|
466
|
+
// backoff sleep, or immediately after a hidden retry turn settles).
|
|
508
467
|
_sessionGeneration++;
|
|
509
468
|
|
|
510
469
|
state400.reset();
|
|
@@ -513,22 +472,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
513
472
|
stateOther.reset();
|
|
514
473
|
stateContinuation.reset();
|
|
515
474
|
// Do NOT reset _continueInProgress here — the in-flight loop's
|
|
516
|
-
// finally block
|
|
517
|
-
//
|
|
518
|
-
// break the mutex invariant and could allow a second loop to start.
|
|
519
|
-
_lastInvisibleContinueTime = 0;
|
|
475
|
+
// finally block releases its owner token. Resetting it here could allow
|
|
476
|
+
// a second loop to start before the old one has settled.
|
|
520
477
|
_userAborted = false;
|
|
478
|
+
|
|
479
|
+
_terminalInputUnsubscribe?.();
|
|
480
|
+
_terminalInputUnsubscribe = null;
|
|
481
|
+
|
|
482
|
+
if (ctx.mode === "tui") {
|
|
483
|
+
_terminalInputUnsubscribe = ctx.ui.onTerminalInput(data => {
|
|
484
|
+
if (
|
|
485
|
+
data !== "\x1b" ||
|
|
486
|
+
!_continueInProgress ||
|
|
487
|
+
_continueGeneration !== _sessionGeneration
|
|
488
|
+
) {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_userAborted = true;
|
|
493
|
+
ctx.abort();
|
|
494
|
+
return { consume: true };
|
|
495
|
+
});
|
|
496
|
+
}
|
|
521
497
|
});
|
|
522
498
|
|
|
523
499
|
// Retry loop driver — the core of pi-retry.
|
|
524
500
|
//
|
|
525
501
|
// Unlike the original one-shot design, this function loops. After each
|
|
526
|
-
//
|
|
502
|
+
// hidden AgentSession turn it checks the result:
|
|
527
503
|
// - Success (stopReason !== "error"): loop exits, agent is done.
|
|
528
504
|
// - Error (stopReason === "error"): sleep with backoff, then retry.
|
|
529
505
|
// - User abort (stopReason "aborted"): loop exits immediately.
|
|
530
506
|
//
|
|
531
|
-
// The backoff sleep happens AFTER
|
|
507
|
+
// The backoff sleep happens AFTER the hidden turn settles and processEvents
|
|
532
508
|
// has settled, so it does NOT block the agent. The agent is idle during
|
|
533
509
|
// the sleep and can respond to user input (e.g. Escape to abort).
|
|
534
510
|
//
|
|
@@ -538,7 +514,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
538
514
|
async function triggerInvisibleContinue() {
|
|
539
515
|
if (!_agent) return;
|
|
540
516
|
|
|
541
|
-
// Guard: if the user aborted,
|
|
517
|
+
// Guard: if the user aborted, do not queue another retry turn.
|
|
542
518
|
if (_userAborted) return;
|
|
543
519
|
|
|
544
520
|
// Guard: mutex — if a previous continue is still in-flight, skip
|
|
@@ -548,6 +524,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
548
524
|
// Capture the current session generation. If /new fires while we're
|
|
549
525
|
// looping, _sessionGeneration will increment and the loop will exit.
|
|
550
526
|
const myGeneration = _sessionGeneration;
|
|
527
|
+
const myInputGeneration = _inputGeneration;
|
|
528
|
+
_continueGeneration = myGeneration;
|
|
529
|
+
_continueInputGeneration = myInputGeneration;
|
|
551
530
|
|
|
552
531
|
try {
|
|
553
532
|
// Wait for the current run to finish (activeRun resolves in
|
|
@@ -556,16 +535,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
556
535
|
|
|
557
536
|
// Re-check after waitForIdle: the user may have aborted or the
|
|
558
537
|
// session may have changed while we were waiting.
|
|
559
|
-
if (
|
|
538
|
+
if (
|
|
539
|
+
_userAborted ||
|
|
540
|
+
_sessionGeneration !== myGeneration ||
|
|
541
|
+
_inputGeneration !== myInputGeneration
|
|
542
|
+
) return;
|
|
560
543
|
|
|
561
544
|
let attempt = 0;
|
|
562
545
|
|
|
563
546
|
// Loop until success, abort, or session change.
|
|
564
547
|
while (true) {
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
548
|
+
if (
|
|
549
|
+
_userAborted ||
|
|
550
|
+
_sessionGeneration !== myGeneration ||
|
|
551
|
+
_inputGeneration !== myInputGeneration
|
|
552
|
+
) return;
|
|
553
|
+
|
|
554
|
+
// Preserve the trigger kind before removing a trailing error from
|
|
555
|
+
// live state. The error remains in the session journal for history.
|
|
556
|
+
const isErrorRetry = lastMessageIsRetryableError();
|
|
569
557
|
removeErrorFromAgentState();
|
|
570
558
|
|
|
571
559
|
attempt++;
|
|
@@ -578,22 +566,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
578
566
|
// Polls _userAborted and _sessionGeneration every 100ms so ESC
|
|
579
567
|
// and /new take effect within 100ms instead of waiting for the
|
|
580
568
|
// full backoff (up to 60s).
|
|
581
|
-
const interrupted = await interruptibleSleep(
|
|
582
|
-
|
|
569
|
+
const interrupted = await interruptibleSleep(
|
|
570
|
+
delay,
|
|
571
|
+
myGeneration,
|
|
572
|
+
myInputGeneration,
|
|
573
|
+
);
|
|
574
|
+
if (interrupted || _inputGeneration !== myInputGeneration) return;
|
|
583
575
|
|
|
584
576
|
try {
|
|
585
|
-
|
|
577
|
+
pi.sendMessage(
|
|
578
|
+
{
|
|
579
|
+
customType: isErrorRetry
|
|
580
|
+
? RETRY_TRIGGER_CUSTOM_TYPE
|
|
581
|
+
: CONTINUATION_CUSTOM_TYPE,
|
|
582
|
+
content: [],
|
|
583
|
+
display: false,
|
|
584
|
+
details: undefined,
|
|
585
|
+
},
|
|
586
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// sendMessage is fire-and-forget, but AgentSession publishes the
|
|
590
|
+
// low-level run synchronously before returning. Waiting on Agent
|
|
591
|
+
// keeps this retry loop intact without bypassing session state.
|
|
592
|
+
await Promise.resolve();
|
|
593
|
+
await _agent.waitForIdle();
|
|
586
594
|
} catch {
|
|
587
|
-
// "Agent is already processing" or other transient error —
|
|
588
|
-
// the session or another driver is handling it.
|
|
589
595
|
return;
|
|
590
596
|
}
|
|
591
597
|
|
|
592
598
|
// Re-check after prompt: the user may have hit ESC during the
|
|
593
599
|
// prompt, or /new may have fired — don't keep retrying.
|
|
594
|
-
if (
|
|
600
|
+
if (
|
|
601
|
+
_userAborted ||
|
|
602
|
+
_sessionGeneration !== myGeneration ||
|
|
603
|
+
_inputGeneration !== myInputGeneration
|
|
604
|
+
) return;
|
|
595
605
|
|
|
596
|
-
//
|
|
606
|
+
// The hidden AgentSession turn completed. Check the result.
|
|
597
607
|
if (!lastMessageIsRetryableError()) {
|
|
598
608
|
// Success or non-error terminal state — exit the loop.
|
|
599
609
|
return;
|
|
@@ -602,13 +612,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
602
612
|
// Error again — loop back for another attempt.
|
|
603
613
|
}
|
|
604
614
|
} finally {
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
// mutex — resetting it here would clobber that.
|
|
608
|
-
if (_sessionGeneration === myGeneration) {
|
|
615
|
+
// Release the mutex only if this loop still owns it.
|
|
616
|
+
if (_continueGeneration === myGeneration) {
|
|
609
617
|
_continueInProgress = false;
|
|
618
|
+
_continueGeneration = null;
|
|
619
|
+
_continueInputGeneration = null;
|
|
610
620
|
}
|
|
611
|
-
_lastInvisibleContinueTime = Date.now();
|
|
612
621
|
}
|
|
613
622
|
}
|
|
614
623
|
|
package/src/error-patterns.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
11
11
|
|
|
12
|
+
export const RETRY_TRIGGER_CUSTOM_TYPE = "pi-retry:retry";
|
|
13
|
+
export const CONTINUATION_CUSTOM_TYPE = "pi-retry:continue";
|
|
14
|
+
|
|
12
15
|
// ── Specific pattern groups (used for categorisation / messaging) ──
|
|
13
16
|
|
|
14
17
|
const ERROR_400_413_PATTERNS = [
|
|
@@ -69,8 +72,8 @@ const BUILTIN_HANDLED_PATTERNS = [
|
|
|
69
72
|
// compaction exactly when pi-core's _checkCompaction will detect overflow and
|
|
70
73
|
// compact + retry. kept in sync manually — pi-ai is not a direct dependency.
|
|
71
74
|
//
|
|
72
|
-
// Why these are NOT retried by pi-retry:
|
|
73
|
-
//
|
|
75
|
+
// Why these are NOT retried by pi-retry: a hidden retry turn would re-send
|
|
76
|
+
// the same oversized context, so it overflows again → infinite loop
|
|
74
77
|
// (pi-retry's loop is uncapped for errors). pi-core instead compacts and
|
|
75
78
|
// retries once via agent.continue(); with static compaction (pi-vcc) that
|
|
76
79
|
// reliably reduces context, so the single retry succeeds.
|