pi-goal-list-loop-audit 0.37.1 β 0.37.2
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 +22 -0
- package/docs/INDEX.md +1 -1
- package/extensions/goal-continuation.ts +11 -0
- package/extensions/goal-loop-core.ts +13 -0
- package/extensions/goal-loop-display.ts +20 -11
- package/extensions/loops/goal-activation.ts +61 -0
- package/extensions/loops/goal-orchestrator.ts +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.2 β queued vs monitoring visuals and long-running daemon handling (2026-09-01)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
Monitor icon for long-running daemon/supervisor goals: goals matching
|
|
7
|
+
`daemon|supervisor|keep.*running|monitor|healthz` or with `total >1h` are
|
|
8
|
+
displayed as `π MONITORING` (dim) instead of `β³ QUEUED` (accent), with
|
|
9
|
+
"next check" instead of "awaiting pi turn", and are scheduled with
|
|
10
|
+
`GLLA_MONITOR_INTERVAL_MS` (default 120s) instead of immediate continuation
|
|
11
|
+
to avoid constant queued churn.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
`QUEUED` now renders as `β³ QUEUED` in accent blue (distinct from `BUSY`
|
|
15
|
+
warning yellow) with `awaiting pi turn` detail, fixing the "mistook it
|
|
16
|
+
for stuck" report (note.md 2026-09-01, screenshots 190127/193438/193529).
|
|
17
|
+
`LIVE Β· WORKING` remains success-accent with signal.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
Stale `followUp` continuations that survived `archiveCurrentGoal` via Pi's
|
|
21
|
+
`followUpQueue` are sanitized at `message_end` (ExtensionRunner β
|
|
22
|
+
AgentSession) plus timer disarm at archival β `LENGTH_CONTINUE` and foreign
|
|
23
|
+
ctx are exempt.
|
|
24
|
+
|
|
3
25
|
## 0.37.1 β auditor watchdogs, continuous list handoff, and audit hardening (2026-09-01)
|
|
4
26
|
|
|
5
27
|
### Added
|
package/docs/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ For shipped docs, the relevant entry points are:
|
|
|
18
18
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
19
19
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
20
20
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
21
|
-
v0.35.14βv0.37.
|
|
21
|
+
v0.35.14βv0.37.2 continue through the supervisor freeze (`/glla pause`),
|
|
22
22
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
23
23
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
24
24
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
isStaleApiError,
|
|
48
48
|
supervisorPaused,
|
|
49
49
|
objectiveIsUserSeeded,
|
|
50
|
+
isMonitorGoal,
|
|
50
51
|
type Goal,
|
|
51
52
|
type ObjectiveRepairTarget,
|
|
52
53
|
} from "./goal-loop-core.js";
|
|
@@ -63,6 +64,12 @@ import {
|
|
|
63
64
|
} from "./goal-loop-dispatch.js";
|
|
64
65
|
import { BACKOFF_IDLE_RETRY_MS, HEARTBEAT_MAX_NUDGES } from "./goal-loop-backoff.js";
|
|
65
66
|
import { LENGTH_CONTINUE_MAX, LENGTH_CONTINUE_TEXT } from "./length-continue.js";
|
|
67
|
+
|
|
68
|
+
const DEFAULT_MONITOR_CHECK_INTERVAL_MS = 120_000;
|
|
69
|
+
const configuredMonitorIntervalMs = Number(process.env.GLLA_MONITOR_INTERVAL_MS);
|
|
70
|
+
const MONITOR_CHECK_INTERVAL_MS = Number.isFinite(configuredMonitorIntervalMs) && configuredMonitorIntervalMs > 0
|
|
71
|
+
? Math.max(1_000, configuredMonitorIntervalMs)
|
|
72
|
+
: DEFAULT_MONITOR_CHECK_INTERVAL_MS;
|
|
66
73
|
import { VISION_ASSIST_GUIDANCE } from "./vision-assist.js";
|
|
67
74
|
import { loadSettings } from "./goal-settings.js";
|
|
68
75
|
import { clearLoopTimer, isLoopActive } from "./goal-loop.js";
|
|
@@ -991,6 +998,10 @@ export function scheduleContinuation(ctx: ExtensionContext, force = false, delay
|
|
|
991
998
|
} catch {
|
|
992
999
|
return;
|
|
993
1000
|
}
|
|
1001
|
+
// v0.37.x: monitor goals (daemon, long-running >1h) check less frequently to avoid constant QUEUED churn.
|
|
1002
|
+
if (delayMs === undefined && state.goal && isMonitorGoal(state.goal)) {
|
|
1003
|
+
delay = Math.max(delay, MONITOR_CHECK_INTERVAL_MS);
|
|
1004
|
+
}
|
|
994
1005
|
// v0.34.104 ([Image-#1]): the post-list-completion settle window delays
|
|
995
1006
|
// the first continuation after a queue auto-advance. Any real agent
|
|
996
1007
|
// activity during the window clears `postCompletionSettleUntil`, so a
|
|
@@ -568,6 +568,19 @@ export function objectiveIsUserSeeded(goal: Pick<Goal, "objective" | "createdVia
|
|
|
568
568
|
});
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
+
/**
|
|
572
|
+
* A long-running goal whose next turn is a health check should not look like
|
|
573
|
+
* a wedged queue. Keep this predicate pure and shared by scheduling and both
|
|
574
|
+
* TUI surfaces so a goal cannot be throttled without receiving the matching
|
|
575
|
+
* monitoring icon (or vice versa).
|
|
576
|
+
*/
|
|
577
|
+
export function isMonitorGoal(goal: Pick<Goal, "objective" | "createdAt">, now = Date.now()): boolean {
|
|
578
|
+
const objective = goal.objective.toLowerCase();
|
|
579
|
+
if (/daemon|supervisor|keep.*running|monitor|healthz|book-daemon/.test(objective)) return true;
|
|
580
|
+
const started = Date.parse(goal.createdAt);
|
|
581
|
+
return Number.isFinite(started) && now - started > 60 * 60 * 1000;
|
|
582
|
+
}
|
|
583
|
+
|
|
571
584
|
/**
|
|
572
585
|
* During a LIST drafting session the agent must not add items one by one
|
|
573
586
|
* with list_add/list_activate β that bypasses the user's Confirm gate
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
import { truncateToWidth as tuiTruncateToWidth, visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
15
|
import type { DurableDeferRecommendationInput, Goal, MainModelRecovery, State } from "./goal-loop-core.js";
|
|
16
|
-
import { buildDurableDeferRecommendation, compactDisplayText, formatMainModelRecoveryStatus, isPersistenceDegraded, lastPersistenceFailure, sanitizeDisplayText, sanitizeProviderAuditReport, sanitizeProviderDisplayText, stripThinkBlocks } from "./goal-loop-core.js";
|
|
16
|
+
import { buildDurableDeferRecommendation, compactDisplayText, formatMainModelRecoveryStatus, isMonitorGoal, isPersistenceDegraded, lastPersistenceFailure, sanitizeDisplayText, sanitizeProviderAuditReport, sanitizeProviderDisplayText, stripThinkBlocks } from "./goal-loop-core.js";
|
|
17
|
+
|
|
18
|
+
export { isMonitorGoal };
|
|
17
19
|
import { HELD_ON_RESTORE, type LoopState } from "./goal-loop-forever.js";
|
|
18
20
|
import { auditorSurfaceSuppressed } from "./loops/goal-auditor-surface.js";
|
|
19
21
|
|
|
@@ -95,7 +97,7 @@ export interface RecentActionDisplay {
|
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
/** v0.33.0: widget extras β the refire streak plus the recent-action feed. */
|
|
98
|
-
export type GoalDisplayActivity = "active" | "awaiting-first-turn" | "working" | "busy" | "queued" | "idle";
|
|
100
|
+
export type GoalDisplayActivity = "active" | "awaiting-first-turn" | "working" | "busy" | "queued" | "monitoring" | "idle";
|
|
99
101
|
|
|
100
102
|
export interface ModelProvenanceDisplay {
|
|
101
103
|
/** The primary model reference selected for the supervised work. */
|
|
@@ -354,7 +356,8 @@ function activityStatusMarker(activity: GoalDisplayActivity | undefined, now: nu
|
|
|
354
356
|
switch (activity) {
|
|
355
357
|
case "working": return activityBadge("LIVE Β· WORKING", now, theme);
|
|
356
358
|
case "busy": return activityStateBadge("BUSY", theme, "warning");
|
|
357
|
-
case "queued": return activityStateBadge("QUEUED", theme, "
|
|
359
|
+
case "queued": return activityStateBadge("β³ QUEUED", theme, "accent");
|
|
360
|
+
case "monitoring": return activityStateBadge("π MONITORING", theme, "dim");
|
|
358
361
|
case "idle": return activityStateBadge("IDLE", theme, "warning");
|
|
359
362
|
case "awaiting-first-turn": return activityStateBadge("AWAITING FIRST TURN", theme, "warning");
|
|
360
363
|
case "active": return activityStateBadge("ACTIVE", theme, "accent");
|
|
@@ -757,9 +760,11 @@ function lastAuditorTool(audit: AuditDisplayProgress | null | undefined): string
|
|
|
757
760
|
return typeof name === "string" && name.trim() ? truncate(name, 30) : undefined;
|
|
758
761
|
}
|
|
759
762
|
|
|
760
|
-
function goalDisplayActivity(g: Goal, extras?: WidgetExtras): GoalDisplayActivity {
|
|
763
|
+
function goalDisplayActivity(g: Goal, extras?: WidgetExtras, now = Date.now()): GoalDisplayActivity {
|
|
761
764
|
if (g.status !== "active") return "active";
|
|
762
|
-
|
|
765
|
+
const activity = extras?.activity ?? "active";
|
|
766
|
+
if (activity === "queued" && isMonitorGoal(g, now)) return "monitoring";
|
|
767
|
+
return activity;
|
|
763
768
|
}
|
|
764
769
|
|
|
765
770
|
function hostLastActivity(extras: WidgetExtras | undefined, now: number): string {
|
|
@@ -1031,7 +1036,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1031
1036
|
if (attention) {
|
|
1032
1037
|
return withRecovery(`glla: ${paint(theme, attention.color, `β ${attention.label}`)}${heldSuffix}`);
|
|
1033
1038
|
}
|
|
1034
|
-
const activity = goalDisplayActivity(g, extras);
|
|
1039
|
+
const activity = goalDisplayActivity(g, extras, now);
|
|
1035
1040
|
// v0.34.97: while the post-compaction grace window is open, surface
|
|
1036
1041
|
// "compactingβ¦" so the user knows the session just shrank. The chip
|
|
1037
1042
|
// survives reload because lastCompactionAt is persisted on State.
|
|
@@ -1074,11 +1079,14 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1074
1079
|
const n = state.list?.length ?? 0;
|
|
1075
1080
|
const live = activity === "working";
|
|
1076
1081
|
const queued = activity === "queued";
|
|
1082
|
+
const monitoring = activity === "monitoring";
|
|
1077
1083
|
const marker = live
|
|
1078
1084
|
? activityBadge("LIVE Β· WORKING", now, theme)
|
|
1079
|
-
:
|
|
1080
|
-
? activityStateBadge("
|
|
1081
|
-
:
|
|
1085
|
+
: monitoring
|
|
1086
|
+
? activityStateBadge("π MONITORING", theme, "dim")
|
|
1087
|
+
: queued
|
|
1088
|
+
? activityStateBadge("β³ QUEUED", theme, "accent")
|
|
1089
|
+
: activityStateBadge("ACTIVE", theme, "accent");
|
|
1082
1090
|
// When recovery parks a queued goal, name the blocker β `[QUEUED] 12m
|
|
1083
1091
|
// 26s` reads as a stalled queue with no WHY. State.mainModelRecovery is
|
|
1084
1092
|
// the bounded envelope's parked state; the status line says what is
|
|
@@ -1096,8 +1104,9 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1096
1104
|
// v0.34.124: the QUEUED "why" β an accepted dispatch that pi has not
|
|
1097
1105
|
// started, and the last real activity age. A ticking timer with no
|
|
1098
1106
|
// freshness told the user nothing (note.md 221249).
|
|
1099
|
-
queued && extras?.turnPending ? "turn
|
|
1100
|
-
|
|
1107
|
+
queued && extras?.turnPending ? "awaiting pi turn" : "",
|
|
1108
|
+
monitoring ? "next check" : "",
|
|
1109
|
+
(queued || monitoring) ? hostLastActivity(extras, now).replace(/^ Β· /, "") : "",
|
|
1101
1110
|
n > 0 ? `${n} queued` : "",
|
|
1102
1111
|
].filter(Boolean);
|
|
1103
1112
|
return withRecovery(`glla: ${marker}${details.length > 0 ? ` ${details.join(" Β· ")}` : ""}${recoverySuffix}${heldSuffix}`);
|
|
@@ -671,6 +671,36 @@ function refuseForeignCommand(ctx: ExtensionContext): boolean {
|
|
|
671
671
|
return true;
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
+
export function __testOnlyClassifyStaleContinuation(content: string, cwd: string): string | null {
|
|
675
|
+
const goalIdMatch = content.match(/\[GOAL CHECKPOINT goalId=([^\]\s]+)\]/);
|
|
676
|
+
const loopMatch = content.match(/\[LOOP ITERATION (\d+)\]/);
|
|
677
|
+
const isStall = content.includes("[STALL WARNING");
|
|
678
|
+
const isLengthContinue = content.includes("Your previous response was cut off") || content.includes("Response hit the output-token cap");
|
|
679
|
+
if (isLengthContinue) return null;
|
|
680
|
+
if (goalIdMatch) {
|
|
681
|
+
const gid = goalIdMatch[1]!;
|
|
682
|
+
if (!state.goal) return `no active goal (expected ${gid})`;
|
|
683
|
+
if (state.goal.id !== gid) return `goal mismatch (expected ${gid}, active ${state.goal.id})`;
|
|
684
|
+
if (state.goal.status !== "active") return `goal ${gid} not active (status=${state.goal.status})`;
|
|
685
|
+
try {
|
|
686
|
+
const p = archivedGoalPath(cwd, gid);
|
|
687
|
+
if (fs.existsSync(p)) return `goal ${gid} already archived`;
|
|
688
|
+
} catch {}
|
|
689
|
+
if ((state.goal as any).stopReason) return `goal ${gid} terminal stopReason present`;
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
if (loopMatch) {
|
|
693
|
+
if (!state.loop?.active) return `loop not active (iteration ${loopMatch[1]})`;
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
if (isStall) {
|
|
697
|
+
if (!state.goal || state.goal.status !== "active") return "stall warning with no active goal";
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
if (!state.goal && !state.loop?.active) return "no active supervision for generic goal-event";
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
674
704
|
export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
675
705
|
// Factories run before lifecycle events, so observe readiness now; the
|
|
676
706
|
// admitted session_start below decides which bus/generation may control a
|
|
@@ -973,6 +1003,37 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
973
1003
|
draftingUserReplies++;
|
|
974
1004
|
});
|
|
975
1005
|
|
|
1006
|
+
// v0.37.2: stale queued goal-event continuations (hidden display:false
|
|
1007
|
+
// custom messages) can survive goal archival via Pi's followUp queue.
|
|
1008
|
+
// Pi has no public clear queue API, so sanitize at delivery: replace
|
|
1009
|
+
// stale content before it is persisted or drives a turn.
|
|
1010
|
+
pi.on("message_end", async (event: any, ctx: ExtensionContext) => {
|
|
1011
|
+
const msg: any = event?.message;
|
|
1012
|
+
if (!msg || msg.role !== "custom" || msg.customType !== "goal-event") return;
|
|
1013
|
+
if (isForeignCtx(ctx)) return;
|
|
1014
|
+
let content = "";
|
|
1015
|
+
if (typeof msg.content === "string") content = msg.content;
|
|
1016
|
+
else if (Array.isArray(msg.content)) content = msg.content.map((c: any) => typeof c === "string" ? c : (c?.text ?? "")).join("\n");
|
|
1017
|
+
else if (msg.content != null) content = String(msg.content);
|
|
1018
|
+
const staleReason = __testOnlyClassifyStaleContinuation(content, ctx.cwd);
|
|
1019
|
+
if (!staleReason) return;
|
|
1020
|
+
const goalIdMatch = content.match(/\[GOAL CHECKPOINT goalId=([^\]\s]+)\]/);
|
|
1021
|
+
const loopMatch = content.match(/\[LOOP ITERATION (\d+)\]/);
|
|
1022
|
+
const sanitizedContent = `[GLLA: discarded stale continuation β ${staleReason}. No action required. Live state: ${state.goal ? `goal ${state.goal.id} (${state.goal.status})` : state.loop?.active ? `loop active iteration ${state.loop.iteration}` : "idle (no goal/loop)"}.]`;
|
|
1023
|
+
appendLedger(ctx.cwd, "stale_continuation_sanitized", {
|
|
1024
|
+
reason: staleReason,
|
|
1025
|
+
goalId: goalIdMatch?.[1],
|
|
1026
|
+
loopIteration: loopMatch?.[1],
|
|
1027
|
+
originalPreview: content.slice(0, 200),
|
|
1028
|
+
});
|
|
1029
|
+
const sanitized = {
|
|
1030
|
+
...msg,
|
|
1031
|
+
content: sanitizedContent,
|
|
1032
|
+
display: false,
|
|
1033
|
+
};
|
|
1034
|
+
return { message: sanitized };
|
|
1035
|
+
});
|
|
1036
|
+
|
|
976
1037
|
// v0.15.1: ask_user_question answers arrive as tool results, not chat
|
|
977
1038
|
// messages β count answered (non-cancelled) questionnaires as replies too.
|
|
978
1039
|
pi.on("tool_result", async (event: any, eventCtx: ExtensionContext) => {
|
|
@@ -1237,6 +1237,9 @@ function archiveCurrentGoal(
|
|
|
1237
1237
|
}
|
|
1238
1238
|
releaseContinuationDispatchStandDown();
|
|
1239
1239
|
clearDispatchRecord(ctx.cwd);
|
|
1240
|
+
clearContinuationTimer();
|
|
1241
|
+
clearContinuationStartWatchdog();
|
|
1242
|
+
clearQueueStuckProbe();
|
|
1240
1243
|
postCompactResumeOwed = false; // v0.33.1: the dead goal's compact debt/resync dies with it
|
|
1241
1244
|
postCompactResyncPending = false;
|
|
1242
1245
|
// v0.34.120: archive is the durable history; a terminal goal must not
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
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",
|