pi-goal-list-loop-audit 0.28.0 → 0.28.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.
@@ -156,6 +156,12 @@ export interface Goal {
156
156
  stopReason?: string;
157
157
  pauseReason?: string;
158
158
  pauseSuggestedAction?: string;
159
+ /** v0.28.1 (S1/S2): stale-handle interrupt marker. Set INSTEAD of pausing
160
+ * when pi invalidates the extension handle mid-goal — the goal stays
161
+ * active so a fresh session auto-resumes it via the restore gate. Cleared
162
+ * on that auto-resume. */
163
+ interruptedAt?: string;
164
+ interruptedReason?: string;
159
165
  /** v0.25.0 (contract item 22): auditor objections extracted as TODOs when
160
166
  * aggressiveMode keeps the goal active past the disapproval cap. Rendered
161
167
  * into every continuation prompt until the next audit clears them. */
@@ -137,6 +137,11 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
137
137
  return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}`;
138
138
  }
139
139
  if (g.status === "active") {
140
+ // v0.28.1 (S1/S2): a stale-handle interrupt keeps the goal ACTIVE (the
141
+ // next fresh session auto-resumes it) — say so instead of looking healthy.
142
+ if (g.interruptedAt) {
143
+ return `glla: ${g.policy} ${paint(theme, "error", "⚠ interrupted — stale handle · auto-resumes on pi restart")}`;
144
+ }
140
145
  // v0.24.7: list policy gets its own wording — a queue item is not a goal.
141
146
  // Before: "glla: list ● 3m 19s · list 29" (policy label AND queue counter
142
147
  // both said "list"). After: "glla: list ● 3m 19s · 29 queued". Goal
@@ -198,24 +198,57 @@ let extensionApi: ExtensionAPI | null = null;
198
198
  // failure shape. Detect the stale signature once and go terminally loud.
199
199
  let extensionApiStale = false;
200
200
 
201
- /** v0.26.7: a stale api is terminal for this process — pause/stop loudly
202
- * with restart guidance instead of retrying sends that can never land. */
201
+ /** v0.26.7: a stale api is terminal for this process — go loudly with
202
+ * restart guidance instead of retrying sends that can never land.
203
+ * v0.28.1 (S1/S2): goals STAY ACTIVE with an interrupt marker instead of
204
+ * pausing — the restore gate only auto-resumes ACTIVE goals, so pausing
205
+ * here stranded goals until manual /goal resume (hegemon/sraaal shape).
206
+ * sendContinuation's extensionApiStale guard already stops further sends
207
+ * in this doomed process; the next fresh session auto-resumes. */
203
208
  function goStaleTerminal(ctx: ExtensionContext, where: string): void {
204
209
  if (extensionApiStale) return; // already terminal — don't re-spam
205
210
  extensionApiStale = true;
206
211
  appendLedger(ctx.cwd, "extension_api_stale", { where, kind: isLoopActive() ? "loop" : "goal" });
207
- 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.";
212
+ 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) an active goal auto-resumes on the fresh session; loops need /loop start.";
208
213
  if (isLoopActive()) {
209
214
  clearLoopTimer();
210
215
  state.loop = { ...state.loop!, active: false, stopReason: `extension api stale: ${guidance}` };
211
216
  persistState(ctx);
212
217
  } else if (state.goal && state.goal.status === "active") {
213
- updateGoal({ status: "paused", pauseReason: "extension api stale (pi session replacement)", pauseSuggestedAction: guidance }, ctx);
218
+ updateGoal({ interruptedAt: nowIso(), interruptedReason: `extension api stale (${where})` }, ctx);
214
219
  }
215
220
  ctx.ui.notify(`glla: ${guidance}`, "warning");
216
221
  notifyExternal(ctx, `glla: extension api stale — restart pi. (${where})`);
217
222
  }
218
223
 
224
+ /** v0.28.1 (S3): side-effect-free staleness probe — getSessionName()
225
+ * routes through pi's assertActive() and throws the stale signature iff
226
+ * pi invalidated this factory handle (session replacement). A positive
227
+ * result is cached in extensionApiStale. */
228
+ function probeExtensionApiStale(): boolean {
229
+ if (extensionApiStale) return true;
230
+ if (!extensionApi) return false;
231
+ try {
232
+ extensionApi.getSessionName();
233
+ } catch (err) {
234
+ if (isStaleApiError(err)) extensionApiStale = true;
235
+ }
236
+ return extensionApiStale;
237
+ }
238
+
239
+ /** v0.28.1 (S3): command-entry staleness probe + honest warning. Returns
240
+ * true when the handle is stale — callers must skip send-dependent paths
241
+ * and must NOT claim work started (S3's "created — starting now" lie). */
242
+ function warnIfStaleAtEntry(ctx: ExtensionContext, what: string): boolean {
243
+ if (!probeExtensionApiStale()) return false;
244
+ appendLedger(ctx.cwd, "extension_api_stale", { where: `entry probe (${what})` });
245
+ ctx.ui.notify(
246
+ `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 and the active goal auto-resumes.`,
247
+ "warning",
248
+ );
249
+ return true;
250
+ }
251
+
219
252
  // The most recent ExtensionContext seen from any event or command handler.
220
253
  // pi replaces sessions (newSession/fork/reload) and stale ctx throws on use,
221
254
  // so timers must never capture a ctx — they read lastCtx at fire time.
@@ -842,8 +875,18 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
842
875
  draftingUserReplies = 0;
843
876
  draftingBlockedProposals = 0;
844
877
  draftingSeedInFlight = true; // our injected prompt also arrives as a user message — don't count it
845
- } catch {
878
+ } catch (err) {
846
879
  draftingTarget = null;
880
+ // v0.28.1 (E6): the seed send used to fail SILENTLY — the user pressed
881
+ // Enter on /goal and nothing happened. Now: loud, and stale handles get
882
+ // the honest restart guidance.
883
+ if (isStaleApiError(err)) {
884
+ extensionApiStale = true;
885
+ appendLedger(ctx.cwd, "extension_api_stale", { where: "startDrafting seed" });
886
+ ctx.ui.notify("glla: can't start the drafting interview — this session's extension handle is stale (pi session replacement). Restart pi and re-run the command.", "warning");
887
+ } else {
888
+ ctx.ui.notify(`glla: couldn't start the drafting interview (${err instanceof Error ? err.message : String(err)}) — try again.`, "warning");
889
+ }
847
890
  }
848
891
  }
849
892
 
@@ -880,6 +923,10 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
880
923
  // =================================================================
881
924
 
882
925
  async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): Promise<void> {
926
+ // v0.28.1 (S3): probe at the creation entry — no "created — starting now"
927
+ // lie in a doomed process. (The draft path's seed send has its own loud
928
+ // stale handling — E6.)
929
+ const staleEntry = warnIfStaleAtEntry(ctx, "/goal");
883
930
  let raw = args.trim();
884
931
  // Users naturally quote the objective ("/goal \"do X\""); strip one layer of
885
932
  // surrounding matching quotes so they don't leak into the goal text.
@@ -909,6 +956,13 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
909
956
  iterationCounter = 0;
910
957
  consecutiveErrorIterations = 0;
911
958
  consecutiveNoToolIterations = 0;
959
+ if (staleEntry) {
960
+ // v0.28.1 (S3): the goal is persisted — mark the interrupt so the next
961
+ // fresh session auto-resumes, and tell the truth instead of "starting now".
962
+ updateGoal({ interruptedAt: nowIso(), interruptedReason: "created in a stale session" }, ctx);
963
+ ctx.ui.notify(`Goal ${goal.id} created and safe in .pi-glla/ — this stale process can't send continuations. Restart pi and it auto-resumes.`, "warning");
964
+ return;
965
+ }
912
966
  ctx.ui.notify(`Goal ${goal.id} created — starting now. Auditor will verify on completion.`, "info");
913
967
  scheduleContinuation(ctx, true);
914
968
  }
@@ -949,6 +1003,12 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
949
1003
 
950
1004
  async function cmdResume(ctx: ExtensionContext): Promise<void> {
951
1005
  if (!state.goal || state.goal.status !== "paused") return;
1006
+ // v0.28.1 (S1/S3): resuming in a stale session used to flip status to
1007
+ // active, claim "Resumed goal", then re-pause on the stale send failure
1008
+ // (or zombie — S1). Now: persist the resume (the next fresh session
1009
+ // auto-resumes ACTIVE goals), mark the interrupt, tell the truth, and
1010
+ // skip the send that can never land.
1011
+ const staleEntry = warnIfStaleAtEntry(ctx, "/goal resume");
952
1012
  // v0.12.0: refresh the token cap from CURRENT settings on resume — goals
953
1013
  // snapshot the cap at creation, so a goal paused under an old default
954
1014
  // (e.g. 10M) would re-pause instantly even after the default changed.
@@ -956,7 +1016,8 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
956
1016
  const usage = state.goal.usage
957
1017
  ? { tokensUsed: state.goal.usage.tokensUsed, tokensLimit: freshLimit }
958
1018
  : undefined;
959
- updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(usage ? { usage } : {}) }, ctx);
1019
+ updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
1020
+ if (staleEntry) return;
960
1021
  // v0.22.5: say what was resumed — with a non-empty list this also resumes
961
1022
  // the queue (the active goal IS the list's head item).
962
1023
  // v0.22.7: name WHAT was resumed — list items resume through /list.
@@ -1111,6 +1172,8 @@ async function bulkAddFromFile(ctx: ExtensionContext, abs: string): Promise<void
1111
1172
  }
1112
1173
 
1113
1174
  async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
1175
+ // v0.28.1 (S3): honest staleness warning; read-only subcommands still work.
1176
+ warnIfStaleAtEntry(ctx, "/list");
1114
1177
  const parts = args.trim().split(/\s+/);
1115
1178
  const sub = (parts[0] ?? "").toLowerCase();
1116
1179
  const rest = args.trim().slice(sub.length).trim();
@@ -2407,6 +2470,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2407
2470
  };
2408
2471
  }
2409
2472
  const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
2473
+ // v0.28.1 (S3): honest staleness warning before any Confirm attempt.
2474
+ warnIfStaleAtEntry(liveCtx, "goal drafting");
2410
2475
  // Multi-item list draft: one Confirm for the whole batch.
2411
2476
  if (p.items && p.items.length > 0) {
2412
2477
  // v0.23.7: show ALL items in full — the user approves the whole
@@ -2424,7 +2489,14 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2424
2489
  "Confirm list batch",
2425
2490
  `${p.items.length} items:\n${preview}${batchActivates ? "\n\n(List is empty — confirming ACTIVATES item 1 immediately as the active goal.)" : ""}`,
2426
2491
  );
2427
- } catch {
2492
+ } catch (err) {
2493
+ // v0.28.1 (T1): a stale confirm is NOT a rejection — nothing was
2494
+ // refused; the dialog simply can't render in a doomed process.
2495
+ if (isStaleApiError(err)) {
2496
+ extensionApiStale = true;
2497
+ appendLedger(liveCtx.cwd, "extension_api_stale", { where: "batch confirm" });
2498
+ return { content: [{ type: "text", text: "The Confirm dialog could not render: pi invalidated this session's extension handle (session replacement). This is NOT a rejection — do NOT refine or re-propose. Tell the user to restart pi, then re-run the drafting flow." }], details: {} };
2499
+ }
2428
2500
  batchConfirmed = false;
2429
2501
  }
2430
2502
  }
@@ -2465,7 +2537,13 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2465
2537
  } else {
2466
2538
  try {
2467
2539
  confirmed = await liveCtx.ui.confirm(isListDraft ? "Confirm list item" : "Confirm goal", `${p.objective.trim()}${contractBlock}${activationNote}`);
2468
- } catch {
2540
+ } catch (err) {
2541
+ // v0.28.1 (T1): a stale confirm is NOT "Draft rejected by the user".
2542
+ if (isStaleApiError(err)) {
2543
+ extensionApiStale = true;
2544
+ appendLedger(liveCtx.cwd, "extension_api_stale", { where: "draft confirm" });
2545
+ return { content: [{ type: "text", text: "The Confirm dialog could not render: pi invalidated this session's extension handle (session replacement). This is NOT a rejection — do NOT refine or re-propose. Tell the user to restart pi, then re-run the drafting flow." }], details: {} };
2546
+ }
2469
2547
  confirmed = false;
2470
2548
  }
2471
2549
  }
@@ -4032,8 +4110,12 @@ export default function (pi: ExtensionAPI): void {
4032
4110
  }
4033
4111
  } else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
4034
4112
  if (autoResume) {
4113
+ // v0.28.1 (S2): clear the stale-handle interrupt marker — this IS
4114
+ // the auto-resume the marker promised.
4115
+ const wasInterrupted = !!state.goal.interruptedAt;
4116
+ if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
4035
4117
  ctx.ui.notify(
4036
- `Resuming ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
4118
+ `Resuming ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}${wasInterrupted ? " — auto-resumed after the stale-handle interrupt" : ""}`,
4037
4119
  "info",
4038
4120
  );
4039
4121
  scheduleContinuation(ctx, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
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",
@@ -46,6 +46,8 @@
46
46
  "stopReason": { "type": "string" },
47
47
  "pauseReason": { "type": "string" },
48
48
  "pauseSuggestedAction": { "type": "string" },
49
+ "interruptedAt": { "type": "string" },
50
+ "interruptedReason": { "type": "string" },
49
51
  "activePath": { "type": "string" },
50
52
  "archivedPath": { "type": "string" },
51
53
  "usage": {