pi-goal-list-loop-audit 0.35.69 → 0.35.71
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 +29 -0
- package/INSTALL.md +4 -1
- package/README.md +5 -1
- package/docs/DESIGN.md +17 -0
- package/extensions/goal-commands.ts +1 -0
- package/extensions/goal-heartbeat.ts +1 -1
- package/extensions/goal-loop-backoff.ts +21 -10
- package/extensions/goal-loop-subagents.ts +5 -3
- package/extensions/goal-settings.ts +17 -0
- package/extensions/loops/goal-activation.ts +64 -49
- package/extensions/loops/goal-settings-ui.ts +16 -0
- package/extensions/settings-menu.ts +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.71 — bounded Pi-core retry containment (2026-08-26)
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
GLLA now exposes a configurable zero-stream retry budget (default 3,
|
|
7
|
+
allowed range 0–10). When Pi remains BUSY without stream activity, GLLA
|
|
8
|
+
still aborts and durably parks the owner, then re-dispatches repeatedly
|
|
9
|
+
within that finite budget before requiring explicit resume. A real stream
|
|
10
|
+
starts a fresh budget; supervisor pause, generation fences, and durable
|
|
11
|
+
recovery semantics remain intact. This contains the Pi-core retry-sleeper
|
|
12
|
+
failure at GLLA's public boundary without modifying Pi or any other plugin.
|
|
13
|
+
|
|
14
|
+
### Tests
|
|
15
|
+
Added repeated-silence exhaustion, budget persistence/validation, settings
|
|
16
|
+
menu/editor coverage, and existing lifecycle/park/no-storm regressions.
|
|
17
|
+
|
|
18
|
+
## 0.35.70 — optional-provider boundary and recovery alignment (2026-08-26)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
GLLA now delegates the managed subagent agent-directory lookup to pi's
|
|
22
|
+
host resolver, honoring custom `PI_CODING_AGENT_DIR` values instead of
|
|
23
|
+
assuming `~/.pi/agent`. The core extension remains independent of the
|
|
24
|
+
optional `@tintinweb/pi-subagents` provider.
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
Added a genuine no-provider load smoke test that forbids the optional
|
|
28
|
+
provider import while loading the extension, plus custom-agent-directory
|
|
29
|
+
coverage. The existing context-overflow recovery path remains the single
|
|
30
|
+
configured fallback route; no hardcoded free model was introduced.
|
|
31
|
+
|
|
3
32
|
## 0.35.69 — metricless-loop cadence (2026-08-26)
|
|
4
33
|
|
|
5
34
|
### Added
|
package/INSTALL.md
CHANGED
|
@@ -147,7 +147,10 @@ Useful controls:
|
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
`/glla pause` freezes supervisor automation without killing active work.
|
|
150
|
-
`/glla resume` releases it. A
|
|
150
|
+
`/glla resume` releases it. A BUSY/no-stream Pi turn is aborted and parked by
|
|
151
|
+
GLLA, then automatically re-dispatched within the **Zero-stream retries**
|
|
152
|
+
budget (default 3, configurable from 0–10); exhaustion requires an explicit
|
|
153
|
+
mode-correct resume. A user abort means stop; recovery is not silently
|
|
151
154
|
re-fired behind your back.
|
|
152
155
|
|
|
153
156
|
## Auditor model requirement
|
package/README.md
CHANGED
|
@@ -329,7 +329,9 @@ that silence means success. Error text is **not trusted** to pick a retry policy
|
|
|
329
329
|
failure wording is retained as bounded diagnostics, not interpreted as
|
|
330
330
|
proof of a quota or billing state.
|
|
331
331
|
|
|
332
|
-
- automatic retries are bounded and visible;
|
|
332
|
+
- automatic retries are bounded and visible; a BUSY/no-stream turn is parked
|
|
333
|
+
and re-dispatched within the configurable **Zero-stream retries** budget
|
|
334
|
+
(default 3, range 0–10), then requires explicit resume;
|
|
333
335
|
- `/goal resume`, `/list resume`, and `/loop resume` are explicit recovery
|
|
334
336
|
paths;
|
|
335
337
|
- a user abort means stop, not “try again behind my back”;
|
|
@@ -358,6 +360,8 @@ Open `/glla` for the settings table. The most important choices are:
|
|
|
358
360
|
choices win;
|
|
359
361
|
- **Subagent hang escalation:** warning-only at `0`, or one child-specific
|
|
360
362
|
action after a confirmed frozen interval;
|
|
363
|
+
- **Zero-stream retries:** automatic GLLA recovery attempts after a busy,
|
|
364
|
+
stream-silent Pi turn; `0` keeps recovery manual and `1–10` bounds repeats;
|
|
361
365
|
- **Audit cap and retry cadence:** bounds for repeated objections and
|
|
362
366
|
infrastructure recovery.
|
|
363
367
|
|
package/docs/DESIGN.md
CHANGED
|
@@ -354,6 +354,23 @@ replacement without delivering a successor `session_start`:
|
|
|
354
354
|
and normalize old files. Canonical persisted state uses `retry-waiting`,
|
|
355
355
|
`retryAttempts`, `retryFirstAt`, and `retryUntil`.
|
|
356
356
|
|
|
357
|
+
## Addendum v0.35.71 (GLLA-only external-failure containment)
|
|
358
|
+
|
|
359
|
+
GLLA is a workflow supervisor, not a maintenance fork of its host ecosystem.
|
|
360
|
+
The repository changes only GLLA-owned code. Pi-core, OS, provider, and
|
|
361
|
+
third-party-plugin failures may be observed through public hooks and contained
|
|
362
|
+
by durable GLLA state (for example, a BUSY/no-stream turn can be aborted,
|
|
363
|
+
parked, and retried within a finite configured budget), but GLLA never edits
|
|
364
|
+
those external implementations or claims to repair them. Reports whose only
|
|
365
|
+
code lives outside GLLA are explicitly documented and disposed of as
|
|
366
|
+
out-of-scope.
|
|
367
|
+
|
|
368
|
+
The zero-stream budget defaults to three automatic re-dispatches and accepts
|
|
369
|
+
0–10. Each uninterrupted silent attempt consumes one slot; a real stream
|
|
370
|
+
starts a fresh episode. Exhaustion leaves the goal/list item/loop parked for
|
|
371
|
+
explicit resume. Generation, supervisor-pause, persistence, and dispatch
|
|
372
|
+
ownership fences remain authoritative throughout.
|
|
373
|
+
|
|
357
374
|
## Addendum v0.35.0 (long-running judgment, Designer, and drafting chain)
|
|
358
375
|
|
|
359
376
|
- **Judgment is a prompt-level contract**: drafting and continuation preserve
|
|
@@ -2413,6 +2413,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2413
2413
|
fmt("aggressiveMode", "aggressiveMode"),
|
|
2414
2414
|
fmt("stuckMaxInterventions", "stuckMaxInterventions"),
|
|
2415
2415
|
fmt("stallEscalationRefires", "stallEscalation"),
|
|
2416
|
+
fmt("zombieRetryMaxAttempts", "zombieRetryMaxAttempts"),
|
|
2416
2417
|
fmt("subagentHangEscalationMinutes", "subagentHangActionMinutes"),
|
|
2417
2418
|
fmt("wedgeAlertMinutes", "wedgeAlert"),
|
|
2418
2419
|
fmt("stallShortWords", "stallShortWords"),
|
|
@@ -1036,7 +1036,7 @@ function heartbeatTick(): void {
|
|
|
1036
1036
|
// v0.29.16/v0.35.x: zombie-run watchdog. pi reports BUSY (a run is
|
|
1037
1037
|
// "active") but zero stream events for the warning window — the provider
|
|
1038
1038
|
// stream has likely hung silently, and queued continuations cannot land.
|
|
1039
|
-
// Keep the first warning human-readable, then apply
|
|
1039
|
+
// Keep the first warning human-readable, then apply bounded automatic
|
|
1040
1040
|
// abort/cleanup after the grace window. A notification-only Esc instruction
|
|
1041
1041
|
// left list items ACTIVE for 85–96 minutes in the field; the bounded abort
|
|
1042
1042
|
// now parks the item and leaves /list resume + /list cancel as explicit,
|
|
@@ -248,11 +248,17 @@ export function accountTurnForNudgesRich(
|
|
|
248
248
|
// Field (note.md Next §1, screenshot 20260821_152311): turns dispatched by
|
|
249
249
|
// ACCEPTING a Confirm dialog hang with zero provider stream activity often
|
|
250
250
|
// enough that users repeatedly return to "action needed - this won't fix
|
|
251
|
-
// itself" parks. The watchdog's abort is correct; the missing half is
|
|
252
|
-
//
|
|
253
|
-
//
|
|
251
|
+
// itself" parks. The watchdog's abort is correct; the missing half is a
|
|
252
|
+
// finite repeated re-dispatch budget per silence streak. This pure decision
|
|
253
|
+
// is consumed by the timer in goal-activation.ts.
|
|
254
254
|
|
|
255
255
|
export const ZOMBIE_RETRY_DELAY_MS = 90_000;
|
|
256
|
+
/** Maximum automatic re-dispatches after one uninterrupted zero-stream
|
|
257
|
+
* episode. Three attempts give a transient Pi-core retry sleeper several
|
|
258
|
+
* chances to recover without turning the watchdog into an infinite loop. */
|
|
259
|
+
export const DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS = 3;
|
|
260
|
+
/** Hand-edited settings are bounded to keep recovery finite and reviewable. */
|
|
261
|
+
export const MAX_ZOMBIE_RETRY_ATTEMPTS = 10;
|
|
256
262
|
|
|
257
263
|
export interface ZombieRetryStreak {
|
|
258
264
|
key: string;
|
|
@@ -262,21 +268,26 @@ export interface ZombieRetryStreak {
|
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
/** A new owner key OR stream activity newer than the recorded abort point
|
|
265
|
-
* starts a fresh streak; otherwise the streak deepens
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* retry. */
|
|
271
|
+
* starts a fresh streak; otherwise the streak deepens until the caller's
|
|
272
|
+
* finite retry budget is exhausted. A retried turn that actually streams
|
|
273
|
+
* advances lastStreamActivityAt past the previous abort's observation point,
|
|
274
|
+
* so any later independent hang earns a fresh budget. */
|
|
270
275
|
export function zombieRetryDecision(
|
|
271
276
|
observedStreamAt: number,
|
|
272
277
|
key: string,
|
|
273
278
|
prev: ZombieRetryStreak,
|
|
279
|
+
maxAttempts = DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS,
|
|
274
280
|
): { retry: boolean; streak: ZombieRetryStreak } {
|
|
281
|
+
const limit = Number.isInteger(maxAttempts) && maxAttempts >= 0
|
|
282
|
+
? Math.min(maxAttempts, MAX_ZOMBIE_RETRY_ATTEMPTS)
|
|
283
|
+
: DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS;
|
|
275
284
|
const sameEpisode = key === prev.key && observedStreamAt <= prev.lastAbortStreamAt;
|
|
285
|
+
// Keep one terminal refusal count beyond the retry budget so repeated
|
|
286
|
+
// heartbeat observations remain cheap and deterministic.
|
|
276
287
|
const streak: ZombieRetryStreak = sameEpisode
|
|
277
|
-
? { key, count: Math.min(prev.count + 1,
|
|
288
|
+
? { key, count: Math.min(prev.count + 1, MAX_ZOMBIE_RETRY_ATTEMPTS + 1), lastAbortStreamAt: observedStreamAt }
|
|
278
289
|
: { key, count: 1, lastAbortStreamAt: observedStreamAt };
|
|
279
|
-
return { retry: streak.count
|
|
290
|
+
return { retry: streak.count <= limit, streak };
|
|
280
291
|
}
|
|
281
292
|
|
|
282
293
|
// =================================================================
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
// to add embedded copies here.
|
|
26
26
|
|
|
27
27
|
import * as fs from "node:fs";
|
|
28
|
-
import * as os from "node:os";
|
|
29
28
|
import * as path from "node:path";
|
|
29
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
|
|
31
31
|
/** Frontmatter marker identifying files this module wrote. Files without it
|
|
32
32
|
* are user-owned: never modified, never deleted. */
|
|
@@ -178,9 +178,11 @@ const EMBEDDED_DEFAULTS: Record<string, EmbeddedAgentDefault> = {
|
|
|
178
178
|
export const OVERRIDABLE_AGENT_TYPES = Object.keys(EMBEDDED_DEFAULTS);
|
|
179
179
|
|
|
180
180
|
/** Default global agent dir (pi-subagents reads $PI_CODING_AGENT_DIR/agents,
|
|
181
|
-
* default ~/.pi/agent/agents).
|
|
181
|
+
* default ~/.pi/agent/agents). Delegate to pi's runtime resolver so custom
|
|
182
|
+
* `PI_CODING_AGENT_DIR` and future app-specific config-dir names stay aligned
|
|
183
|
+
* with the host rather than being silently ignored by glla. */
|
|
182
184
|
export function defaultAgentDir(): string {
|
|
183
|
-
return
|
|
185
|
+
return getAgentDir();
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
/** Build the override .md file content. With `model`, the pin is written;
|
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
DEFAULT_MAIN_MODEL_PRIMARY_PROBE_MINUTES,
|
|
31
31
|
normalizeMainModelFallbackRefs,
|
|
32
32
|
} from "./main-model-recovery.js";
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS,
|
|
35
|
+
MAX_ZOMBIE_RETRY_ATTEMPTS,
|
|
36
|
+
} from "./goal-loop-backoff.js";
|
|
33
37
|
|
|
34
38
|
export interface Settings {
|
|
35
39
|
/** Where glla's durable state directory lives. This is global-only because
|
|
@@ -169,6 +173,9 @@ export interface Settings {
|
|
|
169
173
|
/** v0.27.3: a turn with no tool calls AND fewer words than this is a
|
|
170
174
|
* nudge. Default 15 words. Higher = stricter (more pauses). */
|
|
171
175
|
stallShortWords?: number;
|
|
176
|
+
/** Maximum automatic re-dispatches after one uninterrupted busy/no-stream
|
|
177
|
+
* episode (default 3; 0 = manual resume only; max 10). */
|
|
178
|
+
zombieRetryMaxAttempts?: number;
|
|
172
179
|
/** v0.27.3: a turn with no tool calls whose text trigram-similarity to
|
|
173
180
|
* the prior assistant turn exceeds this is a nudge. Default 0.6. Higher
|
|
174
181
|
* = stricter (more pauses). */
|
|
@@ -281,6 +288,9 @@ export const DEFAULT_SETTINGS: Settings = {
|
|
|
281
288
|
// v0.35.64: warn at the short detection thresholds, then take one
|
|
282
289
|
// child-specific action after a much longer confirmed frozen interval.
|
|
283
290
|
subagentHangEscalationMinutes: 30,
|
|
291
|
+
// v0.35.x: repeated Pi-core busy/no-stream recovery is useful for a
|
|
292
|
+
// transient retry sleeper, but must remain finite and user-configurable.
|
|
293
|
+
zombieRetryMaxAttempts: DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS,
|
|
284
294
|
};
|
|
285
295
|
|
|
286
296
|
// Re-exported for compatibility; the dependency-free state-root module owns
|
|
@@ -329,6 +339,12 @@ function normalizeLoadedSettings(settings: Settings): Settings {
|
|
|
329
339
|
|| (settings.subagentHangEscalationMinutes !== 0 && settings.subagentHangEscalationMinutes < 5)) {
|
|
330
340
|
settings.subagentHangEscalationMinutes = 30;
|
|
331
341
|
}
|
|
342
|
+
if (typeof settings.zombieRetryMaxAttempts !== "number"
|
|
343
|
+
|| !Number.isInteger(settings.zombieRetryMaxAttempts)
|
|
344
|
+
|| settings.zombieRetryMaxAttempts < 0
|
|
345
|
+
|| settings.zombieRetryMaxAttempts > MAX_ZOMBIE_RETRY_ATTEMPTS) {
|
|
346
|
+
settings.zombieRetryMaxAttempts = DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS;
|
|
347
|
+
}
|
|
332
348
|
// v0.34.142: these old policy knobs no longer control recovery. Drop
|
|
333
349
|
// them from the effective object so stale files cannot resurrect the old
|
|
334
350
|
// behavior or make the settings UI imply that quota inspection exists.
|
|
@@ -416,6 +432,7 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
|
|
|
416
432
|
"stuckMaxInterventions",
|
|
417
433
|
"subagentHangEscalationMinutes",
|
|
418
434
|
"stallEscalationRefires",
|
|
435
|
+
"zombieRetryMaxAttempts",
|
|
419
436
|
"stallShortWords",
|
|
420
437
|
"stallSimilarityThreshold",
|
|
421
438
|
"postaudit",
|
|
@@ -350,6 +350,7 @@ import {
|
|
|
350
350
|
PENDING_LATCH_STUCK_MS,
|
|
351
351
|
shouldFirePendingLatchWatchdog,
|
|
352
352
|
AUDITOR_WALL_TIMEOUT_MS,
|
|
353
|
+
DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS,
|
|
353
354
|
ZOMBIE_RETRY_DELAY_MS,
|
|
354
355
|
zombieRetryDecision,
|
|
355
356
|
type ZombieRetryStreak,
|
|
@@ -427,29 +428,20 @@ export function enqueueFaultRepairTask(ctx: ExtensionContext, objective: string,
|
|
|
427
428
|
appendLedger(ctx.cwd, "faulty_objective_repair_promoted", { goalId: repair.id, targetId: target?.id, position: 1, source: target?.source ?? "unknown" });
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
/** v0.35.
|
|
431
|
+
/** v0.35.x — bounded repeated automatic recovery after a confirmed
|
|
432
|
+
* zero-stream abort.
|
|
431
433
|
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* streak, mirroring main-model recovery's timer pattern:
|
|
434
|
+
* A Pi-core retry sleeper can keep the host BUSY long after the useful stream
|
|
435
|
+
* has stopped. GLLA owns the safe boundary: abort the stale host turn, park
|
|
436
|
+
* durable work, and re-dispatch while the configured zero-stream retry budget
|
|
437
|
+
* remains. A real stream resets the episode; uninterrupted silence eventually
|
|
438
|
+
* reaches the budget and stays parked instead of producing an infinite retry
|
|
439
|
+
* storm.
|
|
439
440
|
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* - a SECOND consecutive silence (retry hung too, or the original turn
|
|
445
|
-
* never streamed again) parks permanently — no retry storm;
|
|
446
|
-
* - `/glla pause` freezes the retry like every other automatic
|
|
447
|
-
* side-effect: the parked claim stays durable for manual resume;
|
|
448
|
-
* - manual /goal resume · /loop resume remain available at all times.
|
|
449
|
-
*
|
|
450
|
-
* The streak counter is process-memory by design: if the session restarts
|
|
451
|
-
* inside the retry window the park stands and manual resume applies — an
|
|
452
|
-
* honest degradation, not a phantom promise.
|
|
441
|
+
* `/glla pause` freezes the retry like every other automatic side-effect, and
|
|
442
|
+
* manual /goal resume · /loop resume remain available at all times. The streak
|
|
443
|
+
* counter is process-memory by design: a session restart inside the retry
|
|
444
|
+
* window leaves the park standing for manual resume.
|
|
453
445
|
*/
|
|
454
446
|
const ZOMBIE_PAUSE_REASON = "automatic zero-stream abort — no provider activity was observed";
|
|
455
447
|
|
|
@@ -466,25 +458,32 @@ function clearInBandProviderFailure(): void {
|
|
|
466
458
|
inBandProviderFailureRaw = null;
|
|
467
459
|
}
|
|
468
460
|
|
|
469
|
-
/** Arm the
|
|
470
|
-
*
|
|
471
|
-
* copy
|
|
461
|
+
/** Arm the next automatic re-dispatch after a successful zombie abort.
|
|
462
|
+
* The configured retry budget keeps repeated recovery finite; the caller gets
|
|
463
|
+
* the attempt number so user-facing copy and ledger evidence stay truthful. */
|
|
472
464
|
function scheduleZombieAutoRetry(
|
|
473
465
|
ctx: ExtensionContext,
|
|
474
466
|
generation: number,
|
|
475
467
|
goalId: string | undefined,
|
|
476
468
|
observedStreamAt: number,
|
|
477
469
|
silentMinutes: number,
|
|
478
|
-
): boolean {
|
|
470
|
+
): { scheduled: boolean; attempt: number; maxAttempts: number } {
|
|
479
471
|
const key = goalId ?? "loop";
|
|
480
|
-
const
|
|
472
|
+
const maxAttempts = zombieRetryMaxAttemptsOverride
|
|
473
|
+
?? loadSettings(ctx.cwd).zombieRetryMaxAttempts
|
|
474
|
+
?? DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS;
|
|
475
|
+
const { retry, streak } = zombieRetryDecision(observedStreamAt, key, zombieRetryStreak, maxAttempts);
|
|
481
476
|
zombieRetryStreak = streak;
|
|
477
|
+
const plan = { scheduled: retry, attempt: streak.count, maxAttempts };
|
|
482
478
|
appendLedger(ctx.cwd, retry ? "zombie_auto_retry_scheduled" : "zombie_auto_retry_refused_streak", {
|
|
483
479
|
goalId,
|
|
484
480
|
streakCount: streak.count,
|
|
481
|
+
maxAttempts,
|
|
485
482
|
delayMs: zombieRetryDelayOverride ?? ZOMBIE_RETRY_DELAY_MS,
|
|
486
483
|
});
|
|
487
|
-
if (!retry) return
|
|
484
|
+
if (!retry) return plan;
|
|
485
|
+
const retryAttempt = plan.attempt;
|
|
486
|
+
const retryMaxAttempts = plan.maxAttempts;
|
|
488
487
|
if (zombieRetryTimer) {
|
|
489
488
|
clearTimeout(zombieRetryTimer);
|
|
490
489
|
zombieRetryTimer = null;
|
|
@@ -513,10 +512,15 @@ function scheduleZombieAutoRetry(
|
|
|
513
512
|
pauseSuggestedAction: undefined,
|
|
514
513
|
...(usage ? { usage } : {}),
|
|
515
514
|
}, fresh);
|
|
516
|
-
appendLedger(fresh.cwd, "zombie_auto_retry_dispatched", {
|
|
515
|
+
appendLedger(fresh.cwd, "zombie_auto_retry_dispatched", {
|
|
516
|
+
goalId: goal.id,
|
|
517
|
+
policy: goal.policy,
|
|
518
|
+
attempt: retryAttempt,
|
|
519
|
+
maxAttempts: retryMaxAttempts,
|
|
520
|
+
});
|
|
517
521
|
releaseZombieAbortKey();
|
|
518
522
|
try {
|
|
519
|
-
fresh.ui.notify(`Automatic retry starting — the earlier ${goal.policy === "list" ? "list item" : "goal"} turn was aborted after ${silentMinutes}m of zero stream activity.
|
|
523
|
+
fresh.ui.notify(`Automatic retry ${retryAttempt}/${retryMaxAttempts} starting — the earlier ${goal.policy === "list" ? "list item" : "goal"} turn was aborted after ${silentMinutes}m of zero stream activity. If it hangs again, GLLA keeps retrying within this budget before requiring ${activeGoalSurfaceCommand("resume")}.`, "info");
|
|
520
524
|
} catch { /* best-effort */ }
|
|
521
525
|
scheduleContinuation(fresh, true);
|
|
522
526
|
return;
|
|
@@ -529,14 +533,19 @@ function scheduleZombieAutoRetry(
|
|
|
529
533
|
loop.active = true;
|
|
530
534
|
loop.stopReason = undefined;
|
|
531
535
|
persistState(fresh);
|
|
532
|
-
appendLedger(fresh.cwd, "zombie_auto_retry_dispatched", {
|
|
536
|
+
appendLedger(fresh.cwd, "zombie_auto_retry_dispatched", {
|
|
537
|
+
kind: "loop",
|
|
538
|
+
iteration: loop.iteration,
|
|
539
|
+
attempt: retryAttempt,
|
|
540
|
+
maxAttempts: retryMaxAttempts,
|
|
541
|
+
});
|
|
533
542
|
releaseZombieAbortKey();
|
|
534
543
|
try {
|
|
535
|
-
fresh.ui.notify(`Automatic retry starting — the loop was stopped after a zero-stream abort.
|
|
544
|
+
fresh.ui.notify(`Automatic retry ${retryAttempt}/${retryMaxAttempts} starting — the loop was stopped after a zero-stream abort. GLLA keeps retrying within this budget; if it hangs again after the budget, it stays stopped for /loop resume.`, "info");
|
|
536
545
|
} catch { /* best-effort */ }
|
|
537
546
|
scheduleLoopTick(fresh);
|
|
538
547
|
}, zombieRetryDelayOverride ?? ZOMBIE_RETRY_DELAY_MS);
|
|
539
|
-
return
|
|
548
|
+
return plan;
|
|
540
549
|
}
|
|
541
550
|
|
|
542
551
|
/** Test-only: reset the module-global retry streak/timer state. */
|
|
@@ -546,9 +555,11 @@ export function __testOnlyResetZombieAutoRetry(): void {
|
|
|
546
555
|
zombieRetryTimer = null;
|
|
547
556
|
}
|
|
548
557
|
zombieRetryStreak = { key: "", count: 0, lastAbortStreamAt: 0 };
|
|
558
|
+
zombieRetryMaxAttemptsOverride = null;
|
|
549
559
|
}
|
|
550
560
|
|
|
551
561
|
let zombieRetryDelayOverride: number | null = null;
|
|
562
|
+
let zombieRetryMaxAttemptsOverride: number | null = null;
|
|
552
563
|
|
|
553
564
|
/** Test-only: shrink the automatic-retry delay (null restores the 90s
|
|
554
565
|
* production default). Never called by production code. */
|
|
@@ -556,15 +567,19 @@ export function __testOnlySetZombieRetryDelay(ms: number | null): void {
|
|
|
556
567
|
zombieRetryDelayOverride = ms;
|
|
557
568
|
}
|
|
558
569
|
|
|
559
|
-
/**
|
|
560
|
-
*
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
*
|
|
567
|
-
*
|
|
570
|
+
/** Test-only: pin the retry budget without depending on the caller's global
|
|
571
|
+
* settings file. Null restores the loaded setting/default. */
|
|
572
|
+
export function __testOnlySetZombieRetryMaxAttempts(attempts: number | null): void {
|
|
573
|
+
zombieRetryMaxAttemptsOverride = attempts;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** v0.35.x: terminate one confirmed zero-stream host turn and park the
|
|
577
|
+
* owning goal/list item before asking pi to abort. This is deliberately an
|
|
578
|
+
* activation-owned operation: it can clear the durable continuation sidecar,
|
|
579
|
+
* stand down re-arms, and validate the current generation before touching the
|
|
580
|
+
* host. A later /goal or /list resume explicitly clears the stand-down and
|
|
581
|
+
* starts a new attempt; each uninterrupted silence can also consume the
|
|
582
|
+
* configured bounded retry budget through scheduleZombieAutoRetry below. */
|
|
568
583
|
export function abortZombieRun(
|
|
569
584
|
ctx: ExtensionContext,
|
|
570
585
|
generation: number,
|
|
@@ -596,7 +611,7 @@ export function abortZombieRun(
|
|
|
596
611
|
status: "paused",
|
|
597
612
|
pauseKind: "error",
|
|
598
613
|
pauseReason: reason,
|
|
599
|
-
pauseSuggestedAction: `The stalled ${noun} turn was aborted safely and the work is saved. ${activeGoalSurfaceCommand("resume")} retries
|
|
614
|
+
pauseSuggestedAction: `The stalled ${noun} turn was aborted safely and the work is saved. Automatic retries use the configured zero-stream budget; ${activeGoalSurfaceCommand("resume")} retries immediately and ${activeGoalSurfaceCommand("cancel")} discards it.`,
|
|
600
615
|
}, current);
|
|
601
616
|
} else if (loop) {
|
|
602
617
|
clearLoopTimer();
|
|
@@ -621,14 +636,14 @@ export function abortZombieRun(
|
|
|
621
636
|
});
|
|
622
637
|
const resume = goal ? activeGoalSurfaceCommand("resume") : "/loop resume";
|
|
623
638
|
const cancel = goal ? activeGoalSurfaceCommand("cancel") : "/loop stop";
|
|
624
|
-
// v0.35.
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
const
|
|
639
|
+
// v0.35.x: keep the park as a bounded waystation for repeated
|
|
640
|
+
// zero-stream attempts. Once the configured budget is exhausted the
|
|
641
|
+
// refusal remains manual and durable.
|
|
642
|
+
const retryPlan = scheduleZombieAutoRetry(current, generation, goal?.id, observedStreamAt, silentMinutes);
|
|
628
643
|
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. ${
|
|
629
|
-
|
|
630
|
-
? `
|
|
631
|
-
: `${resume} retries it, or ${cancel} discards/stops it.`
|
|
644
|
+
retryPlan.scheduled
|
|
645
|
+
? `Automatic retry ${retryPlan.attempt}/${retryPlan.maxAttempts} starts in ~90s; ${resume} retries immediately instead.`
|
|
646
|
+
: `${resume} retries it, or ${cancel} discards/stops it; the automatic retry budget is exhausted.`
|
|
632
647
|
}`;
|
|
633
648
|
current.ui.notify(message, abortError ? "warning" : "info");
|
|
634
649
|
notifyExternal(current, message);
|
|
@@ -333,6 +333,7 @@ import {
|
|
|
333
333
|
PENDING_LATCH_STUCK_MS,
|
|
334
334
|
shouldFirePendingLatchWatchdog,
|
|
335
335
|
AUDITOR_WALL_TIMEOUT_MS,
|
|
336
|
+
MAX_ZOMBIE_RETRY_ATTEMPTS,
|
|
336
337
|
} from "../goal-loop-backoff.js";
|
|
337
338
|
|
|
338
339
|
import {
|
|
@@ -1322,6 +1323,21 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
|
|
|
1322
1323
|
}
|
|
1323
1324
|
return;
|
|
1324
1325
|
}
|
|
1326
|
+
case "zombieRetryMaxAttempts": {
|
|
1327
|
+
const v = await ctx.ui.input("Automatic retries after a busy zero-stream abort", "integer from 0 to 10; 0 = manual resume only; empty = default 3");
|
|
1328
|
+
if (v !== undefined) {
|
|
1329
|
+
const raw = v.trim();
|
|
1330
|
+
const n = Number.parseInt(raw, 10);
|
|
1331
|
+
if (/^\d+$/.test(raw) && Number.isInteger(n) && n >= 0 && n <= MAX_ZOMBIE_RETRY_ATTEMPTS) {
|
|
1332
|
+
saveSettings("global", ctx.cwd, { zombieRetryMaxAttempts: n });
|
|
1333
|
+
} else if (!raw) {
|
|
1334
|
+
saveSettings("global", ctx.cwd, { zombieRetryMaxAttempts: undefined });
|
|
1335
|
+
} else {
|
|
1336
|
+
ctx.ui.notify(`Automatic retry count must be an integer from 0 to ${MAX_ZOMBIE_RETRY_ATTEMPTS}: ${v}`, "warning");
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1325
1341
|
case "stallShortWords": {
|
|
1326
1342
|
const v = await ctx.ui.input("Stall short words threshold", "non-negative integer; 0 = off, empty = default 15");
|
|
1327
1343
|
if (v !== undefined) {
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
DEFAULT_STALL_SIM_THRESHOLD,
|
|
39
39
|
DEFAULT_STALL_SHORT_WORDS,
|
|
40
|
+
DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS,
|
|
40
41
|
} from "./goal-loop-backoff.ts";
|
|
41
42
|
import type { Settings } from "./goal-settings.ts";
|
|
42
43
|
import { MAX_MAIN_MODEL_FALLBACKS } from "./main-model-recovery.ts";
|
|
@@ -453,6 +454,14 @@ export function buildSettingsRows(
|
|
|
453
454
|
description:
|
|
454
455
|
"heartbeat refires with no turn before the goal pauses / loop stops (0 = never)",
|
|
455
456
|
},
|
|
457
|
+
{
|
|
458
|
+
id: "zombieRetryMaxAttempts",
|
|
459
|
+
section: "stall-brakes",
|
|
460
|
+
label: "Zero-stream retries",
|
|
461
|
+
valueText: show("zombieRetryMaxAttempts", `${DEFAULT_ZOMBIE_RETRY_MAX_ATTEMPTS}`),
|
|
462
|
+
sourceText: src("zombieRetryMaxAttempts"),
|
|
463
|
+
description: "automatic retries after a busy zero-stream abort (0 = manual only; max 10)",
|
|
464
|
+
},
|
|
456
465
|
{
|
|
457
466
|
id: "stallShortWords",
|
|
458
467
|
section: "stall-brakes",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.71",
|
|
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. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "dracon",
|