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.
@@ -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
@@ -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-latch watchdog needs idle&&pending and pi reported no 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
- if (auditorRan && (state.goal.auditInfraStreak ?? 0) > 0) updateGoal({ auditInfraStreak: undefined }, ctx);
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. Fix the auditor model (/glla model=...), then /goal resume.`, "warning");
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 (errorBrakeStreak >= 6) {
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: "Probing at the top of each hour — rate-limit windows typically expire on clock-hour boundaries. /goal resume retries now.",
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; /goal resume retries now.`, "warning");
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: errorBrakeStreak, reason });
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(errorBrakeStreak, 4);
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
- pauseSuggestedAction: `Transient provider flake? The goal auto-resumes once in ${cooldownMin}m if still paused for this reason — or /goal resume now.`,
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}.`, "warning");
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
- errorBrakeStreak = 0; // v0.28.25: a healthy turn clears the brake cooldown
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.13",
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",
@@ -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" },