pi-goal-list-loop-audit 0.29.11 → 0.29.13

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.
@@ -1045,10 +1045,15 @@ export function shouldSuppressHeartbeatForRecentShip(args: {
1045
1045
  /** Best-effort "when did work last ship" for a repo: newest of the HEAD
1046
1046
  * commit time and the .pi-glla state file mtime. Null when unknown. */
1047
1047
  /** v0.26.7: pi's exact stale-runtime error signature — thrown by every
1048
- * runtime-bound method after pi invalidates the extension on session
1049
- * replacement (newSession/fork/switchSession/reload; compaction reaches
1050
- * the same teardown in pi 0.82.x). See dist/core/extensions/loader.js
1051
- * createExtensionRuntime().invalidate. */
1048
+ * runtime-bound method after pi invalidates the extension. v0.29.12 source
1049
+ * audit (pi 0.83.0 dist): the ONLY invalidate() caller is AgentSession
1050
+ * .dispose(), reachable solely via session replacement (newSession/
1051
+ * switchSession/fork/quit). COMPACTION NEVER DISPOSES — manual, auto, and
1052
+ * overflow compaction all rebuild context in place. Field note 2026-07-30:
1053
+ * 10 stale events in 4 days in endless-td, repeatedly ~3 min after
1054
+ * compactions with NO new session file and zombie command handlers still
1055
+ * answering — a replacement that disposed but never re-ran factories (or
1056
+ * a same-file switchSession); the disposing path is unidentified pi-side. */
1052
1057
  export function isStaleApiError(err: unknown): boolean {
1053
1058
  return err instanceof Error && err.message.includes("stale after session replacement");
1054
1059
  }
@@ -177,7 +177,7 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
177
177
  // v0.29.11: the fresh session HOLDS it (hold-everything restore gate;
178
178
  // autoresume=on resumes for you) — name the verb, don't promise auto.
179
179
  if (g.interruptedAt) {
180
- return `glla: ${g.policy} ${paint(theme, "error", "⚠ interrupted — stale handle · restart pi → /glla resume")}${heldSuffix}`;
180
+ return `glla: ${g.policy} ${paint(theme, "error", "⚠ interrupted — stale handle · /reload → /glla resume")}${heldSuffix}`;
181
181
  }
182
182
  // v0.24.7: list policy gets its own wording — a queue item is not a goal.
183
183
  // v0.28.11 (U10): goal policy joins it — "list 29" read as a command
@@ -58,6 +58,11 @@ export interface Settings {
58
58
  /** Consecutive stuck interventions before a loop stops (default 5,
59
59
  * 10 under aggressiveMode). */
60
60
  stuckMaxInterventions?: number;
61
+ /** v0.29.13: on a stale-handle terminal, inject /reload into our own
62
+ * tmux pane (keystroke self-heal; pi walls ctx.reload() behind
63
+ * assertActive). Default true; only acts when TMUX and TMUX_PANE are
64
+ * set — pi outside tmux just gets the manual warning. */
65
+ autoReloadOnStale?: boolean;
61
66
  /** v0.26.1: consecutive heartbeat refires without a real turn before
62
67
  * the goal pauses / loop stops (default 5; 0 = never escalate). */
63
68
  stallEscalationRefires?: number;
@@ -186,6 +191,7 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
186
191
  "quotaRetryMinutes",
187
192
  "stuckMaxInterventions",
188
193
  "stallEscalationRefires",
194
+ "autoReloadOnStale",
189
195
  "stallShortWords",
190
196
  "stallSimilarityThreshold",
191
197
  "postaudit",
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import * as fs from "node:fs";
18
+ import { exec } from "node:child_process";
18
19
  import * as os from "node:os";
19
20
  import * as path from "node:path";
20
21
 
@@ -219,7 +220,7 @@ function goStaleTerminal(ctx: ExtensionContext, where: string): void {
219
220
  if (extensionApiStale) return; // already terminal — don't re-spam
220
221
  extensionApiStale = true;
221
222
  appendLedger(ctx.cwd, "extension_api_stale", { where, kind: isLoopActive() ? "loop" : "goal" });
222
- const guidance = "pi invalidated this session's extension handle (session replacement — compaction triggers it in pi 0.82.x). Sends can never land in this process. Restart pi (or reload extensions) — the goal/loop holds on the fresh session: /glla resume continues it (autoresume=on resumes for you).";
223
+ const guidance = "pi invalidated this session's extension handle (session replacement — the session was disposed and this process's sends can never land). Run /reload — extensions rebuild IN PLACE, no pi restart needed — then /glla resume (autoresume=on resumes for you). Restart pi only if /reload itself fails.";
223
224
  if (isLoopActive()) {
224
225
  clearLoopTimer();
225
226
  state.loop = { ...state.loop!, active: false, stopReason: `extension api stale: ${guidance}` };
@@ -229,6 +230,29 @@ function goStaleTerminal(ctx: ExtensionContext, where: string): void {
229
230
  }
230
231
  ctx.ui.notify(`glla: ${guidance}`, "warning");
231
232
  notifyExternal(ctx, `glla: extension api stale — restart pi. (${where})`);
233
+ attemptTmuxAutoReload(ctx, where);
234
+ }
235
+
236
+ /** v0.29.13: automatic recovery — the zombie handle can't call ctx.reload()
237
+ * (pi walls EVERY runtime method behind assertActive), but fs/child_process
238
+ * are extension-side and still work. When pi runs inside tmux (this rig's
239
+ * shape), inject /reload as keystrokes into our own pane: pi rebuilds the
240
+ * extension runtime in place, the fresh instance loads .pi-glla state and
241
+ * holds (autoresume=on resumes for you). Opt out: autoReloadOnStale=false.
242
+ * Best-effort: the manual warning already fired, so failures cost nothing. */
243
+ function attemptTmuxAutoReload(ctx: ExtensionContext, where: string): void {
244
+ try {
245
+ if (loadSettings(ctx.cwd).autoReloadOnStale === false) return;
246
+ const pane = process.env.TMUX_PANE;
247
+ if (!process.env.TMUX || !pane || !/^%\d+$/.test(pane)) return;
248
+ appendLedger(ctx.cwd, "auto_reload_injected", { where, pane });
249
+ ctx.ui.notify("glla: injecting /reload into this pane via tmux — extensions rebuild in place and the fresh instance holds state. /glla resume after (automatic with autoresume=on).", "info");
250
+ // Escape dismisses any overlay; the dead session sits at an empty prompt.
251
+ const cmd = `tmux send-keys -t ${pane} Escape && sleep 0.3 && tmux send-keys -t ${pane} -l '/reload' && tmux send-keys -t ${pane} Enter`;
252
+ exec(cmd, () => { /* best-effort */ });
253
+ } catch {
254
+ /* never let recovery take the warning path down */
255
+ }
232
256
  }
233
257
 
234
258
  /** TEST-ONLY hook (tests/harness): the stale flag is process-terminal in
@@ -260,7 +284,7 @@ function warnIfStaleAtEntry(ctx: ExtensionContext, what: string): boolean {
260
284
  if (!probeExtensionApiStale()) return false;
261
285
  appendLedger(ctx.cwd, "extension_api_stale", { where: `entry probe (${what})` });
262
286
  ctx.ui.notify(
263
- `glla: this session's extension handle is stale (pi session replacement) — ${what} can't send continuations in this process. State is safe in .pi-glla/ — restart pi, then /glla resume (autoresume=on resumes for you).`,
287
+ `glla: this session's extension handle is stale (pi session replacement) — ${what} can't send continuations in this process. State is safe in .pi-glla/ — run /reload (extensions rebuild in place), then /glla resume. Restart pi only if /reload fails.`,
264
288
  "warning",
265
289
  );
266
290
  return true;
@@ -4503,6 +4527,10 @@ async function cmdGllaWipe(ctx: ExtensionContext): Promise<void> {
4503
4527
  * genuinely differ per type (tweak/finish/next/decide/refine) stay typed.
4504
4528
  */
4505
4529
  async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
4530
+ // v0.29.12: a zombie instance (handle dead after session replacement)
4531
+ // used to answer "Nothing to resume" — the resume path must name the
4532
+ // real recovery (/reload rebuilds extensions in place), not mislead.
4533
+ if (warnIfStaleAtEntry(ctx, "/glla resume")) return;
4506
4534
  const g = state.goal;
4507
4535
  const goalResumable = g && g.status === "paused";
4508
4536
  const loopResumable = state.loop && !state.loop.active && state.loop.stopReason === HELD_ON_RESTORE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.29.11",
3
+ "version": "0.29.13",
4
4
  "description": "Goal. Loop. Audit. Done. \u2014 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 \u2014 only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",