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. */
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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"
|
|
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:
|
|
4327
|
-
pauseSuggestedAction: "
|
|
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
|
|
4330
|
-
|
|
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
|
-
|
|
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.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",
|
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": {
|