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, `├─ ${
|
|
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
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
|
486
|
-
|
|
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
|
-
|
|
495
|
-
|
|
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 (
|
|
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):
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
const mins = Math.round(
|
|
508
|
-
|
|
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
|
|
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
|
|
522
|
-
pauseSuggestedAction: "The session
|
|
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(
|
|
525
|
-
notifyExternal(ctx,
|
|
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(
|
|
549
|
-
notifyExternal(ctx,
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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");
|
|
2015
|
-
loopTimer = setTimeout(() => sendLoopTurn(),
|
|
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(
|
|
2811
|
-
notifyExternal(ctx,
|
|
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(
|
|
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(
|
|
2979
|
-
notifyExternal(ctx,
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|