@monotykamary/pi-retry 0.6.4 → 0.6.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/README.md +1 -0
- package/package.json +1 -1
- package/retry.ts +119 -120
- package/src/error-patterns.ts +5 -2
package/README.md
CHANGED
|
@@ -138,6 +138,7 @@ const BACKOFF_MULTIPLIER = 2; // Double each time
|
|
|
138
138
|
5. **Retry or continue (both invisible)** — Wait (exponential backoff for errors), then trigger a new turn via `pi.sendMessage()` with `customType`, `display: false`, and `triggerTurn: true`
|
|
139
139
|
6. **Context cleanup** — The `context` event strips all custom-type triggers before the LLM sees them (insurance against custom `convertToLlm` overrides)
|
|
140
140
|
7. **Indefinite continuation** — Max_tokens auto-continues are uncapped; each continuation produces valid output and the model naturally terminates when done
|
|
141
|
+
8. **Lifecycle exposure** — Emits `pi-retry:started`, `pi-retry:completed`, and `pi-retry:cancelled` on Pi's shared extension event bus with a matching `retryId`, allowing status integrations to suppress intermediate completion signals
|
|
141
142
|
|
|
142
143
|
The pi's built-in `transform-messages` already strips aborted/errored assistant messages from the LLM context, so the model never sees the failed attempts.
|
|
143
144
|
|
package/package.json
CHANGED
package/retry.ts
CHANGED
|
@@ -17,8 +17,14 @@ 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
|
|
|
24
|
+
const RETRY_STARTED_EVENT = "pi-retry:started";
|
|
25
|
+
const RETRY_COMPLETED_EVENT = "pi-retry:completed";
|
|
26
|
+
const RETRY_CANCELLED_EVENT = "pi-retry:cancelled";
|
|
27
|
+
|
|
22
28
|
/**
|
|
23
29
|
* Unified retry extension — retries EVERY error by default.
|
|
24
30
|
*
|
|
@@ -33,29 +39,22 @@ import {
|
|
|
33
39
|
* - Automatic detection and retry for ALL errors (catch-all)
|
|
34
40
|
* - Indefinite retry with exponential backoff (capped at 60s)
|
|
35
41
|
* - Auto-continuation when model hits max output tokens (stopReason "length")
|
|
36
|
-
* - ALL triggers are invisible —
|
|
42
|
+
* - ALL triggers are invisible — hidden AgentSession turns are filtered before the LLM call
|
|
37
43
|
* - Unified manual controls via /retry command
|
|
38
44
|
*
|
|
39
|
-
*
|
|
40
|
-
* -
|
|
41
|
-
* -
|
|
42
|
-
* -
|
|
43
|
-
* - No convertToLlm involvement, no filter needed, no session artifact
|
|
45
|
+
* Continuation mechanism:
|
|
46
|
+
* - A hidden custom message starts or joins a canonical AgentSession turn
|
|
47
|
+
* - A context hook removes that marker before provider serialization
|
|
48
|
+
* - AgentSession remains authoritative for busy state and queued messages
|
|
44
49
|
*
|
|
45
50
|
* Retry loop design:
|
|
46
51
|
* - The agent_end handler detects retryable errors but does NOT sleep.
|
|
47
52
|
* It fires triggerInvisibleContinue() immediately, keeping processEvents
|
|
48
53
|
* unblocked so the agent can finish its run and become idle.
|
|
49
54
|
* - 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([]).
|
|
55
|
+
* removes error assistant messages from live state, queues a hidden
|
|
56
|
+
* AgentSession turn, and checks the result. On error it sleeps outside
|
|
57
|
+
* processEvents and retries. On success or user abort the loop exits.
|
|
59
58
|
*/
|
|
60
59
|
|
|
61
60
|
// Capture the live Agent instance when AgentSession subscribes to it.
|
|
@@ -70,64 +69,6 @@ Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
|
|
|
70
69
|
return _origSubscribe.apply(this, args);
|
|
71
70
|
};
|
|
72
71
|
|
|
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
72
|
// Monkey-patch AgentSession._prepareRetry to suppress the built-in retry
|
|
132
73
|
// when pi-retry's loop is driving. Without this, both the built-in retry
|
|
133
74
|
// and pi-retry race to handle the same error: the built-in retry counts
|
|
@@ -143,7 +84,10 @@ Agent.prototype.continue = function (this: Agent) {
|
|
|
143
84
|
// built-in retry works normally as a fallback.
|
|
144
85
|
const _origPrepareRetry = (AgentSession.prototype as any)._prepareRetry;
|
|
145
86
|
(AgentSession.prototype as any)._prepareRetry = function(this: any, message: any) {
|
|
146
|
-
if (
|
|
87
|
+
if (
|
|
88
|
+
_continueInProgress &&
|
|
89
|
+
_continueInputGeneration === _inputGeneration
|
|
90
|
+
) {
|
|
147
91
|
return Promise.resolve(false);
|
|
148
92
|
}
|
|
149
93
|
return _origPrepareRetry.call(this, message);
|
|
@@ -160,21 +104,18 @@ const stateContinuation = new ContinuationState();
|
|
|
160
104
|
|
|
161
105
|
// Abort flag: set when turn_end reports stopReason "aborted", cleared on
|
|
162
106
|
// session_start and on fresh user activity. Prevents triggerInvisibleContinue()
|
|
163
|
-
// from
|
|
107
|
+
// from starting a hidden retry turn after the user explicitly cancelled.
|
|
164
108
|
let _userAborted = false;
|
|
165
109
|
|
|
166
110
|
// Mutex: only one triggerInvisibleContinue may be in-flight at a time.
|
|
167
111
|
// Without this, concurrent agent_end events (or a manual /retry during an
|
|
168
|
-
// automatic retry)
|
|
169
|
-
// producing "Agent is already processing".
|
|
112
|
+
// automatic retry) could queue duplicate turns for the same failure.
|
|
170
113
|
let _continueInProgress = false;
|
|
171
114
|
// Session generation that owns the retry mutex and its Escape handler.
|
|
172
115
|
let _continueGeneration: number | null = null;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// triggerInvisibleContinue just ran and the session's continue() unblocks.
|
|
177
|
-
let _lastInvisibleContinueTime = 0;
|
|
116
|
+
let _continueInputGeneration: number | null = null;
|
|
117
|
+
let _inputGeneration = 0;
|
|
118
|
+
let _retryLifecycleId = 0;
|
|
178
119
|
|
|
179
120
|
// Session generation counter: incremented on every session_start.
|
|
180
121
|
// The retry loop captures the current generation when it starts and exits
|
|
@@ -186,14 +127,22 @@ let _terminalInputUnsubscribe: (() => void) | null = null;
|
|
|
186
127
|
// Interruptible sleep: polls _userAborted and _sessionGeneration every
|
|
187
128
|
// 100ms. Returns true if interrupted (abort or session change), false if
|
|
188
129
|
// the full delay elapsed normally.
|
|
189
|
-
function interruptibleSleep(
|
|
130
|
+
function interruptibleSleep(
|
|
131
|
+
ms: number,
|
|
132
|
+
generation: number,
|
|
133
|
+
inputGeneration: number,
|
|
134
|
+
): Promise<boolean> {
|
|
190
135
|
if (ms <= 0) return Promise.resolve(false);
|
|
191
136
|
return new Promise(resolve => {
|
|
192
137
|
const checkInterval = 100;
|
|
193
138
|
let elapsed = 0;
|
|
194
139
|
const timer = setInterval(() => {
|
|
195
140
|
elapsed += checkInterval;
|
|
196
|
-
if (
|
|
141
|
+
if (
|
|
142
|
+
_userAborted ||
|
|
143
|
+
_sessionGeneration !== generation ||
|
|
144
|
+
_inputGeneration !== inputGeneration
|
|
145
|
+
) {
|
|
197
146
|
clearInterval(timer);
|
|
198
147
|
resolve(true);
|
|
199
148
|
} else if (elapsed >= ms) {
|
|
@@ -227,6 +176,25 @@ function lastMessageIsRetryableError(): boolean {
|
|
|
227
176
|
|
|
228
177
|
export default function (pi: ExtensionAPI) {
|
|
229
178
|
|
|
179
|
+
const markRealPromptStart = () => {
|
|
180
|
+
_inputGeneration++;
|
|
181
|
+
};
|
|
182
|
+
pi.on("input", (event) => {
|
|
183
|
+
if (event.source === "interactive" || event.source === "rpc") {
|
|
184
|
+
markRealPromptStart();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
pi.on("before_agent_start", markRealPromptStart);
|
|
188
|
+
|
|
189
|
+
pi.on("context", (event) => {
|
|
190
|
+
const messages = event.messages.filter((message: any) => !(
|
|
191
|
+
message.role === "custom" &&
|
|
192
|
+
(message.customType === RETRY_TRIGGER_CUSTOM_TYPE ||
|
|
193
|
+
message.customType === CONTINUATION_CUSTOM_TYPE)
|
|
194
|
+
));
|
|
195
|
+
if (messages.length !== event.messages.length) return { messages };
|
|
196
|
+
});
|
|
197
|
+
|
|
230
198
|
// Reset retry counters on successful completion (not max_tokens, not error)
|
|
231
199
|
pi.on("turn_end", async (event, ctx) => {
|
|
232
200
|
const msg = event.message as any;
|
|
@@ -240,7 +208,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
208
|
stateOther.reset();
|
|
241
209
|
stateContinuation.endContinuation();
|
|
242
210
|
// Signal to any in-flight triggerInvisibleContinue or pending retry
|
|
243
|
-
// that the user has cancelled —
|
|
211
|
+
// that the user has cancelled — do not queue another retry turn.
|
|
244
212
|
_userAborted = true;
|
|
245
213
|
return;
|
|
246
214
|
}
|
|
@@ -276,12 +244,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
276
244
|
return;
|
|
277
245
|
}
|
|
278
246
|
|
|
279
|
-
// Guard: if the user aborted,
|
|
247
|
+
// Guard: if the user aborted, do not queue another retry turn.
|
|
280
248
|
if (_userAborted) return;
|
|
281
249
|
|
|
282
250
|
// If the retry loop is already driving, don't interfere — it will
|
|
283
251
|
// see the new error on its next loop iteration.
|
|
284
|
-
if (
|
|
252
|
+
if (
|
|
253
|
+
_continueInProgress &&
|
|
254
|
+
_continueInputGeneration === _inputGeneration
|
|
255
|
+
) return;
|
|
285
256
|
|
|
286
257
|
// Check for max_tokens stop — auto-continue (invisible to LLM)
|
|
287
258
|
if (hasMaxTokensStop(lastAssistant) && !stateContinuation.getIsContinuing()) {
|
|
@@ -297,20 +268,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
297
268
|
|
|
298
269
|
// Context overflow: defer to compaction. Do NOT retry here.
|
|
299
270
|
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// re-send the same oversized context → overflow again → infinite loop
|
|
304
|
-
// (pi-retry's error loop is uncapped). Meanwhile pi-retry's _continueInProgress
|
|
305
|
-
// mutex would block pi-core's own compaction-retry (agent.continue()), so
|
|
306
|
-
// pi-core never gets to compact either.
|
|
307
|
-
//
|
|
308
|
-
// Instead, return without firing triggerInvisibleContinue. pi-core's
|
|
309
|
-
// _checkCompaction (which runs in _handlePostAgentRun regardless of
|
|
310
|
-
// extensions) detects the same overflow, compacts (statically via pi-vcc
|
|
311
|
-
// when installed), and retries once via agent.continue() with the reduced
|
|
312
|
-
// context. _continueInProgress stays false, so pi-core's continue is
|
|
313
|
-
// unblocked.
|
|
271
|
+
// Retrying the same oversized context before compaction would produce an
|
|
272
|
+
// uncapped overflow loop. Leave _continueInProgress false so Pi can run
|
|
273
|
+
// its normal compaction and retry path with the reduced context.
|
|
314
274
|
if (isContextOverflowError(lastAssistant)) {
|
|
315
275
|
ctx.ui.notify(
|
|
316
276
|
"Context overflow — deferring to compaction (auto-retry after compact).",
|
|
@@ -403,7 +363,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
403
363
|
status += "Max Tokens Continuation:\n";
|
|
404
364
|
status += ` Continuations used: ${stateContinuation.getCount()}\n`;
|
|
405
365
|
status += ` Is continuing: ${stateContinuation.getIsContinuing()}\n`;
|
|
406
|
-
status += ` Trigger:
|
|
366
|
+
status += ` Trigger: hidden AgentSession turn (filtered before provider call)\n\n`;
|
|
407
367
|
|
|
408
368
|
// Config
|
|
409
369
|
status += "Configuration:\n";
|
|
@@ -508,7 +468,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
508
468
|
pi.on("session_start", async (_event, ctx) => {
|
|
509
469
|
// Bump the generation counter so any in-flight retry loop from a
|
|
510
470
|
// previous session exits on its next checkpoint (within 100ms during
|
|
511
|
-
// backoff sleep, or immediately after
|
|
471
|
+
// backoff sleep, or immediately after a hidden retry turn settles).
|
|
512
472
|
_sessionGeneration++;
|
|
513
473
|
|
|
514
474
|
state400.reset();
|
|
@@ -519,7 +479,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
519
479
|
// Do NOT reset _continueInProgress here — the in-flight loop's
|
|
520
480
|
// finally block releases its owner token. Resetting it here could allow
|
|
521
481
|
// a second loop to start before the old one has settled.
|
|
522
|
-
_lastInvisibleContinueTime = 0;
|
|
523
482
|
_userAborted = false;
|
|
524
483
|
|
|
525
484
|
_terminalInputUnsubscribe?.();
|
|
@@ -545,12 +504,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
545
504
|
// Retry loop driver — the core of pi-retry.
|
|
546
505
|
//
|
|
547
506
|
// Unlike the original one-shot design, this function loops. After each
|
|
548
|
-
//
|
|
507
|
+
// hidden AgentSession turn it checks the result:
|
|
549
508
|
// - Success (stopReason !== "error"): loop exits, agent is done.
|
|
550
509
|
// - Error (stopReason === "error"): sleep with backoff, then retry.
|
|
551
510
|
// - User abort (stopReason "aborted"): loop exits immediately.
|
|
552
511
|
//
|
|
553
|
-
// The backoff sleep happens AFTER
|
|
512
|
+
// The backoff sleep happens AFTER the hidden turn settles and processEvents
|
|
554
513
|
// has settled, so it does NOT block the agent. The agent is idle during
|
|
555
514
|
// the sleep and can respond to user input (e.g. Escape to abort).
|
|
556
515
|
//
|
|
@@ -560,17 +519,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
560
519
|
async function triggerInvisibleContinue() {
|
|
561
520
|
if (!_agent) return;
|
|
562
521
|
|
|
563
|
-
// Guard: if the user aborted,
|
|
522
|
+
// Guard: if the user aborted, do not queue another retry turn.
|
|
564
523
|
if (_userAborted) return;
|
|
565
524
|
|
|
566
525
|
// Guard: mutex — if a previous continue is still in-flight, skip
|
|
567
526
|
if (_continueInProgress) return;
|
|
568
527
|
_continueInProgress = true;
|
|
528
|
+
const retryLifecycleId = ++_retryLifecycleId;
|
|
529
|
+
let didRetryComplete = false;
|
|
530
|
+
pi.events.emit(RETRY_STARTED_EVENT, { retryId: retryLifecycleId });
|
|
569
531
|
|
|
570
532
|
// Capture the current session generation. If /new fires while we're
|
|
571
533
|
// looping, _sessionGeneration will increment and the loop will exit.
|
|
572
534
|
const myGeneration = _sessionGeneration;
|
|
535
|
+
const myInputGeneration = _inputGeneration;
|
|
573
536
|
_continueGeneration = myGeneration;
|
|
537
|
+
_continueInputGeneration = myInputGeneration;
|
|
574
538
|
|
|
575
539
|
try {
|
|
576
540
|
// Wait for the current run to finish (activeRun resolves in
|
|
@@ -579,16 +543,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
579
543
|
|
|
580
544
|
// Re-check after waitForIdle: the user may have aborted or the
|
|
581
545
|
// session may have changed while we were waiting.
|
|
582
|
-
if (
|
|
546
|
+
if (
|
|
547
|
+
_userAborted ||
|
|
548
|
+
_sessionGeneration !== myGeneration ||
|
|
549
|
+
_inputGeneration !== myInputGeneration
|
|
550
|
+
) return;
|
|
583
551
|
|
|
584
552
|
let attempt = 0;
|
|
585
553
|
|
|
586
554
|
// Loop until success, abort, or session change.
|
|
587
555
|
while (true) {
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
556
|
+
if (
|
|
557
|
+
_userAborted ||
|
|
558
|
+
_sessionGeneration !== myGeneration ||
|
|
559
|
+
_inputGeneration !== myInputGeneration
|
|
560
|
+
) return;
|
|
561
|
+
|
|
562
|
+
// Preserve the trigger kind before removing a trailing error from
|
|
563
|
+
// live state. The error remains in the session journal for history.
|
|
564
|
+
const isErrorRetry = lastMessageIsRetryableError();
|
|
592
565
|
removeErrorFromAgentState();
|
|
593
566
|
|
|
594
567
|
attempt++;
|
|
@@ -601,24 +574,47 @@ export default function (pi: ExtensionAPI) {
|
|
|
601
574
|
// Polls _userAborted and _sessionGeneration every 100ms so ESC
|
|
602
575
|
// and /new take effect within 100ms instead of waiting for the
|
|
603
576
|
// full backoff (up to 60s).
|
|
604
|
-
const interrupted = await interruptibleSleep(
|
|
605
|
-
|
|
577
|
+
const interrupted = await interruptibleSleep(
|
|
578
|
+
delay,
|
|
579
|
+
myGeneration,
|
|
580
|
+
myInputGeneration,
|
|
581
|
+
);
|
|
582
|
+
if (interrupted || _inputGeneration !== myInputGeneration) return;
|
|
606
583
|
|
|
607
584
|
try {
|
|
608
|
-
|
|
585
|
+
pi.sendMessage(
|
|
586
|
+
{
|
|
587
|
+
customType: isErrorRetry
|
|
588
|
+
? RETRY_TRIGGER_CUSTOM_TYPE
|
|
589
|
+
: CONTINUATION_CUSTOM_TYPE,
|
|
590
|
+
content: [],
|
|
591
|
+
display: false,
|
|
592
|
+
details: undefined,
|
|
593
|
+
},
|
|
594
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// sendMessage is fire-and-forget, but AgentSession publishes the
|
|
598
|
+
// low-level run synchronously before returning. Waiting on Agent
|
|
599
|
+
// keeps this retry loop intact without bypassing session state.
|
|
600
|
+
await Promise.resolve();
|
|
601
|
+
await _agent.waitForIdle();
|
|
609
602
|
} catch {
|
|
610
|
-
// "Agent is already processing" or other transient error —
|
|
611
|
-
// the session or another driver is handling it.
|
|
612
603
|
return;
|
|
613
604
|
}
|
|
614
605
|
|
|
615
606
|
// Re-check after prompt: the user may have hit ESC during the
|
|
616
607
|
// prompt, or /new may have fired — don't keep retrying.
|
|
617
|
-
if (
|
|
608
|
+
if (
|
|
609
|
+
_userAborted ||
|
|
610
|
+
_sessionGeneration !== myGeneration ||
|
|
611
|
+
_inputGeneration !== myInputGeneration
|
|
612
|
+
) return;
|
|
618
613
|
|
|
619
|
-
//
|
|
614
|
+
// The hidden AgentSession turn completed. Check the result.
|
|
620
615
|
if (!lastMessageIsRetryableError()) {
|
|
621
616
|
// Success or non-error terminal state — exit the loop.
|
|
617
|
+
didRetryComplete = true;
|
|
622
618
|
return;
|
|
623
619
|
}
|
|
624
620
|
|
|
@@ -627,10 +623,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
627
623
|
} finally {
|
|
628
624
|
// Release the mutex only if this loop still owns it.
|
|
629
625
|
if (_continueGeneration === myGeneration) {
|
|
626
|
+
pi.events.emit(didRetryComplete ? RETRY_COMPLETED_EVENT : RETRY_CANCELLED_EVENT, {
|
|
627
|
+
retryId: retryLifecycleId,
|
|
628
|
+
});
|
|
630
629
|
_continueInProgress = false;
|
|
631
630
|
_continueGeneration = null;
|
|
631
|
+
_continueInputGeneration = null;
|
|
632
632
|
}
|
|
633
|
-
_lastInvisibleContinueTime = Date.now();
|
|
634
633
|
}
|
|
635
634
|
}
|
|
636
635
|
|
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.
|