pi-goal-list-loop-audit 0.28.28 → 0.28.29

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.
@@ -482,35 +482,65 @@ const COMPACTION_GRACE_MS = 3 * 60_000;
482
482
  // brake (1m, 2m, 4m, 8m, 16m cap). A successful turn resets both.
483
483
  const ERROR_RETRY_LADDER_MS = [5_000, 15_000, 45_000, 90_000, 180_000];
484
484
  let errorBrakeStreak = 0;
485
- const SEND_REARM_LEDGER_EVERY = 600; // 600 × 50ms = 30s
486
- const SEND_REARM_ESCALATE_AT = 6000; // 6000 × 50ms = 5 minutes
485
+ const SEND_REARM_LEDGER_MILESTONES_MS = [2 * 60_000, 5 * 60_000, 10 * 60_000];
486
+ // v0.28.29: escalation is TIME-based and ACTIVITY-gated. A busy session is
487
+ // NORMAL — the user conversing, or one long subagent turn — and the old
488
+ // flat-50ms × 6000-count rule misread 5 minutes of busy as "wedged" and
489
+ // paused the goal (the polis field report). Escalate only after 15 minutes
490
+ // of failed sends AND no session activity in the last 5 minutes (a wedged
491
+ // queue shows no events at all; a busy one streams constantly).
492
+ const SEND_REARM_ESCALATE_AFTER_MS = 15 * 60_000;
493
+ const SEND_REARM_ESCALATE_SILENT_MS = 5 * 60_000;
494
+ let continuationRearmSince = 0;
495
+ let loopRearmSince = 0;
496
+ let continuationRearmMilestone = 0;
497
+ let loopRearmMilestone = 0;
498
+
499
+ /** v0.28.29: busy-retry cadence backs off — 50ms for the first beats
500
+ * (instant pickup right after a turn ends), then 250ms, 1s, 5s, 15s, 30s
501
+ * cap. agent_end reschedules independently, so the slow tail costs nothing
502
+ * in the common case; it only caps the ledger/CPU spam of a long busy stretch. */
503
+ function sendRearmDelayMs(streak: number): number {
504
+ if (streak <= 4) return 50;
505
+ if (streak <= 8) return 250;
506
+ if (streak <= 12) return 1_000;
507
+ if (streak === 13) return 5_000;
508
+ if (streak === 14) return 15_000;
509
+ return 30_000;
510
+ }
487
511
 
488
512
  function accountSendRearm(ctx: ExtensionContext, kind: "continuation" | "loop"): void {
489
513
  const streak = kind === "continuation" ? ++continuationRearmStreak : ++loopRearmStreak;
490
514
  if (streak === 1) {
515
+ if (kind === "continuation") { continuationRearmSince = Date.now(); continuationRearmMilestone = 0; } else { loopRearmSince = Date.now(); loopRearmMilestone = 0; }
491
516
  appendLedger(ctx.cwd, "send_rearm_start", { kind });
492
517
  return;
493
518
  }
494
- if (streak % SEND_REARM_LEDGER_EVERY === 0) {
495
- appendLedger(ctx.cwd, "send_rearm_storm", { kind, streak, minutes: Math.round((streak * BACKOFF_IDLE_RETRY_MS) / 60000) });
519
+ const since = kind === "continuation" ? continuationRearmSince : loopRearmSince;
520
+ const elapsed = Date.now() - since;
521
+ const milestone = kind === "continuation" ? continuationRearmMilestone : loopRearmMilestone;
522
+ if (milestone < SEND_REARM_LEDGER_MILESTONES_MS.length && elapsed >= SEND_REARM_LEDGER_MILESTONES_MS[milestone]!) {
523
+ if (kind === "continuation") continuationRearmMilestone++; else loopRearmMilestone++;
524
+ appendLedger(ctx.cwd, "send_rearm_storm", { kind, streak, minutes: Math.round(elapsed / 60000) });
496
525
  }
497
- if (streak >= SEND_REARM_ESCALATE_AT) {
498
- if (kind === "continuation") continuationRearmStreak = 0; else loopRearmStreak = 0;
526
+ if (elapsed >= SEND_REARM_ESCALATE_AFTER_MS && Date.now() - lastActivityAt >= SEND_REARM_ESCALATE_SILENT_MS) {
527
+ if (kind === "continuation") { continuationRearmStreak = 0; continuationRearmSince = 0; } else { loopRearmStreak = 0; loopRearmSince = 0; }
499
528
  escalateSendRearmStorm(ctx, kind);
500
529
  }
501
530
  }
502
531
 
503
532
  function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "loop"): void {
504
- // Same loud-terminal shape as escalateStallNow (v0.24.7): a 5-minute
505
- // re-arm storm means the session never goes idle for us wedged queue
506
- // or a busy-forever session. Bounded and surfaced, not silent.
507
- const mins = Math.round((SEND_REARM_ESCALATE_AT * BACKOFF_IDLE_RETRY_MS) / 60000);
508
- appendLedger(ctx.cwd, "send_rearm_escalated", { kind, streak: SEND_REARM_ESCALATE_AT });
533
+ // Same loud-terminal shape as escalateStallNow (v0.24.7). v0.28.29: this
534
+ // only fires on a REAL wedge now (15m of failed sends + 5m of zero
535
+ // session activity) busy-but-alive sessions never reach it.
536
+ const mins = Math.round(SEND_REARM_ESCALATE_AFTER_MS / 60000);
537
+ const silent = Math.round(SEND_REARM_ESCALATE_SILENT_MS / 60000);
538
+ appendLedger(ctx.cwd, "send_rearm_escalated", { kind, afterMinutes: mins, silentMinutes: silent });
509
539
  if (kind === "loop" && isLoopActive()) {
510
540
  clearLoopTimer();
511
- 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.` };
541
+ state.loop = { ...state.loop!, active: false, stopReason: `send-retry storm: ${mins}m of re-arms with no session activity for ${silent}m the session is wedged. Restart pi, then /loop start again.` };
512
542
  persistState(ctx);
513
- ctx.ui.notify(`Loop stopped: send-retry storm (${mins}m). Restart pi and /loop start.`, "warning");
543
+ ctx.ui.notify(`Loop stopped: send-retry storm (${mins}m, session silent ${silent}m). Restart pi and /loop start.`, "warning");
514
544
  notifyExternal(ctx, "Loop stopped: send-retry storm.");
515
545
  return;
516
546
  }
@@ -518,10 +548,10 @@ function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "l
518
548
  updateGoal({
519
549
  status: "paused",
520
550
  pauseKind: "error",
521
- pauseReason: `send-retry storm: ${mins}m of 50ms re-arms — the session never went idle for the continuation`,
522
- pauseSuggestedAction: "The session never went idle for the send (wedged queue or permanently busy). Restart pi, then /goal resume.",
551
+ pauseReason: `send-retry storm: ${mins}m of re-arms with no session activity for ${silent}m — the session never went idle for the continuation`,
552
+ pauseSuggestedAction: "The session produced no events while the send retried (wedged queue). Restart pi, then /goal resume.",
523
553
  }, ctx);
524
- ctx.ui.notify(`Goal paused: send-retry storm (${mins}m). Restart pi, then /goal resume.`, "warning");
554
+ ctx.ui.notify(`Goal paused: send-retry storm (${mins}m, session silent ${silent}m). Restart pi, then /goal resume.`, "warning");
525
555
  notifyExternal(ctx, "Goal paused: send-retry storm.");
526
556
  }
527
557
  }
@@ -736,10 +766,10 @@ function sendContinuation(goalId: string): void {
736
766
  return;
737
767
  }
738
768
  if (!ctx.isIdle() || ctx.hasPendingMessages()) {
739
- // v0.28.5 (E3): count + ledger + escalate the re-arm storm.
740
769
  accountSendRearm(ctx, "continuation");
741
770
  continuationScheduledFor = goalId;
742
- continuationTimer = setTimeout(() => sendContinuation(goalId), BACKOFF_IDLE_RETRY_MS);
771
+ // v0.28.29: backing-off cadence (was flat 50ms — 6,000 spins in 5m).
772
+ continuationTimer = setTimeout(() => sendContinuation(goalId), sendRearmDelayMs(continuationRearmStreak));
743
773
  continuationTimer.unref?.();
744
774
  return;
745
775
  }
@@ -750,7 +780,7 @@ function sendContinuation(goalId: string): void {
750
780
  content: continuationPrompt(state.goal!),
751
781
  display: false,
752
782
  }, { triggerTurn: true, deliverAs: "followUp" });
753
- continuationRearmStreak = 0; // v0.28.5 (E3): a landed send clears the storm
783
+ continuationRearmStreak = 0; continuationRearmSince = 0; // v0.28.5 (E3): a landed send clears the storm
754
784
  appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
755
785
  } catch (err) {
756
786
  appendLedger(ctx.cwd, "goal_continuation_send_failed", { goalId, error: err instanceof Error ? err.message : String(err) });
@@ -2011,8 +2041,8 @@ function sendLoopTurn(): void {
2011
2041
  if (!isLoopActive() || !extensionApi) return;
2012
2042
  const ctx = freshCtx();
2013
2043
  if (!ctx || !ctx.isIdle() || ctx.hasPendingMessages()) {
2014
- if (ctx) accountSendRearm(ctx, "loop"); // v0.28.5 (E3)
2015
- loopTimer = setTimeout(() => sendLoopTurn(), BACKOFF_IDLE_RETRY_MS);
2044
+ if (ctx) accountSendRearm(ctx, "loop");
2045
+ loopTimer = setTimeout(() => sendLoopTurn(), sendRearmDelayMs(loopRearmStreak)); // v0.28.29: backing-off cadence
2016
2046
  loopTimer.unref?.();
2017
2047
  return;
2018
2048
  }
@@ -2059,7 +2089,7 @@ function sendLoopTurn(): void {
2059
2089
  }, { triggerTurn: true, deliverAs: "followUp" });
2060
2090
  // v0.26.1: the send path is ledgered — the hegemon zombie spun 619
2061
2091
  // refires with zero visibility into whether sends were landing.
2062
- loopRearmStreak = 0; // v0.28.5 (E3): a landed turn clears the storm
2092
+ loopRearmStreak = 0; loopRearmSince = 0; // v0.28.5 (E3): a landed turn clears the storm
2063
2093
  appendLedger(ctx.cwd, "loop_turn_sent", { iteration: loop.iteration });
2064
2094
  } catch (err) {
2065
2095
  // stale API — next agent_end reschedules (but if none comes, the
@@ -4702,8 +4732,8 @@ export default function (pi: ExtensionAPI): void {
4702
4732
  // v0.28.24: a compaction is LEGITIMATE busy time — reset the send-rearm
4703
4733
  // storm streaks (π-web nearly escalated a "send-retry storm" pause during
4704
4734
  // a 3.5-minute compact) and open the post-compaction stall grace.
4705
- continuationRearmStreak = 0;
4706
- loopRearmStreak = 0;
4735
+ continuationRearmStreak = 0; continuationRearmSince = 0;
4736
+ loopRearmStreak = 0; loopRearmSince = 0;
4707
4737
  compactionGraceUntil = Date.now() + COMPACTION_GRACE_MS;
4708
4738
  const settle = setTimeout(() => {
4709
4739
  const c = freshCtx();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.28",
3
+ "version": "0.28.29",
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",