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.
- package/extensions/goal-loop-display.ts +85 -41
- package/extensions/loops/goal.ts +93 -4
- package/package.json +1 -1
|
@@ -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?:
|
|
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?:
|
|
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?:
|
|
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
|
|
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.
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
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
|
|
289
|
-
|
|
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)
|
|
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?:
|
|
371
|
-
// v0.
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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.
|
|
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",
|