pi-goal-list-loop-audit 0.27.0 → 0.27.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.
|
@@ -37,6 +37,36 @@ export function truncate(s: string, max: number): string {
|
|
|
37
37
|
return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + "…";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Word-wrap to `width`, capped at `maxLines` (v0.27.1). A pause is the one
|
|
42
|
+
* state where the FULL text matters — the reason often carries a decision
|
|
43
|
+
* the user must make (dedup choices, impossible-verdict narrowing), and a
|
|
44
|
+
* 60-char truncate hid it. Over-long words are hard-split; when the cap
|
|
45
|
+
* cuts content the last line ends with "…" (the pause-time notification
|
|
46
|
+
* and /goal status always carry the full text).
|
|
47
|
+
*/
|
|
48
|
+
export function wrap(s: string, width: number, maxLines: number): string[] {
|
|
49
|
+
const norm = s.replace(/\s+/g, " ").trim();
|
|
50
|
+
const words = norm.split(" ").filter(Boolean);
|
|
51
|
+
const all: string[] = [];
|
|
52
|
+
let cur = "";
|
|
53
|
+
for (let w of words) {
|
|
54
|
+
const next = cur ? `${cur} ${w}` : w;
|
|
55
|
+
if (next.length <= width) { cur = next; continue; }
|
|
56
|
+
if (cur) all.push(cur);
|
|
57
|
+
while (w.length > width) { all.push(w.slice(0, width)); w = w.slice(width); }
|
|
58
|
+
cur = w;
|
|
59
|
+
}
|
|
60
|
+
if (cur) all.push(cur);
|
|
61
|
+
if (all.length === 0) all.push("");
|
|
62
|
+
if (all.length <= maxLines) return all;
|
|
63
|
+
const out = all.slice(0, maxLines);
|
|
64
|
+
// The last kept line already fits within width — truncate() would leave it
|
|
65
|
+
// unmarked, so force the ellipsis to signal "more in /goal status".
|
|
66
|
+
out[maxLines - 1] = out[maxLines - 1]!.slice(0, Math.max(0, width - 1)) + "…";
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
/**
|
|
41
71
|
* Width-aware truncation budget (v0.22.2). The hardcoded caps are FLOORS for
|
|
42
72
|
* narrow terminals; when the terminal is wider, lines may use the available
|
|
@@ -191,8 +221,29 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
191
221
|
return lines;
|
|
192
222
|
}
|
|
193
223
|
if (g.status === "paused" && g.pauseReason) {
|
|
194
|
-
|
|
195
|
-
|
|
224
|
+
const isErr = pauseIsError(g);
|
|
225
|
+
const budget = budgetFor(width, 3, 60);
|
|
226
|
+
// v0.27.1: wrap reason + suggested action over up to 3 lines each
|
|
227
|
+
// (see wrap()); before, both were truncated at ~60 chars and the actual
|
|
228
|
+
// question in a decision-pause never reached the user.
|
|
229
|
+
wrap(g.pauseReason, budget, 3).forEach((w, i) => {
|
|
230
|
+
lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, isErr ? "error" : "warning", w)}`);
|
|
231
|
+
});
|
|
232
|
+
// v0.27.1: what survives the pause — the first question at a pause is
|
|
233
|
+
// "did I lose the work?". Answer it on the card.
|
|
234
|
+
const spent: string[] = [];
|
|
235
|
+
const tokUsed = g.usage?.tokensUsed ?? 0;
|
|
236
|
+
if (tokUsed > 0) spent.push(`${fmtTokens(tokUsed)} tok spent`);
|
|
237
|
+
const audits = g.auditHistory?.length ?? 0;
|
|
238
|
+
if (audits > 0) spent.push(`${audits} audit${audits === 1 ? "" : "s"}`);
|
|
239
|
+
const savedLine = `saved${spent.length > 0 ? ` — ${spent.join(" · ")}` : ""} · resumes exactly here`;
|
|
240
|
+
if (g.pauseSuggestedAction) {
|
|
241
|
+
lines.push(`├─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
|
|
242
|
+
const wrapped = wrap(g.pauseSuggestedAction, budget, 3);
|
|
243
|
+
wrapped.forEach((w, i) => lines.push(`${i === wrapped.length - 1 ? "└─" : "│ "} ${paint(theme, "dim", w)}`));
|
|
244
|
+
} else {
|
|
245
|
+
lines.push(`└─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
|
|
246
|
+
}
|
|
196
247
|
return lines;
|
|
197
248
|
}
|
|
198
249
|
const next = nextPending(g);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// pi-goal-list-loop-audit — v0.27.2
|
|
2
|
+
// extensions/length-continue.ts
|
|
3
|
+
//
|
|
4
|
+
// Folded-in auto-continue for output-token truncation (was the standalone
|
|
5
|
+
// pi-length-continue 0.1.0, now deprecated). When ONE assistant response
|
|
6
|
+
// exceeds the model's provider-side per-response output cap, pi ends the
|
|
7
|
+
// turn with stopReason "length" and idles — a dead stop on unattended rigs.
|
|
8
|
+
// The tracker decides when to re-trigger; goal.ts's agent_end handler wires
|
|
9
|
+
// it BEFORE all turn bookkeeping: a truncated turn is not a completed turn
|
|
10
|
+
// (no telemetry), not a stall (no no-tool nudge), and must not run the
|
|
11
|
+
// loop measure or the normal goal continuation on half a response.
|
|
12
|
+
//
|
|
13
|
+
// Guards (same as the standalone):
|
|
14
|
+
// - consecutive cap: after MAX back-to-back truncations, give up (once)
|
|
15
|
+
// instead of burning quota in a truncation ping-pong. Any normally
|
|
16
|
+
// finished turn resets the counter.
|
|
17
|
+
// - the caller skips when messages are already pending (a queued message
|
|
18
|
+
// triggers a turn anyway) and routes stale-handle errors to
|
|
19
|
+
// goStaleTerminal (pi#7154).
|
|
20
|
+
|
|
21
|
+
export const LENGTH_CONTINUE_MAX = 3;
|
|
22
|
+
|
|
23
|
+
export const LENGTH_CONTINUE_TEXT = [
|
|
24
|
+
"Your previous response was cut off at the model's per-response output token limit.",
|
|
25
|
+
"Continue EXACTLY where you stopped — finish the current artifact, then keep going.",
|
|
26
|
+
"Keep each individual response shorter from here: split large file writes into multiple smaller write/edit calls across turns instead of one giant response.",
|
|
27
|
+
].join(" ");
|
|
28
|
+
|
|
29
|
+
export interface LengthContinueTick {
|
|
30
|
+
/** Send the continue message this round. */
|
|
31
|
+
fire: boolean;
|
|
32
|
+
/** The cap was just exceeded — notify the give-up exactly once. */
|
|
33
|
+
giveUpNow: boolean;
|
|
34
|
+
/** Current consecutive truncation streak (after this tick). */
|
|
35
|
+
consecutive: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function makeLengthContinueTracker(max: number = LENGTH_CONTINUE_MAX) {
|
|
39
|
+
let consecutive = 0;
|
|
40
|
+
let gaveUp = false;
|
|
41
|
+
return {
|
|
42
|
+
tick(stopped: boolean): LengthContinueTick {
|
|
43
|
+
if (!stopped) {
|
|
44
|
+
consecutive = 0;
|
|
45
|
+
gaveUp = false;
|
|
46
|
+
return { fire: false, giveUpNow: false, consecutive: 0 };
|
|
47
|
+
}
|
|
48
|
+
consecutive++;
|
|
49
|
+
if (consecutive > max) {
|
|
50
|
+
const giveUpNow = !gaveUp;
|
|
51
|
+
gaveUp = true;
|
|
52
|
+
return { fire: false, giveUpNow, consecutive };
|
|
53
|
+
}
|
|
54
|
+
return { fire: true, giveUpNow: false, consecutive };
|
|
55
|
+
},
|
|
56
|
+
get consecutive(): number {
|
|
57
|
+
return consecutive;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Session-level singleton — one tracker per extension runtime. The factory
|
|
63
|
+
// calls resetLengthContinue() so an extension reload starts clean.
|
|
64
|
+
let tracker = makeLengthContinueTracker();
|
|
65
|
+
|
|
66
|
+
export function tickLengthContinue(stopped: boolean): LengthContinueTick {
|
|
67
|
+
return tracker.tick(stopped);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resetLengthContinue(): void {
|
|
71
|
+
tracker = makeLengthContinueTracker();
|
|
72
|
+
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -86,6 +86,12 @@ import {
|
|
|
86
86
|
writeGoalMd,
|
|
87
87
|
missingGllaTools,
|
|
88
88
|
} from "../goal-loop-core.js";
|
|
89
|
+
import {
|
|
90
|
+
LENGTH_CONTINUE_MAX,
|
|
91
|
+
LENGTH_CONTINUE_TEXT,
|
|
92
|
+
resetLengthContinue,
|
|
93
|
+
tickLengthContinue,
|
|
94
|
+
} from "../length-continue.js";
|
|
89
95
|
import {
|
|
90
96
|
isQuotaError,
|
|
91
97
|
isSubagentQuotaResult,
|
|
@@ -527,6 +533,25 @@ function sendContinuation(goalId: string): void {
|
|
|
527
533
|
}
|
|
528
534
|
}
|
|
529
535
|
|
|
536
|
+
// v0.27.2: send the truncation-continue nudge. Same guards as
|
|
537
|
+
// sendContinuation (stale api = terminal), independent of goal state —
|
|
538
|
+
// plain sessions truncate too.
|
|
539
|
+
function sendLengthContinue(ctx: ExtensionContext, consecutive: number): void {
|
|
540
|
+
if (!extensionApi || extensionApiStale) return;
|
|
541
|
+
try {
|
|
542
|
+
extensionApi.sendMessage({
|
|
543
|
+
customType: GOAL_EVENT_ENTRY,
|
|
544
|
+
content: LENGTH_CONTINUE_TEXT,
|
|
545
|
+
display: true,
|
|
546
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
547
|
+
appendLedger(ctx.cwd, "length_continue_sent", { consecutive });
|
|
548
|
+
ctx.ui.notify(`Response hit the output-token cap — auto-continuing (${consecutive}/${LENGTH_CONTINUE_MAX})`, "warning");
|
|
549
|
+
} catch (err) {
|
|
550
|
+
appendLedger(ctx.cwd, "length_continue_send_failed", { consecutive, error: err instanceof Error ? err.message : String(err) });
|
|
551
|
+
if (isStaleApiError(err)) goStaleTerminal(ctx, "sendLengthContinue");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
530
555
|
function continuationPrompt(goal: Goal): string {
|
|
531
556
|
// Read the .md file as the template, then substitute {{tokens}}.
|
|
532
557
|
// For v0.1.0 we inline-substitute so we don't need fs at runtime.
|
|
@@ -2254,8 +2279,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2254
2279
|
pauseReason: p.reason,
|
|
2255
2280
|
pauseSuggestedAction: p.suggestedAction,
|
|
2256
2281
|
}, ctx);
|
|
2257
|
-
|
|
2258
|
-
|
|
2282
|
+
// v0.27.1: surface the FULL pause contract — reason AND suggested
|
|
2283
|
+
// action. Before, the action only appeared in /goal status and the
|
|
2284
|
+
// widget truncated both at ~60 chars, so decision-pauses ("choose a
|
|
2285
|
+
// or b") reached the user as an unreadable fragment.
|
|
2286
|
+
ctx.ui.notify(`Goal paused: ${p.reason}${p.suggestedAction ? `\n\n→ ${p.suggestedAction}` : ""}`, "info");
|
|
2287
|
+
notifyExternal(ctx, `Goal paused: ${(p.suggestedAction ? `${p.reason} → ${p.suggestedAction}` : p.reason).slice(0, 200)}`);
|
|
2259
2288
|
return { content: [{ type: "text", text: "Goal paused. /goal resume to continue." }], details: {} };
|
|
2260
2289
|
},
|
|
2261
2290
|
}));
|
|
@@ -3459,6 +3488,7 @@ function warnOnCommandCollision(ctx: ExtensionContext): void {
|
|
|
3459
3488
|
export default function (pi: ExtensionAPI): void {
|
|
3460
3489
|
extensionApi = pi;
|
|
3461
3490
|
extensionApiStale = false; // a fresh factory run means a fresh runtime (reload path)
|
|
3491
|
+
resetLengthContinue(); // v0.27.2: fresh runtime, fresh truncation streak
|
|
3462
3492
|
startHeartbeat();
|
|
3463
3493
|
startUITicker();
|
|
3464
3494
|
// Four top-level commands, that's all (v0.8.0 consolidation):
|
|
@@ -3774,6 +3804,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3774
3804
|
// continuation loop.
|
|
3775
3805
|
if (isForeignCtx(ctx)) return;
|
|
3776
3806
|
noteActivity(true);
|
|
3807
|
+
// v0.27.2: folded-in length-continue (standalone pi-length-continue is
|
|
3808
|
+
// deprecated). A response cut by the per-response output cap is NOT a
|
|
3809
|
+
// completed turn (no telemetry), NOT a stall (no no-tool nudge), and
|
|
3810
|
+
// must not run the loop measure or the normal goal continuation on half
|
|
3811
|
+
// a response — re-trigger immediately with split-smaller guidance and
|
|
3812
|
+
// skip ALL turn bookkeeping; the NEXT agent_end processes the run.
|
|
3813
|
+
// Works with no goal active (plain sessions truncate too).
|
|
3814
|
+
const lastA = [...(event.messages as any[])].reverse().find((m) => m.role === "assistant");
|
|
3815
|
+
const lc = tickLengthContinue(lastA?.stopReason === "length");
|
|
3816
|
+
if (lc.giveUpNow) {
|
|
3817
|
+
ctx.ui.notify(`glla: response hit the output-token cap ${LENGTH_CONTINUE_MAX}× in a row — stepping aside. Ask the model to split the work into smaller pieces.`, "warning");
|
|
3818
|
+
notifyExternal(ctx, "Response truncated 3× in a row — giving up auto-continue.");
|
|
3819
|
+
}
|
|
3820
|
+
if (lastA?.stopReason === "length") {
|
|
3821
|
+
if (lc.fire && !ctx.hasPendingMessages()) sendLengthContinue(ctx, lc.consecutive);
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3777
3824
|
// v0.25.2: per-goal turn telemetry (/glla stats).
|
|
3778
3825
|
if (state.goal && state.goal.status === "active") {
|
|
3779
3826
|
const t = state.goal.telemetry ?? { turns: 0, fileWrites: 0, bashCalls: 0 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
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",
|