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

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.
@@ -276,11 +276,16 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
276
276
  // queue work were a standalone goal.
277
277
  const isList = g.policy === "list";
278
278
  const statusWord = g.status === "active" ? paint(theme, "success", "active") : g.status;
279
+ // v0.28.30: the status line ALWAYS names the type (user note: "I don't
280
+ // always see the type — I'd need to scroll up to see if goal/list/loop").
281
+ // Before, only list items were named; a plain goal's card said "paused ·
282
+ // 3m" with no type word. The loop surface has its own card.
283
+ const typeWord = isList ? "list item · " : "goal · ";
279
284
  // Token segment only when a budget is set (v0.22.0): the guard is opt-in,
280
285
  // and "0/0 tok" carried no information when off.
281
286
  const tokenLimit = g.usage?.tokensLimit ?? 0;
282
287
  const tokens = tokenLimit > 0 ? ` · ${paint(theme, "dim", `${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(tokenLimit)} tok`)}` : "";
283
- const lines = [head, `├─ ${isList ? "list item · " : ""}${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))}${tokens}`];
288
+ const lines = [head, `├─ ${typeWord}${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))}${tokens}`];
284
289
  if (g.status === "auditing") {
285
290
  lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
286
291
  // v0.25.4: auditor-quiet stall — progress events stopped arriving
@@ -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,11 +548,11 @@ 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");
525
- notifyExternal(ctx, "Goal paused: send-retry storm.");
554
+ ctx.ui.notify(`${goalNoun()} paused: send-retry storm (${mins}m, session silent ${silent}m). Restart pi, then /goal resume.`, "warning");
555
+ notifyExternal(ctx, `${goalNoun()} paused: send-retry storm.`);
526
556
  }
527
557
  }
528
558
 
@@ -545,8 +575,8 @@ function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
545
575
  pauseReason: `stalled: ${threshold} continuation refires landed no turn`,
546
576
  pauseSuggestedAction: "The continuation chain is broken in this process (wedged message queue or stale API). Restart pi, then /goal resume.",
547
577
  }, ctx);
548
- ctx.ui.notify(`Goal paused: ${threshold} refires produced no turn. Restart pi, then /goal resume.`, "warning");
549
- notifyExternal(ctx, "Goal paused: stalled (continuation not landing).");
578
+ ctx.ui.notify(`${goalNoun()} paused: ${threshold} refires produced no turn. Restart pi, then /goal resume.`, "warning");
579
+ notifyExternal(ctx, `${goalNoun()} paused: stalled (continuation not landing).`);
550
580
  return true;
551
581
  }
552
582
  return true;
@@ -629,7 +659,7 @@ function heartbeatTick(): void {
629
659
  })
630
660
  ) {
631
661
  lastWedgeAlertAt = Date.now();
632
- const msg = `Goal appears wedged: no activity for ${Math.round((Date.now() - lastActivityAt) / 60_000)}m while the session is busy — likely a hung command (test/build/dev server without a timeout). Check the session; Esc kills a stuck tool call.`;
662
+ const msg = `${goalNoun()} appears wedged: no activity for ${Math.round((Date.now() - lastActivityAt) / 60_000)}m while the session is busy — likely a hung command (test/build/dev server without a timeout). Check the session; Esc kills a stuck tool call.`;
633
663
  appendLedger(ctx.cwd, "wedge_alert", { silentMs: Date.now() - lastActivityAt });
634
664
  ctx.ui.notify(msg, "warning");
635
665
  notifyExternal(ctx, msg);
@@ -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) });
@@ -880,6 +910,9 @@ let persistenceDegradedNotified = false;
880
910
 
881
911
  /** v0.28.11 (U9): objective-first notifies — truncate long objectives. */
882
912
  const shortObj = (s: string): string => (s.length > 90 ? `${s.slice(0, 87)}…` : s);
913
+ /** v0.28.30: terminology — a list item is not a goal (user note: "we seem
914
+ * to call everything goal"). User-facing pause/abort notifies name the policy. */
915
+ const goalNoun = (): string => (state.goal?.policy === "list" ? "List item" : "Goal");
883
916
  function notifyPersistenceState(ctx: ExtensionContext): void {
884
917
  if (isPersistenceDegraded() && !persistenceDegradedNotified) {
885
918
  persistenceDegradedNotified = true;
@@ -1491,7 +1524,7 @@ async function cmdCancel(ctx: ExtensionContext): Promise<void> {
1491
1524
  }
1492
1525
  archiveCurrentGoal(ctx, "aborted", "user cancelled");
1493
1526
  ctx.abort();
1494
- ctx.ui.notify(`Goal aborted.${isLoopActive() ? " A loop is still active — /loop stop ends it." : ""}`, "info");
1527
+ ctx.ui.notify(`${goalNoun()} aborted.${isLoopActive() ? " A loop is still active — /loop stop ends it." : ""}`, "info");
1495
1528
  }
1496
1529
 
1497
1530
  // ---- v0.28.23: decision picker popup ----
@@ -2011,8 +2044,8 @@ function sendLoopTurn(): void {
2011
2044
  if (!isLoopActive() || !extensionApi) return;
2012
2045
  const ctx = freshCtx();
2013
2046
  if (!ctx || !ctx.isIdle() || ctx.hasPendingMessages()) {
2014
- if (ctx) accountSendRearm(ctx, "loop"); // v0.28.5 (E3)
2015
- loopTimer = setTimeout(() => sendLoopTurn(), BACKOFF_IDLE_RETRY_MS);
2047
+ if (ctx) accountSendRearm(ctx, "loop");
2048
+ loopTimer = setTimeout(() => sendLoopTurn(), sendRearmDelayMs(loopRearmStreak)); // v0.28.29: backing-off cadence
2016
2049
  loopTimer.unref?.();
2017
2050
  return;
2018
2051
  }
@@ -2059,7 +2092,7 @@ function sendLoopTurn(): void {
2059
2092
  }, { triggerTurn: true, deliverAs: "followUp" });
2060
2093
  // v0.26.1: the send path is ledgered — the hegemon zombie spun 619
2061
2094
  // refires with zero visibility into whether sends were landing.
2062
- loopRearmStreak = 0; // v0.28.5 (E3): a landed turn clears the storm
2095
+ loopRearmStreak = 0; loopRearmSince = 0; // v0.28.5 (E3): a landed turn clears the storm
2063
2096
  appendLedger(ctx.cwd, "loop_turn_sent", { iteration: loop.iteration });
2064
2097
  } catch (err) {
2065
2098
  // stale API — next agent_end reschedules (but if none comes, the
@@ -2807,8 +2840,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2807
2840
  pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) or restart pi, then /goal resume. Your work was NOT judged.",
2808
2841
  }, ctx);
2809
2842
  appendLedger(ctx.cwd, "goal_paused", { reason: `auditor infra streak ${infraStreak}: ${result.error.slice(0, 120)}` });
2810
- ctx.ui.notify(`Goal paused: auditor infrastructure failed ${infraStreak}× in a row. Fix the auditor model (/glla model=...), then /goal resume.`, "warning");
2811
- notifyExternal(ctx, `Goal paused: auditor infrastructure ${infraStreak}× — model likely broken.`);
2843
+ ctx.ui.notify(`${goalNoun()} paused: auditor infrastructure failed ${infraStreak}× in a row. Fix the auditor model (/glla model=...), then /goal resume.`, "warning");
2844
+ notifyExternal(ctx, `${goalNoun()} paused: auditor infrastructure ${infraStreak}× — model likely broken.`);
2812
2845
  return {
2813
2846
  content: [{
2814
2847
  type: "text",
@@ -2915,7 +2948,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2915
2948
  pauseReason: `auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap})`,
2916
2949
  pauseSuggestedAction: "Read the audit history (/goal status), fix the actual gap or /goal tweak the objective, then /goal resume. Raise the cap with /glla auditcap=N.",
2917
2950
  }, ctx);
2918
- ctx.ui.notify(`Goal paused: auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap}). /goal status for the reports; /goal resume to continue.`, "warning");
2951
+ ctx.ui.notify(`${goalNoun()} paused: auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap}). /goal status for the reports; /goal resume to continue.`, "warning");
2919
2952
  maybeDecisionPopup(ctx);
2920
2953
  appendLedger(ctx.cwd, "goal_paused", { reason: `disapproval cap: ${trailingDisapprovals} consecutive (cap ${auditCap})` });
2921
2954
  notifyExternal(ctx, `Goal paused: ${trailingDisapprovals} consecutive auditor disapprovals`);
@@ -2975,8 +3008,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2975
3008
  // action. Before, the action only appeared in /goal status and the
2976
3009
  // widget truncated both at ~60 chars, so decision-pauses ("choose a
2977
3010
  // or b") reached the user as an unreadable fragment.
2978
- ctx.ui.notify(`Goal paused: ${p.reason}${p.suggestedAction ? `\n\n→ ${p.suggestedAction}` : ""}`, "info");
2979
- notifyExternal(ctx, `Goal paused: ${(p.suggestedAction ? `${p.reason} → ${p.suggestedAction}` : p.reason).slice(0, 200)}`);
3011
+ ctx.ui.notify(`${goalNoun()} paused: ${p.reason}${p.suggestedAction ? `\n\n→ ${p.suggestedAction}` : ""}`, "info");
3012
+ notifyExternal(ctx, `${goalNoun()} paused: ${(p.suggestedAction ? `${p.reason} → ${p.suggestedAction}` : p.reason).slice(0, 200)}`);
2980
3013
  return { content: [{ type: "text", text: "Goal paused. /goal resume to continue." }], details: {} };
2981
3014
  },
2982
3015
  }));
@@ -4702,8 +4735,8 @@ export default function (pi: ExtensionAPI): void {
4702
4735
  // v0.28.24: a compaction is LEGITIMATE busy time — reset the send-rearm
4703
4736
  // storm streaks (π-web nearly escalated a "send-retry storm" pause during
4704
4737
  // a 3.5-minute compact) and open the post-compaction stall grace.
4705
- continuationRearmStreak = 0;
4706
- loopRearmStreak = 0;
4738
+ continuationRearmStreak = 0; continuationRearmSince = 0;
4739
+ loopRearmStreak = 0; loopRearmSince = 0;
4707
4740
  compactionGraceUntil = Date.now() + COMPACTION_GRACE_MS;
4708
4741
  const settle = setTimeout(() => {
4709
4742
  const c = freshCtx();
@@ -5027,7 +5060,7 @@ export default function (pi: ExtensionAPI): void {
5027
5060
  pauseReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive unproductive turns (no tools, short or repetitive)`,
5028
5061
  pauseSuggestedAction: "Inspect the goal — /goal resume to retry, /goal tweak to narrow it, /goal cancel to abort.",
5029
5062
  }, ctx);
5030
- ctx.ui.notify(`Goal paused: stalled (${HEARTBEAT_MAX_NUDGES} unproductive turns).`, "warning");
5063
+ ctx.ui.notify(`${goalNoun()} paused: stalled (${HEARTBEAT_MAX_NUDGES} unproductive turns).`, "warning");
5031
5064
  maybeDecisionPopup(ctx);
5032
5065
  notifyExternal(ctx, "Goal paused: stalled (no tool calls).");
5033
5066
  return;
@@ -5076,7 +5109,7 @@ export default function (pi: ExtensionAPI): void {
5076
5109
  pauseReason: `token limit exceeded (${used.toLocaleString()} > ${limit.toLocaleString()})`,
5077
5110
  pauseSuggestedAction: "/glla tokenlimit=<n> to raise the cap (or 0 to disable), then /goal resume",
5078
5111
  }, ctx);
5079
- ctx.ui.notify(`Goal paused: token limit exceeded (${used.toLocaleString()} > ${limit.toLocaleString()}). /glla tokenlimit=<n> to raise, 0 to disable.`, "warning");
5112
+ ctx.ui.notify(`${goalNoun()} paused: token limit exceeded (${used.toLocaleString()} > ${limit.toLocaleString()}). /glla tokenlimit=<n> to raise, 0 to disable.`, "warning");
5080
5113
  notifyExternal(ctx, `Goal paused: token limit exceeded (${used} > ${limit}).`);
5081
5114
  return;
5082
5115
  }
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.30",
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",