pi-goal-list-loop-audit 0.32.0 → 0.33.0

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.
@@ -48,6 +48,31 @@ export function truncate(s: string, max: number): string {
48
48
  return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + "…";
49
49
  }
50
50
 
51
+ /** v0.33.0: 5-cell meter with a rounding guard (command-code's rule — never
52
+ * shows empty or full unless the value truly is 0 or 1). */
53
+ export function meter(frac: number, cells = 5): string {
54
+ if (!Number.isFinite(frac) || frac <= 0) return "▱".repeat(cells);
55
+ if (frac >= 1) return "▰".repeat(cells);
56
+ let filled = Math.round(frac * cells);
57
+ if (filled === 0) filled = 1;
58
+ if (filled === cells) filled = cells - 1;
59
+ return "▰".repeat(filled) + "▱".repeat(cells - filled);
60
+ }
61
+
62
+ /** v0.33.0: one finished tool call, for the slim card's "last action" line. */
63
+ export interface RecentActionDisplay {
64
+ name: string;
65
+ arg?: string;
66
+ ms: number;
67
+ ok: boolean;
68
+ }
69
+
70
+ /** v0.33.0: widget extras — the refire streak plus the recent-action feed. */
71
+ export interface WidgetExtras {
72
+ stalls?: number;
73
+ recent?: RecentActionDisplay[];
74
+ }
75
+
51
76
  /**
52
77
  * Word-wrap to `width`, capped at `maxLines` (v0.27.1). A pause is the one
53
78
  * state where the FULL text matters — the reason often carries a decision
@@ -134,7 +159,7 @@ export interface AuditDisplayProgress {
134
159
  * One-line status for ctx.ui.setStatus("pi-glla", …).
135
160
  * Returns undefined when nothing is being supervised (clears the segment).
136
161
  */
137
- export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, extras?: { stalls?: number }): string | undefined {
162
+ export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, extras?: WidgetExtras): string | undefined {
138
163
  if (state.loop?.active) {
139
164
  const l = state.loop;
140
165
  // v0.26.1: surface the refire streak — a spinning supervisor is the
@@ -223,7 +248,7 @@ function countTotal(g: Goal): number {
223
248
  * Widget lines for ctx.ui.setWidget("pi-glla", lines).
224
249
  * Returns undefined when nothing is worth showing.
225
250
  */
226
- export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
251
+ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] | undefined {
227
252
  const inner = buildWidgetLinesInner(state, audit, now, theme, width, extras);
228
253
  // v0.28.6 (E1): a persistence failure outranks everything — first line,
229
254
  // on every render, until a write lands again.
@@ -234,7 +259,7 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
234
259
  return inner;
235
260
  }
236
261
 
237
- function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
262
+ function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] | undefined {
238
263
  if (state.loop?.active) return loopLines(state.loop, now, theme, width, extras);
239
264
  const g = state.goal;
240
265
  const held = heldLoop(state);
@@ -242,7 +267,7 @@ function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null
242
267
  // v0.28.17: no visible goal — the held loop gets its own card.
243
268
  return held ? heldLoopLines(held, now, theme, width) : undefined;
244
269
  }
245
- const lines = goalLines(g, state, audit, now, theme, width);
270
+ const lines = goalLines(g, state, audit, now, theme, width, extras);
246
271
  // v0.28.17: a held loop rides the goal card as a trailing line.
247
272
  if (held) {
248
273
  lines.push(`${paint(theme, "warning", "⏸")} ${truncate(held.target, budgetFor(width, 3, 64))}`);
@@ -262,7 +287,7 @@ function heldLoopLines(l: LoopState, now: number, theme?: DisplayTheme, width?:
262
287
 
263
288
  // Branch lines sit flush-left (pi-tasks convention): pi's widget renderer
264
289
  // adds its own one-space gutter, so any indent here doubles up.
265
- function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme, width?: number): string[] {
290
+ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] {
266
291
  // Head glyph is ● (not ◆): U+25C6 renders as a color-emoji diamond in some
267
292
  // terminal fonts and ignores ANSI color; ● takes the paint everywhere.
268
293
  const icon =
@@ -271,22 +296,37 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
271
296
  : g.status === "auditing"
272
297
  ? paint(theme, "accent", "⟡")
273
298
  : paint(theme, "success", "●");
274
- const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), budgetFor(width, 3, 64))}`;
299
+ const headBase = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), budgetFor(width, 3, 48))}`;
275
300
  // v0.24.7: a list item is named as such and points at /list — before,
276
301
  // the widget called it "active" and hinted "/goal status", reading as if
277
302
  // queue work were a standalone goal.
278
303
  const isList = g.policy === "list";
279
304
  const statusWord = g.status === "active" ? paint(theme, "success", "active") : g.status;
280
- // v0.28.30: the status line ALWAYS names the type (user note: "I don't
281
- // always see the type I'd need to scroll up to see if goal/list/loop").
282
- // Before, only list items were named; a plain goal's card said "paused ·
283
- // 3m" with no type word. The loop surface has its own card.
284
- const typeWord = isList ? "list item · " : "goal · ";
305
+ // v0.33.0: slim card — status folds INTO the head line as middot segments
306
+ // (filter(Boolean).join, the universal CLI idiom). Line 2 is the live
307
+ // "last action · next task" line; the footer stays the hint line.
308
+ // v0.28.30: the type stays visible — v0.33.0 names it via the "list item"
309
+ // header segment (list policy) and the distinct card icons (● goal,
310
+ // ∞/↓/↑ loop, ⟡ auditing, ⏸ paused) + the type-named footer verbs.
285
311
  // Token segment only when a budget is set (v0.22.0): the guard is opt-in,
286
312
  // and "0/0 tok" carried no information when off.
287
313
  const tokenLimit = g.usage?.tokensLimit ?? 0;
288
- const tokens = tokenLimit > 0 ? ` · ${paint(theme, "dim", `${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(tokenLimit)} tok`)}` : "";
289
- const lines = [head, `├─ ${typeWord}${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))}${tokens}`];
314
+ const headSegs: string[] = [];
315
+ if (isList) headSegs.push("list item");
316
+ headSegs.push(statusWord);
317
+ headSegs.push(fmtElapsed(now - Date.parse(g.createdAt)));
318
+ const taskTotal = countTotal(g);
319
+ if (taskTotal > 0) headSegs.push(`${countDone(g)}/${taskTotal} ${paint(theme, "dim", meter(countDone(g) / taskTotal))}`);
320
+ const tokUsed0 = g.usage?.tokensUsed ?? 0;
321
+ if (tokenLimit > 0) headSegs.push(paint(theme, "dim", `${fmtTokens(tokUsed0)}/${fmtTokens(tokenLimit)} ${meter(tokUsed0 / tokenLimit)}`));
322
+ else if (tokUsed0 > 0) headSegs.push(paint(theme, "dim", `${fmtTokens(tokUsed0)} tok`));
323
+ // v0.28.30: the type stays visible — v0.33.0 names it via the "list item"
324
+ // header segment (list policy) and the distinct card icons (● goal,
325
+ // ∞/↓/↑ loop, ⟡ auditing, ⏸ paused) + the type-named footer verbs.
326
+ // Token segment only when a budget is set (v0.22.0): the guard is opt-in,
327
+ // and "0/0 tok" carried no information when off.
328
+ const head = `${headBase} ${paint(theme, "dim", "·")} ${headSegs.join(` ${paint(theme, "dim", "·")} `)}`;
329
+ const lines = [head];
290
330
  if (g.status === "auditing") {
291
331
  lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
292
332
  // v0.25.4: auditor-quiet stall — progress events stopped arriving
@@ -357,8 +397,16 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
357
397
  }
358
398
  return lines;
359
399
  }
400
+ // v0.33.0: "last action · next task" — Claude's done-row format meets the
401
+ // pending queue. Segments join with a dim middot; missing ones drop out.
402
+ const act = extras?.recent?.[extras.recent.length - 1];
403
+ const mid: string[] = [];
404
+ if (act) {
405
+ mid.push(`${paint(theme, act.ok ? "success" : "error", act.ok ? "✓" : "✗")} ${act.name}${act.arg ? ` ${paint(theme, "dim", truncate(act.arg, 24))}` : ""}${act.ms > 0 ? ` ${paint(theme, "dim", `(${fmtElapsed(act.ms)})`)}` : ""}`);
406
+ }
360
407
  const next = nextPending(g);
361
- if (next) lines.push(`├─ next: ${truncate(next, budgetFor(width, 9, 56))}`);
408
+ if (next) mid.push(`next: ${truncate(next, budgetFor(width, 9, 40))}`);
409
+ if (mid.length > 0) lines.push(`├─ ${mid.join(` ${paint(theme, "dim", "·")} `)}`);
362
410
  const queue = state.list?.length ?? 0;
363
411
  const footer = isList
364
412
  ? `${queue > 0 ? `${queue} queued · ` : ""}/list · /glla`
@@ -367,34 +415,30 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
367
415
  return lines;
368
416
  }
369
417
 
370
- function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] {
371
- // v0.26.1: the refire streak, shown only while nonzero.
372
- const stallNote = (extras?.stalls ?? 0) > 0 ? ` · ${paint(theme, "warning", `stalls:${extras!.stalls}`)}` : "";
373
- // v0.23.0: metricless spec loop no arrow/best/stall, no plateau.
374
- if (!l.measureCmd) {
375
- const lines = [
376
- `${paint(theme, "accent", "")} ${truncate(l.target, budgetFor(width, 3, 64))}`,
377
- `├─ loop ∞ iter ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations}` : ""} · ${fmtElapsed(now - Date.parse(l.startedAt))}${stallNote}`,
378
- `└─ ${paint(theme, "dim", "metricless work the spec (no plateau)")}`,
379
- ];
380
- if (l.branchName) lines.push(`⎇ ${paint(theme, "muted", truncate(l.branchName, budgetFor(width, 3, 50)))}`);
381
- return lines;
418
+ function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] {
419
+ // v0.33.0: slim loop card header icon names the kind (∞ metricless,
420
+ // ↓/↑ metric), all state folds into middot segments; line 2 is the live
421
+ // "last action" line; footer is hints. The old per-line "loop iter" /
422
+ // "best/last/stall" rows collapse into the header.
423
+ const stallNote = (extras?.stalls ?? 0) > 0 ? ` ${paint(theme, "dim", "·")} ${paint(theme, "warning", `stalls:${extras!.stalls}`)}` : "";
424
+ const icon = !l.measureCmd ? paint(theme, "accent", "") : paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
425
+ const segs: string[] = [];
426
+ segs.push(`iter ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations} ${paint(theme, "dim", meter(l.iteration / l.maxIterations))}` : ""}`);
427
+ segs.push(fmtElapsed(now - Date.parse(l.startedAt)));
428
+ if (l.measureCmd) {
429
+ segs.push(`best ${paint(theme, "success", `${l.bestValue ?? "n/a"}`)}`);
430
+ const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
431
+ segs.push(l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText);
382
432
  }
383
- const arrow = paint(theme, "accent", l.direction === "min" ? "" : "");
384
- const best = paint(theme, "success", `${l.bestValue ?? "n/a"}`);
385
- const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
386
- const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
387
- const lines = [
388
- `${paint(theme, "accent", "●")} ${truncate(l.target, budgetFor(width, 3, 64))}`,
389
- `├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
390
- `├─ best ${best} · last ${l.lastValue ?? "n/a"} · ${stall}`,
391
- // v0.29.15: the audit loop's measure is orchestrator-owned shell — the
392
- // raw grep reads like leaked internals ("that weird line"). Name what
393
- // it measures instead; user-authored measures still show raw.
394
- `└─ ${paint(theme, "dim", l.kind === "audit"
395
- ? "metric: closed findings ('- [x]' count)"
396
- : truncate(l.measureCmd, budgetFor(width, 3, 56)))}`,
397
- ];
433
+ const lines = [`${icon} ${truncate(l.target, budgetFor(width, 3, 44))} ${paint(theme, "dim", "·")} ${segs.join(` ${paint(theme, "dim", "·")} `)}${stallNote}`];
434
+ const act = extras?.recent?.[extras.recent.length - 1];
435
+ if (act) {
436
+ lines.push(`├─ ${paint(theme, act.ok ? "success" : "error", act.ok ? "✓" : "✗")} ${act.name}${act.arg ? ` ${paint(theme, "dim", truncate(act.arg, 24))}` : ""}${act.ms > 0 ? ` ${paint(theme, "dim", `(${fmtElapsed(act.ms)})`)}` : ""}`);
437
+ }
438
+ const footer = !l.measureCmd
439
+ ? "metricless (no plateau) · /loop stop · /loop polish"
440
+ : `${l.kind === "audit" ? "metric: closed findings" : truncate(l.measureCmd, budgetFor(width, 3, 30))} · /loop stop`;
441
+ lines.push(`└─ ${paint(theme, "dim", footer)}`);
398
442
  if (l.branchName) lines.push(`⎇ ${paint(theme, "muted", truncate(l.branchName, budgetFor(width, 3, 50)))}`);
399
443
  return lines;
400
444
  }
@@ -630,6 +630,34 @@ function isSupervising(): boolean {
630
630
  let latestAuditProgress: AuditDisplayProgress | null = null;
631
631
  let uiTicker: NodeJS.Timeout | null = null;
632
632
 
633
+ // v0.33.0: slim widget "last action" feed — a tiny ring of finished tool
634
+ // calls {name, arg, ms, ok} captured from the tool_call/tool_result stream.
635
+ // Display-only, never persisted; cleared implicitly as new actions land.
636
+ const recentActions: import("../goal-loop-display.js").RecentActionDisplay[] = [];
637
+ const inFlightToolCalls = new Map<string, { name: string; arg?: string; at: number }>();
638
+ function summarizeToolArg(name: string, input: any): string | undefined {
639
+ if (!input || typeof input !== "object") return undefined;
640
+ const v = input.file_path ?? input.path ?? input.command ?? input.pattern ?? input.query ?? input.url ?? input.title;
641
+ if (typeof v !== "string" || v.length === 0) return undefined;
642
+ const base = name === "bash" ? v : v.split("/").pop() || v;
643
+ return base.length <= 24 ? base : base.slice(0, 23) + "…";
644
+ }
645
+ function noteToolCall(event: any): void {
646
+ const name = String(event?.toolName ?? "?");
647
+ const id = String(event?.toolCallId ?? event?.id ?? `anon-${Date.now()}`);
648
+ if (inFlightToolCalls.size > 20) inFlightToolCalls.delete(inFlightToolCalls.keys().next().value!);
649
+ inFlightToolCalls.set(id, { name, arg: summarizeToolArg(name, event?.input ?? event?.args), at: Date.now() });
650
+ }
651
+ function noteToolResult(event: any): void {
652
+ const id = String(event?.toolCallId ?? event?.id ?? "");
653
+ const f = id ? inFlightToolCalls.get(id) : undefined;
654
+ if (id) inFlightToolCalls.delete(id);
655
+ const ok = !Boolean(event?.isError ?? event?.error);
656
+ const name = f?.name ?? String(event?.toolName ?? "?");
657
+ recentActions.push({ name, arg: f?.arg ?? summarizeToolArg(name, event?.input ?? event?.args), ms: f ? Date.now() - f.at : 0, ok });
658
+ if (recentActions.length > 3) recentActions.shift();
659
+ }
660
+
633
661
  function refreshUI(ctx: ExtensionContext): void {
634
662
  if (!ctx.hasUI) return;
635
663
  try {
@@ -638,7 +666,7 @@ function refreshUI(ctx: ExtensionContext): void {
638
666
  // uses the room instead of cutting at fixed ~60-char floors.
639
667
  const width = process.stdout.columns || 80;
640
668
  ctx.ui.setStatus("pi-glla", buildStatusText(state, latestAuditProgress, Date.now(), theme, { stalls: consecutiveStalls }));
641
- ctx.ui.setWidget("pi-glla", buildWidgetLines(state, latestAuditProgress, Date.now(), theme, width, { stalls: consecutiveStalls }));
669
+ ctx.ui.setWidget("pi-glla", buildWidgetLines(state, latestAuditProgress, Date.now(), theme, width, { stalls: consecutiveStalls, recent: recentActions }));
642
670
  } catch {
643
671
  // stale ctx — next event refreshes
644
672
  }
@@ -668,6 +696,16 @@ let loopRearmStreak = 0;
668
696
  // whose turn trigger was still dead — pausing a resumable goal 4 minutes
669
697
  // after the compact instead of giving pi room to recover.
670
698
  let compactionGraceUntil = 0;
699
+ // v0.32.1 (pi-goal-x's lesson — "recover from compacts smarter"): a compact
700
+ // leaves a RESUME DEBT, not just two fixed-offset settle probes that can both
701
+ // lose (field: hellhunter 4-min dangle 2026-07-31; polis stall same day).
702
+ // postCompactResumeOwed discharges only when a real turn starts (agent_start);
703
+ // every heartbeat tick past grace retries it. postCompactResyncPending arms a
704
+ // deterministic [POST-COMPACTION RESYNC] block on the next continuation/loop
705
+ // message (pi-goal-x's #5) so the compacted agent re-anchors on artifact
706
+ // state instead of lost chat history.
707
+ let postCompactResumeOwed = false;
708
+ let postCompactResyncPending = false;
671
709
  const COMPACTION_GRACE_MS = 3 * 60_000;
672
710
  // v0.28.25: provider-error retry cadence. Field-observed in dracon-utilities
673
711
  // (kimi, 19-session fleet on one provider account): a "concurrent request
@@ -827,6 +865,24 @@ function heartbeatTick(): void {
827
865
  if (!absorbStaleIfSuperseded(ctx)) goStaleTerminal(ctx, "heartbeat probe");
828
866
  return;
829
867
  }
868
+ // v0.32.1: post-compaction resume debt — retry on every heartbeat tick
869
+ // past grace until a turn actually starts. Fixed-offset settles alone
870
+ // can both lose (pi busy at 2s AND at grace+2s = a dangling chain).
871
+ if (postCompactResumeOwed && isSupervising() && !abortedStandDown) {
872
+ try {
873
+ if (ctx.isIdle() && !ctx.hasPendingMessages() && continuationTimer === null && loopTimer === null) {
874
+ if (isLoopActive()) {
875
+ appendLedger(ctx.cwd, "compaction_resume_owed_refire", { kind: "loop" });
876
+ scheduleLoopTick(ctx);
877
+ } else if (isActionableGoal()) {
878
+ appendLedger(ctx.cwd, "compaction_resume_owed_refire", { kind: "goal" });
879
+ scheduleContinuation(ctx, true);
880
+ } else {
881
+ postCompactResumeOwed = false; // nothing to resume — discharge
882
+ }
883
+ }
884
+ } catch { /* next tick */ }
885
+ }
830
886
  // v0.29.16: zombie-run watchdog. pi reports BUSY (a run is "active") but
831
887
  // zero stream events for 20 min = the provider stream hung silently —
832
888
  // queued continuations can't land, and every other watchdog stays quiet
@@ -1048,11 +1104,13 @@ function sendContinuation(goalId: string): void {
1048
1104
  }
1049
1105
  if (!extensionApi || extensionApiStale) return;
1050
1106
  try {
1107
+ const resync = postCompactResyncPending ? buildPostCompactResync() : "";
1051
1108
  extensionApi.sendMessage({
1052
1109
  customType: GOAL_EVENT_ENTRY,
1053
- content: continuationPrompt(state.goal!),
1110
+ content: resync + continuationPrompt(state.goal!),
1054
1111
  display: false,
1055
1112
  }, { triggerTurn: true, deliverAs: "followUp" });
1113
+ if (resync) postCompactResyncPending = false; // consumed only by a landed send
1056
1114
  continuationRearmStreak = 0; continuationRearmSince = 0; // v0.28.5 (E3): a landed send clears the storm
1057
1115
  appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
1058
1116
  } catch (err) {
@@ -1105,6 +1163,25 @@ function sendLengthContinue(ctx: ExtensionContext, consecutive: number): void {
1105
1163
  }
1106
1164
  }
1107
1165
 
1166
+ /** v0.32.1: deterministic post-compaction re-anchor (pi-goal-x's #5) —
1167
+ * prepended to the first continuation/loop message after a compact. */
1168
+ function buildPostCompactResync(): string {
1169
+ const lines: string[] = [
1170
+ "[POST-COMPACTION RESYNC] The transcript was just compacted. Trust the artifacts on disk and .pi-glla/ state — NOT your memory of the prior chat. Re-read files before editing them.",
1171
+ ];
1172
+ if (state.goal) {
1173
+ lines.push(`Goal ${state.goal.id} — status ${state.goal.status}`);
1174
+ lines.push(`Objective: ${state.goal.objective.slice(0, 200)}`);
1175
+ const next = findNextPendingTask(state.goal.taskList?.tasks ?? []);
1176
+ if (next) lines.push(`Next pending task: \`${next.id}\` — ${next.title}`);
1177
+ const lastAudit = state.goal.auditHistory?.[state.goal.auditHistory.length - 1];
1178
+ if (lastAudit) lines.push(`Last audit: ${lastAudit.approved ? "APPROVED" : lastAudit.impossible ? "IMPOSSIBLE" : "disapproved"} (${lastAudit.at})`);
1179
+ } else if (state.loop?.active) {
1180
+ lines.push(`Loop: ${state.loop.target.slice(0, 160)} — iteration ${state.loop.iteration}`);
1181
+ }
1182
+ return lines.join("\n") + "\n\n";
1183
+ }
1184
+
1108
1185
  function continuationPrompt(goal: Goal): string {
1109
1186
  // Read the .md file as the template, then substitute {{tokens}}.
1110
1187
  // For v0.1.0 we inline-substitute so we don't need fs at runtime.
@@ -2617,11 +2694,13 @@ function sendLoopTurn(): void {
2617
2694
  // instruction (metricless loops; metric loops already vary via values).
2618
2695
  const variantNote = metricless ? continueVariant(loop.iteration) : "";
2619
2696
  try {
2697
+ const loopResync = postCompactResyncPending ? buildPostCompactResync() : "";
2620
2698
  extensionApi.sendMessage({
2621
2699
  customType: GOAL_EVENT_ENTRY,
2622
- content: loopPrompt(loop, regressionNote, strategyNote, boundsNote, interventionNote, variantNote),
2700
+ content: loopResync + loopPrompt(loop, regressionNote, strategyNote, boundsNote, interventionNote, variantNote),
2623
2701
  display: false,
2624
2702
  }, { triggerTurn: true, deliverAs: "followUp" });
2703
+ if (loopResync) postCompactResyncPending = false; // consumed only by a landed send
2625
2704
  // v0.26.1: the send path is ledgered — the hegemon zombie spun 619
2626
2705
  // refires with zero visibility into whether sends were landing.
2627
2706
  loopRearmStreak = 0; loopRearmSince = 0; // v0.28.5 (E3): a landed turn clears the storm
@@ -5708,6 +5787,11 @@ export default function (pi: ExtensionAPI): void {
5708
5787
  continuationRearmStreak = 0; continuationRearmSince = 0;
5709
5788
  loopRearmStreak = 0; loopRearmSince = 0;
5710
5789
  compactionGraceUntil = Date.now() + COMPACTION_GRACE_MS;
5790
+ // v0.32.1: arm the resume debt + the resync block (the settle probes
5791
+ // below stay as the fast path; the heartbeat now retries the debt on
5792
+ // EVERY post-grace tick until agent_start discharges it).
5793
+ postCompactResumeOwed = true;
5794
+ postCompactResyncPending = true;
5711
5795
  const settle = setTimeout(() => {
5712
5796
  const c = freshCtx();
5713
5797
  if (!c) return;
@@ -5761,6 +5845,7 @@ export default function (pi: ExtensionAPI): void {
5761
5845
  // v0.15.1: ask_user_question answers arrive as tool results, not chat
5762
5846
  // messages — count answered (non-cancelled) questionnaires as replies too.
5763
5847
  pi.on("tool_result", async (event: any) => {
5848
+ noteToolResult(event); // v0.33.0: slim widget "last action" feed
5764
5849
  // v0.24.0: roll loop tool-result fingerprints (same-tool-same-result
5765
5850
  // detection) — recorded for ANY tool result while a loop is active.
5766
5851
  if (isLoopActive()) {
@@ -6334,10 +6419,11 @@ export default function (pi: ExtensionAPI): void {
6334
6419
  scheduleContinuation(ctx, false);
6335
6420
  });
6336
6421
 
6337
- pi.on("tool_call", () => {
6422
+ pi.on("tool_call", (event: any) => {
6338
6423
  toolCallsThisTurn++;
6339
6424
  noteActivity(true);
6340
6425
  lastStreamActivityAt = Date.now();
6426
+ noteToolCall(event); // v0.33.0
6341
6427
  // v0.24.0: count loop-iteration tool calls (narration-only detection).
6342
6428
  if (isLoopActive()) {
6343
6429
  state.loop!.toolsThisTurn = (state.loop!.toolsThisTurn ?? 0) + 1;
@@ -6351,6 +6437,9 @@ export default function (pi: ExtensionAPI): void {
6351
6437
  });
6352
6438
  pi.on("agent_start", () => {
6353
6439
  lastStreamActivityAt = Date.now();
6440
+ // v0.32.1: a real turn started — the post-compaction resume debt is
6441
+ // discharged (the heartbeat stops retrying it).
6442
+ postCompactResumeOwed = false;
6354
6443
  });
6355
6444
  pi.on("turn_start", () => {
6356
6445
  lastStreamActivityAt = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
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",