pi-goal-list-loop-audit 0.26.6 → 0.26.7

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.
@@ -931,6 +931,15 @@ export function shouldSuppressHeartbeatForRecentShip(args: {
931
931
 
932
932
  /** Best-effort "when did work last ship" for a repo: newest of the HEAD
933
933
  * commit time and the .pi-glla state file mtime. Null when unknown. */
934
+ /** v0.26.7: pi's exact stale-runtime error signature — thrown by every
935
+ * runtime-bound method after pi invalidates the extension on session
936
+ * replacement (newSession/fork/switchSession/reload; compaction reaches
937
+ * the same teardown in pi 0.82.x). See dist/core/extensions/loader.js
938
+ * createExtensionRuntime().invalidate. */
939
+ export function isStaleApiError(err: unknown): boolean {
940
+ return err instanceof Error && err.message.includes("stale after session replacement");
941
+ }
942
+
934
943
  export function lastShippedAtMs(cwd: string): number | null {
935
944
  // v0.26.6: the .pi-glla/active.jsonl MTIME term was REMOVED — the
936
945
  // heartbeat's own ledger writes refreshed it every 15s, which made the
@@ -51,6 +51,7 @@ import {
51
51
  crossRecommendMode,
52
52
  formatListDepth,
53
53
  shouldEscalateStall,
54
+ isStaleApiError,
54
55
  mergeSettings,
55
56
  parseListImport,
56
57
 
@@ -177,6 +178,30 @@ const HELD_ON_RESTORE = "held: restored in a fresh session";
177
178
  // The ExtensionAPI captured in the factory. sendMessage lives on the API,
178
179
  // not on ExtensionContext, so continuation sends need it at module scope.
179
180
  let extensionApi: ExtensionAPI | null = null;
181
+ // v0.26.7: pi invalidates the extension runtime on session replacement
182
+ // (newSession/fork/switchSession/reload — and the compaction path reaches
183
+ // it via teardownCurrent in pi 0.82.x). Once stale, every sendMessage
184
+ // throws FOREVER in this process — retrying for hours is the hegemon
185
+ // failure shape. Detect the stale signature once and go terminally loud.
186
+ let extensionApiStale = false;
187
+
188
+ /** v0.26.7: a stale api is terminal for this process — pause/stop loudly
189
+ * with restart guidance instead of retrying sends that can never land. */
190
+ function goStaleTerminal(ctx: ExtensionContext, where: string): void {
191
+ if (extensionApiStale) return; // already terminal — don't re-spam
192
+ extensionApiStale = true;
193
+ appendLedger(ctx.cwd, "extension_api_stale", { where, kind: isLoopActive() ? "loop" : "goal" });
194
+ 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), then /goal resume / /loop start.";
195
+ if (isLoopActive()) {
196
+ clearLoopTimer();
197
+ state.loop = { ...state.loop!, active: false, stopReason: `extension api stale: ${guidance}` };
198
+ persistState(ctx);
199
+ } else if (state.goal && state.goal.status === "active") {
200
+ updateGoal({ status: "paused", pauseReason: "extension api stale (pi session replacement)", pauseSuggestedAction: guidance }, ctx);
201
+ }
202
+ ctx.ui.notify(`glla: ${guidance}`, "warning");
203
+ notifyExternal(ctx, `glla: extension api stale — restart pi. (${where})`);
204
+ }
180
205
 
181
206
  // The most recent ExtensionContext seen from any event or command handler.
182
207
  // pi replaces sessions (newSession/fork/reload) and stale ctx throws on use,
@@ -486,7 +511,7 @@ function sendContinuation(goalId: string): void {
486
511
  continuationTimer.unref?.();
487
512
  return;
488
513
  }
489
- if (!extensionApi) return;
514
+ if (!extensionApi || extensionApiStale) return;
490
515
  try {
491
516
  extensionApi.sendMessage({
492
517
  customType: GOAL_EVENT_ENTRY,
@@ -496,7 +521,9 @@ function sendContinuation(goalId: string): void {
496
521
  appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
497
522
  } catch (err) {
498
523
  appendLedger(ctx.cwd, "goal_continuation_send_failed", { goalId, error: err instanceof Error ? err.message : String(err) });
499
- // API went stale mid-flight; next agent_end/session_start will reschedule.
524
+ // v0.26.7: stale runtime = terminal (sends can never land); anything
525
+ // else is transient — next agent_end/session_start reschedules.
526
+ if (isStaleApiError(err)) goStaleTerminal(ctx, "sendContinuation");
500
527
  }
501
528
  }
502
529
 
@@ -1268,7 +1295,7 @@ function isLoopActive(): boolean {
1268
1295
 
1269
1296
  /** Run the user's measure command. Orchestrator-side, never agent-side. */
1270
1297
  async function runMeasure(ctx: ExtensionContext, cmd: string): Promise<number | null> {
1271
- if (!extensionApi) return null;
1298
+ if (!extensionApi || extensionApiStale) return null;
1272
1299
  try {
1273
1300
  const result = await extensionApi.exec("bash", ["-c", cmd], { cwd: ctx.cwd, timeout: MEASURE_TIMEOUT_MS });
1274
1301
  const stdout = (result as any)?.stdout ?? "";
@@ -1392,6 +1419,8 @@ function sendLoopTurn(): void {
1392
1419
  // stale API — next agent_end reschedules (but if none comes, the
1393
1420
  // heartbeat's stall escalation stops the spin — v0.26.1).
1394
1421
  appendLedger(ctx.cwd, "loop_turn_send_failed", { error: err instanceof Error ? err.message : String(err) });
1422
+ // v0.26.7: stale runtime is terminal, not transient — go loud now.
1423
+ if (isStaleApiError(err)) goStaleTerminal(ctx, "sendLoopTurn");
1395
1424
  }
1396
1425
  }
1397
1426
 
@@ -3380,6 +3409,7 @@ function warnOnCommandCollision(ctx: ExtensionContext): void {
3380
3409
 
3381
3410
  export default function (pi: ExtensionAPI): void {
3382
3411
  extensionApi = pi;
3412
+ extensionApiStale = false; // a fresh factory run means a fresh runtime (reload path)
3383
3413
  startHeartbeat();
3384
3414
  startUITicker();
3385
3415
  // Four top-level commands, that's all (v0.8.0 consolidation):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.26.6",
3
+ "version": "0.26.7",
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",