pi-goal-list-loop-audit 0.34.13 → 0.34.15
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.
- package/extensions/goal-loop-core.ts +2 -0
- package/extensions/loops/goal.ts +100 -17
- package/package.json +1 -1
- package/schemas/goal.schema.json +1 -0
|
@@ -176,6 +176,8 @@ export interface Goal {
|
|
|
176
176
|
* At 3 the goal pauses loudly — a broken auditor model must not spin a
|
|
177
177
|
* silent retry-forever loop. Cleared on any real auditor run. */
|
|
178
178
|
auditInfraStreak?: number;
|
|
179
|
+
/** v0.34.15: persisted error-brake rung — survives /reload so the 6-brake park can engage. */
|
|
180
|
+
errorBrakeStreak?: number;
|
|
179
181
|
/** v0.28.26: the completion claim captured when an audit attempt is
|
|
180
182
|
* quota-blocked. The quota retry re-runs the AUDITOR directly with this
|
|
181
183
|
* stored claim instead of re-engaging the agent — re-engaging produced a
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -411,6 +411,30 @@ function attemptAutoRecovery(ctx: ExtensionContext, where: string): boolean {
|
|
|
411
411
|
return true;
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
/** v0.34.14: /reload rebind detector. The extension runs INSIDE pi, so
|
|
415
|
+
* process.pid IS pi's pid: an instance that boots and finds its OWN pid
|
|
416
|
+
* already in the owner file is a /reload rebuild of a live session, not a
|
|
417
|
+
* cold boot. Rebinds always resume active goals/loops — holding mid-work
|
|
418
|
+
* after an in-place rebuild is pure friction (user directive: keep going
|
|
419
|
+
* unless we must stop; "the list is not continuing" after /reload,
|
|
420
|
+
* hellhunter 2026-08-01). Cold boots (new pid) still honor autoresume=off.
|
|
421
|
+
* Sidecar, not the ledger: read-before-write must be atomic-ish and the
|
|
422
|
+
* ledger is append-only. */
|
|
423
|
+
const SESSION_OWNER_FILE = "session-owner.json";
|
|
424
|
+
function claimSessionOwnerAndDetectRebind(cwd: string): boolean {
|
|
425
|
+
try {
|
|
426
|
+
const p = path.join(piGlaDir(cwd), SESSION_OWNER_FILE);
|
|
427
|
+
let prevPid: number | null = null;
|
|
428
|
+
try {
|
|
429
|
+
prevPid = (JSON.parse(fs.readFileSync(p, "utf-8")) as { pid?: number }).pid ?? null;
|
|
430
|
+
} catch { /* absent or corrupt — first boot */ }
|
|
431
|
+
fs.writeFileSync(p, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }));
|
|
432
|
+
return prevPid !== null && prevPid === process.pid;
|
|
433
|
+
} catch {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
414
438
|
/** v0.34.13: consume the sidecar marker on session restore. Single-use,
|
|
415
439
|
* freshness-bounded — a stale marker from an abandoned recovery must not
|
|
416
440
|
* surprise-resume a later session. */
|
|
@@ -680,7 +704,7 @@ const ZOMBIE_RUN_ALERT_THROTTLE_MS = 10 * 60_000;
|
|
|
680
704
|
// (sendMessage never threw; session reported idle) but started NO turn —
|
|
681
705
|
// transcript frozen, tokens flat, 10+ minutes of refires into the void.
|
|
682
706
|
// Same family as the post-compaction dropped trigger (v0.26.5), but the
|
|
683
|
-
// pending
|
|
707
|
+
// the pending latch needs idle&&pending and pi reported no pending
|
|
684
708
|
// here, and the zombie watchdog needs busy — this shape falls between both
|
|
685
709
|
// chairs. Disarm signal = real activity (agent_end/tool_call) AFTER the
|
|
686
710
|
// last send; a landed turn — even a lazy text-only one — disarms it.
|
|
@@ -825,7 +849,6 @@ const COMPACTION_GRACE_MS = 3 * 60_000;
|
|
|
825
849
|
// budget now spans ~5.5m) and escalate the brake cooldown per consecutive
|
|
826
850
|
// brake (1m, 2m, 4m, 8m, 16m cap). A successful turn resets both.
|
|
827
851
|
const ERROR_RETRY_LADDER_MS = [5_000, 15_000, 45_000, 90_000, 180_000];
|
|
828
|
-
let errorBrakeStreak = 0;
|
|
829
852
|
const SEND_REARM_LEDGER_MILESTONES_MS = [2 * 60_000, 5 * 60_000, 10 * 60_000];
|
|
830
853
|
// v0.28.29: escalation is TIME-based and ACTIVITY-gated. A busy session is
|
|
831
854
|
// NORMAL — the user conversing, or one long subagent turn — and the old
|
|
@@ -1235,6 +1258,39 @@ function freshCtx(): ExtensionContext | null {
|
|
|
1235
1258
|
}
|
|
1236
1259
|
}
|
|
1237
1260
|
|
|
1261
|
+
// v0.34.15 (hegemon 2026-08-01): pi ACCEPTED the continuation — footer showed
|
|
1262
|
+
// "1 queued" — but the turn trigger was dead, so the message sat queued while
|
|
1263
|
+
// pi idled. The 0.34.11 watchdog gates on "pi reported NO pending" and the
|
|
1264
|
+
// stall ladder takes ~10 minutes; a send that lands queued-without-a-turn is
|
|
1265
|
+
// a CONFIRMED dead trigger (hegemon law), so probe once, ~45s after every
|
|
1266
|
+
// landed send, and go straight to auto-recovery. A consumed message (even an
|
|
1267
|
+
// instant-429 turn consumes it) or any real activity disarms the probe.
|
|
1268
|
+
function queueStuckProbeMs(): number {
|
|
1269
|
+
return Number(process.env.GLLA_QUEUE_STUCK_MS ?? 45_000);
|
|
1270
|
+
}
|
|
1271
|
+
let queueStuckProbe: ReturnType<typeof setTimeout> | null = null;
|
|
1272
|
+
function armQueueStuckProbe(ctx: ExtensionContext, sentAt: number): void {
|
|
1273
|
+
if (queueStuckProbe) clearTimeout(queueStuckProbe);
|
|
1274
|
+
queueStuckProbe = setTimeout(() => {
|
|
1275
|
+
queueStuckProbe = null;
|
|
1276
|
+
try {
|
|
1277
|
+
if (isForeignCtx(ctx)) return; // stale instance — the live one probes
|
|
1278
|
+
if (!isSupervising()) return; // paused/completed meanwhile
|
|
1279
|
+
if (lastContinuationSentAt !== sentAt) return; // a newer send armed its own probe
|
|
1280
|
+
if (lastRealActivityAt > sentAt) return; // the turn started and worked
|
|
1281
|
+
if (!ctx.isIdle()) return; // a turn is running — healthy
|
|
1282
|
+
if (!ctx.hasPendingMessages()) return; // consumed — even an instant 429 consumes
|
|
1283
|
+
appendLedger(ctx.cwd, "queue_stuck_detected", { waitedMs: Date.now() - sentAt });
|
|
1284
|
+
if (!attemptAutoRecovery(ctx, "queue-stuck continuation")) {
|
|
1285
|
+
const msg = `${goalNoun()}: the continuation is QUEUED but pi won't start a turn — the turn trigger is dead (re-sends only queue). Cure: /reload — the goal resumes itself after the rebuild (autoresume off? /glla resume).`;
|
|
1286
|
+
ctx.ui.notify(msg, "warning");
|
|
1287
|
+
notifyExternal(ctx, msg);
|
|
1288
|
+
}
|
|
1289
|
+
} catch { /* stale ctx — the live instance owns the probe now */ }
|
|
1290
|
+
}, queueStuckProbeMs());
|
|
1291
|
+
queueStuckProbe.unref?.();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1238
1294
|
function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: number): void {
|
|
1239
1295
|
abortedStandDown = false; // v0.29.5: any explicit schedule ends the stand-down
|
|
1240
1296
|
if (!isActionableGoal()) return;
|
|
@@ -1291,6 +1347,7 @@ function sendContinuation(goalId: string): void {
|
|
|
1291
1347
|
continuationRearmStreak = 0; continuationRearmSince = 0; // v0.28.5 (E3): a landed send clears the storm
|
|
1292
1348
|
appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
|
|
1293
1349
|
lastContinuationSentAt = Date.now();
|
|
1350
|
+
armQueueStuckProbe(ctx, lastContinuationSentAt);
|
|
1294
1351
|
} catch (err) {
|
|
1295
1352
|
appendLedger(ctx.cwd, "goal_continuation_send_failed", { goalId, error: err instanceof Error ? err.message : String(err) });
|
|
1296
1353
|
// v0.26.7: stale runtime = terminal (sends can never land); anything
|
|
@@ -2973,6 +3030,7 @@ function sendLoopTurn(): void {
|
|
|
2973
3030
|
loopRearmStreak = 0; loopRearmSince = 0; // v0.28.5 (E3): a landed turn clears the storm
|
|
2974
3031
|
appendLedger(ctx.cwd, "loop_turn_sent", { iteration: loop.iteration });
|
|
2975
3032
|
lastContinuationSentAt = Date.now();
|
|
3033
|
+
armQueueStuckProbe(ctx, lastContinuationSentAt);
|
|
2976
3034
|
} catch (err) {
|
|
2977
3035
|
// stale API — next agent_end reschedules (but if none comes, the
|
|
2978
3036
|
// heartbeat's stall escalation stops the spin — v0.26.1).
|
|
@@ -3679,7 +3737,13 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
3679
3737
|
// logged as disapprovals.
|
|
3680
3738
|
const auditorRan = result.output.trim().length > 0;
|
|
3681
3739
|
// v0.28.5 (E2): a REAL auditor run clears the infra-error streak.
|
|
3682
|
-
|
|
3740
|
+
// v0.34.14: …but only a CLEAN one. A STALLED run returns the partial
|
|
3741
|
+
// output it streamed before the abort — non-empty, so auditorRan is
|
|
3742
|
+
// true — while result.error still marks it an infrastructure failure.
|
|
3743
|
+
// Clearing the streak on those meant the 3-strike breaker at :3874
|
|
3744
|
+
// NEVER engaged: pully 2026-08-01 looped 10-min stall cycles for 4h
|
|
3745
|
+
// (the auditor hung on an ssh/sudo verification every attempt).
|
|
3746
|
+
if (auditorRan && !result.error && (state.goal.auditInfraStreak ?? 0) > 0) updateGoal({ auditInfraStreak: undefined }, ctx);
|
|
3683
3747
|
const history = state.goal.auditHistory ?? [];
|
|
3684
3748
|
if (auditorRan) {
|
|
3685
3749
|
// v0.25.4: strip think-block leakage (MiniMax-M3 `</think>`
|
|
@@ -3878,11 +3942,11 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
3878
3942
|
auditHistory: history,
|
|
3879
3943
|
auditInfraStreak: infraStreak,
|
|
3880
3944
|
pauseKind: "error",
|
|
3881
|
-
pauseReason: `auditor infrastructure failed ${infraStreak}× in a row — the auditor model is likely broken (last: ${result.error.slice(0, 120)})`,
|
|
3945
|
+
pauseReason: `auditor infrastructure failed ${infraStreak}× in a row — the auditor model is likely broken OR a verification command is hanging (ssh/sudo/long test runs stall the stream) (last: ${result.error.slice(0, 120)})`,
|
|
3882
3946
|
pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) or restart pi, then /goal resume. Your work was NOT judged.",
|
|
3883
3947
|
}, ctx);
|
|
3884
3948
|
appendLedger(ctx.cwd, "goal_paused", { reason: `auditor infra streak ${infraStreak}: ${result.error.slice(0, 120)}` });
|
|
3885
|
-
ctx.ui.notify(`${goalNoun()} paused: auditor infrastructure failed ${infraStreak}× in a row
|
|
3949
|
+
ctx.ui.notify(`${goalNoun()} paused: auditor infrastructure failed ${infraStreak}× in a row — model broken or a verification command hanging (ssh/sudo/long runs). Fix with /glla model=... or unblock the command, then /goal resume.`, "warning");
|
|
3886
3950
|
notifyExternal(ctx, `${goalNoun()} paused: auditor infrastructure ${infraStreak}× — model likely broken.`);
|
|
3887
3951
|
return {
|
|
3888
3952
|
content: [{
|
|
@@ -6418,9 +6482,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6418
6482
|
// v0.34.13: an auto-recovery /reload carries its own resume consent —
|
|
6419
6483
|
// the sidecar marker overrides autoresume=off for THIS restore only.
|
|
6420
6484
|
const recoveryResume = consumeRecoveryResume(ctx.cwd);
|
|
6485
|
+
// v0.34.14: a /reload rebind (same pi pid) ALWAYS resumes — the session
|
|
6486
|
+
// is live mid-work; holding is the "list is not continuing" bug.
|
|
6487
|
+
const rebindResume = claimSessionOwnerAndDetectRebind(ctx.cwd);
|
|
6488
|
+
if (rebindResume) appendLedger(ctx.cwd, "rebind_resume", { pid: process.pid });
|
|
6421
6489
|
if (isLoopActive()) {
|
|
6422
6490
|
const l = state.loop!;
|
|
6423
|
-
if (autoResume || recoveryResume) {
|
|
6491
|
+
if (autoResume || recoveryResume || rebindResume) {
|
|
6424
6492
|
ctx.ui.notify(
|
|
6425
6493
|
`Resuming loop (iteration ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"}, best ${l.bestValue ?? "n/a"}, stall ${l.stallCount}/${l.plateauWindow}): ${l.target.slice(0, 60)}`,
|
|
6426
6494
|
"info",
|
|
@@ -6441,7 +6509,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6441
6509
|
// "load it but not auto start it"). Interrupted goals hold like
|
|
6442
6510
|
// everything else; autoresume=on (unattended rigs) still auto-resumes
|
|
6443
6511
|
// them, and the marker is cleared only on that promised auto-resume.
|
|
6444
|
-
if (autoResume || recoveryResume) {
|
|
6512
|
+
if (autoResume || recoveryResume || rebindResume) {
|
|
6445
6513
|
// v0.28.1 (S2): clear the stale-handle interrupt marker — this IS
|
|
6446
6514
|
// the auto-resume the marker promised.
|
|
6447
6515
|
if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
|
|
@@ -6697,12 +6765,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6697
6765
|
// 60-second provider hiccup waiting on a manual /goal resume.
|
|
6698
6766
|
const detail = text.trim() ? ` (last: ${text.trim().replace(/\s+/g, " ").slice(0, 160)})` : "";
|
|
6699
6767
|
const reason = `5 consecutive errors${detail}`;
|
|
6768
|
+
// v0.34.15: the streak now lives ON THE GOAL so it survives the
|
|
6769
|
+
// auto-recovery /reloads that used to zero the module counter —
|
|
6770
|
+
// hegemon 2026-08-01: a hard-exhausted MiniMax plan churned 1-minute
|
|
6771
|
+
// probes for an hour because every reload reset the ladder to rung 1
|
|
6772
|
+
// and the 6-brake park (v0.29.9) could never engage.
|
|
6773
|
+
const brakeStreak = state.goal!.errorBrakeStreak ?? 0;
|
|
6774
|
+
// v0.34.15: a quota/rate-limit wall is NOT a flake — the card must
|
|
6775
|
+
// say "resuming won't help; switch /model or wait out the window"
|
|
6776
|
+
// (the raw 429 text was in `detail` but nobody parses JSON on a card).
|
|
6777
|
+
const quotaWall = /rate.?limit|usage limit|quota|insufficient|credits/i.test(detail);
|
|
6700
6778
|
// v0.29.1: brake-cycle CAP. The v0.28.25 ladder slows the thrash
|
|
6701
6779
|
// (1m→16m) but never STOPS it — junk-runner/hellhunter/pully each
|
|
6702
6780
|
// burned 4+ pause↔retry cycles against provider windows that last
|
|
6703
6781
|
// hours. After 6 consecutive brakes: park. v0.29.9: the park keeps
|
|
6704
6782
|
// probing at the top of each hour (clock-aligned window resets).
|
|
6705
|
-
if (
|
|
6783
|
+
if (brakeStreak >= 6) {
|
|
6706
6784
|
// v0.29.9: park — but keep probing at the top of each hour
|
|
6707
6785
|
// (user: "simply adding an hourly retry … just to pick up work
|
|
6708
6786
|
// faster assuming the retry expired"). Coding-plan rate-limit
|
|
@@ -6716,18 +6794,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6716
6794
|
status: "paused",
|
|
6717
6795
|
pauseKind: "error",
|
|
6718
6796
|
pauseReason: `${reason} — 6 error-brakes in a row; the provider has been erroring for an extended window`,
|
|
6719
|
-
pauseSuggestedAction:
|
|
6797
|
+
pauseSuggestedAction: quotaWall
|
|
6798
|
+
? "Provider quota/rate-limit wall — resuming won't help until the window resets. Hourly top-of-hour probes will pick work back up; switch /model to a different provider to continue immediately."
|
|
6799
|
+
: "Probing at the top of each hour — rate-limit windows typically expire on clock-hour boundaries. /goal resume retries now.",
|
|
6720
6800
|
}, ctx);
|
|
6721
|
-
ctx.ui.notify(`${goalNoun()} parked: ${reason} — 6 brakes in a row. Hourly top-of-hour probes will pick work back up when the window opens
|
|
6801
|
+
ctx.ui.notify(`${goalNoun()} parked: ${reason} — 6 brakes in a row. ${quotaWall ? "Quota/rate-limit wall — switching /model continues immediately; otherwise hourly" : "Hourly"} top-of-hour probes will pick work back up when the window opens.`, "warning");
|
|
6722
6802
|
notifyExternal(ctx, `${goalNoun()} parked: provider erroring across 6 error-brake cycles — hourly top-of-hour probes scheduled.`);
|
|
6723
|
-
appendLedger(ctx.cwd, "error_brake_capped", { streak:
|
|
6803
|
+
appendLedger(ctx.cwd, "error_brake_capped", { streak: brakeStreak, reason });
|
|
6724
6804
|
const probeMs = msUntilNextHourBoundary(Date.now());
|
|
6725
6805
|
scheduleQuotaRetry(ctx, probeMs / 1000, reason, () => {
|
|
6726
6806
|
// Re-check: only probe if STILL parked by the error-brake cap —
|
|
6727
6807
|
// a user pause/resume/cancel meanwhile is never stomped.
|
|
6728
6808
|
if (state.goal && state.goal.status === "paused" && state.goal.pauseKind === "error"
|
|
6729
6809
|
&& (state.goal.pauseReason ?? "").includes("error-brakes in a row")) {
|
|
6730
|
-
appendLedger(ctx.cwd, "hourly_rate_probe", { goalId: state.goal.id, streak: errorBrakeStreak });
|
|
6810
|
+
appendLedger(ctx.cwd, "hourly_rate_probe", { goalId: state.goal.id, streak: state.goal.errorBrakeStreak ?? 0 });
|
|
6731
6811
|
updateGoal({ status: "active" }, ctx);
|
|
6732
6812
|
appendLedger(ctx.cwd, "goal_resumed", { via: "hourly-rate-probe" });
|
|
6733
6813
|
ctx.ui.notify("Hourly probe: resuming (rate-limit windows typically expire at the top of the hour).", "info");
|
|
@@ -6738,17 +6818,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6738
6818
|
}
|
|
6739
6819
|
// v0.28.25: the cooldown escalates per CONSECUTIVE brake — a fleet-wide
|
|
6740
6820
|
// 403 window is not cleared by re-braking every 60 seconds.
|
|
6741
|
-
const cooldownMs = 60_000 * 2 ** Math.min(
|
|
6821
|
+
const cooldownMs = 60_000 * 2 ** Math.min(brakeStreak, 4);
|
|
6742
6822
|
const cooldownMin = Math.round(cooldownMs / 60_000);
|
|
6743
|
-
errorBrakeStreak++;
|
|
6744
6823
|
updateGoal({
|
|
6745
6824
|
status: "paused",
|
|
6746
6825
|
pauseKind: "wait",
|
|
6747
6826
|
pauseResumeAt: new Date(Date.now() + cooldownMs).toISOString(),
|
|
6748
6827
|
pauseReason: reason,
|
|
6749
|
-
|
|
6828
|
+
errorBrakeStreak: brakeStreak + 1,
|
|
6829
|
+
pauseSuggestedAction: quotaWall
|
|
6830
|
+
? `Provider quota/rate-limit wall — resuming won't help until the window resets. Switch /model to a different provider to continue now, or let the probe auto-resume in ${cooldownMin}m.`
|
|
6831
|
+
: `Transient provider flake? The goal auto-resumes once in ${cooldownMin}m if still paused for this reason — or /goal resume now.`,
|
|
6750
6832
|
}, ctx);
|
|
6751
|
-
ctx.ui.notify(`Goal paused: ${reason}
|
|
6833
|
+
ctx.ui.notify(`Goal paused: ${reason}.${quotaWall ? " Quota/rate-limit wall — resuming won't help until the window resets; switch /model to continue now." : ""}`, "warning");
|
|
6752
6834
|
notifyExternal(ctx, `Goal paused: ${reason}.`);
|
|
6753
6835
|
appendLedger(ctx.cwd, "goal_paused", { reason });
|
|
6754
6836
|
scheduleQuotaRetry(ctx, cooldownMs / 1000, reason, () => {
|
|
@@ -6799,7 +6881,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6799
6881
|
} else {
|
|
6800
6882
|
consecutiveErrorIterations = 0;
|
|
6801
6883
|
consecutiveAbortIterations = 0;
|
|
6802
|
-
|
|
6884
|
+
// v0.28.25/v0.34.15: a healthy turn clears the (now persisted) brake streak
|
|
6885
|
+
if ((state.goal?.errorBrakeStreak ?? 0) > 0) updateGoal({ errorBrakeStreak: undefined }, ctx);
|
|
6803
6886
|
}
|
|
6804
6887
|
|
|
6805
6888
|
// No wall-clock cap by design: a goal ends via completion, explicit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.15",
|
|
4
4
|
"description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. An isolated extension-less auditor re-verifies every completion with raw evidence; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|
package/schemas/goal.schema.json
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"interruptedAt": { "type": "string" },
|
|
54
54
|
"interruptedReason": { "type": "string" },
|
|
55
55
|
"auditInfraStreak": { "type": "number" },
|
|
56
|
+
"errorBrakeStreak": { "type": "number" },
|
|
56
57
|
"pendingCompletion": { "type": "object" },
|
|
57
58
|
"createdVia": { "type": "string" },
|
|
58
59
|
"activePath": { "type": "string" },
|