pi-goal-list-loop-audit 0.28.4 → 0.28.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.
|
@@ -162,6 +162,10 @@ export interface Goal {
|
|
|
162
162
|
* on that auto-resume. */
|
|
163
163
|
interruptedAt?: string;
|
|
164
164
|
interruptedReason?: string;
|
|
165
|
+
/** v0.28.5 (E2): trailing auditor INFRA-structure errors (not verdicts).
|
|
166
|
+
* At 3 the goal pauses loudly — a broken auditor model must not spin a
|
|
167
|
+
* silent retry-forever loop. Cleared on any real auditor run. */
|
|
168
|
+
auditInfraStreak?: number;
|
|
165
169
|
/** v0.25.0 (contract item 22): auditor objections extracted as TODOs when
|
|
166
170
|
* aggressiveMode keeps the goal active past the disapproval cap. Rendered
|
|
167
171
|
* into every continuation prompt until the next audit clears them. */
|
|
@@ -494,10 +498,60 @@ export function ensureDirs(cwd: string): void {
|
|
|
494
498
|
fs.mkdirSync(archiveDir(cwd), { recursive: true });
|
|
495
499
|
}
|
|
496
500
|
|
|
501
|
+
// =================================================================
|
|
502
|
+
// Persistence degradation (v0.28.6, audit E1)
|
|
503
|
+
// =================================================================
|
|
504
|
+
// A disk failure (ENOSPC, EACCES, a wedged mount) used to THROW out of
|
|
505
|
+
// appendLedger/writeGoalMd mid-handler — killing the orchestrator turn and
|
|
506
|
+
// silently diverging RAM from disk. Now every persistence step runs
|
|
507
|
+
// through runPersistStep: failures are caught, the session-wide degraded
|
|
508
|
+
// flag latches (the TUI shows it; the first failure notifies loudly), RAM
|
|
509
|
+
// state stays authoritative, and the next SUCCESSFUL step auto-clears the
|
|
510
|
+
// flag (self-healing — the "dirty" marker that write-then-mutate ordering
|
|
511
|
+
// cannot otherwise provide).
|
|
512
|
+
|
|
513
|
+
export interface PersistenceFailure {
|
|
514
|
+
what: string;
|
|
515
|
+
error: string;
|
|
516
|
+
at: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let persistenceDegraded = false;
|
|
520
|
+
let lastFailure: PersistenceFailure | null = null;
|
|
521
|
+
|
|
522
|
+
export function isPersistenceDegraded(): boolean {
|
|
523
|
+
return persistenceDegraded;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function lastPersistenceFailure(): PersistenceFailure | null {
|
|
527
|
+
return lastFailure;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Run one persistence step. On failure: latch the degraded flag, remember
|
|
531
|
+
* the error, return undefined (NEVER throw into an orchestrator handler).
|
|
532
|
+
* On success: clear the flag — a landing write means the disk is back. */
|
|
533
|
+
export function runPersistStep<T>(what: string, fn: () => T): T | undefined {
|
|
534
|
+
try {
|
|
535
|
+
const out = fn();
|
|
536
|
+
if (persistenceDegraded) {
|
|
537
|
+
persistenceDegraded = false;
|
|
538
|
+
lastFailure = null;
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
} catch (err) {
|
|
542
|
+
persistenceDegraded = true;
|
|
543
|
+
lastFailure = { what, error: err instanceof Error ? err.message : String(err), at: new Date().toISOString() };
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
497
548
|
export function readState(cwd: string): State {
|
|
498
549
|
const file = ledgerPath(cwd);
|
|
499
|
-
|
|
500
|
-
|
|
550
|
+
// v0.28.6 (E1): an unreadable ledger (EACCES, EIO) degrades loudly
|
|
551
|
+
// instead of throwing out of session_start.
|
|
552
|
+
const raw = runPersistStep("readState", () => (fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : ""));
|
|
553
|
+
if (raw === undefined || raw === "") return { ...DEFAULT_STATE };
|
|
554
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
501
555
|
if (lines.length === 0) return { ...DEFAULT_STATE };
|
|
502
556
|
let parsed: Partial<State> = {};
|
|
503
557
|
for (const line of lines) {
|
|
@@ -505,7 +559,8 @@ export function readState(cwd: string): State {
|
|
|
505
559
|
const evt = JSON.parse(line);
|
|
506
560
|
if (evt.type === "state") parsed = { ...parsed, ...evt.value };
|
|
507
561
|
} catch {
|
|
508
|
-
// skip malformed lines
|
|
562
|
+
// skip malformed lines — a truncated trailing line (mid-write kill)
|
|
563
|
+
// must not lose the rest of the state
|
|
509
564
|
}
|
|
510
565
|
}
|
|
511
566
|
return {
|
|
@@ -516,16 +571,23 @@ export function readState(cwd: string): State {
|
|
|
516
571
|
}
|
|
517
572
|
|
|
518
573
|
export function appendLedger(cwd: string, type: string, value: unknown): void {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
574
|
+
// v0.28.6 (E1): guarded — a disk failure degrades loudly, never throws
|
|
575
|
+
// into an orchestrator handler.
|
|
576
|
+
runPersistStep("appendLedger", () => {
|
|
577
|
+
ensureDirs(cwd);
|
|
578
|
+
const line = JSON.stringify({ type, value, at: new Date().toISOString() });
|
|
579
|
+
fs.appendFileSync(ledgerPath(cwd), line + "\n");
|
|
580
|
+
});
|
|
522
581
|
}
|
|
523
582
|
|
|
524
583
|
export function writeGoalMd(cwd: string, goal: Goal): string {
|
|
525
|
-
ensureDirs(cwd);
|
|
526
584
|
const file = goalMdPath(cwd, goal.id);
|
|
527
|
-
|
|
528
|
-
|
|
585
|
+
runPersistStep("writeGoalMd", () => {
|
|
586
|
+
ensureDirs(cwd);
|
|
587
|
+
fs.writeFileSync(file, renderGoalMarkdown(goal));
|
|
588
|
+
});
|
|
589
|
+
// Return the intended path even on failure so activePath stays sane —
|
|
590
|
+
// the degraded flag carries the truth that the write did not land.
|
|
529
591
|
return file;
|
|
530
592
|
}
|
|
531
593
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Goal, State } from "./goal-loop-core.js";
|
|
14
|
+
import { isPersistenceDegraded, lastPersistenceFailure } from "./goal-loop-core.js";
|
|
14
15
|
import type { LoopState } from "./goal-loop-forever.js";
|
|
15
16
|
|
|
16
17
|
// ---- formatters ----
|
|
@@ -185,6 +186,17 @@ function countTotal(g: Goal): number {
|
|
|
185
186
|
* Returns undefined when nothing is worth showing.
|
|
186
187
|
*/
|
|
187
188
|
export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
|
|
189
|
+
const inner = buildWidgetLinesInner(state, audit, now, theme, width, extras);
|
|
190
|
+
// v0.28.6 (E1): a persistence failure outranks everything — first line,
|
|
191
|
+
// on every render, until a write lands again.
|
|
192
|
+
if (inner && isPersistenceDegraded()) {
|
|
193
|
+
const err = lastPersistenceFailure();
|
|
194
|
+
return [paint(theme, "error", `⚠ persistence degraded — .pi-glla writes failing (${truncate(err?.error ?? "disk error", 40)}); state in RAM`), ...inner];
|
|
195
|
+
}
|
|
196
|
+
return inner;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
|
|
188
200
|
if (state.loop?.active) return loopLines(state.loop, now, theme, width, extras);
|
|
189
201
|
const g = state.goal;
|
|
190
202
|
if (!g) return undefined;
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -85,6 +85,9 @@ import {
|
|
|
85
85
|
statusLabel,
|
|
86
86
|
writeGoalMd,
|
|
87
87
|
missingGllaTools,
|
|
88
|
+
runPersistStep,
|
|
89
|
+
isPersistenceDegraded,
|
|
90
|
+
lastPersistenceFailure,
|
|
88
91
|
} from "../goal-loop-core.js";
|
|
89
92
|
import {
|
|
90
93
|
LENGTH_CONTINUE_MAX,
|
|
@@ -360,6 +363,55 @@ function startUITicker(): void {
|
|
|
360
363
|
|
|
361
364
|
/** v0.26.5: shared loud-stop for both stall paths (refire streak and
|
|
362
365
|
* pending-latch streak). Returns true when it escalated. */
|
|
366
|
+
// v0.28.5 (E3): send-retry re-arm accounting. The 50ms BACKOFF_IDLE_RETRY
|
|
367
|
+
// re-arm loop used to spin for HOURS with zero ledger events while the idle
|
|
368
|
+
// watchdogs stayed suppressed. Now: counted, ledgered (start + every 30s),
|
|
369
|
+
// and escalated loudly past 5 minutes.
|
|
370
|
+
let continuationRearmStreak = 0;
|
|
371
|
+
let loopRearmStreak = 0;
|
|
372
|
+
const SEND_REARM_LEDGER_EVERY = 600; // 600 × 50ms = 30s
|
|
373
|
+
const SEND_REARM_ESCALATE_AT = 6000; // 6000 × 50ms = 5 minutes
|
|
374
|
+
|
|
375
|
+
function accountSendRearm(ctx: ExtensionContext, kind: "continuation" | "loop"): void {
|
|
376
|
+
const streak = kind === "continuation" ? ++continuationRearmStreak : ++loopRearmStreak;
|
|
377
|
+
if (streak === 1) {
|
|
378
|
+
appendLedger(ctx.cwd, "send_rearm_start", { kind });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (streak % SEND_REARM_LEDGER_EVERY === 0) {
|
|
382
|
+
appendLedger(ctx.cwd, "send_rearm_storm", { kind, streak, minutes: Math.round((streak * BACKOFF_IDLE_RETRY_MS) / 60000) });
|
|
383
|
+
}
|
|
384
|
+
if (streak >= SEND_REARM_ESCALATE_AT) {
|
|
385
|
+
if (kind === "continuation") continuationRearmStreak = 0; else loopRearmStreak = 0;
|
|
386
|
+
escalateSendRearmStorm(ctx, kind);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "loop"): void {
|
|
391
|
+
// Same loud-terminal shape as escalateStallNow (v0.24.7): a 5-minute
|
|
392
|
+
// re-arm storm means the session never goes idle for us — wedged queue
|
|
393
|
+
// or a busy-forever session. Bounded and surfaced, not silent.
|
|
394
|
+
const mins = Math.round((SEND_REARM_ESCALATE_AT * BACKOFF_IDLE_RETRY_MS) / 60000);
|
|
395
|
+
appendLedger(ctx.cwd, "send_rearm_escalated", { kind, streak: SEND_REARM_ESCALATE_AT });
|
|
396
|
+
if (kind === "loop" && isLoopActive()) {
|
|
397
|
+
clearLoopTimer();
|
|
398
|
+
state.loop = { ...state.loop!, active: false, stopReason: `send-retry storm: ${mins}m of 50ms re-arms — the session never went idle for the loop turn. Restart pi, then /loop start again.` };
|
|
399
|
+
persistState(ctx);
|
|
400
|
+
ctx.ui.notify(`Loop stopped: send-retry storm (${mins}m). Restart pi and /loop start.`, "warning");
|
|
401
|
+
notifyExternal(ctx, "Loop stopped: send-retry storm.");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (state.goal && state.goal.status === "active") {
|
|
405
|
+
updateGoal({
|
|
406
|
+
status: "paused",
|
|
407
|
+
pauseReason: `send-retry storm: ${mins}m of 50ms re-arms — the session never went idle for the continuation`,
|
|
408
|
+
pauseSuggestedAction: "The session never went idle for the send (wedged queue or permanently busy). Restart pi, then /goal resume.",
|
|
409
|
+
}, ctx);
|
|
410
|
+
ctx.ui.notify(`Goal paused: send-retry storm (${mins}m). Restart pi, then /goal resume.`, "warning");
|
|
411
|
+
notifyExternal(ctx, "Goal paused: send-retry storm.");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
363
415
|
function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
|
|
364
416
|
if (!shouldEscalateStall(consecutiveStalls, threshold)) return false;
|
|
365
417
|
consecutiveStalls = 0;
|
|
@@ -494,6 +546,9 @@ let continuationScheduledFor: string | null = null;
|
|
|
494
546
|
let iterationCounter = 0;
|
|
495
547
|
let toolCallsThisTurn = 0;
|
|
496
548
|
let consecutiveErrorIterations = 0;
|
|
549
|
+
// v0.28.5 (E8): user aborts are NOT provider errors — separate counter,
|
|
550
|
+
// separate brake message, and no auto-resume (aborting is user intent).
|
|
551
|
+
let consecutiveAbortIterations = 0;
|
|
497
552
|
let consecutiveNoToolIterations = 0;
|
|
498
553
|
|
|
499
554
|
// =================================================================
|
|
@@ -555,6 +610,8 @@ function sendContinuation(goalId: string): void {
|
|
|
555
610
|
return;
|
|
556
611
|
}
|
|
557
612
|
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
613
|
+
// v0.28.5 (E3): count + ledger + escalate the re-arm storm.
|
|
614
|
+
accountSendRearm(ctx, "continuation");
|
|
558
615
|
continuationScheduledFor = goalId;
|
|
559
616
|
continuationTimer = setTimeout(() => sendContinuation(goalId), BACKOFF_IDLE_RETRY_MS);
|
|
560
617
|
continuationTimer.unref?.();
|
|
@@ -567,6 +624,7 @@ function sendContinuation(goalId: string): void {
|
|
|
567
624
|
content: continuationPrompt(state.goal!),
|
|
568
625
|
display: false,
|
|
569
626
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
627
|
+
continuationRearmStreak = 0; // v0.28.5 (E3): a landed send clears the storm
|
|
570
628
|
appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
|
|
571
629
|
} catch (err) {
|
|
572
630
|
appendLedger(ctx.cwd, "goal_continuation_send_failed", { goalId, error: err instanceof Error ? err.message : String(err) });
|
|
@@ -685,9 +743,29 @@ function createGoal(objective: string, ctx: ExtensionContext, policy: "goal" | "
|
|
|
685
743
|
|
|
686
744
|
function persistState(ctx: ExtensionContext): void {
|
|
687
745
|
appendLedger(ctx.cwd, "state", { goal: state.goal, list: state.list ?? [], loop: state.loop ?? null });
|
|
746
|
+
notifyPersistenceState(ctx); // v0.28.6 (E1): loud on the first failure, all-clear on recovery
|
|
688
747
|
refreshUI(ctx); // every state transition flows through here → the TUI is always current
|
|
689
748
|
}
|
|
690
749
|
|
|
750
|
+
// v0.28.6 (E1): persistence-degradation notify — once per failure streak,
|
|
751
|
+
// once per recovery. The TUI flag (buildWidgetLines) carries the standing
|
|
752
|
+
// state; these notifies are the LOUD part.
|
|
753
|
+
let persistenceDegradedNotified = false;
|
|
754
|
+
function notifyPersistenceState(ctx: ExtensionContext): void {
|
|
755
|
+
if (isPersistenceDegraded() && !persistenceDegradedNotified) {
|
|
756
|
+
persistenceDegradedNotified = true;
|
|
757
|
+
const err = lastPersistenceFailure();
|
|
758
|
+
ctx.ui.notify(
|
|
759
|
+
`⚠ Persistence degraded: ${err?.what ?? "disk write"} failed (${(err?.error ?? "unknown").slice(0, 80)}). State lives in RAM and re-syncs on the next successful write — .pi-glla may be missing recent entries. Fix the disk (space/permissions) and it self-heals.`,
|
|
760
|
+
"warning",
|
|
761
|
+
);
|
|
762
|
+
notifyExternal(ctx, "pi-goal-list-loop-audit: persistence degraded — .pi-glla writes failing.");
|
|
763
|
+
} else if (!isPersistenceDegraded() && persistenceDegradedNotified) {
|
|
764
|
+
persistenceDegradedNotified = false;
|
|
765
|
+
ctx.ui.notify("Persistence recovered — .pi-glla writes are landing again.", "info");
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
691
769
|
function setGoal(goal: Goal, ctx: ExtensionContext): void {
|
|
692
770
|
state = { goal, list: state.list ?? [] }; // preserve the list!
|
|
693
771
|
const file = writeGoalMd(ctx.cwd, goal);
|
|
@@ -710,9 +788,16 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
|
|
|
710
788
|
ensureDirs(ctx.cwd);
|
|
711
789
|
const target = archivedGoalPath(ctx.cwd, goal.id);
|
|
712
790
|
const md = renderGoalMarkdown({ ...goal, status, stopReason });
|
|
713
|
-
|
|
714
|
-
//
|
|
715
|
-
|
|
791
|
+
// v0.28.6 (E1): guarded — and the active md is only removed when the
|
|
792
|
+
// archive actually LANDED (degraded mode must not destroy the only copy).
|
|
793
|
+
const archived = runPersistStep("archiveCurrentGoal", () => {
|
|
794
|
+
ensureDirs(ctx.cwd);
|
|
795
|
+
fs.writeFileSync(target, md);
|
|
796
|
+
return true;
|
|
797
|
+
}) === true;
|
|
798
|
+
if (archived) {
|
|
799
|
+
try { fs.unlinkSync(goalMdPath(ctx.cwd, goal.id)); } catch {}
|
|
800
|
+
}
|
|
716
801
|
state = { goal: { ...goal, status, archivedPath: path.relative(ctx.cwd, target) || target, stopReason }, list: state.list ?? [] };
|
|
717
802
|
appendLedger(ctx.cwd, "goal_archived", { goalId: goal.id, status, stopReason });
|
|
718
803
|
persistState(ctx);
|
|
@@ -834,6 +919,7 @@ function activateNextListItem(ctx: ExtensionContext, n = 1): boolean {
|
|
|
834
919
|
setGoal(goal, ctx);
|
|
835
920
|
iterationCounter = 0;
|
|
836
921
|
consecutiveErrorIterations = 0;
|
|
922
|
+
consecutiveAbortIterations = 0;
|
|
837
923
|
ctx.ui.notify(`List item #${n} activated (${rest.length} remaining): ${goal.objective.slice(0, 80)}`, "info");
|
|
838
924
|
scheduleContinuation(ctx, true);
|
|
839
925
|
return true;
|
|
@@ -981,6 +1067,7 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
|
|
|
981
1067
|
// Reset counters
|
|
982
1068
|
iterationCounter = 0;
|
|
983
1069
|
consecutiveErrorIterations = 0;
|
|
1070
|
+
consecutiveAbortIterations = 0;
|
|
984
1071
|
consecutiveNoToolIterations = 0;
|
|
985
1072
|
if (staleEntry) {
|
|
986
1073
|
// v0.28.1 (S3): the goal is persisted — mark the interrupt so the next
|
|
@@ -1506,6 +1593,7 @@ function sendLoopTurn(): void {
|
|
|
1506
1593
|
if (!isLoopActive() || !extensionApi) return;
|
|
1507
1594
|
const ctx = freshCtx();
|
|
1508
1595
|
if (!ctx || !ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
1596
|
+
if (ctx) accountSendRearm(ctx, "loop"); // v0.28.5 (E3)
|
|
1509
1597
|
loopTimer = setTimeout(() => sendLoopTurn(), BACKOFF_IDLE_RETRY_MS);
|
|
1510
1598
|
loopTimer.unref?.();
|
|
1511
1599
|
return;
|
|
@@ -1553,6 +1641,7 @@ function sendLoopTurn(): void {
|
|
|
1553
1641
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
1554
1642
|
// v0.26.1: the send path is ledgered — the hegemon zombie spun 619
|
|
1555
1643
|
// refires with zero visibility into whether sends were landing.
|
|
1644
|
+
loopRearmStreak = 0; // v0.28.5 (E3): a landed turn clears the storm
|
|
1556
1645
|
appendLedger(ctx.cwd, "loop_turn_sent", { iteration: loop.iteration });
|
|
1557
1646
|
} catch (err) {
|
|
1558
1647
|
// stale API — next agent_end reschedules (but if none comes, the
|
|
@@ -2083,6 +2172,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2083
2172
|
// (abort, auth failure, no model) are surfaced via pauseReason, not
|
|
2084
2173
|
// logged as disapprovals.
|
|
2085
2174
|
const auditorRan = result.output.trim().length > 0;
|
|
2175
|
+
// v0.28.5 (E2): a REAL auditor run clears the infra-error streak.
|
|
2176
|
+
if (auditorRan && (state.goal.auditInfraStreak ?? 0) > 0) updateGoal({ auditInfraStreak: undefined }, ctx);
|
|
2086
2177
|
const history = state.goal.auditHistory ?? [];
|
|
2087
2178
|
if (auditorRan) {
|
|
2088
2179
|
// v0.25.4: strip think-block leakage (MiniMax-M3 `</think>`
|
|
@@ -2229,6 +2320,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2229
2320
|
updateGoal({
|
|
2230
2321
|
status: "paused",
|
|
2231
2322
|
auditHistory: history,
|
|
2323
|
+
auditInfraStreak: undefined, // quota reached the auditor — infra streak broken
|
|
2232
2324
|
pauseReason: `auditor quota: ${result.error}`,
|
|
2233
2325
|
pauseSuggestedAction: `Quota auto-retry in ${retryMin}m — or /goal resume to retry now`,
|
|
2234
2326
|
}, ctx);
|
|
@@ -2253,9 +2345,34 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2253
2345
|
details: {},
|
|
2254
2346
|
};
|
|
2255
2347
|
}
|
|
2348
|
+
// v0.28.5 (E2): bound the silent retry-forever. Each infra error
|
|
2349
|
+
// used to reschedule a continuation unconditionally — a broken
|
|
2350
|
+
// auditor model spun forever (the 39-error incident). At 3 trailing
|
|
2351
|
+
// infra errors the model is broken, not unlucky: pause LOUDLY.
|
|
2352
|
+
const infraStreak = (state.goal.auditInfraStreak ?? 0) + 1;
|
|
2353
|
+
if (infraStreak >= 3) {
|
|
2354
|
+
updateGoal({
|
|
2355
|
+
status: "paused",
|
|
2356
|
+
auditHistory: history,
|
|
2357
|
+
auditInfraStreak: infraStreak,
|
|
2358
|
+
pauseReason: `auditor infrastructure failed ${infraStreak}× in a row — the auditor model is likely broken (last: ${result.error.slice(0, 120)})`,
|
|
2359
|
+
pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) or restart pi, then /goal resume. Your work was NOT judged.",
|
|
2360
|
+
}, ctx);
|
|
2361
|
+
appendLedger(ctx.cwd, "goal_paused", { reason: `auditor infra streak ${infraStreak}: ${result.error.slice(0, 120)}` });
|
|
2362
|
+
ctx.ui.notify(`Goal paused: auditor infrastructure failed ${infraStreak}× in a row. Fix the auditor model (/glla model=...), then /goal resume.`, "warning");
|
|
2363
|
+
notifyExternal(ctx, `Goal paused: auditor infrastructure ${infraStreak}× — model likely broken.`);
|
|
2364
|
+
return {
|
|
2365
|
+
content: [{
|
|
2366
|
+
type: "text",
|
|
2367
|
+
text: `The auditor has now failed ${infraStreak} times in a row with infrastructure errors (NOT verdicts; last: ${result.error}). The goal is PAUSED — the retry-forever loop stops here. Fix the auditor model with /glla model=provider/id (or restart pi), then /goal resume and call complete_goal again. Do not change your deliverable for this.`,
|
|
2368
|
+
}],
|
|
2369
|
+
details: {},
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2256
2372
|
updateGoal({
|
|
2257
2373
|
status: "active",
|
|
2258
2374
|
auditHistory: history,
|
|
2375
|
+
auditInfraStreak: infraStreak,
|
|
2259
2376
|
pauseReason: `auditor infrastructure${retriedOnce ? " (retried once)" : ""}: ${result.error}`,
|
|
2260
2377
|
pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) and call complete_goal again — your work was NOT judged",
|
|
2261
2378
|
}, ctx);
|
|
@@ -2599,6 +2716,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2599
2716
|
setGoal(goal, liveCtx);
|
|
2600
2717
|
iterationCounter = 0;
|
|
2601
2718
|
consecutiveErrorIterations = 0;
|
|
2719
|
+
consecutiveAbortIterations = 0;
|
|
2602
2720
|
scheduleContinuation(liveCtx, true);
|
|
2603
2721
|
return {
|
|
2604
2722
|
content: [{ type: "text", text: `Goal confirmed and activated (id ${goal.id}). Begin work now; call complete_goal only when the objective is genuinely satisfied.` }],
|
|
@@ -4318,20 +4436,56 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4318
4436
|
updateGoal({ usage: { tokensUsed: used, tokensLimit: limit } }, ctx);
|
|
4319
4437
|
}
|
|
4320
4438
|
|
|
4321
|
-
if (stopReason === "error"
|
|
4439
|
+
if (stopReason === "error") {
|
|
4322
4440
|
consecutiveErrorIterations++;
|
|
4441
|
+
consecutiveAbortIterations = 0;
|
|
4323
4442
|
if (consecutiveErrorIterations >= 5) {
|
|
4443
|
+
// v0.28.5 (E8): carry the REAL error text — the pause used to say
|
|
4444
|
+
// literally "5 consecutive errors: error" (stopReason, not the
|
|
4445
|
+
// provider error). And give transient flakes ONE capped auto-resume
|
|
4446
|
+
// (60s, reason re-checked) — the E8 incident lost 1.5h to a
|
|
4447
|
+
// 60-second provider hiccup waiting on a manual /goal resume.
|
|
4448
|
+
const detail = text.trim() ? ` (last: ${text.trim().replace(/\s+/g, " ").slice(0, 160)})` : "";
|
|
4449
|
+
const reason = `5 consecutive errors${detail}`;
|
|
4450
|
+
updateGoal({
|
|
4451
|
+
status: "paused",
|
|
4452
|
+
pauseReason: reason,
|
|
4453
|
+
pauseSuggestedAction: "Transient provider flake? The goal auto-resumes once in 60s if still paused for this reason — or /goal resume now.",
|
|
4454
|
+
}, ctx);
|
|
4455
|
+
ctx.ui.notify(`Goal paused: ${reason}.`, "warning");
|
|
4456
|
+
notifyExternal(ctx, `Goal paused: ${reason}.`);
|
|
4457
|
+
appendLedger(ctx.cwd, "goal_paused", { reason });
|
|
4458
|
+
scheduleQuotaRetry(ctx, 60, reason, () => {
|
|
4459
|
+
// Re-check: only auto-resume if STILL paused for the error brake
|
|
4460
|
+
// (a user /goal pause during the window is not stomped).
|
|
4461
|
+
if (state.goal && state.goal.status === "paused" && (state.goal.pauseReason ?? "").startsWith("5 consecutive errors")) {
|
|
4462
|
+
updateGoal({ status: "active" }, ctx);
|
|
4463
|
+
appendLedger(ctx.cwd, "goal_resumed", { via: "error-brake-retry" });
|
|
4464
|
+
ctx.ui.notify("Auto-resumed after the 5-error brake (60s cooldown).", "info");
|
|
4465
|
+
scheduleContinuation(ctx, true);
|
|
4466
|
+
}
|
|
4467
|
+
}, "5 consecutive errors — auto-retry");
|
|
4468
|
+
return;
|
|
4469
|
+
}
|
|
4470
|
+
} else if (stopReason === "aborted") {
|
|
4471
|
+
// v0.28.5 (E8): user aborts are not provider errors. Separate brake,
|
|
4472
|
+
// honest message, and NO auto-resume — aborting five turns in a row
|
|
4473
|
+
// is the user telling the goal to stop; we stay stopped.
|
|
4474
|
+
consecutiveAbortIterations++;
|
|
4475
|
+
consecutiveErrorIterations = 0;
|
|
4476
|
+
if (consecutiveAbortIterations >= 5) {
|
|
4324
4477
|
updateGoal({
|
|
4325
4478
|
status: "paused",
|
|
4326
|
-
pauseReason:
|
|
4327
|
-
pauseSuggestedAction: "
|
|
4479
|
+
pauseReason: "5 consecutive aborts (user interrupted)",
|
|
4480
|
+
pauseSuggestedAction: "You interrupted 5 turns in a row — the goal stays paused until you /goal resume (or /goal cancel).",
|
|
4328
4481
|
}, ctx);
|
|
4329
|
-
ctx.ui.notify("Goal paused: 5 consecutive
|
|
4330
|
-
|
|
4482
|
+
ctx.ui.notify("Goal paused: 5 consecutive aborts (user interrupted).", "warning");
|
|
4483
|
+
appendLedger(ctx.cwd, "goal_paused", { reason: "5 consecutive aborts (user interrupted)" });
|
|
4331
4484
|
return;
|
|
4332
4485
|
}
|
|
4333
4486
|
} else {
|
|
4334
4487
|
consecutiveErrorIterations = 0;
|
|
4488
|
+
consecutiveAbortIterations = 0;
|
|
4335
4489
|
}
|
|
4336
4490
|
|
|
4337
4491
|
// No wall-clock cap by design: a goal ends via completion, explicit
|
|
@@ -65,12 +65,14 @@ export function cancelQuotaRetry(): void {
|
|
|
65
65
|
/** Schedule a one-shot auto-resume after the quota window. The fire
|
|
66
66
|
* callback re-checks the goal is STILL paused for the quota reason before
|
|
67
67
|
* resuming (contract item 10/12 — a user /goal pause during the window
|
|
68
|
-
* must not be stomped).
|
|
68
|
+
* must not be stomped). v0.28.5: `label` generalizes the notify so the
|
|
69
|
+
* 5-consecutive-errors brake can reuse the same capped one-shot machinery. */
|
|
69
70
|
export function scheduleQuotaRetry(
|
|
70
71
|
ctx: ExtensionContext,
|
|
71
72
|
retryAfterSec: number,
|
|
72
73
|
reason: string,
|
|
73
74
|
fire: () => void,
|
|
75
|
+
label = "Auditor quota exhausted — auto-retry",
|
|
74
76
|
): void {
|
|
75
77
|
cancelQuotaRetry();
|
|
76
78
|
const ms = Math.max(1_000, retryAfterSec * 1_000);
|
|
@@ -84,7 +86,7 @@ export function scheduleQuotaRetry(
|
|
|
84
86
|
}, ms);
|
|
85
87
|
quotaRetryTimer.unref?.();
|
|
86
88
|
ctx.ui.notify(
|
|
87
|
-
|
|
89
|
+
`${label} in ${Math.round(retryAfterSec / 60)}m (${reason.slice(0, 80)}). /goal resume retries now.`,
|
|
88
90
|
"info",
|
|
89
91
|
);
|
|
90
92
|
}
|
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.6",
|
|
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",
|
package/schemas/goal.schema.json
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"pauseSuggestedAction": { "type": "string" },
|
|
49
49
|
"interruptedAt": { "type": "string" },
|
|
50
50
|
"interruptedReason": { "type": "string" },
|
|
51
|
+
"auditInfraStreak": { "type": "number" },
|
|
51
52
|
"activePath": { "type": "string" },
|
|
52
53
|
"archivedPath": { "type": "string" },
|
|
53
54
|
"usage": {
|