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 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.1 continue through the supervisor freeze (`/glla pause`),
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, "warning");
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
- return extras?.activity ?? "active";
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
- : queued
1080
- ? activityStateBadge("QUEUED", theme, "warning")
1081
- : activityStateBadge("ACTIVE", theme, "accent");
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 pending" : "",
1100
- queued ? hostLastActivity(extras, now).replace(/^ Β· /, "") : "",
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.1",
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",