pi-goal-list-loop-audit 0.28.4 → 0.28.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.
@@ -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. */
@@ -360,6 +360,55 @@ function startUITicker(): void {
360
360
 
361
361
  /** v0.26.5: shared loud-stop for both stall paths (refire streak and
362
362
  * pending-latch streak). Returns true when it escalated. */
363
+ // v0.28.5 (E3): send-retry re-arm accounting. The 50ms BACKOFF_IDLE_RETRY
364
+ // re-arm loop used to spin for HOURS with zero ledger events while the idle
365
+ // watchdogs stayed suppressed. Now: counted, ledgered (start + every 30s),
366
+ // and escalated loudly past 5 minutes.
367
+ let continuationRearmStreak = 0;
368
+ let loopRearmStreak = 0;
369
+ const SEND_REARM_LEDGER_EVERY = 600; // 600 × 50ms = 30s
370
+ const SEND_REARM_ESCALATE_AT = 6000; // 6000 × 50ms = 5 minutes
371
+
372
+ function accountSendRearm(ctx: ExtensionContext, kind: "continuation" | "loop"): void {
373
+ const streak = kind === "continuation" ? ++continuationRearmStreak : ++loopRearmStreak;
374
+ if (streak === 1) {
375
+ appendLedger(ctx.cwd, "send_rearm_start", { kind });
376
+ return;
377
+ }
378
+ if (streak % SEND_REARM_LEDGER_EVERY === 0) {
379
+ appendLedger(ctx.cwd, "send_rearm_storm", { kind, streak, minutes: Math.round((streak * BACKOFF_IDLE_RETRY_MS) / 60000) });
380
+ }
381
+ if (streak >= SEND_REARM_ESCALATE_AT) {
382
+ if (kind === "continuation") continuationRearmStreak = 0; else loopRearmStreak = 0;
383
+ escalateSendRearmStorm(ctx, kind);
384
+ }
385
+ }
386
+
387
+ function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "loop"): void {
388
+ // Same loud-terminal shape as escalateStallNow (v0.24.7): a 5-minute
389
+ // re-arm storm means the session never goes idle for us — wedged queue
390
+ // or a busy-forever session. Bounded and surfaced, not silent.
391
+ const mins = Math.round((SEND_REARM_ESCALATE_AT * BACKOFF_IDLE_RETRY_MS) / 60000);
392
+ appendLedger(ctx.cwd, "send_rearm_escalated", { kind, streak: SEND_REARM_ESCALATE_AT });
393
+ if (kind === "loop" && isLoopActive()) {
394
+ clearLoopTimer();
395
+ 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.` };
396
+ persistState(ctx);
397
+ ctx.ui.notify(`Loop stopped: send-retry storm (${mins}m). Restart pi and /loop start.`, "warning");
398
+ notifyExternal(ctx, "Loop stopped: send-retry storm.");
399
+ return;
400
+ }
401
+ if (state.goal && state.goal.status === "active") {
402
+ updateGoal({
403
+ status: "paused",
404
+ pauseReason: `send-retry storm: ${mins}m of 50ms re-arms — the session never went idle for the continuation`,
405
+ pauseSuggestedAction: "The session never went idle for the send (wedged queue or permanently busy). Restart pi, then /goal resume.",
406
+ }, ctx);
407
+ ctx.ui.notify(`Goal paused: send-retry storm (${mins}m). Restart pi, then /goal resume.`, "warning");
408
+ notifyExternal(ctx, "Goal paused: send-retry storm.");
409
+ }
410
+ }
411
+
363
412
  function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
364
413
  if (!shouldEscalateStall(consecutiveStalls, threshold)) return false;
365
414
  consecutiveStalls = 0;
@@ -494,6 +543,9 @@ let continuationScheduledFor: string | null = null;
494
543
  let iterationCounter = 0;
495
544
  let toolCallsThisTurn = 0;
496
545
  let consecutiveErrorIterations = 0;
546
+ // v0.28.5 (E8): user aborts are NOT provider errors — separate counter,
547
+ // separate brake message, and no auto-resume (aborting is user intent).
548
+ let consecutiveAbortIterations = 0;
497
549
  let consecutiveNoToolIterations = 0;
498
550
 
499
551
  // =================================================================
@@ -555,6 +607,8 @@ function sendContinuation(goalId: string): void {
555
607
  return;
556
608
  }
557
609
  if (!ctx.isIdle() || ctx.hasPendingMessages()) {
610
+ // v0.28.5 (E3): count + ledger + escalate the re-arm storm.
611
+ accountSendRearm(ctx, "continuation");
558
612
  continuationScheduledFor = goalId;
559
613
  continuationTimer = setTimeout(() => sendContinuation(goalId), BACKOFF_IDLE_RETRY_MS);
560
614
  continuationTimer.unref?.();
@@ -567,6 +621,7 @@ function sendContinuation(goalId: string): void {
567
621
  content: continuationPrompt(state.goal!),
568
622
  display: false,
569
623
  }, { triggerTurn: true, deliverAs: "followUp" });
624
+ continuationRearmStreak = 0; // v0.28.5 (E3): a landed send clears the storm
570
625
  appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
571
626
  } catch (err) {
572
627
  appendLedger(ctx.cwd, "goal_continuation_send_failed", { goalId, error: err instanceof Error ? err.message : String(err) });
@@ -834,6 +889,7 @@ function activateNextListItem(ctx: ExtensionContext, n = 1): boolean {
834
889
  setGoal(goal, ctx);
835
890
  iterationCounter = 0;
836
891
  consecutiveErrorIterations = 0;
892
+ consecutiveAbortIterations = 0;
837
893
  ctx.ui.notify(`List item #${n} activated (${rest.length} remaining): ${goal.objective.slice(0, 80)}`, "info");
838
894
  scheduleContinuation(ctx, true);
839
895
  return true;
@@ -981,6 +1037,7 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
981
1037
  // Reset counters
982
1038
  iterationCounter = 0;
983
1039
  consecutiveErrorIterations = 0;
1040
+ consecutiveAbortIterations = 0;
984
1041
  consecutiveNoToolIterations = 0;
985
1042
  if (staleEntry) {
986
1043
  // v0.28.1 (S3): the goal is persisted — mark the interrupt so the next
@@ -1506,6 +1563,7 @@ function sendLoopTurn(): void {
1506
1563
  if (!isLoopActive() || !extensionApi) return;
1507
1564
  const ctx = freshCtx();
1508
1565
  if (!ctx || !ctx.isIdle() || ctx.hasPendingMessages()) {
1566
+ if (ctx) accountSendRearm(ctx, "loop"); // v0.28.5 (E3)
1509
1567
  loopTimer = setTimeout(() => sendLoopTurn(), BACKOFF_IDLE_RETRY_MS);
1510
1568
  loopTimer.unref?.();
1511
1569
  return;
@@ -1553,6 +1611,7 @@ function sendLoopTurn(): void {
1553
1611
  }, { triggerTurn: true, deliverAs: "followUp" });
1554
1612
  // v0.26.1: the send path is ledgered — the hegemon zombie spun 619
1555
1613
  // refires with zero visibility into whether sends were landing.
1614
+ loopRearmStreak = 0; // v0.28.5 (E3): a landed turn clears the storm
1556
1615
  appendLedger(ctx.cwd, "loop_turn_sent", { iteration: loop.iteration });
1557
1616
  } catch (err) {
1558
1617
  // stale API — next agent_end reschedules (but if none comes, the
@@ -2083,6 +2142,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2083
2142
  // (abort, auth failure, no model) are surfaced via pauseReason, not
2084
2143
  // logged as disapprovals.
2085
2144
  const auditorRan = result.output.trim().length > 0;
2145
+ // v0.28.5 (E2): a REAL auditor run clears the infra-error streak.
2146
+ if (auditorRan && (state.goal.auditInfraStreak ?? 0) > 0) updateGoal({ auditInfraStreak: undefined }, ctx);
2086
2147
  const history = state.goal.auditHistory ?? [];
2087
2148
  if (auditorRan) {
2088
2149
  // v0.25.4: strip think-block leakage (MiniMax-M3 `</think>`
@@ -2229,6 +2290,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2229
2290
  updateGoal({
2230
2291
  status: "paused",
2231
2292
  auditHistory: history,
2293
+ auditInfraStreak: undefined, // quota reached the auditor — infra streak broken
2232
2294
  pauseReason: `auditor quota: ${result.error}`,
2233
2295
  pauseSuggestedAction: `Quota auto-retry in ${retryMin}m — or /goal resume to retry now`,
2234
2296
  }, ctx);
@@ -2253,9 +2315,34 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2253
2315
  details: {},
2254
2316
  };
2255
2317
  }
2318
+ // v0.28.5 (E2): bound the silent retry-forever. Each infra error
2319
+ // used to reschedule a continuation unconditionally — a broken
2320
+ // auditor model spun forever (the 39-error incident). At 3 trailing
2321
+ // infra errors the model is broken, not unlucky: pause LOUDLY.
2322
+ const infraStreak = (state.goal.auditInfraStreak ?? 0) + 1;
2323
+ if (infraStreak >= 3) {
2324
+ updateGoal({
2325
+ status: "paused",
2326
+ auditHistory: history,
2327
+ auditInfraStreak: infraStreak,
2328
+ pauseReason: `auditor infrastructure failed ${infraStreak}× in a row — the auditor model is likely broken (last: ${result.error.slice(0, 120)})`,
2329
+ pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) or restart pi, then /goal resume. Your work was NOT judged.",
2330
+ }, ctx);
2331
+ appendLedger(ctx.cwd, "goal_paused", { reason: `auditor infra streak ${infraStreak}: ${result.error.slice(0, 120)}` });
2332
+ ctx.ui.notify(`Goal paused: auditor infrastructure failed ${infraStreak}× in a row. Fix the auditor model (/glla model=...), then /goal resume.`, "warning");
2333
+ notifyExternal(ctx, `Goal paused: auditor infrastructure ${infraStreak}× — model likely broken.`);
2334
+ return {
2335
+ content: [{
2336
+ type: "text",
2337
+ 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.`,
2338
+ }],
2339
+ details: {},
2340
+ };
2341
+ }
2256
2342
  updateGoal({
2257
2343
  status: "active",
2258
2344
  auditHistory: history,
2345
+ auditInfraStreak: infraStreak,
2259
2346
  pauseReason: `auditor infrastructure${retriedOnce ? " (retried once)" : ""}: ${result.error}`,
2260
2347
  pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) and call complete_goal again — your work was NOT judged",
2261
2348
  }, ctx);
@@ -2599,6 +2686,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2599
2686
  setGoal(goal, liveCtx);
2600
2687
  iterationCounter = 0;
2601
2688
  consecutiveErrorIterations = 0;
2689
+ consecutiveAbortIterations = 0;
2602
2690
  scheduleContinuation(liveCtx, true);
2603
2691
  return {
2604
2692
  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 +4406,56 @@ export default function (pi: ExtensionAPI): void {
4318
4406
  updateGoal({ usage: { tokensUsed: used, tokensLimit: limit } }, ctx);
4319
4407
  }
4320
4408
 
4321
- if (stopReason === "error" || stopReason === "aborted") {
4409
+ if (stopReason === "error") {
4322
4410
  consecutiveErrorIterations++;
4411
+ consecutiveAbortIterations = 0;
4323
4412
  if (consecutiveErrorIterations >= 5) {
4413
+ // v0.28.5 (E8): carry the REAL error text — the pause used to say
4414
+ // literally "5 consecutive errors: error" (stopReason, not the
4415
+ // provider error). And give transient flakes ONE capped auto-resume
4416
+ // (60s, reason re-checked) — the E8 incident lost 1.5h to a
4417
+ // 60-second provider hiccup waiting on a manual /goal resume.
4418
+ const detail = text.trim() ? ` (last: ${text.trim().replace(/\s+/g, " ").slice(0, 160)})` : "";
4419
+ const reason = `5 consecutive errors${detail}`;
4420
+ updateGoal({
4421
+ status: "paused",
4422
+ pauseReason: reason,
4423
+ pauseSuggestedAction: "Transient provider flake? The goal auto-resumes once in 60s if still paused for this reason — or /goal resume now.",
4424
+ }, ctx);
4425
+ ctx.ui.notify(`Goal paused: ${reason}.`, "warning");
4426
+ notifyExternal(ctx, `Goal paused: ${reason}.`);
4427
+ appendLedger(ctx.cwd, "goal_paused", { reason });
4428
+ scheduleQuotaRetry(ctx, 60, reason, () => {
4429
+ // Re-check: only auto-resume if STILL paused for the error brake
4430
+ // (a user /goal pause during the window is not stomped).
4431
+ if (state.goal && state.goal.status === "paused" && (state.goal.pauseReason ?? "").startsWith("5 consecutive errors")) {
4432
+ updateGoal({ status: "active" }, ctx);
4433
+ appendLedger(ctx.cwd, "goal_resumed", { via: "error-brake-retry" });
4434
+ ctx.ui.notify("Auto-resumed after the 5-error brake (60s cooldown).", "info");
4435
+ scheduleContinuation(ctx, true);
4436
+ }
4437
+ }, "5 consecutive errors — auto-retry");
4438
+ return;
4439
+ }
4440
+ } else if (stopReason === "aborted") {
4441
+ // v0.28.5 (E8): user aborts are not provider errors. Separate brake,
4442
+ // honest message, and NO auto-resume — aborting five turns in a row
4443
+ // is the user telling the goal to stop; we stay stopped.
4444
+ consecutiveAbortIterations++;
4445
+ consecutiveErrorIterations = 0;
4446
+ if (consecutiveAbortIterations >= 5) {
4324
4447
  updateGoal({
4325
4448
  status: "paused",
4326
- pauseReason: `5 consecutive errors: ${stopReason}`,
4327
- pauseSuggestedAction: "Use /goal resume to retry, or /goal cancel to abort.",
4449
+ pauseReason: "5 consecutive aborts (user interrupted)",
4450
+ pauseSuggestedAction: "You interrupted 5 turns in a row — the goal stays paused until you /goal resume (or /goal cancel).",
4328
4451
  }, ctx);
4329
- ctx.ui.notify("Goal paused: 5 consecutive errors.", "warning");
4330
- notifyExternal(ctx, "Goal paused: 5 consecutive errors.");
4452
+ ctx.ui.notify("Goal paused: 5 consecutive aborts (user interrupted).", "warning");
4453
+ appendLedger(ctx.cwd, "goal_paused", { reason: "5 consecutive aborts (user interrupted)" });
4331
4454
  return;
4332
4455
  }
4333
4456
  } else {
4334
4457
  consecutiveErrorIterations = 0;
4458
+ consecutiveAbortIterations = 0;
4335
4459
  }
4336
4460
 
4337
4461
  // 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
- `Auditor quota exhausted — auto-retry in ${Math.round(retryAfterSec / 60)}m (${reason.slice(0, 80)}). /goal resume retries now.`,
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.4",
3
+ "version": "0.28.5",
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",
@@ -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": {