pi-goal-list-loop-audit 0.23.1 → 0.23.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/README.md CHANGED
@@ -184,6 +184,7 @@ No external watchdog plugin needed.
184
184
  /glla notify='cmd "$1"' # push on complete/pause/stop → GLOBAL
185
185
  /glla tokenlimit=10000000 # per-goal token budget (default: off) → GLOBAL
186
186
  /glla tokenlimit=0 # explicitly no cap (the default)
187
+ /glla wedgealert=30 # hung-command alert minutes (default: 45, 0 = off)
187
188
  /glla project tokenlimit=500 # rare per-project override
188
189
  ```
189
190
 
@@ -202,6 +203,15 @@ value like 10000000 is a runaway threshold, not a big-goal threshold
202
203
  this cap — it has its own brakes
203
204
  (max iterations + plateau).
204
205
 
206
+ ## Wedge alert
207
+
208
+ The turn-based watchdogs can't see one failure shape: the session is busy
209
+ but silent for a long stretch because ONE unbounded command (a test suite
210
+ that never exits, a dev server) is holding the whole goal hostage. The
211
+ heartbeat watches the wall clock: busy + no activity for 45 minutes →
212
+ in-session warning + your configured notify push, once per interval while
213
+ it persists. Tune with `/glla wedgealert=<minutes>` (0 = off).
214
+
205
215
  ## Compatibility (what goes well, what conflicts)
206
216
 
207
217
  **The Two-Driver Rule**: any plugin that drives agent turns on `agent_end`
@@ -68,6 +68,36 @@ export function humanMs(ms: number): string {
68
68
  export const HEARTBEAT_INTERVAL_MS = 15_000;
69
69
  export const HEARTBEAT_STALL_MS = 60_000;
70
70
  export const HEARTBEAT_MAX_NUDGES = 3;
71
+ /** v0.23.2: default wall-clock wedge threshold — a busy session with no
72
+ * activity for this long is almost always a hung unbounded command
73
+ * (test suite / dev server) holding the whole goal hostage. The
74
+ * turn-based watchdogs are blind to it (it's ONE long turn); only the
75
+ * wall clock sees it. 0 in settings = off. */
76
+ export const WEDGE_ALERT_DEFAULT_MINUTES = 45;
77
+
78
+ export interface WedgeInput {
79
+ /** A goal is active (autoContinue) or a loop is running. */
80
+ supervising: boolean;
81
+ /** Session is BUSY (mid-turn). An idle quiet session is the
82
+ * heartbeat's job, not the wedge alert's. */
83
+ sessionBusy: boolean;
84
+ /** Milliseconds since the last observed agent activity. */
85
+ silentMs: number;
86
+ /** Milliseconds since the last wedge alert fired (throttle). */
87
+ msSinceLastAlert: number;
88
+ /** Threshold in ms; 0 disables the alert entirely. */
89
+ thresholdMs: number;
90
+ }
91
+
92
+ /** Should the wedge alert fire right now? Alerts at most once per
93
+ * threshold interval while the wedge persists; any activity re-arms. */
94
+ export function shouldWedgeAlert(input: WedgeInput): boolean {
95
+ if (!input.supervising) return false;
96
+ if (!input.sessionBusy) return false;
97
+ if (input.thresholdMs <= 0) return false;
98
+ if (input.silentMs < input.thresholdMs) return false;
99
+ return input.msSinceLastAlert >= input.thresholdMs;
100
+ }
71
101
 
72
102
  export interface HeartbeatInput {
73
103
  /** A goal is active (autoContinue) or a loop is running. */
@@ -77,6 +77,8 @@ import {
77
77
  HEARTBEAT_MAX_NUDGES,
78
78
  HEARTBEAT_STALL_MS,
79
79
  shouldHeartbeatRefire,
80
+ WEDGE_ALERT_DEFAULT_MINUTES,
81
+ shouldWedgeAlert,
80
82
  } from "../goal-loop-backoff.js";
81
83
 
82
84
  // =================================================================
@@ -122,6 +124,7 @@ const countedLoopTokenMessages = new Set<string>();
122
124
 
123
125
  // Heartbeat self-watchdog state: liveness is the loop's own job.
124
126
  let lastActivityAt = Date.now();
127
+ let lastWedgeAlertAt = 0;
125
128
  let heartbeatNudges = 0;
126
129
  let heartbeatTimer: NodeJS.Timeout | null = null;
127
130
 
@@ -180,6 +183,27 @@ function heartbeatTick(): void {
180
183
  msSinceActivity: Date.now() - lastActivityAt,
181
184
  stallMs: HEARTBEAT_STALL_MS,
182
185
  });
186
+ // Wedge alert (v0.23.2): session BUSY but silent for the threshold —
187
+ // the classic hung-command case (a test suite that never exits holds
188
+ // the entire goal hostage; field-observed at 5,056s and 6,800s on the
189
+ // same wedged tool call). Independent of the refire path, which only
190
+ // watches idle sessions.
191
+ const wedgeMinutes = loadSettings(ctx.cwd).wedgeAlertMinutes ?? WEDGE_ALERT_DEFAULT_MINUTES;
192
+ if (
193
+ shouldWedgeAlert({
194
+ supervising: isSupervising(),
195
+ sessionBusy: !sessionIdle,
196
+ silentMs: Date.now() - lastActivityAt,
197
+ msSinceLastAlert: Date.now() - lastWedgeAlertAt,
198
+ thresholdMs: wedgeMinutes * 60_000,
199
+ })
200
+ ) {
201
+ lastWedgeAlertAt = Date.now();
202
+ const msg = `Goal appears wedged: no activity for ${Math.round((Date.now() - lastActivityAt) / 60_000)}m while the session is busy — likely a hung command (test/build/dev server without a timeout). Check the session; Esc kills a stuck tool call.`;
203
+ appendLedger(ctx.cwd, "wedge_alert", { silentMs: Date.now() - lastActivityAt });
204
+ ctx.ui.notify(msg, "warning");
205
+ notifyExternal(ctx, msg);
206
+ }
183
207
  if (!fire) return;
184
208
  noteActivity();
185
209
  appendLedger(ctx.cwd, "heartbeat_refire", { nudgesSoFar: heartbeatNudges });
@@ -1976,6 +2000,9 @@ interface Settings {
1976
2000
  /** Per-goal token budget; crossing it pauses the goal. Off by default
1977
2001
  * (opt-in guard, v0.12.0): unset/0 = no budget. */
1978
2002
  tokenLimit?: number;
2003
+ /** v0.23.2: minutes of busy-but-silent before the wedge alert fires
2004
+ * (hung-command detector). Unset = 45; 0 = off. */
2005
+ wedgeAlertMinutes?: number;
1979
2006
  /** on → restored goals/loops/lists auto-resume even in fresh sessions
1980
2007
  * (unattended rigs). Default off: restore holds until /goal resume. */
1981
2008
  autoResume?: boolean;
@@ -2023,7 +2050,7 @@ function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknow
2023
2050
  const glob = readSettingsFile(globalSettingsPath());
2024
2051
  const effective = loadSettings(cwd);
2025
2052
  const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
2026
- const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "autoResume"];
2053
+ const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume"];
2027
2054
  for (const k of keys) {
2028
2055
  if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
2029
2056
  else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
@@ -2122,6 +2149,7 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2122
2149
  `Auditor thinking — ${show("auditorThinkingLevel", "(session, floor high)")}`,
2123
2150
  `Notify command — ${show("notifyCmd", "(off)")}`,
2124
2151
  `Token limit per goal — ${show("tokenLimit", "(off)")}`,
2152
+ `Wedge alert minutes — ${show("wedgeAlertMinutes", `(${WEDGE_ALERT_DEFAULT_MINUTES}m default)`)}`,
2125
2153
  "Done",
2126
2154
  ],
2127
2155
  );
@@ -2147,6 +2175,14 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2147
2175
  else if (!v.trim()) saveSettings("global", ctx.cwd, { tokenLimit: undefined });
2148
2176
  else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2149
2177
  }
2178
+ } else if (choice.startsWith("Wedge alert")) {
2179
+ const v = await ctx.ui.input("Wedge alert threshold (minutes)", "non-negative integer; 0 = off, empty = default 45");
2180
+ if (v !== undefined) {
2181
+ const n = Number.parseInt(v.trim(), 10);
2182
+ if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: n });
2183
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: undefined });
2184
+ else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2185
+ }
2150
2186
  }
2151
2187
  } catch {
2152
2188
  return;
@@ -2161,6 +2197,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2161
2197
  // /glla thinking=high write to GLOBAL config
2162
2198
  // /glla notify='cmd $1' write to GLOBAL config
2163
2199
  // /glla tokenlimit=2000000 write to GLOBAL config
2200
+ // /glla wedgealert=30 hung-command alert minutes (0=off, unset=45)
2164
2201
  // /glla project model=... write to PROJECT override (rare)
2165
2202
  // /glla model=unset remove key (from global; project model=unset for project)
2166
2203
  const trimmed = args.trim();
@@ -2221,6 +2258,19 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2221
2258
  } else {
2222
2259
  ctx.ui.notify(`tokenlimit must be a positive integer, got: ${value}`, "warning");
2223
2260
  }
2261
+ } else if (key === "wedgealert") {
2262
+ if (value === "unset") {
2263
+ patch.wedgeAlertMinutes = undefined;
2264
+ changed = true;
2265
+ } else {
2266
+ const n = Number.parseInt(value, 10);
2267
+ if (Number.isFinite(n) && n >= 0) {
2268
+ patch.wedgeAlertMinutes = n; // 0 = off; unset = default 45
2269
+ changed = true;
2270
+ } else {
2271
+ ctx.ui.notify(`wedgealert must be a non-negative integer (minutes, 0 = off), got: ${value}`, "warning");
2272
+ }
2273
+ }
2224
2274
  } else if (key === "autoresume") {
2225
2275
  if (["on", "true", "1", "yes"].includes(value)) {
2226
2276
  patch.autoResume = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.23.1",
3
+ "version": "0.23.2",
4
4
  "description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",