pi-goal-list-loop-audit 0.28.24 → 0.28.25
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.
|
@@ -162,6 +162,8 @@ export interface HeartbeatInput {
|
|
|
162
162
|
/** Milliseconds since the last observed agent activity. */
|
|
163
163
|
msSinceActivity: number;
|
|
164
164
|
stallMs?: number;
|
|
165
|
+
/** v0.28.25: consecutive stall refires so far — spaces refires exponentially. */
|
|
166
|
+
consecutiveStalls?: number;
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
/** Should the heartbeat re-fire the continuation right now? */
|
|
@@ -169,7 +171,15 @@ export function shouldHeartbeatRefire(input: HeartbeatInput): boolean {
|
|
|
169
171
|
if (!input.supervising) return false;
|
|
170
172
|
if (!input.sessionIdle) return false;
|
|
171
173
|
if (input.timerPending) return false;
|
|
172
|
-
|
|
174
|
+
// v0.28.25: exponential spacing between stall refires — 1m, 2m, 4m, 8m
|
|
175
|
+
// (cap 8×). Field-observed in junk-runner: the flat 60s gate burned all
|
|
176
|
+
// 5 refires in ~4 minutes into a just-compacted session, pausing a
|
|
177
|
+
// resumable goal instead of giving the provider/queue time to recover.
|
|
178
|
+
// noteActivity() runs at each refire, so msSinceActivity measures the
|
|
179
|
+
// silence SINCE the last refire — scaling the threshold scales the gap.
|
|
180
|
+
const stallMs = input.stallMs ?? HEARTBEAT_STALL_MS;
|
|
181
|
+
const scale = 2 ** Math.min(input.consecutiveStalls ?? 0, 3);
|
|
182
|
+
return input.msSinceActivity >= stallMs * scale;
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
/**
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -471,6 +471,17 @@ let loopRearmStreak = 0;
|
|
|
471
471
|
// after the compact instead of giving pi room to recover.
|
|
472
472
|
let compactionGraceUntil = 0;
|
|
473
473
|
const COMPACTION_GRACE_MS = 3 * 60_000;
|
|
474
|
+
// v0.28.25: provider-error retry cadence. Field-observed in dracon-utilities
|
|
475
|
+
// (kimi, 19-session fleet on one provider account): a "concurrent request
|
|
476
|
+
// limit" 403 storm got 5 retries BACK-TO-BACK (delay 0 after each errored
|
|
477
|
+
// turn — the session is idle at agent_end, so scheduleContinuation fired
|
|
478
|
+
// instantly) and the brake then cycled on a flat 60s cooldown for 1h 38m.
|
|
479
|
+
// The condition clears on a minutes-to-fleet scale, not milliseconds:
|
|
480
|
+
// ladder the inter-error retries (5s, 15s, 45s, 90s, 3m — the 5-retry
|
|
481
|
+
// budget now spans ~5.5m) and escalate the brake cooldown per consecutive
|
|
482
|
+
// brake (1m, 2m, 4m, 8m, 16m cap). A successful turn resets both.
|
|
483
|
+
const ERROR_RETRY_LADDER_MS = [5_000, 15_000, 45_000, 90_000, 180_000];
|
|
484
|
+
let errorBrakeStreak = 0;
|
|
474
485
|
const SEND_REARM_LEDGER_EVERY = 600; // 600 × 50ms = 30s
|
|
475
486
|
const SEND_REARM_ESCALATE_AT = 6000; // 6000 × 50ms = 5 minutes
|
|
476
487
|
|
|
@@ -592,6 +603,7 @@ function heartbeatTick(): void {
|
|
|
592
603
|
timerPending: continuationTimer !== null || loopTimer !== null,
|
|
593
604
|
msSinceActivity: Date.now() - lastActivityAt,
|
|
594
605
|
stallMs: HEARTBEAT_STALL_MS,
|
|
606
|
+
consecutiveStalls,
|
|
595
607
|
});
|
|
596
608
|
// Wedge alert (v0.23.2): session BUSY but silent for the threshold —
|
|
597
609
|
// the classic hung-command case (a test suite that never exits holds
|
|
@@ -688,7 +700,7 @@ function freshCtx(): ExtensionContext | null {
|
|
|
688
700
|
}
|
|
689
701
|
}
|
|
690
702
|
|
|
691
|
-
function scheduleContinuation(ctx: ExtensionContext, force = false): void {
|
|
703
|
+
function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: number): void {
|
|
692
704
|
if (!isActionableGoal()) return;
|
|
693
705
|
rememberCtx(ctx);
|
|
694
706
|
const goalId = state.goal!.id;
|
|
@@ -696,7 +708,7 @@ function scheduleContinuation(ctx: ExtensionContext, force = false): void {
|
|
|
696
708
|
clearContinuationTimer();
|
|
697
709
|
let delay = 0;
|
|
698
710
|
try {
|
|
699
|
-
delay = ctx.isIdle() && !ctx.hasPendingMessages() ? 0 : BACKOFF_IDLE_RETRY_MS;
|
|
711
|
+
delay = delayMs ?? (ctx.isIdle() && !ctx.hasPendingMessages() ? 0 : BACKOFF_IDLE_RETRY_MS);
|
|
700
712
|
} catch {
|
|
701
713
|
return;
|
|
702
714
|
}
|
|
@@ -4821,33 +4833,46 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4821
4833
|
if (consecutiveErrorIterations >= 5) {
|
|
4822
4834
|
// v0.28.5 (E8): carry the REAL error text — the pause used to say
|
|
4823
4835
|
// literally "5 consecutive errors: error" (stopReason, not the
|
|
4824
|
-
// provider error). And give transient flakes ONE
|
|
4825
|
-
// (
|
|
4836
|
+
// provider error). And give transient flakes ONE auto-resume per brake
|
|
4837
|
+
// (escalating cooldown, reason re-checked) — the E8 incident lost 1.5h to a
|
|
4826
4838
|
// 60-second provider hiccup waiting on a manual /goal resume.
|
|
4827
4839
|
const detail = text.trim() ? ` (last: ${text.trim().replace(/\s+/g, " ").slice(0, 160)})` : "";
|
|
4828
4840
|
const reason = `5 consecutive errors${detail}`;
|
|
4841
|
+
// v0.28.25: the cooldown escalates per CONSECUTIVE brake — a fleet-wide
|
|
4842
|
+
// 403 window is not cleared by re-braking every 60 seconds.
|
|
4843
|
+
const cooldownMs = 60_000 * 2 ** Math.min(errorBrakeStreak, 4);
|
|
4844
|
+
const cooldownMin = Math.round(cooldownMs / 60_000);
|
|
4845
|
+
errorBrakeStreak++;
|
|
4829
4846
|
updateGoal({
|
|
4830
4847
|
status: "paused",
|
|
4831
4848
|
pauseKind: "wait",
|
|
4832
|
-
pauseResumeAt: new Date(Date.now() +
|
|
4849
|
+
pauseResumeAt: new Date(Date.now() + cooldownMs).toISOString(),
|
|
4833
4850
|
pauseReason: reason,
|
|
4834
|
-
pauseSuggestedAction:
|
|
4851
|
+
pauseSuggestedAction: `Transient provider flake? The goal auto-resumes once in ${cooldownMin}m if still paused for this reason — or /goal resume now.`,
|
|
4835
4852
|
}, ctx);
|
|
4836
4853
|
ctx.ui.notify(`Goal paused: ${reason}.`, "warning");
|
|
4837
4854
|
notifyExternal(ctx, `Goal paused: ${reason}.`);
|
|
4838
4855
|
appendLedger(ctx.cwd, "goal_paused", { reason });
|
|
4839
|
-
scheduleQuotaRetry(ctx,
|
|
4856
|
+
scheduleQuotaRetry(ctx, cooldownMs / 1000, reason, () => {
|
|
4840
4857
|
// Re-check: only auto-resume if STILL paused for the error brake
|
|
4841
4858
|
// (a user /goal pause during the window is not stomped).
|
|
4842
4859
|
if (state.goal && state.goal.status === "paused" && (state.goal.pauseReason ?? "").startsWith("5 consecutive errors")) {
|
|
4843
4860
|
updateGoal({ status: "active" }, ctx);
|
|
4844
4861
|
appendLedger(ctx.cwd, "goal_resumed", { via: "error-brake-retry" });
|
|
4845
|
-
ctx.ui.notify("Auto-resumed after the 5-error brake (
|
|
4862
|
+
ctx.ui.notify("Auto-resumed after the 5-error brake (cooldown elapsed).", "info");
|
|
4846
4863
|
scheduleContinuation(ctx, true);
|
|
4847
4864
|
}
|
|
4848
4865
|
}, "5 consecutive errors — auto-retry");
|
|
4849
4866
|
return;
|
|
4850
4867
|
}
|
|
4868
|
+
// v0.28.25: under the brake, the retry rides the exponential ladder —
|
|
4869
|
+
// NOT the immediate scheduleContinuation at the bottom of this handler
|
|
4870
|
+
// (an errored turn leaves the session idle, so the default delay is 0:
|
|
4871
|
+
// exactly how 5 retries fired back-to-back in dracon-utilities).
|
|
4872
|
+
const retryDelayMs = ERROR_RETRY_LADDER_MS[Math.min(consecutiveErrorIterations - 1, ERROR_RETRY_LADDER_MS.length - 1)];
|
|
4873
|
+
appendLedger(ctx.cwd, "error_retry_backoff", { attempt: consecutiveErrorIterations, delayMs: retryDelayMs });
|
|
4874
|
+
scheduleContinuation(ctx, true, retryDelayMs);
|
|
4875
|
+
return;
|
|
4851
4876
|
} else if (stopReason === "aborted") {
|
|
4852
4877
|
// v0.28.5 (E8): user aborts are not provider errors. Separate brake,
|
|
4853
4878
|
// honest message, and NO auto-resume — aborting five turns in a row
|
|
@@ -4868,6 +4893,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4868
4893
|
} else {
|
|
4869
4894
|
consecutiveErrorIterations = 0;
|
|
4870
4895
|
consecutiveAbortIterations = 0;
|
|
4896
|
+
errorBrakeStreak = 0; // v0.28.25: a healthy turn clears the brake cooldown
|
|
4871
4897
|
}
|
|
4872
4898
|
|
|
4873
4899
|
// No wall-clock cap by design: a goal ends via completion, explicit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.25",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|