pi-goal-list-loop-audit 0.34.11 → 0.34.13
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.
|
@@ -202,7 +202,14 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
|
|
|
202
202
|
const kind = pauseKind(g);
|
|
203
203
|
if (kind === "decision") return `glla: ${paint(theme, "accent", "⏸ decision needed")}${heldSuffix}`;
|
|
204
204
|
if (kind === "error") return `glla: ${paint(theme, "error", `⏸ action needed — ${truncate(g.pauseReason ?? "", 30)}`)}${heldSuffix}`;
|
|
205
|
-
if (kind === "wait")
|
|
205
|
+
if (kind === "wait") {
|
|
206
|
+
// v0.34.12: live countdown (the UI ticker keeps rendering through a
|
|
207
|
+
// timed wait) — "resumes in 23m" beats a static clock time, and a
|
|
208
|
+
// passed resumeAt says "resuming…" instead of lying about the past.
|
|
209
|
+
const rms = g.pauseResumeAt ? Date.parse(g.pauseResumeAt) - now : Number.NaN;
|
|
210
|
+
const when = Number.isNaN(rms) ? "" : rms <= 0 ? " · resuming…" : ` · resumes in ${fmtElapsed(rms)}`;
|
|
211
|
+
return `glla: ${paint(theme, "dim", `⏳ waiting${when}`)}${heldSuffix}`;
|
|
212
|
+
}
|
|
206
213
|
const label = `paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
|
|
207
214
|
return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}${heldSuffix}`;
|
|
208
215
|
}
|
|
@@ -73,6 +73,14 @@ export interface Settings {
|
|
|
73
73
|
* assertActive). Default true; only acts when TMUX and TMUX_PANE are
|
|
74
74
|
* set — pi outside tmux just gets the manual warning. */
|
|
75
75
|
autoReloadOnStale?: boolean;
|
|
76
|
+
/** v0.34.13: auto-recovery ladder — when a wedge is detected that only a
|
|
77
|
+
* /reload cures (unanswered continuation, send-retry storm), inject the
|
|
78
|
+
* /reload ITSELF via the v0.29.13 tmux/WezTerm transport and resume the
|
|
79
|
+
* goal/loop after the rebuild (sidecar marker — autoresume=off is a
|
|
80
|
+
* restore-time setting, not a recovery veto). Default true. The one
|
|
81
|
+
* class it does NOT cross: pi restart (transcript-writer dead) — that
|
|
82
|
+
* stays a loud stop for the human. */
|
|
83
|
+
autoRecovery?: boolean;
|
|
76
84
|
/** v0.26.1: consecutive heartbeat refires without a real turn before
|
|
77
85
|
* the goal pauses / loop stops (default 5; 0 = never escalate). */
|
|
78
86
|
stallEscalationRefires?: number;
|
|
@@ -207,6 +215,7 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
|
|
|
207
215
|
"stuckMaxInterventions",
|
|
208
216
|
"stallEscalationRefires",
|
|
209
217
|
"autoReloadOnStale",
|
|
218
|
+
"autoRecovery",
|
|
210
219
|
"stallShortWords",
|
|
211
220
|
"stallSimilarityThreshold",
|
|
212
221
|
"postaudit",
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -346,9 +346,9 @@ function goStaleTerminal(ctx: ExtensionContext, where: string): void {
|
|
|
346
346
|
* fires from the entry-probe path (the most common stale discovery).
|
|
347
347
|
* Opt out: autoReloadOnStale=false. Best-effort: the manual warning
|
|
348
348
|
* already fired, so failures cost nothing. */
|
|
349
|
-
function attemptAutoReload(ctx: ExtensionContext, where: string):
|
|
349
|
+
function attemptAutoReload(ctx: ExtensionContext, where: string): boolean {
|
|
350
350
|
try {
|
|
351
|
-
if (loadSettings(ctx.cwd).autoReloadOnStale === false) return;
|
|
351
|
+
if (loadSettings(ctx.cwd).autoReloadOnStale === false) return false;
|
|
352
352
|
const tmuxPane = process.env.TMUX_PANE;
|
|
353
353
|
const wezPane = process.env.WEZTERM_PANE;
|
|
354
354
|
let transport: "tmux" | "wezterm";
|
|
@@ -372,13 +372,58 @@ function attemptAutoReload(ctx: ExtensionContext, where: string): void {
|
|
|
372
372
|
cmd = `wezterm cli send-text --pane-id ${pane} --no-paste '/reload\r'`;
|
|
373
373
|
} else {
|
|
374
374
|
appendLedger(ctx.cwd, "auto_reload_skipped", { where, reason: "no supported multiplexer (tmux/WezTerm) in env" });
|
|
375
|
-
return;
|
|
375
|
+
return false;
|
|
376
376
|
}
|
|
377
377
|
appendLedger(ctx.cwd, "auto_reload_injected", { where, transport, pane });
|
|
378
378
|
ctx.ui.notify("glla: injecting /reload into this pane — extensions rebuild in place and the fresh instance holds state. /glla resume after (automatic with autoresume=on).", "info");
|
|
379
379
|
exec(cmd, () => { /* best-effort */ });
|
|
380
|
+
return true;
|
|
380
381
|
} catch {
|
|
381
382
|
/* never let recovery take the warning path down */
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** v0.34.13: one rung of the auto-recovery ladder. Returns true when a
|
|
388
|
+
* /reload was injected (caller should NOT also pause/alert the manual
|
|
389
|
+
* cure). Writes the sidecar resume marker FIRST so the fresh instance
|
|
390
|
+
* resumes the goal/loop even with autoresume=off. Throttled — a second
|
|
391
|
+
* wedge inside the window is the pi-restart class and must reach the
|
|
392
|
+
* user as a loud stop, not another reload. */
|
|
393
|
+
let lastAutoRecoveryAt = 0;
|
|
394
|
+
function attemptAutoRecovery(ctx: ExtensionContext, where: string): boolean {
|
|
395
|
+
if (loadSettings(ctx.cwd).autoRecovery === false) return false;
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
if (now - lastAutoRecoveryAt < AUTO_RECOVERY_THROTTLE_MS) return false;
|
|
398
|
+
// Inject FIRST — the throttle stamp + marker must reflect a real
|
|
399
|
+
// recovery, not a no-transport skip (else the watchdog would mislabel
|
|
400
|
+
// the next wedge as the pi-restart class).
|
|
401
|
+
if (!attemptAutoReload(ctx, where)) return false;
|
|
402
|
+
lastAutoRecoveryAt = now;
|
|
403
|
+
try {
|
|
404
|
+
fs.writeFileSync(
|
|
405
|
+
path.join(piGlaDir(ctx.cwd), RECOVERY_RESUME_MARKER),
|
|
406
|
+
JSON.stringify({ at: new Date(now).toISOString(), where }),
|
|
407
|
+
);
|
|
408
|
+
} catch { /* best-effort — the reload still helps; restore just holds */ }
|
|
409
|
+
appendLedger(ctx.cwd, "auto_recovery_reload", { where });
|
|
410
|
+
ctx.ui.notify(`glla auto-recovery: ${where} — the ${isLoopActive() ? "loop" : "goal/list item"} resumes ITSELF after the rebuild (no /glla resume needed; the sidecar marker carries the consent). If this recurs within ${Math.round(AUTO_RECOVERY_THROTTLE_MS / 60_000)}m it's the pi-restart class and you'll get a loud stop.`, "warning");
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** v0.34.13: consume the sidecar marker on session restore. Single-use,
|
|
415
|
+
* freshness-bounded — a stale marker from an abandoned recovery must not
|
|
416
|
+
* surprise-resume a later session. */
|
|
417
|
+
function consumeRecoveryResume(cwd: string): boolean {
|
|
418
|
+
try {
|
|
419
|
+
const p = path.join(piGlaDir(cwd), RECOVERY_RESUME_MARKER);
|
|
420
|
+
if (!fs.existsSync(p)) return false;
|
|
421
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
422
|
+
fs.unlinkSync(p);
|
|
423
|
+
const at = Date.parse((JSON.parse(raw) as { at?: string }).at ?? "");
|
|
424
|
+
return !Number.isNaN(at) && Date.now() - at < RECOVERY_RESUME_FRESH_MS;
|
|
425
|
+
} catch {
|
|
426
|
+
return false;
|
|
382
427
|
}
|
|
383
428
|
}
|
|
384
429
|
|
|
@@ -641,6 +686,25 @@ const ZOMBIE_RUN_ALERT_THROTTLE_MS = 10 * 60_000;
|
|
|
641
686
|
// last send; a landed turn — even a lazy text-only one — disarms it.
|
|
642
687
|
const CONTINUATION_UNANSWERED_MS = 150_000;
|
|
643
688
|
const CONTINUATION_UNANSWERED_THROTTLE_MS = 300_000;
|
|
689
|
+
// v0.34.12: eager-continuation settle delay. Hellhunter 2026-08-01 (post-
|
|
690
|
+
// restart): every turn cycle paid a 60s heartbeat tax because the eager
|
|
691
|
+
// continuation fires AT agent_end — exactly pi's turn-teardown blackhole
|
|
692
|
+
// window — so it vanished and the 60s heartbeat refire did the real work.
|
|
693
|
+
// Ledger showed the tell: goal_continuation_sent pairs + a refire per
|
|
694
|
+
// cycle. Sending 2.5s AFTER agent_end lets teardown settle; the send lands
|
|
695
|
+
// and the next turn starts immediately. 2.5s per turn beats 60s per turn.
|
|
696
|
+
const EAGER_CONTINUATION_SETTLE_MS = Number(process.env.GLLA_EAGER_SETTLE_MS ?? 2_500);
|
|
697
|
+
// v0.34.13: auto-recovery ladder ("keep going unless we MUST stop — a
|
|
698
|
+
// question, or done" — user directive 2026-08-01). A wedge that only a
|
|
699
|
+
// /reload cures should /reload ITSELF: inject the keystrokes (v0.29.13
|
|
700
|
+
// transport) with a sidecar marker so the fresh instance RESUMES even when
|
|
701
|
+
// autoresume=off (the consent came from autoRecovery at recovery time, not
|
|
702
|
+
// the restore-time setting). Throttled to one attempt per window: a
|
|
703
|
+
// recurrence inside the window is the transcript-writer-dead class, which
|
|
704
|
+
// only a pi restart cures — that one stays a loud stop for the human.
|
|
705
|
+
const AUTO_RECOVERY_THROTTLE_MS = 600_000;
|
|
706
|
+
const RECOVERY_RESUME_MARKER = "recovery-resume.json";
|
|
707
|
+
const RECOVERY_RESUME_FRESH_MS = 300_000;
|
|
644
708
|
// v0.29.19: dead-turn caps (agent_end exemption path). 6 consecutive
|
|
645
709
|
// provider-error turns = a real outage, not bad luck — stop honestly.
|
|
646
710
|
// 3 consecutive user aborts = the user means it (user aborts mean STOP).
|
|
@@ -718,7 +782,9 @@ function startUITicker(): void {
|
|
|
718
782
|
if (uiTicker) return;
|
|
719
783
|
uiTicker = setInterval(() => {
|
|
720
784
|
const ctx = freshCtx();
|
|
721
|
-
|
|
785
|
+
// v0.34.12: keep ticking during a timed wait-pause too — the status
|
|
786
|
+
// line counts down to resumeAt live (pully field request 2026-08-01).
|
|
787
|
+
if (ctx && (isSupervising() || (state.goal?.status === "paused" && !!state.goal.pauseResumeAt))) refreshUI(ctx);
|
|
722
788
|
}, 1_000);
|
|
723
789
|
uiTicker.unref?.();
|
|
724
790
|
}
|
|
@@ -815,6 +881,10 @@ function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "l
|
|
|
815
881
|
const silent = Math.round(SEND_REARM_ESCALATE_SILENT_MS / 60000);
|
|
816
882
|
appendLedger(ctx.cwd, "send_rearm_escalated", { kind, afterMinutes: mins, silentMinutes: silent });
|
|
817
883
|
if (kind === "loop" && isLoopActive()) {
|
|
884
|
+
if (attemptAutoRecovery(ctx, "send-retry storm")) {
|
|
885
|
+
appendLedger(ctx.cwd, "send_rearm_escalated_suppressed", { reason: "auto-recovery reload" });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
818
888
|
clearLoopTimer();
|
|
819
889
|
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. Press Escape to cancel the stuck run (pi's own rate-limit retry holds it; pi prints "escape to cancel"), then /loop resume — the loop holds on restore. If still wedged: /reload rebuilds extensions in place, then /loop resume again. Restart pi only if /reload itself fails.` };
|
|
820
890
|
persistState(ctx);
|
|
@@ -838,6 +908,14 @@ function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "l
|
|
|
838
908
|
return;
|
|
839
909
|
}
|
|
840
910
|
if (state.goal && state.goal.status === "active") {
|
|
911
|
+
// v0.34.13: keep going unless we MUST stop — try the auto-recovery
|
|
912
|
+
// /reload before spending the user's attention on a pause. A reload
|
|
913
|
+
// that fails to cure throttles the next attempt, and the pause below
|
|
914
|
+
// fires then as today.
|
|
915
|
+
if (attemptAutoRecovery(ctx, "send-retry storm")) {
|
|
916
|
+
appendLedger(ctx.cwd, "send_rearm_escalated_suppressed", { reason: "auto-recovery reload" });
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
841
919
|
updateGoal({
|
|
842
920
|
status: "paused",
|
|
843
921
|
pauseKind: "error",
|
|
@@ -853,6 +931,12 @@ function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
|
|
|
853
931
|
if (!shouldEscalateStall(consecutiveStalls, threshold)) return false;
|
|
854
932
|
consecutiveStalls = 0;
|
|
855
933
|
appendLedger(ctx.cwd, "stall_escalated", { threshold, kind: isLoopActive() ? "loop" : "goal" });
|
|
934
|
+
// v0.34.13: recovery before stop — usually throttled (the 2.5min
|
|
935
|
+
// watchdog already tried), in which case the stop proceeds as today.
|
|
936
|
+
if (attemptAutoRecovery(ctx, "stall escalation")) {
|
|
937
|
+
appendLedger(ctx.cwd, "stall_escalated_suppressed", { reason: "auto-recovery reload", threshold });
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
856
940
|
if (isLoopActive()) {
|
|
857
941
|
clearLoopTimer();
|
|
858
942
|
state.loop = { ...state.loop!, active: false, stopReason: `stalled: ${threshold} continuation refires landed no turn — the session is not continuing (wedged message queue or stale API). Press Escape to cancel any stuck run, then /loop resume — the loop holds on restore. If the handle is stale, /reload rebuilds extensions in place; restart pi only if /reload fails.` };
|
|
@@ -962,9 +1046,17 @@ function heartbeatTick(): void {
|
|
|
962
1046
|
) {
|
|
963
1047
|
lastUnansweredAlertAt = Date.now();
|
|
964
1048
|
appendLedger(ctx.cwd, "continuation_unanswered", { silentMs: Date.now() - lastContinuationSentAt });
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1049
|
+
// v0.34.13: recover, don't just report. Only the failure modes reach
|
|
1050
|
+
// the user: throttled (a reload already failed to cure → the
|
|
1051
|
+
// pi-restart class) or recovery unavailable (setting off / no pane).
|
|
1052
|
+
if (!attemptAutoRecovery(ctx, "continuation unanswered")) {
|
|
1053
|
+
const mins = Math.round((Date.now() - lastContinuationSentAt) / 60_000);
|
|
1054
|
+
const msg = lastAutoRecoveryAt > 0
|
|
1055
|
+
? `glla: auto-recovery /reload did NOT unstick this session — still no turn ${mins}m after the continuation. This class kills pi's transcript writer; only a pi RESTART cures it: restart pi in this tab, then /glla resume (the goal holds in .pi-glla state).`
|
|
1056
|
+
: `glla: pi accepted the continuation ${mins}m ago but NO turn has started — no tool calls, no tokens, transcript frozen (the turn trigger is wedged). Cure: /reload — autoresume re-fires the ${isLoopActive() ? "loop" : "goal/list item"}. (auto-recovery is off or no tmux/WezTerm pane — /glla settings autoRecovery=on enables the automatic form.)`;
|
|
1057
|
+
ctx.ui.notify(msg, "warning");
|
|
1058
|
+
notifyExternal(ctx, msg);
|
|
1059
|
+
}
|
|
968
1060
|
}
|
|
969
1061
|
// v0.29.1: stranded-audit recovery. A goal left in "auditing" with NO
|
|
970
1062
|
// in-flight audit means the auditor's result never landed (wedged queue
|
|
@@ -6323,9 +6415,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6323
6415
|
persistState(ctx);
|
|
6324
6416
|
appendLedger(ctx.cwd, "audit_loop_target_migrated", { from: "audit-every-iteration", to: "fix-first" });
|
|
6325
6417
|
}
|
|
6418
|
+
// v0.34.13: an auto-recovery /reload carries its own resume consent —
|
|
6419
|
+
// the sidecar marker overrides autoresume=off for THIS restore only.
|
|
6420
|
+
const recoveryResume = consumeRecoveryResume(ctx.cwd);
|
|
6326
6421
|
if (isLoopActive()) {
|
|
6327
6422
|
const l = state.loop!;
|
|
6328
|
-
if (autoResume) {
|
|
6423
|
+
if (autoResume || recoveryResume) {
|
|
6329
6424
|
ctx.ui.notify(
|
|
6330
6425
|
`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)}`,
|
|
6331
6426
|
"info",
|
|
@@ -6346,7 +6441,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6346
6441
|
// "load it but not auto start it"). Interrupted goals hold like
|
|
6347
6442
|
// everything else; autoresume=on (unattended rigs) still auto-resumes
|
|
6348
6443
|
// them, and the marker is cleared only on that promised auto-resume.
|
|
6349
|
-
if (autoResume) {
|
|
6444
|
+
if (autoResume || recoveryResume) {
|
|
6350
6445
|
// v0.28.1 (S2): clear the stale-handle interrupt marker — this IS
|
|
6351
6446
|
// the auto-resume the marker promised.
|
|
6352
6447
|
if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
|
|
@@ -6711,7 +6806,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6711
6806
|
// pause/cancel, the stall watchdog, the 5-consecutive-errors pause, or
|
|
6712
6807
|
// the token guard — never via an elapsed-time cutoff.
|
|
6713
6808
|
|
|
6714
|
-
|
|
6809
|
+
// v0.34.12: NOT immediately — agent_end is the blackhole boundary.
|
|
6810
|
+
scheduleContinuation(ctx, false, EAGER_CONTINUATION_SETTLE_MS);
|
|
6715
6811
|
});
|
|
6716
6812
|
|
|
6717
6813
|
pi.on("tool_call", (event: any) => {
|
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.13",
|
|
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",
|