pi-goal-list-loop-audit 0.35.13 → 0.35.32
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/CHANGELOG.md +366 -0
- package/INSTALL.md +3 -3
- package/LIST-PHILOSOPHY.md +90 -0
- package/PLAN.md +316 -0
- package/README.md +108 -13
- package/docs/DESIGN-subagent-visibility.md +76 -0
- package/docs/DESIGN.md +59 -8
- package/docs/GLLA-POSITIONING-AND-DECOMPOSITION-2026-08-08.md +6 -1
- package/docs/INDEX.md +4 -0
- package/docs/VISION-ASSIST.md +2 -2
- package/extensions/auditor-extensions.ts +289 -0
- package/extensions/goal-agents-panel.ts +185 -0
- package/extensions/goal-commands.ts +120 -6
- package/extensions/goal-continuation.ts +45 -2
- package/extensions/goal-heartbeat.ts +176 -5
- package/extensions/goal-loop-auditor-process.ts +26 -0
- package/extensions/goal-loop-auditor.ts +3 -5
- package/extensions/goal-loop-backoff.ts +79 -0
- package/extensions/goal-loop-core.ts +97 -4
- package/extensions/goal-loop-display.ts +107 -8
- package/extensions/goal-loop-forever.ts +39 -1
- package/extensions/goal-loop-shield.ts +114 -25
- package/extensions/goal-loop.ts +66 -1
- package/extensions/goal-recovery.ts +8 -2
- package/extensions/goal-settings.ts +23 -0
- package/extensions/goal-state.ts +11 -0
- package/extensions/loops/goal-activation.ts +226 -25
- package/extensions/loops/goal-auditor-hooks.ts +39 -3
- package/extensions/loops/goal-list-queue.ts +17 -1
- package/extensions/loops/goal-orchestrator.ts +28 -1
- package/extensions/loops/goal-settings-ui.ts +96 -5
- package/extensions/loops/goal-tools.ts +82 -28
- package/extensions/loops/goal-ui.ts +105 -1
- package/extensions/loops/goal.ts +7 -7
- package/extensions/multi-model-picker.ts +45 -7
- package/extensions/settings-menu.ts +10 -0
- package/package.json +8 -3
- package/schemas/goal.schema.json +2 -0
- package/scripts/goal-auditor-launch.mjs +19 -1
- package/scripts/goal-auditor-worker.mjs +15 -0
- package/scripts/verify-auditor-extensions-offline.mjs +59 -0
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
extractPendingTasks,
|
|
55
55
|
isFullAuditObjective,
|
|
56
56
|
resolveEffectiveAggressiveSettings,
|
|
57
|
+
supervisorPaused,
|
|
57
58
|
appendAuditLog,
|
|
58
59
|
computeListDepth,
|
|
59
60
|
formatAuditLog,
|
|
@@ -258,6 +259,7 @@ import {
|
|
|
258
259
|
import { consumeRecoveryResume } from "../goal-recovery.js"; // decomposition step 3 (v0.34.111)
|
|
259
260
|
import {
|
|
260
261
|
createGoalHeartbeat,
|
|
262
|
+
releaseZombieAbortKey,
|
|
261
263
|
endSubagentHangProbe,
|
|
262
264
|
markSubagentHangProgress,
|
|
263
265
|
startHeartbeat,
|
|
@@ -339,6 +341,9 @@ import {
|
|
|
339
341
|
PENDING_LATCH_STUCK_MS,
|
|
340
342
|
shouldFirePendingLatchWatchdog,
|
|
341
343
|
AUDITOR_WALL_TIMEOUT_MS,
|
|
344
|
+
ZOMBIE_RETRY_DELAY_MS,
|
|
345
|
+
zombieRetryDecision,
|
|
346
|
+
type ZombieRetryStreak,
|
|
342
347
|
} from "../goal-loop-backoff.js";
|
|
343
348
|
|
|
344
349
|
import {
|
|
@@ -351,6 +356,7 @@ import {
|
|
|
351
356
|
cmdSettings,
|
|
352
357
|
createGoalCommands,
|
|
353
358
|
enqueueItems,
|
|
359
|
+
hydrateListQueueFromDisk,
|
|
354
360
|
maybeDecisionPopup,
|
|
355
361
|
probeAutoNotify,
|
|
356
362
|
recentlyCompletedObjectives,
|
|
@@ -411,12 +417,137 @@ export function enqueueFaultRepairTask(ctx: ExtensionContext, objective: string,
|
|
|
411
417
|
appendLedger(ctx.cwd, "faulty_objective_repair_promoted", { goalId: repair.id, targetId: target?.id, position: 1, source: target?.source ?? "unknown" });
|
|
412
418
|
}
|
|
413
419
|
|
|
414
|
-
/** v0.35.
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
+
/** v0.35.17 — bounded automatic retry after a confirmed zero-stream abort.
|
|
421
|
+
*
|
|
422
|
+
* Field evidence (note.md Next §1, screenshot 20260821_152311): turns
|
|
423
|
+
* dispatched by ACCEPTING a Confirm dialog hang with zero provider stream
|
|
424
|
+
* activity often enough that users repeatedly return to "Goal paused after
|
|
425
|
+
* 30m ... action needed - this won't fix itself" sessions. The watchdog's
|
|
426
|
+
* abort is correct (it stops the token bleed); what was missing is the
|
|
427
|
+
* self-heal half. This adds ONE bounded automatic re-dispatch per silent
|
|
428
|
+
* streak, mirroring main-model recovery's timer pattern:
|
|
429
|
+
*
|
|
430
|
+
* - exactly ONE automatic retry per zero-stream STREAK: a retried turn
|
|
431
|
+
* that actually streams advances lastStreamActivityAt past the previous
|
|
432
|
+
* abort's observation point, so any LATER independent hang starts a
|
|
433
|
+
* fresh streak and earns its own single retry;
|
|
434
|
+
* - a SECOND consecutive silence (retry hung too, or the original turn
|
|
435
|
+
* never streamed again) parks permanently — no retry storm;
|
|
436
|
+
* - `/glla pause` freezes the retry like every other automatic
|
|
437
|
+
* side-effect: the parked claim stays durable for manual resume;
|
|
438
|
+
* - manual /goal resume · /loop resume remain available at all times.
|
|
439
|
+
*
|
|
440
|
+
* The streak counter is process-memory by design: if the session restarts
|
|
441
|
+
* inside the retry window the park stands and manual resume applies — an
|
|
442
|
+
* honest degradation, not a phantom promise.
|
|
443
|
+
*/
|
|
444
|
+
const ZOMBIE_PAUSE_REASON = "automatic zero-stream abort — no provider activity was observed";
|
|
445
|
+
|
|
446
|
+
// Streak state is process-memory by design (see the block comment above
|
|
447
|
+
// scheduleZombieAutoRetry): a session restart inside the retry window leaves
|
|
448
|
+
// the park standing for manual resume — an honest degradation.
|
|
449
|
+
let zombieRetryStreak: ZombieRetryStreak = { key: "", count: 0, lastAbortStreamAt: 0 };
|
|
450
|
+
let zombieRetryTimer: NodeJS.Timeout | null = null;
|
|
451
|
+
|
|
452
|
+
/** Arm the one-shot automatic re-dispatch after a successful zombie abort.
|
|
453
|
+
* Returns true when a retry was scheduled (the caller adjusts its user-facing
|
|
454
|
+
* copy accordingly). */
|
|
455
|
+
function scheduleZombieAutoRetry(
|
|
456
|
+
ctx: ExtensionContext,
|
|
457
|
+
generation: number,
|
|
458
|
+
goalId: string | undefined,
|
|
459
|
+
observedStreamAt: number,
|
|
460
|
+
silentMinutes: number,
|
|
461
|
+
): boolean {
|
|
462
|
+
const key = goalId ?? "loop";
|
|
463
|
+
const { retry, streak } = zombieRetryDecision(observedStreamAt, key, zombieRetryStreak);
|
|
464
|
+
zombieRetryStreak = streak;
|
|
465
|
+
appendLedger(ctx.cwd, retry ? "zombie_auto_retry_scheduled" : "zombie_auto_retry_refused_streak", {
|
|
466
|
+
goalId,
|
|
467
|
+
streakCount: streak.count,
|
|
468
|
+
delayMs: zombieRetryDelayOverride ?? ZOMBIE_RETRY_DELAY_MS,
|
|
469
|
+
});
|
|
470
|
+
if (!retry) return false;
|
|
471
|
+
if (zombieRetryTimer) {
|
|
472
|
+
clearTimeout(zombieRetryTimer);
|
|
473
|
+
zombieRetryTimer = null;
|
|
474
|
+
}
|
|
475
|
+
// scheduleSessionTimeout carries the generation/stale/zombie fences — an
|
|
476
|
+
// old session's callback can never re-arm work after stale/shutdown/reload.
|
|
477
|
+
zombieRetryTimer = scheduleSessionTimeout(() => {
|
|
478
|
+
zombieRetryTimer = null;
|
|
479
|
+
// v0.35.15 semantics: a frozen supervisor keeps everything automatic off.
|
|
480
|
+
// The parked claim stays durable; /glla resume then /goal resume apply.
|
|
481
|
+
if (supervisorPaused(state)) return;
|
|
482
|
+
const fresh = freshCtxForGeneration(generation);
|
|
483
|
+
if (!fresh) return;
|
|
484
|
+
const goal = state.goal;
|
|
485
|
+
if (goalId !== undefined || goal) {
|
|
486
|
+
if (!goal || goal.status !== "paused" || goal.id !== goalId) return;
|
|
487
|
+
if (goal.pauseReason !== ZOMBIE_PAUSE_REASON) return; // superseded pause — not ours to clear
|
|
488
|
+
const freshLimit = loadSettings(fresh.cwd).tokenLimit ?? DEFAULT_TOKEN_LIMIT;
|
|
489
|
+
const usage = goal.usage ? { tokensUsed: goal.usage.tokensUsed, tokensLimit: freshLimit } : undefined;
|
|
490
|
+
abortedStandDown = false;
|
|
491
|
+
setContinuationDispatchStoodDownRef(false);
|
|
492
|
+
updateGoal({
|
|
493
|
+
status: "active",
|
|
494
|
+
pauseKind: undefined,
|
|
495
|
+
pauseReason: undefined,
|
|
496
|
+
pauseSuggestedAction: undefined,
|
|
497
|
+
...(usage ? { usage } : {}),
|
|
498
|
+
}, fresh);
|
|
499
|
+
appendLedger(fresh.cwd, "zombie_auto_retry_dispatched", { goalId: goal.id, policy: goal.policy });
|
|
500
|
+
releaseZombieAbortKey();
|
|
501
|
+
try {
|
|
502
|
+
fresh.ui.notify(`Automatic retry starting — the earlier ${goal.policy === "list" ? "list item" : "goal"} turn was aborted after ${silentMinutes}m of zero stream activity. One bounded retry is dispatched now; if it hangs again the ${goal.policy === "list" ? "item" : "goal"} stays paused for ${activeGoalSurfaceCommand("resume")}.`, "info");
|
|
503
|
+
} catch { /* best-effort */ }
|
|
504
|
+
scheduleContinuation(fresh, true);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// Loop owner: revive the preserved iteration and tick once.
|
|
508
|
+
const loop = state.loop;
|
|
509
|
+
if (!loop || !loop.stopReason?.includes("zero-stream abort")) return;
|
|
510
|
+
abortedStandDown = false;
|
|
511
|
+
setContinuationDispatchStoodDownRef(false);
|
|
512
|
+
loop.active = true;
|
|
513
|
+
loop.stopReason = undefined;
|
|
514
|
+
persistState(fresh);
|
|
515
|
+
appendLedger(fresh.cwd, "zombie_auto_retry_dispatched", { kind: "loop", iteration: loop.iteration });
|
|
516
|
+
releaseZombieAbortKey();
|
|
517
|
+
try {
|
|
518
|
+
fresh.ui.notify(`Automatic retry starting — the loop was stopped after a zero-stream abort. One bounded retry ticks now; if it hangs again it stays stopped for /loop resume.`, "info");
|
|
519
|
+
} catch { /* best-effort */ }
|
|
520
|
+
scheduleLoopTick(fresh);
|
|
521
|
+
}, zombieRetryDelayOverride ?? ZOMBIE_RETRY_DELAY_MS);
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Test-only: reset the module-global retry streak/timer state. */
|
|
526
|
+
export function __testOnlyResetZombieAutoRetry(): void {
|
|
527
|
+
if (zombieRetryTimer) {
|
|
528
|
+
clearTimeout(zombieRetryTimer);
|
|
529
|
+
zombieRetryTimer = null;
|
|
530
|
+
}
|
|
531
|
+
zombieRetryStreak = { key: "", count: 0, lastAbortStreamAt: 0 };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let zombieRetryDelayOverride: number | null = null;
|
|
535
|
+
|
|
536
|
+
/** Test-only: shrink the automatic-retry delay (null restores the 90s
|
|
537
|
+
* production default). Never called by production code. */
|
|
538
|
+
export function __testOnlySetZombieRetryDelay(ms: number | null): void {
|
|
539
|
+
zombieRetryDelayOverride = ms;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** v0.35.x (amended v0.35.17): terminate one confirmed zero-stream host turn
|
|
543
|
+
* and park the owning goal/list item before asking pi to abort. This is
|
|
544
|
+
* deliberately an activation-owned operation: it can clear the durable
|
|
545
|
+
* continuation sidecar, stand down re-arms, and validate the current
|
|
546
|
+
* generation before touching the host. A later /goal or /list resume
|
|
547
|
+
* explicitly clears the stand-down and starts a new attempt — and since
|
|
548
|
+
* v0.35.17, the FIRST silence of a streak additionally arms ONE bounded
|
|
549
|
+
* automatic retry (scheduleZombieAutoRetry below); only a second consecutive
|
|
550
|
+
* zero-stream abort leaves the park purely manual. */
|
|
420
551
|
export function abortZombieRun(
|
|
421
552
|
ctx: ExtensionContext,
|
|
422
553
|
generation: number,
|
|
@@ -441,7 +572,7 @@ export function abortZombieRun(
|
|
|
441
572
|
setContinuationDispatchStoodDownRef(true);
|
|
442
573
|
abortedStandDown = true;
|
|
443
574
|
const silentMinutes = Math.max(1, Math.round((Date.now() - observedStreamAt) / 60_000));
|
|
444
|
-
const reason =
|
|
575
|
+
const reason = ZOMBIE_PAUSE_REASON;
|
|
445
576
|
if (goal) {
|
|
446
577
|
const noun = goal.policy === "list" ? "list item" : "goal";
|
|
447
578
|
updateGoal({
|
|
@@ -473,7 +604,15 @@ export function abortZombieRun(
|
|
|
473
604
|
});
|
|
474
605
|
const resume = goal ? activeGoalSurfaceCommand("resume") : "/loop resume";
|
|
475
606
|
const cancel = goal ? activeGoalSurfaceCommand("cancel") : "/loop stop";
|
|
476
|
-
|
|
607
|
+
// v0.35.17: ONE bounded automatic retry per zero-stream streak — the park
|
|
608
|
+
// becomes a 90-second waystation instead of a dead end for the first
|
|
609
|
+
// silence. A refused streak (second consecutive hang) keeps the manual copy.
|
|
610
|
+
const retryScheduled = scheduleZombieAutoRetry(current, generation, goal?.id, observedStreamAt, silentMinutes);
|
|
611
|
+
const message = `${goal ? goal.policy === "list" ? "List item" : "Goal" : "Loop"} paused after ${silentMinutes}m with zero stream activity; the zombie turn was aborted and queued retries were stopped. Work is saved. ${
|
|
612
|
+
retryScheduled
|
|
613
|
+
? `An automatic retry starts in ~90s; ${resume} retries immediately instead.`
|
|
614
|
+
: `${resume} retries it, or ${cancel} discards/stops it.`
|
|
615
|
+
}`;
|
|
477
616
|
current.ui.notify(message, abortError ? "warning" : "info");
|
|
478
617
|
notifyExternal(current, message);
|
|
479
618
|
return true;
|
|
@@ -520,18 +659,20 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
520
659
|
});
|
|
521
660
|
const settingsHandler = (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdSettings(args, ctx); };
|
|
522
661
|
pi.registerCommand("glla", {
|
|
523
|
-
description: "Open the settings UI for goals, loops, lists, and the auditor. `/glla version` shows the installed package version; `/glla cancel` cancels the active objective; `/glla wipe` Confirm-gatedly clears all live state while preserving history.",
|
|
662
|
+
description: "Open the settings UI for goals, loops, lists, and the auditor. `/glla version` shows the installed package version; `/glla pause` freezes the supervisor's automatic machinery (re-arms/recovery/auto-resume/dispatch) without touching active work; `/glla cancel` cancels the active objective; `/glla wipe` Confirm-gatedly clears all live state while preserving history.",
|
|
524
663
|
getArgumentCompletions: completions([
|
|
525
664
|
// Operational verbs only. Settings and section navigation belong in
|
|
526
665
|
// the bare `/glla` table, so they do not compete with action completion.
|
|
527
666
|
["version", "show the installed package version and registry check"],
|
|
528
667
|
["status", "show goal, list, loop, and pending decisions"],
|
|
529
668
|
["log", "show the recent event trail"],
|
|
530
|
-
["resume", "resume the paused or held live thing"],
|
|
669
|
+
["resume", "resume the paused or held live thing; also unfreezes a /glla pause"],
|
|
670
|
+
["pause", "freeze ALL supervisor automation (re-arms/recovery/dispatch) — active work keeps running"],
|
|
531
671
|
["cancel", "cancel the active objective (list item + queue)"],
|
|
532
672
|
["wipe", "Confirm-gated idempotent reset of all live goal/list/loop state"],
|
|
533
673
|
["stats", "show per-project ledger rollups"],
|
|
534
674
|
["audits", "browse the audit log"],
|
|
675
|
+
["agents", "show tracked subagents; --tail <id> reads a child transcript"],
|
|
535
676
|
["switchlog", "show the model-switch trail (model_switch / forbidden_model_switch)"],
|
|
536
677
|
["tooloverride", "configure agent-tool visibility"],
|
|
537
678
|
]),
|
|
@@ -889,6 +1030,9 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
889
1030
|
});
|
|
890
1031
|
}
|
|
891
1032
|
extensionApi = pi;
|
|
1033
|
+
// Session-scoped resources are reset only after this context has passed
|
|
1034
|
+
// the host-admission gate; child factories never touch host state.
|
|
1035
|
+
resetLengthContinue();
|
|
892
1036
|
sessionHandoffPending = false;
|
|
893
1037
|
// Reset terminal ownership before rememberCtx: this is the only event
|
|
894
1038
|
// allowed to bind a context after a stale/shutdown handoff.
|
|
@@ -936,6 +1080,18 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
936
1080
|
}
|
|
937
1081
|
}
|
|
938
1082
|
replaceState(readState(ctx.cwd));
|
|
1083
|
+
// v0.35.21 (list-invisible-until-restart): the durable queue is the
|
|
1084
|
+
// UNION of the persisted state and the per-item .queue.json sidecars
|
|
1085
|
+
// (v0.34.60 disk-first writes). readState replays only the state
|
|
1086
|
+
// ledger, so a plugin re-init / stale-handle window that reset RAM to
|
|
1087
|
+
// defaults used to leave the sidebar queue blank — active item only,
|
|
1088
|
+
// no "N waiting · up next" line — until a full restart re-ran the
|
|
1089
|
+
// disk merge on some other path. Converge memory to the union at EVERY
|
|
1090
|
+
// lifecycle restore so the rendered surface is truthful immediately.
|
|
1091
|
+
const hydratedFromDisk = hydrateListQueueFromDisk(ctx);
|
|
1092
|
+
if (hydratedFromDisk > 0) {
|
|
1093
|
+
ctx.ui.notify(`glla: restored ${hydratedFromDisk} queued list item(s) from durable sidecars.`, "info");
|
|
1094
|
+
}
|
|
939
1095
|
// v0.35.x: a fresh session_start is the coalesced recovery boundary for
|
|
940
1096
|
// a silent host death. Consume the one-shot arm after durable state is
|
|
941
1097
|
// reloaded so the marker is cleared in the new context, and let the
|
|
@@ -1066,11 +1222,13 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1066
1222
|
const rebindResume = ownerClaim.rebind;
|
|
1067
1223
|
if (rebindResume) appendLedger(ctx.cwd, "rebind_resume", { pid: process.pid });
|
|
1068
1224
|
const explicitRecovery = handoffResume || recoveryResume || rebindResume;
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1225
|
+
// v0.35.23 (note.md Next #2): a non-quit shutdown successor no longer
|
|
1226
|
+
// consents to loop auto-resume by itself — a crash restart IS a cold
|
|
1227
|
+
// load, and cold loads hold for an explicit decision. The SAME-PROCESS
|
|
1228
|
+
// successor case stays consented: pi replaced its session without dying
|
|
1229
|
+
// (the /reload family), so the held work is mid-flight continuity, not
|
|
1230
|
+
// a cold start. Different-pid successors hold like any cold load.
|
|
1231
|
+
const sameProcessSuccessorResume = nonQuitShutdownResume && ownerClaim.previousPid === process.pid;
|
|
1074
1232
|
const heldLoopSuccessorResume = !!state.loop
|
|
1075
1233
|
&& !state.loop.active
|
|
1076
1234
|
&& isLifecycleHeldLoopReason(state.loop.stopReason)
|
|
@@ -1120,7 +1278,15 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1120
1278
|
// autoResume keys are ignored. Once running, the chain auto-continues
|
|
1121
1279
|
// forever unless a super-stuck brake (stall escalation / stale-api /
|
|
1122
1280
|
// latch) stops it loudly.
|
|
1123
|
-
|
|
1281
|
+
// v0.35.23 (note.md Next #2): auto-resume consent is now the RAW global
|
|
1282
|
+
// setting, not the aggressive-mode coercion. resolveEffectiveAggressive
|
|
1283
|
+
// Settings coerced unset→true (aggressiveMode defaults on), which made
|
|
1284
|
+
// stock installs AUTO-RESUME everything on every load — defeating the
|
|
1285
|
+
// documented v0.28.21 tri-state whose undefined default is HOLD, and
|
|
1286
|
+
// the pinned settings-menu copy ("default: hold on EVERY load"). The
|
|
1287
|
+
// aggressive dial still owns its caps; only explicit `autoResume: true`
|
|
1288
|
+
// consents to load-time automation now.
|
|
1289
|
+
const autoResumeSetting = loadGlobalSettings().autoResume;
|
|
1124
1290
|
const autoResume = shouldAutoResumeOnSessionStart(event?.reason, autoResumeSetting);
|
|
1125
1291
|
const mainRecovery = state.mainModelRecovery;
|
|
1126
1292
|
if (mainRecovery?.manualResumeRequired) {
|
|
@@ -1218,13 +1384,13 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1218
1384
|
// stuck, and zero-stream abort reasons are intentionally excluded by the
|
|
1219
1385
|
// shared predicate and remain explicitly resumable only.
|
|
1220
1386
|
if (state.loop && !state.loop.active && isLifecycleHeldLoopReason(state.loop.stopReason)
|
|
1221
|
-
&& (autoResume ||
|
|
1387
|
+
&& (autoResume || explicitRecovery || sameProcessSuccessorResume)) {
|
|
1222
1388
|
const held = state.loop;
|
|
1223
1389
|
state.loop = { ...held, active: true, stopReason: undefined };
|
|
1224
1390
|
persistState(ctx);
|
|
1225
1391
|
releaseContinuationDispatchStandDown();
|
|
1226
1392
|
appendLedger(ctx.cwd, "loop_auto_resumed_on_restore", {
|
|
1227
|
-
via:
|
|
1393
|
+
via: recoveryResume ? "recovery" : rebindResume ? "rebind" : handoffResume ? "handoff" : sameProcessSuccessorResume ? "same-process-successor" : "auto-resume",
|
|
1228
1394
|
iteration: held.iteration,
|
|
1229
1395
|
best: held.bestValue,
|
|
1230
1396
|
});
|
|
@@ -1261,7 +1427,12 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1261
1427
|
// rebind marker were consumed before the startup barrier above.
|
|
1262
1428
|
if (isLoopActive()) {
|
|
1263
1429
|
const l = state.loop!;
|
|
1264
|
-
|
|
1430
|
+
// v0.35.23: a different-pid crash successor no longer consents by
|
|
1431
|
+
// itself — a crash restart IS a cold load, and cold loads hold for an
|
|
1432
|
+
// explicit decision (note.md Next #2). Validated handoffs/rebinds,
|
|
1433
|
+
// SAME-PROCESS successors, and the Auto-resume setting keep today's
|
|
1434
|
+
// continuity.
|
|
1435
|
+
if (autoResume || explicitRecovery || sameProcessSuccessorResume) {
|
|
1265
1436
|
ctx.ui.notify(
|
|
1266
1437
|
`Resuming loop (iteration ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"}, best ${l.bestValue ?? "n/a"}, stall ${l.stallCount}/${l.plateauWindow}): ${displaySlice(l.target, 60)}`,
|
|
1267
1438
|
"info",
|
|
@@ -1359,12 +1530,12 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1359
1530
|
markCompletionAuditRecoveryPending(ctx, `session_start:${startReason}`);
|
|
1360
1531
|
}
|
|
1361
1532
|
const recoveredClaim = state.goal?.pendingCompletion;
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
// Auto-resume
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1367
|
-
const canRecoverNow = explicitRecovery || autoResume
|
|
1533
|
+
// v0.35.23 (note.md Next #2): the old third disjunct — a validated
|
|
1534
|
+
// host lifecycle consuming the durable retry slot even without
|
|
1535
|
+
// Auto-resume — dispatched automation on a cold load. Removed: parked
|
|
1536
|
+
// claims hold for explicit consent like every other pending state;
|
|
1537
|
+
// the else branch below defers them with a truthful ledger event.
|
|
1538
|
+
const canRecoverNow = explicitRecovery || autoResume;
|
|
1368
1539
|
if (canRecoverNow && recoveredClaim && (recoveredClaim.phase ?? "recovery-pending") === "recovery-pending") {
|
|
1369
1540
|
completionAuditRecoveryArmed = true;
|
|
1370
1541
|
const started = maybeAutoRetryParkedCompletionAudit(hostLifecycleStart || explicitRecovery ? "host-rebind" : "session-start");
|
|
@@ -1406,6 +1577,36 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1406
1577
|
// state that doesn't mutate on load) rendered nothing — "can't tell if
|
|
1407
1578
|
// it's on" is a bug. Painting unconditionally also clears/refreshes any
|
|
1408
1579
|
// stale widget carried over from a previous in-process session.
|
|
1580
|
+
// v0.35.23 (note.md Next #2): a consent-less cold load with pending
|
|
1581
|
+
// durable state engages the LOAD HOLD — same freeze gates as /glla
|
|
1582
|
+
// pause (continuation dispatch, loop ticks, heartbeat probes, recovery
|
|
1583
|
+
// timers all check supervisorPaused()), but DISTINCT from it: released
|
|
1584
|
+
// by any explicit work command, never by timers. State stays fully
|
|
1585
|
+
// restored and DISPLAYED — only automation waits for the user.
|
|
1586
|
+
if (!autoResume && !explicitRecovery && !staleRearmedOnSessionStart) {
|
|
1587
|
+
const somethingLive = (!!state.goal && ["active", "auditing"].includes(state.goal.status)) || isLoopActive();
|
|
1588
|
+
const pendingDurableState = !!state.goal || (state.list?.length ?? 0) > 0 || !!state.loop;
|
|
1589
|
+
// v0.35.23: restore itself may have consented to live work (journal
|
|
1590
|
+
// replay activating a deferred item, stale rearm) — a hold must never
|
|
1591
|
+
// freeze what restore deliberately started.
|
|
1592
|
+
if (pendingDurableState && !somethingLive && typeof state.loadHoldAt !== "number") {
|
|
1593
|
+
replaceState({ ...state, loadHoldAt: Date.now() });
|
|
1594
|
+
persistState(ctx);
|
|
1595
|
+
appendLedger(ctx.cwd, "load_hold_engaged", { reason: startReason ?? "startup" });
|
|
1596
|
+
ctx.ui.notify(
|
|
1597
|
+
"Loaded without starting: your goal/list/loop state is restored and shown below, but automation is HELD for your decision. /goal resume, /list resume, or /list next starts work; enable Auto-resume in /glla settings to restore load-time automation.",
|
|
1598
|
+
"warning",
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
} else if ((autoResume || explicitRecovery) && typeof state.loadHoldAt === "number") {
|
|
1602
|
+
// v0.35.28 (issue #16): a hold persisted by a PREVIOUS process must
|
|
1603
|
+
// not outlive the user's consent — with auto-resume on, a stale
|
|
1604
|
+
// hold would freeze supervisorPaused()-gated machinery (including
|
|
1605
|
+
// the due-wait backstop) until a manual resume that may never come.
|
|
1606
|
+
replaceState({ ...state, loadHoldAt: undefined });
|
|
1607
|
+
persistState(ctx);
|
|
1608
|
+
appendLedger(ctx.cwd, "load_hold_released", { via: "consenting-reload" });
|
|
1609
|
+
}
|
|
1409
1610
|
refreshUI(ctx);
|
|
1410
1611
|
});
|
|
1411
1612
|
|
|
@@ -114,6 +114,8 @@ import {
|
|
|
114
114
|
isForbiddenModel,
|
|
115
115
|
isGoalRevisionCurrent,
|
|
116
116
|
nextHourlyProbeMs,
|
|
117
|
+
supervisorPaused,
|
|
118
|
+
loadHoldActive,
|
|
117
119
|
type ModelSwitchRecord,
|
|
118
120
|
type ListItem,
|
|
119
121
|
} from "../goal-loop-core.js";
|
|
@@ -715,6 +717,12 @@ export type AutomaticCompletionRecoveryTrigger = "session-start" | "host-rebind"
|
|
|
715
717
|
* is not eligible for another automatic event-triggered attempt.
|
|
716
718
|
*/
|
|
717
719
|
export function maybeAutoRetryParkedCompletionAudit(trigger: AutomaticCompletionRecoveryTrigger): boolean {
|
|
720
|
+
// v0.35.23 (note.md Next #2): a plain LOAD HOLD stops every automatic
|
|
721
|
+
// recovery trigger — pending claims wait for an explicit decision. The
|
|
722
|
+
// single exempt is "main-model-recovery": the claim was parked BY the
|
|
723
|
+
// provider failure, and the recovery ladder healing IS the pinned
|
|
724
|
+
// one-shot consent. Manual /glla pause freezes all triggers regardless.
|
|
725
|
+
if (loadHoldActive(state) && trigger !== "main-model-recovery") return false;
|
|
718
726
|
const goal = state.goal;
|
|
719
727
|
const claim = goal?.pendingCompletion;
|
|
720
728
|
if (!goal || goal.status !== "paused" || !claim) return false;
|
|
@@ -749,7 +757,7 @@ export function maybeAutoRetryParkedCompletionAudit(trigger: AutomaticCompletion
|
|
|
749
757
|
// beginCompletionAudit runs before retryStoredCompletionAudit reaches its
|
|
750
758
|
// first await, atomically replacing the parked claim with a running claim
|
|
751
759
|
// and consuming the durable one-shot marker.
|
|
752
|
-
void retryStoredCompletionAudit("session-recovery");
|
|
760
|
+
void retryStoredCompletionAudit("session-recovery", trigger === "main-model-recovery");
|
|
753
761
|
return true;
|
|
754
762
|
}
|
|
755
763
|
|
|
@@ -810,7 +818,17 @@ export async function runDetachedCompletionWithFallback(
|
|
|
810
818
|
* impossible, semantic infra) → preserve the claim and pause for explicit `/goal resume`, while
|
|
811
819
|
* semantic verdicts remain durable in auditHistory.
|
|
812
820
|
*/
|
|
813
|
-
async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provider-retry"): Promise<void> {
|
|
821
|
+
async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provider-retry", exemptLoadHold = false): Promise<void> {
|
|
822
|
+
// v0.35.15: `/glla pause` freezes automatic audit recovery — provider
|
|
823
|
+
// retries and session-recovery re-starts stay parked while the supervisor
|
|
824
|
+
// is paused. Explicit manual requests (`/goal resume`, `/goal verify`)
|
|
825
|
+
// still run: the user typed them, so they are not "automatic machinery".
|
|
826
|
+
// v0.35.23: exemptLoadHold carves out ONE narrow path from the automatic
|
|
827
|
+
// LOAD HOLD (never from a manual /glla pause): a main-model-recovery
|
|
828
|
+
// triggered retry. The claim was parked by provider infrastructure
|
|
829
|
+
// failure; the recovery ladder healing the provider IS the durable
|
|
830
|
+
// consent for its single retry (pinned v0.35.x).
|
|
831
|
+
if (origin !== "manual" && supervisorPaused(state) && !exemptLoadHold) return;
|
|
814
832
|
const goal = state.goal;
|
|
815
833
|
if (!goal?.pendingCompletion) return;
|
|
816
834
|
const goalId = goal.id;
|
|
@@ -878,6 +896,10 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
|
|
|
878
896
|
verificationSummary: claim.verificationSummary,
|
|
879
897
|
model: candidate.model,
|
|
880
898
|
thinkingLevel: (settings.auditorThinkingLevel ?? "high") as any, // may be "max" — pi ≥0.83 understands it; the dev-types predate it
|
|
899
|
+
// v0.36.0: raw settings allowlist; the process layer resolves
|
|
900
|
+
// entries to install paths before hashing (see
|
|
901
|
+
// goal-loop-auditor-process.ts).
|
|
902
|
+
allowedExtensions: settings.auditorAllowedExtensions,
|
|
881
903
|
runtime: { attemptId: () => newDetachedAuditJobAttemptId(claim.attemptId!), logicalAttemptId: claim.attemptId!, wallTimeoutMs: AUDITOR_WALL_TIMEOUT_MS },
|
|
882
904
|
onProgress: (progress) => {
|
|
883
905
|
publishDetachedAuditProgress(generation, goalId, claim.attemptId!, progress);
|
|
@@ -1030,7 +1052,21 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "provi
|
|
|
1030
1052
|
: state.goal.objective;
|
|
1031
1053
|
const recap = displaySlice(recapSrc, 110);
|
|
1032
1054
|
const approvalVia = `${origin === "manual" ? " on /goal verify" : origin === "session-recovery" ? " after session recovery" : " on the provider retry"}${fallbackUsed ? " after an auditor-model fallback" : ""}`;
|
|
1033
|
-
archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`);
|
|
1055
|
+
const archived = archiveCurrentGoal(liveCtx, "complete", `auditor ${result.model} approved (${origin})`);
|
|
1056
|
+
if (!archived) {
|
|
1057
|
+
// archiveCurrentGoal already preserved the live record and warned the
|
|
1058
|
+
// user. Keep the approved claim recoverable, but never emit a terminal
|
|
1059
|
+
// success after the durable archive failed.
|
|
1060
|
+
updateGoal({
|
|
1061
|
+
status: "paused",
|
|
1062
|
+
pendingCompletion: undefined,
|
|
1063
|
+
pauseKind: "blocked",
|
|
1064
|
+
pauseReason: "completion approved but terminal archive persistence failed",
|
|
1065
|
+
pauseSuggestedAction: `Fix .pi-glla disk access or resolve the archive fence, then ${activeGoalSurfaceCommand("resume")} and call complete_goal again.`,
|
|
1066
|
+
}, liveCtx);
|
|
1067
|
+
appendLedger(liveCtx.cwd, "goal_archive_failed_after_approval", { goalId, attemptId: claim.attemptId, origin });
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1034
1070
|
liveCtx.ui.notify(`✓ done: ${recap} — auditor ${result.model} approved${approvalVia}.`, "info");
|
|
1035
1071
|
notifyExternal(liveCtx, `Goal complete (auditor approved, ${origin}): ${displaySlice(recapSrc, 120)}`);
|
|
1036
1072
|
return;
|
|
@@ -645,7 +645,23 @@ function activateNextListItem(ctx: ExtensionContext, n = 1, opts?: { explicit?:
|
|
|
645
645
|
// completion cascade, /list next, list_activate, list-draft auto-activate)
|
|
646
646
|
// may activate a list item over a live loop, present or future.
|
|
647
647
|
if (isLoopActive()) {
|
|
648
|
-
|
|
648
|
+
// v0.35.22 (note.md Next #3, field 2026-08-21): this refusal used to be
|
|
649
|
+
// LEDGER-ONLY — a queued repair card behind a paused suspicious goal
|
|
650
|
+
// was unstartable AND invisibly so (screenshots 134442/134645: the user
|
|
651
|
+
// is told to run /list next, it silently no-ops while a loop holds the
|
|
652
|
+
// surface). One-active-thing still refuses, but LOUDLY and with the way
|
|
653
|
+
// out; the loop-end cascade (announceQueuedListAfterLoopEnd) surfaces
|
|
654
|
+
// that the item is startable again once the loop ends.
|
|
655
|
+
const head = listQueue()[0];
|
|
656
|
+
appendLedger(ctx.cwd, "list_activation_blocked_loop", {
|
|
657
|
+
...(head ? { queueItemId: head.id, objective: head.objective.slice(0, 200) } : {}),
|
|
658
|
+
});
|
|
659
|
+
ctx.ui.notify(
|
|
660
|
+
head
|
|
661
|
+
? `The queued item cannot start while a loop owns the surface — "${head.objective.slice(0, 80)}" stays queued. /loop stop the loop (or let its bounds end it), then /list next starts the item.`
|
|
662
|
+
: "A loop is active — one active thing at a time. /loop stop it first, then /list next.",
|
|
663
|
+
"warning",
|
|
664
|
+
);
|
|
649
665
|
return false;
|
|
650
666
|
}
|
|
651
667
|
// v0.28.14: carryover resolution runs BEFORE the item is taken — under
|
|
@@ -109,6 +109,7 @@ import {
|
|
|
109
109
|
deleteQueueItemFile,
|
|
110
110
|
missingGllaTools,
|
|
111
111
|
runPersistStep,
|
|
112
|
+
isSafePersistedId,
|
|
112
113
|
isPersistenceDegraded,
|
|
113
114
|
lastPersistenceFailure,
|
|
114
115
|
modelSwitch,
|
|
@@ -116,6 +117,7 @@ import {
|
|
|
116
117
|
isGoalRevisionCurrent,
|
|
117
118
|
nextHourlyPromptMs,
|
|
118
119
|
nextHourlyProbeMs,
|
|
120
|
+
clearLoadHold,
|
|
119
121
|
type ModelSwitchRecord,
|
|
120
122
|
type ListItem,
|
|
121
123
|
} from "../goal-loop-core.js";
|
|
@@ -586,6 +588,14 @@ async function handleMainModelAgentEnd(ctx: ExtensionContext, rawLastA: any, las
|
|
|
586
588
|
|
|
587
589
|
function createGoal(objective: string, ctx: ExtensionContext, policy: "goal" | "list" = "goal"): Goal {
|
|
588
590
|
ensureDirs(ctx.cwd);
|
|
591
|
+
// v0.35.23 (note.md Next #2): creating a goal — via /goal start, a list
|
|
592
|
+
// item activation, or an accepted draft — IS the explicit decision a
|
|
593
|
+
// load hold waits for. Release before the new work's first continuation
|
|
594
|
+
// would otherwise be frozen.
|
|
595
|
+
if (clearLoadHold(state)) {
|
|
596
|
+
persistState(ctx);
|
|
597
|
+
appendLedger(ctx.cwd, "load_hold_released", { via: `goal-created:${policy}` });
|
|
598
|
+
}
|
|
589
599
|
// Extract verification contract if present in objective.
|
|
590
600
|
const { objective: roleCleaned, agentRole } = extractAgentRole(objective);
|
|
591
601
|
const { objective: cleanObj, verificationContract } = extractVerificationContract(roleCleaned);
|
|
@@ -893,6 +903,10 @@ function archiveCurrentGoal(
|
|
|
893
903
|
): boolean {
|
|
894
904
|
if (!state.goal) return false;
|
|
895
905
|
const goal = state.goal;
|
|
906
|
+
if (!isSafePersistedId(goal.id)) {
|
|
907
|
+
ctx.ui.notify("Could not archive the goal — its persisted id is invalid, so no filesystem path was used. Repair the state before retrying.", "warning");
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
896
910
|
const pendingAttemptId = goal.pendingCompletion?.attemptId;
|
|
897
911
|
const target = archivedGoalPath(ctx.cwd, goal.id);
|
|
898
912
|
const terminalGoal: Goal = {
|
|
@@ -984,7 +998,20 @@ function archiveCurrentGoal(
|
|
|
984
998
|
// had a chance to choose a successor.
|
|
985
999
|
const closeArchivedSlot = () => {
|
|
986
1000
|
if (state.goal?.id !== goal.id) return;
|
|
987
|
-
|
|
1001
|
+
// v0.35.30: the archive nulled the slot, which blanked the widget — the
|
|
1002
|
+
// verdict (approval or abort) survived only as one transient toast. Keep
|
|
1003
|
+
// a durable last-outcome record so the widget can answer "did the final
|
|
1004
|
+
// audit actually run?" hours later, after the agent's turn has ended.
|
|
1005
|
+
replaceState({
|
|
1006
|
+
...state,
|
|
1007
|
+
goal: null,
|
|
1008
|
+
lastOutcome: {
|
|
1009
|
+
at: nowIso(),
|
|
1010
|
+
ok: status === "complete",
|
|
1011
|
+
title: stopReason ?? status,
|
|
1012
|
+
recap: (goal.completionSummary?.trim() || goal.objective).replace(/\s+/g, " ").slice(0, 300),
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
988
1015
|
persistState(ctx);
|
|
989
1016
|
};
|
|
990
1017
|
// Loop 2: a list-sourced goal COMPLETED → auto-activate the next item.
|