pi-goal-list-loop-audit 0.34.12 → 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.
|
@@ -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
|
|
|
@@ -649,6 +694,17 @@ const CONTINUATION_UNANSWERED_THROTTLE_MS = 300_000;
|
|
|
649
694
|
// cycle. Sending 2.5s AFTER agent_end lets teardown settle; the send lands
|
|
650
695
|
// and the next turn starts immediately. 2.5s per turn beats 60s per turn.
|
|
651
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;
|
|
652
708
|
// v0.29.19: dead-turn caps (agent_end exemption path). 6 consecutive
|
|
653
709
|
// provider-error turns = a real outage, not bad luck — stop honestly.
|
|
654
710
|
// 3 consecutive user aborts = the user means it (user aborts mean STOP).
|
|
@@ -825,6 +881,10 @@ function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "l
|
|
|
825
881
|
const silent = Math.round(SEND_REARM_ESCALATE_SILENT_MS / 60000);
|
|
826
882
|
appendLedger(ctx.cwd, "send_rearm_escalated", { kind, afterMinutes: mins, silentMinutes: silent });
|
|
827
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
|
+
}
|
|
828
888
|
clearLoopTimer();
|
|
829
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.` };
|
|
830
890
|
persistState(ctx);
|
|
@@ -848,6 +908,14 @@ function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "l
|
|
|
848
908
|
return;
|
|
849
909
|
}
|
|
850
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
|
+
}
|
|
851
919
|
updateGoal({
|
|
852
920
|
status: "paused",
|
|
853
921
|
pauseKind: "error",
|
|
@@ -863,6 +931,12 @@ function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
|
|
|
863
931
|
if (!shouldEscalateStall(consecutiveStalls, threshold)) return false;
|
|
864
932
|
consecutiveStalls = 0;
|
|
865
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
|
+
}
|
|
866
940
|
if (isLoopActive()) {
|
|
867
941
|
clearLoopTimer();
|
|
868
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.` };
|
|
@@ -972,9 +1046,17 @@ function heartbeatTick(): void {
|
|
|
972
1046
|
) {
|
|
973
1047
|
lastUnansweredAlertAt = Date.now();
|
|
974
1048
|
appendLedger(ctx.cwd, "continuation_unanswered", { silentMs: Date.now() - lastContinuationSentAt });
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
+
}
|
|
978
1060
|
}
|
|
979
1061
|
// v0.29.1: stranded-audit recovery. A goal left in "auditing" with NO
|
|
980
1062
|
// in-flight audit means the auditor's result never landed (wedged queue
|
|
@@ -6333,9 +6415,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6333
6415
|
persistState(ctx);
|
|
6334
6416
|
appendLedger(ctx.cwd, "audit_loop_target_migrated", { from: "audit-every-iteration", to: "fix-first" });
|
|
6335
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);
|
|
6336
6421
|
if (isLoopActive()) {
|
|
6337
6422
|
const l = state.loop!;
|
|
6338
|
-
if (autoResume) {
|
|
6423
|
+
if (autoResume || recoveryResume) {
|
|
6339
6424
|
ctx.ui.notify(
|
|
6340
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)}`,
|
|
6341
6426
|
"info",
|
|
@@ -6356,7 +6441,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6356
6441
|
// "load it but not auto start it"). Interrupted goals hold like
|
|
6357
6442
|
// everything else; autoresume=on (unattended rigs) still auto-resumes
|
|
6358
6443
|
// them, and the marker is cleared only on that promised auto-resume.
|
|
6359
|
-
if (autoResume) {
|
|
6444
|
+
if (autoResume || recoveryResume) {
|
|
6360
6445
|
// v0.28.1 (S2): clear the stale-handle interrupt marker — this IS
|
|
6361
6446
|
// the auto-resume the marker promised.
|
|
6362
6447
|
if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
|
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",
|