pi-goal-list-loop-audit 0.31.9 → 0.32.0
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.
|
@@ -338,6 +338,9 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
338
338
|
} finally {
|
|
339
339
|
clearInterval(stallTimer);
|
|
340
340
|
unsub();
|
|
341
|
+
// v0.32.0: dispose the auditor session — each complete_goal leaked one
|
|
342
|
+
// session's subscriptions/stream resources for the parent's lifetime.
|
|
343
|
+
(session as any).dispose?.();
|
|
341
344
|
}
|
|
342
345
|
|
|
343
346
|
if (stalled) {
|
|
@@ -188,6 +188,8 @@ export function loadGlobalSettings(): Settings {
|
|
|
188
188
|
/** Every provenance-tracked key (the /glla headless display + UI). */
|
|
189
189
|
export const SETTINGS_KEYS: Array<keyof Settings> = [
|
|
190
190
|
"auditorModel",
|
|
191
|
+
"auditorModelFallback",
|
|
192
|
+
"auditorSameSessionSwap",
|
|
191
193
|
"auditorThinkingLevel",
|
|
192
194
|
"notifyCmd",
|
|
193
195
|
"tokenLimit",
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -223,6 +223,14 @@ let extensionApi: ExtensionAPI | null = null;
|
|
|
223
223
|
// throws FOREVER in this process — retrying for hours is the hegemon
|
|
224
224
|
// failure shape. Detect the stale signature once and go terminally loud.
|
|
225
225
|
let extensionApiStale = false;
|
|
226
|
+
// v0.32.0: CRITICAL — goStaleTerminal must gate on its OWN flag, not
|
|
227
|
+
// extensionApiStale: probeExtensionApiStale() sets extensionApiStale on
|
|
228
|
+
// detection, so the heartbeat's `probe → goStaleTerminal` sequence always
|
|
229
|
+
// found the flag already true and returned silently — orphan-stale recovery
|
|
230
|
+
// (ledger, loop stop, interruptedAt, warn, AUTO-RELOAD SELF-HEAL) was dead
|
|
231
|
+
// code since v0.29.11. Field proof: hegemon sat stale for days and the
|
|
232
|
+
// wezterm self-heal never fired.
|
|
233
|
+
let staleTerminalDone = false;
|
|
226
234
|
|
|
227
235
|
/** v0.26.7: a stale api is terminal for this process — go loudly with
|
|
228
236
|
* restart guidance instead of retrying sends that can never land.
|
|
@@ -292,16 +300,24 @@ function absorbStaleIfSuperseded(ctx: ExtensionContext): boolean {
|
|
|
292
300
|
extensionApiStale = true; // silence the send paths WITHOUT the terminal theatre
|
|
293
301
|
clearLoopTimer();
|
|
294
302
|
if (continuationTimer) { clearTimeout(continuationTimer); continuationTimer = null; }
|
|
303
|
+
// v0.32.0: the superseded module's heartbeat + UI ticker were IMMORTAL
|
|
304
|
+
// (clearInterval appeared nowhere) — N /reloads = N×2 zombie tickers.
|
|
305
|
+
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
306
|
+
if (uiTicker) { clearInterval(uiTicker); uiTicker = null; }
|
|
295
307
|
return true;
|
|
296
308
|
}
|
|
297
309
|
return false;
|
|
298
310
|
}
|
|
299
311
|
|
|
300
312
|
function goStaleTerminal(ctx: ExtensionContext, where: string): void {
|
|
301
|
-
if (
|
|
313
|
+
if (staleTerminalDone) return; // already terminal — don't re-spam
|
|
314
|
+
staleTerminalDone = true;
|
|
302
315
|
extensionApiStale = true;
|
|
303
316
|
appendLedger(ctx.cwd, "extension_api_stale", { where, kind: isLoopActive() ? "loop" : "goal" });
|
|
304
317
|
const guidance = "pi invalidated this session's extension handle (session replacement — the session was disposed and this process's sends can never land). Run /reload — extensions rebuild IN PLACE, no pi restart needed — then /glla resume (autoresume=on resumes for you). Restart pi only if /reload itself fails.";
|
|
318
|
+
// v0.32.0: kill the continuation re-arm too — otherwise an orphaned goal
|
|
319
|
+
// keeps spinning a flat 50ms retry below every watchdog.
|
|
320
|
+
if (continuationTimer) { clearTimeout(continuationTimer); continuationTimer = null; continuationScheduledFor = null; }
|
|
305
321
|
if (isLoopActive()) {
|
|
306
322
|
clearLoopTimer();
|
|
307
323
|
state.loop = { ...state.loop!, active: false, stopReason: `extension api stale: ${guidance}` };
|
|
@@ -400,6 +416,12 @@ function warnIfStaleAtEntry(ctx: ExtensionContext, what: string): boolean {
|
|
|
400
416
|
// v0.30.0: a successor may already own this session (e.g. /reload
|
|
401
417
|
// re-imported the modules) — the user's command belongs to the fresh
|
|
402
418
|
// instance; say so softly instead of demanding a reload.
|
|
419
|
+
// v0.32.0: the rebind window means a fresh instance is COMING, not here —
|
|
420
|
+
// the old message claimed "handled there" while nothing owned the session.
|
|
421
|
+
if (Date.now() < sessionReplacementUntil) {
|
|
422
|
+
ctx.ui.notify(`glla: this session is rebinding after /reload — ${what} will be handled by the refreshed instance; retry in a moment if it doesn't.`, "info");
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
403
425
|
if (absorbStaleIfSuperseded(ctx)) {
|
|
404
426
|
ctx.ui.notify(`glla: a refreshed instance owns this session — ${what} is handled there; nothing to do.`, "info");
|
|
405
427
|
return true;
|
|
@@ -579,6 +601,8 @@ let carryoverResolved = true;
|
|
|
579
601
|
// set while complete_goal's isolated audit runs, so the heartbeat never
|
|
580
602
|
// refires into an in-flight completion.
|
|
581
603
|
let completionAuditInFlight = false;
|
|
604
|
+
// v0.32.0: consecutive stored-claim quota retries (capped at 5, then hold).
|
|
605
|
+
let quotaRetryStreak = 0;
|
|
582
606
|
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
583
607
|
|
|
584
608
|
const ZOMBIE_RUN_SILENT_MS = 20 * 60_000;
|
|
@@ -1005,6 +1029,9 @@ function sendContinuation(goalId: string): void {
|
|
|
1005
1029
|
if (!isActionableGoal()) return;
|
|
1006
1030
|
const ctx = freshCtx();
|
|
1007
1031
|
if (!ctx) {
|
|
1032
|
+
// v0.32.0: a stale handle must not spin a flat 50ms re-arm below every
|
|
1033
|
+
// watchdog — the heartbeat's terminal path does the theatre; we just stop.
|
|
1034
|
+
if (probeExtensionApiStale()) return;
|
|
1008
1035
|
// No live ctx — retry shortly; the next session event will refresh it.
|
|
1009
1036
|
continuationScheduledFor = goalId;
|
|
1010
1037
|
continuationTimer = setTimeout(() => sendContinuation(goalId), BACKOFF_IDLE_RETRY_MS);
|
|
@@ -1251,7 +1278,9 @@ async function fanOutListAuditFindings(ctx: ExtensionContext): Promise<void> {
|
|
|
1251
1278
|
// Dedupe against the live queue (a re-run must not double-queue a finding
|
|
1252
1279
|
// that's already waiting) — match on the finding text's first 60 chars.
|
|
1253
1280
|
const queuedText = listQueue().map((i) => i.objective).join("\n");
|
|
1254
|
-
|
|
1281
|
+
// v0.32.0: cap one fan-out — a runaway findings file must not enqueue
|
|
1282
|
+
// hundreds of items on a single Confirm.
|
|
1283
|
+
const fresh = open.filter((f) => !queuedText.includes(f.text.slice(0, 60))).slice(0, 50);
|
|
1255
1284
|
const alreadyQueued = open.length - fresh.length;
|
|
1256
1285
|
const decideNote =
|
|
1257
1286
|
decisions.length > 0
|
|
@@ -1440,6 +1469,14 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext, origin: "quota-
|
|
|
1440
1469
|
pauseSuggestedAction: `Quota auto-retry in ${retryMin}m — or /goal resume to retry now`,
|
|
1441
1470
|
}, liveCtx);
|
|
1442
1471
|
appendLedger(liveCtx.cwd, "goal_paused", { reason: `auditor quota: retry in ${quota.retryAfterSec}s (stored-claim retry)` });
|
|
1472
|
+
// v0.32.0: terminal cap — a permanently dead auditor key must not spawn
|
|
1473
|
+
// one auditor per hour forever. 5 consecutive quota retries → hold.
|
|
1474
|
+
quotaRetryStreak++;
|
|
1475
|
+
if (quotaRetryStreak >= 5) {
|
|
1476
|
+
appendLedger(liveCtx.cwd, "quota_retry_capped", { streak: quotaRetryStreak });
|
|
1477
|
+
liveCtx.ui.notify(`Auditor quota retry gave up after ${quotaRetryStreak} consecutive attempts — the claim stays stored; /goal resume retries by hand.`, "warning");
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1443
1480
|
liveCtx.ui.notify(`Auditor still quota-limited — next auto-retry in ${retryMin}m (your completion claim is stored; no action needed).`, "warning");
|
|
1444
1481
|
scheduleQuotaRetry(liveCtx, quota.retryAfterSec, result.error, () => {
|
|
1445
1482
|
if (state.goal && state.goal.status === "paused" && (state.goal.pauseReason ?? "").startsWith("auditor quota:") && state.goal.pendingCompletion) {
|
|
@@ -1452,6 +1489,7 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext, origin: "quota-
|
|
|
1452
1489
|
// Any other outcome — disapproved, impossible, non-quota infra — belongs
|
|
1453
1490
|
// to the agent: resume and let the continuation drive the next step. The
|
|
1454
1491
|
// verdict is durable in auditHistory + /goal status.
|
|
1492
|
+
quotaRetryStreak = 0;
|
|
1455
1493
|
updateGoal({
|
|
1456
1494
|
status: "active",
|
|
1457
1495
|
auditHistory: history,
|
|
@@ -4265,35 +4303,45 @@ function resolveAuditorModel(ctx: ExtensionContext, ref?: string, fallbackRef?:
|
|
|
4265
4303
|
return matches[0] ? { model: matches[0] } : { reason: "no available model matching" };
|
|
4266
4304
|
};
|
|
4267
4305
|
const isSession = (m: any) => sessionModel && m.provider === sessionModel.provider && m.id === sessionModel.id;
|
|
4268
|
-
|
|
4306
|
+
// v0.32.0: per-pin source labels — when the primary is unset, pins[0] IS
|
|
4307
|
+
// the fallback and the old i===0→"setting" map mislabeled it.
|
|
4308
|
+
const pins: Array<{ pin: string; src: "setting" | "fallback-pin" }> = [];
|
|
4309
|
+
if (ref?.trim()) pins.push({ pin: ref.trim(), src: "setting" });
|
|
4310
|
+
if (fallbackRef?.trim()) pins.push({ pin: fallbackRef.trim(), src: "fallback-pin" });
|
|
4269
4311
|
for (let i = 0; i < pins.length; i++) {
|
|
4270
|
-
const pin = pins[i]!;
|
|
4312
|
+
const { pin } = pins[i]!;
|
|
4271
4313
|
const r = tryRef(pin);
|
|
4272
4314
|
if (!r.model) {
|
|
4273
4315
|
// Unavailable pin → cascade: next pin, then the session model (LOUD).
|
|
4274
4316
|
appendLedger(ctx.cwd, "auditor_model_fallback", { configured: pin, reason: r.reason });
|
|
4275
|
-
|
|
4317
|
+
// v0.32.0: the last pin no longer pre-announces the session fallback —
|
|
4318
|
+
// the post-loop block does that (it notified twice before).
|
|
4319
|
+
ctx.ui.notify(`Auditor model "${pin}" is unavailable (${r.reason})${i + 1 < pins.length ? " — trying the fallback pin" : ""}. Fix via /glla → Auditor model.`, "warning");
|
|
4276
4320
|
continue;
|
|
4277
4321
|
}
|
|
4278
4322
|
if (sameSessionSwap && isSession(r.model) && i + 1 < pins.length) {
|
|
4279
4323
|
// The pin IS the session model — the verifier would be the executor's
|
|
4280
4324
|
// own model; auto-swap down the chain (the user's move).
|
|
4281
|
-
appendLedger(ctx.cwd, "auditor_model_same_as_session", { model: `${r.model.provider}/${r.model.id}`, fallback: pins[i + 1] });
|
|
4282
|
-
ctx.ui.notify(`Session model IS the pinned auditor (${r.model.provider}/${r.model.id}) — auditor auto-swapped to ${pins[i + 1]} so the verifier differs.`, "info");
|
|
4325
|
+
appendLedger(ctx.cwd, "auditor_model_same_as_session", { model: `${r.model.provider}/${r.model.id}`, fallback: pins[i + 1]!.pin });
|
|
4326
|
+
ctx.ui.notify(`Session model IS the pinned auditor (${r.model.provider}/${r.model.id}) — auditor auto-swapped to ${pins[i + 1]!.pin} so the verifier differs.`, "info");
|
|
4283
4327
|
continue;
|
|
4284
4328
|
}
|
|
4285
|
-
|
|
4329
|
+
// v0.32.0: the nudge must fire when the LAST pin stands on the session
|
|
4330
|
+
// model — the old `!fallbackRef` guard went SILENT when the fallback pin
|
|
4331
|
+
// itself resolved to the session model (verifier == executor, and hop 0's
|
|
4332
|
+
// notify had just claimed "auto-swapped so the verifier differs" — false).
|
|
4333
|
+
if (sameSessionSwap && isSession(r.model) && i + 1 >= pins.length) {
|
|
4286
4334
|
// Last resort reached and it IS the session model, with no fallback
|
|
4287
4335
|
// ever pinned — the model stands (the session IS the last resort);
|
|
4288
4336
|
// one loud nudge so the user can wire the swap.
|
|
4289
4337
|
appendLedger(ctx.cwd, "auditor_model_same_as_session", { model: `${r.model.provider}/${r.model.id}`, fallback: null });
|
|
4290
4338
|
ctx.ui.notify(`The session model IS the pinned auditor (${r.model.provider}/${r.model.id}) — pin a different /glla → Auditor fallback model so the verifier can differ.`, "warning");
|
|
4291
4339
|
}
|
|
4292
|
-
return { model: r.model, via: i
|
|
4340
|
+
return { model: r.model, via: pins[i]!.src };
|
|
4293
4341
|
}
|
|
4294
4342
|
if (sessionModel) {
|
|
4295
4343
|
if (pins.length > 0) {
|
|
4296
|
-
appendLedger(ctx.cwd, "auditor_model_fallback", { configured: pins.join(" → "), reason: "all pins exhausted" });
|
|
4344
|
+
appendLedger(ctx.cwd, "auditor_model_fallback", { configured: pins.map((p) => p.pin).join(" → "), reason: "all pins exhausted" });
|
|
4297
4345
|
ctx.ui.notify("All pinned auditor models are unavailable — falling back to the session model. Fix via /glla → Auditor model.", "warning");
|
|
4298
4346
|
return { model: sessionModel, via: "session-fallback" };
|
|
4299
4347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
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",
|