pi-goal-list-loop-audit 0.34.18 → 0.34.19
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.
|
@@ -310,17 +310,24 @@ function heldLoopLines(l: LoopState, now: number, theme?: DisplayTheme, width?:
|
|
|
310
310
|
function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] {
|
|
311
311
|
// Head glyph is ● (not ◆): U+25C6 renders as a color-emoji diamond in some
|
|
312
312
|
// terminal fonts and ignores ANSI color; ● takes the paint everywhere.
|
|
313
|
+
const interrupted = g.status === "active" && !!g.interruptedAt;
|
|
313
314
|
const icon =
|
|
314
|
-
|
|
315
|
-
? paint(theme,
|
|
316
|
-
: g.status === "
|
|
317
|
-
? paint(theme, "
|
|
318
|
-
:
|
|
315
|
+
interrupted
|
|
316
|
+
? paint(theme, "error", "⚠")
|
|
317
|
+
: g.status === "paused"
|
|
318
|
+
? paint(theme, pauseIsError(g) ? "error" : "warning", "⏸")
|
|
319
|
+
: g.status === "auditing"
|
|
320
|
+
? paint(theme, "accent", "⟡")
|
|
321
|
+
: paint(theme, "success", "●");
|
|
319
322
|
// v0.24.7: a list item is named as such and points at /list — before,
|
|
320
323
|
// the widget called it "active" and hinted "/goal status", reading as if
|
|
321
324
|
// queue work were a standalone goal.
|
|
322
325
|
const isList = g.policy === "list";
|
|
323
|
-
const statusWord =
|
|
326
|
+
const statusWord = interrupted
|
|
327
|
+
? paint(theme, "error", "interrupted")
|
|
328
|
+
: g.status === "active"
|
|
329
|
+
? paint(theme, "success", "active")
|
|
330
|
+
: g.status;
|
|
324
331
|
// v0.33.0: slim card — status folds INTO the head line as middot segments
|
|
325
332
|
// (filter(Boolean).join, the universal CLI idiom). Line 2 is the live
|
|
326
333
|
// "last action · next task" line; the footer stays the hint line.
|
|
@@ -351,6 +358,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
351
358
|
const objBudget = width && width > 0 ? Math.max(16, width - 1 - 2 - 3 - visibleLen(segsText)) : 48;
|
|
352
359
|
const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), objBudget)} ${paint(theme, "dim", "·")} ${segsText}`;
|
|
353
360
|
const lines = [head];
|
|
361
|
+
if (interrupted) {
|
|
362
|
+
const resumeCmd = isList ? "/list resume" : "/goal resume";
|
|
363
|
+
lines.push(`├─ ${paint(theme, "error", "host session lost — waiting for fresh session_start")}`);
|
|
364
|
+
lines.push(`└─ ${paint(theme, "warning", `/reload to rebind · ${resumeCmd} if it does not resume`)}`);
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
354
367
|
if (g.status === "auditing") {
|
|
355
368
|
lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
|
|
356
369
|
// v0.25.4: auditor-quiet stall — progress events stopped arriving
|
|
@@ -35,6 +35,48 @@ export interface LengthContinueTick {
|
|
|
35
35
|
consecutive: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface ContextUsageLike {
|
|
39
|
+
tokens?: number | null;
|
|
40
|
+
contextWindow?: number;
|
|
41
|
+
percent?: number | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AssistantLengthMessageLike {
|
|
45
|
+
stopReason?: string;
|
|
46
|
+
usage?: {
|
|
47
|
+
output?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT = 90;
|
|
52
|
+
export const LENGTH_CONTINUE_CONTEXT_STARVED_MAX_OUTPUT = 8;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* v0.34.19: distinguish a REAL overlong assistant response from pi's
|
|
56
|
+
* context-safety clamp. Near the configured context ceiling,
|
|
57
|
+
* pi-ai's clampMaxTokensToContext() can reduce max_tokens to 1; MiniMax then
|
|
58
|
+
* returns stopReason "length" with ~1 output token. That is context
|
|
59
|
+
* starvation: auto-compaction must own recovery. Sending LENGTH_CONTINUE_TEXT
|
|
60
|
+
* here queues another 1-token request before pi's post-agent_end compaction
|
|
61
|
+
* check and delays the actual cure (field: darklord 2026-08-02, 198,116 /
|
|
62
|
+
* 198,179 total tokens of a 200,000 window, output=1 twice).
|
|
63
|
+
*/
|
|
64
|
+
export function isContextStarvedLengthStop(
|
|
65
|
+
message: AssistantLengthMessageLike | null | undefined,
|
|
66
|
+
contextUsage: ContextUsageLike | null | undefined,
|
|
67
|
+
): boolean {
|
|
68
|
+
if (message?.stopReason !== "length") return false;
|
|
69
|
+
const output = message.usage?.output;
|
|
70
|
+
if (typeof output !== "number" || !Number.isFinite(output)) return false;
|
|
71
|
+
if (output > LENGTH_CONTINUE_CONTEXT_STARVED_MAX_OUTPUT) return false;
|
|
72
|
+
const percent = typeof contextUsage?.percent === "number"
|
|
73
|
+
? contextUsage.percent
|
|
74
|
+
: typeof contextUsage?.tokens === "number" && typeof contextUsage?.contextWindow === "number" && contextUsage.contextWindow > 0
|
|
75
|
+
? (contextUsage.tokens / contextUsage.contextWindow) * 100
|
|
76
|
+
: null;
|
|
77
|
+
return percent !== null && Number.isFinite(percent) && percent >= LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT;
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
export function makeLengthContinueTracker(max: number = LENGTH_CONTINUE_MAX) {
|
|
39
81
|
let consecutive = 0;
|
|
40
82
|
let gaveUp = false;
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
import {
|
|
93
93
|
LENGTH_CONTINUE_MAX,
|
|
94
94
|
LENGTH_CONTINUE_TEXT,
|
|
95
|
+
isContextStarvedLengthStop,
|
|
95
96
|
resetLengthContinue,
|
|
96
97
|
tickLengthContinue,
|
|
97
98
|
} from "../length-continue.js";
|
|
@@ -231,6 +232,11 @@ let extensionApiStale = false;
|
|
|
231
232
|
// path must still ledger the stale handle, stop stale work, and preserve the
|
|
232
233
|
// interrupt marker so a later fresh lifecycle can restore it.
|
|
233
234
|
let staleTerminalDone = false;
|
|
235
|
+
// v0.34.19: delayed session-owned callbacks capture this generation. A
|
|
236
|
+
// clearTimeout can race a callback already queued by Node; without a
|
|
237
|
+
// generation check, an old compaction/refire callback can run after /reload
|
|
238
|
+
// and schedule work against the fresh session.
|
|
239
|
+
let sessionGeneration = 0;
|
|
234
240
|
|
|
235
241
|
/** v0.26.7: a stale api is terminal for this process — go loudly with
|
|
236
242
|
* restart guidance instead of retrying sends that can never land.
|
|
@@ -309,6 +315,10 @@ function goStaleTerminal(ctx: ExtensionContext, where: string): void {
|
|
|
309
315
|
} else if (state.goal && state.goal.status === "active") {
|
|
310
316
|
updateGoal({ interruptedAt: nowIso(), interruptedReason: `extension api stale (${where})` }, ctx);
|
|
311
317
|
}
|
|
318
|
+
// The stale process loses its ticker immediately, so paint the durable
|
|
319
|
+
// interrupted state synchronously while the old UI handle can still accept
|
|
320
|
+
// updates. The next session_start paints it again from disk.
|
|
321
|
+
refreshUI(ctx);
|
|
312
322
|
ctx.ui.notify(`glla: ${guidance}`, "warning");
|
|
313
323
|
notifyExternal(ctx, `glla: extension api stale — waiting for a fresh session_start; restart pi normally only if no replacement arrives. (${where})`);
|
|
314
324
|
}
|
|
@@ -622,6 +632,10 @@ function releaseInitialSessionLoadBarrier(): void {
|
|
|
622
632
|
}
|
|
623
633
|
|
|
624
634
|
function rememberCtx(ctx: ExtensionContext): void {
|
|
635
|
+
// Late events from a disposed session must never reclaim lastCtx after the
|
|
636
|
+
// lifecycle handoff has been declared. Only session_start clears these
|
|
637
|
+
// gates and may bind a fresh context.
|
|
638
|
+
if (sessionHandoffPending || staleTerminalDone || zombieStoodDown) return;
|
|
625
639
|
let ownerLive = false;
|
|
626
640
|
if (ownerSession && lastCtx) {
|
|
627
641
|
try { lastCtx.isIdle(); ownerLive = true; } catch { /* owner went stale (session replaced) */ }
|
|
@@ -1210,9 +1224,19 @@ function clearContinuationTimer(): void {
|
|
|
1210
1224
|
}
|
|
1211
1225
|
|
|
1212
1226
|
function scheduleSessionTimeout(callback: () => void, delayMs: number): NodeJS.Timeout {
|
|
1227
|
+
const generation = sessionGeneration;
|
|
1213
1228
|
let timer: NodeJS.Timeout;
|
|
1214
1229
|
timer = setTimeout(() => {
|
|
1215
1230
|
sessionTimeouts.delete(timer);
|
|
1231
|
+
// clearTimeout is not enough when the callback is already queued. Do not
|
|
1232
|
+
// let an old session's callback re-arm work after stale/shutdown/reload.
|
|
1233
|
+
if (
|
|
1234
|
+
generation !== sessionGeneration ||
|
|
1235
|
+
sessionHandoffPending ||
|
|
1236
|
+
extensionApiStale ||
|
|
1237
|
+
staleTerminalDone ||
|
|
1238
|
+
zombieStoodDown
|
|
1239
|
+
) return;
|
|
1216
1240
|
callback();
|
|
1217
1241
|
}, delayMs);
|
|
1218
1242
|
sessionTimeouts.add(timer);
|
|
@@ -1222,6 +1246,7 @@ function scheduleSessionTimeout(callback: () => void, delayMs: number): NodeJS.T
|
|
|
1222
1246
|
|
|
1223
1247
|
function clearSessionOwnedTimers(): void {
|
|
1224
1248
|
sessionHandoffPending = true;
|
|
1249
|
+
sessionGeneration++;
|
|
1225
1250
|
initialSessionLoadPending = false;
|
|
1226
1251
|
clearContinuationTimer();
|
|
1227
1252
|
clearLoopTimer();
|
|
@@ -1285,7 +1310,7 @@ function armQueueStuckProbe(sentAt: number): void {
|
|
|
1285
1310
|
}
|
|
1286
1311
|
|
|
1287
1312
|
function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: number): void {
|
|
1288
|
-
if (sessionHandoffPending || initialSessionLoadPending) return;
|
|
1313
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
1289
1314
|
abortedStandDown = false; // v0.29.5: any explicit schedule ends the stand-down
|
|
1290
1315
|
if (!isActionableGoal()) return;
|
|
1291
1316
|
rememberCtx(ctx);
|
|
@@ -1303,7 +1328,7 @@ function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: nu
|
|
|
1303
1328
|
}
|
|
1304
1329
|
|
|
1305
1330
|
function sendContinuation(goalId: string): void {
|
|
1306
|
-
if (sessionHandoffPending || initialSessionLoadPending) return;
|
|
1331
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
1307
1332
|
continuationTimer = null;
|
|
1308
1333
|
continuationScheduledFor = null;
|
|
1309
1334
|
if (!isActionableGoal()) return;
|
|
@@ -2910,7 +2935,7 @@ function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: strin
|
|
|
2910
2935
|
}
|
|
2911
2936
|
|
|
2912
2937
|
function scheduleLoopTick(ctx: ExtensionContext): void {
|
|
2913
|
-
if (sessionHandoffPending || initialSessionLoadPending || !isLoopActive()) return;
|
|
2938
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown || !isLoopActive()) return;
|
|
2914
2939
|
rememberCtx(ctx);
|
|
2915
2940
|
clearLoopTimer();
|
|
2916
2941
|
let delay = 0;
|
|
@@ -2923,7 +2948,7 @@ function scheduleLoopTick(ctx: ExtensionContext): void {
|
|
|
2923
2948
|
}
|
|
2924
2949
|
|
|
2925
2950
|
function sendLoopTurn(): void {
|
|
2926
|
-
if (sessionHandoffPending || initialSessionLoadPending) return;
|
|
2951
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
2927
2952
|
loopTimer = null;
|
|
2928
2953
|
if (!isLoopActive() || !extensionApi) return;
|
|
2929
2954
|
const ctx = freshCtx();
|
|
@@ -6234,6 +6259,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6234
6259
|
// 60s heartbeat notices. Re-arm it as soon as pi settles post-compact.
|
|
6235
6260
|
pi.on("session_compact", async (_event: any, ctx: ExtensionContext) => {
|
|
6236
6261
|
if (isForeignCtx(ctx)) return;
|
|
6262
|
+
// A late compact event can arrive after pi has already invalidated this
|
|
6263
|
+
// extension. It must not reclaim the old ctx or schedule settle refires.
|
|
6264
|
+
if (sessionHandoffPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
6237
6265
|
rememberCtx(ctx);
|
|
6238
6266
|
if (!isSupervising()) return;
|
|
6239
6267
|
appendLedger(ctx.cwd, "session_compact", {});
|
|
@@ -6299,6 +6327,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6299
6327
|
// v0.15.1: ask_user_question answers arrive as tool results, not chat
|
|
6300
6328
|
// messages — count answered (non-cancelled) questionnaires as replies too.
|
|
6301
6329
|
pi.on("tool_result", async (event: any) => {
|
|
6330
|
+
if (sessionHandoffPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
6302
6331
|
noteToolResult(event); // v0.33.0: slim widget "last action" feed
|
|
6303
6332
|
// v0.24.0: roll loop tool-result fingerprints (same-tool-same-result
|
|
6304
6333
|
// detection) — recorded for ANY tool result while a loop is active.
|
|
@@ -6371,6 +6400,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6371
6400
|
if (isForeignCtx(ctx)) return;
|
|
6372
6401
|
extensionApi = pi;
|
|
6373
6402
|
sessionHandoffPending = false;
|
|
6403
|
+
// Reset terminal ownership before rememberCtx: this is the only event
|
|
6404
|
+
// allowed to bind a context after a stale/shutdown handoff.
|
|
6405
|
+
staleTerminalDone = false; // v0.33.1: a rebound session can go terminal again
|
|
6406
|
+
zombieStoodDown = false;
|
|
6407
|
+
sessionGeneration++;
|
|
6408
|
+
// Ephemeral watchdog counters belong to the old session, not the
|
|
6409
|
+
// persisted goal. Reset them so a stale boundary cannot make the next
|
|
6410
|
+
// fresh session inherit a false stall count.
|
|
6411
|
+
heartbeatNudges = 0;
|
|
6412
|
+
consecutiveStalls = 0;
|
|
6374
6413
|
const startReason = typeof event?.reason === "string" ? event.reason : "unknown";
|
|
6375
6414
|
initialSessionLoadPending = isBlankInitialStartup(ctx, startReason);
|
|
6376
6415
|
rememberCtx(ctx);
|
|
@@ -6384,8 +6423,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6384
6423
|
// confirm the new handle actually works.
|
|
6385
6424
|
writeOwnerFile(ctx.cwd);
|
|
6386
6425
|
sessionReplacementUntil = 0;
|
|
6387
|
-
zombieStoodDown = false;
|
|
6388
|
-
staleTerminalDone = false; // v0.33.1: a rebound session must be able to go terminal AGAIN (was: one-shot for the process lifetime)
|
|
6389
6426
|
postCompactResumeOwed = false; // v0.33.1: a compact from a previous session must not resync THIS one
|
|
6390
6427
|
postCompactResyncPending = false;
|
|
6391
6428
|
appendLedger(ctx.cwd, "session_rebound", { reason: startReason });
|
|
@@ -6605,6 +6642,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6605
6642
|
});
|
|
6606
6643
|
|
|
6607
6644
|
pi.on("agent_end", async (event: any, ctx: ExtensionContext) => {
|
|
6645
|
+
// A late agent_end from the disposed session is not a fresh turn. Do not
|
|
6646
|
+
// account it, run length continuation, or schedule another send after a
|
|
6647
|
+
// stale terminal/handoff.
|
|
6648
|
+
if (sessionHandoffPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
6608
6649
|
rememberCtx(ctx);
|
|
6609
6650
|
// v0.23.8: a subagent finishing must not drive the main session's
|
|
6610
6651
|
// continuation loop.
|
|
@@ -6625,7 +6666,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6625
6666
|
const rawPriorA = assistants.length >= 2 ? assistants[assistants.length - 2] : null;
|
|
6626
6667
|
const extractText = (m: any): string => (m && Array.isArray(m.content)) ? m.content.filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n") : "";
|
|
6627
6668
|
const lastA = rawLastA ? { stopReason: rawLastA.stopReason, text: extractText(rawLastA), priorText: extractText(rawPriorA) } : null;
|
|
6628
|
-
|
|
6669
|
+
// v0.34.19: pi-ai clamps max_tokens to the remaining context before the
|
|
6670
|
+
// provider call. At ~99% context that clamp can be 1 token, which the
|
|
6671
|
+
// provider reports as stopReason "length" — but this is NOT an overlong
|
|
6672
|
+
// assistant response. Extension agent_end runs BEFORE pi's own
|
|
6673
|
+
// auto-compaction check (agent-session.js _handlePostAgentRun), so sending
|
|
6674
|
+
// LENGTH_CONTINUE_TEXT here queues another 1-token request and delays the
|
|
6675
|
+
// real cure. Defer to pi compaction; session_compact's resume debt owns
|
|
6676
|
+
// the next continuation. Older pi/test doubles without getContextUsage()
|
|
6677
|
+
// fail open to the legacy true-length path.
|
|
6678
|
+
const contextUsage = (() => {
|
|
6679
|
+
try { return typeof ctx.getContextUsage === "function" ? ctx.getContextUsage() : undefined; } catch { return undefined; }
|
|
6680
|
+
})();
|
|
6681
|
+
const contextStarvedLength = isContextStarvedLengthStop(rawLastA, contextUsage);
|
|
6682
|
+
const lc = tickLengthContinue(lastA?.stopReason === "length" && !contextStarvedLength);
|
|
6683
|
+
if (contextStarvedLength) {
|
|
6684
|
+
appendLedger(ctx.cwd, "length_continue_deferred_context_full", {
|
|
6685
|
+
outputTokens: rawLastA?.usage?.output,
|
|
6686
|
+
contextTokens: contextUsage?.tokens ?? null,
|
|
6687
|
+
contextWindow: contextUsage?.contextWindow ?? null,
|
|
6688
|
+
contextPercent: contextUsage?.percent ?? null,
|
|
6689
|
+
});
|
|
6690
|
+
ctx.ui.notify("glla: output-token stop was context starvation (tiny output at a nearly full context) — yielding to pi auto-compaction instead of re-sending.", "info");
|
|
6691
|
+
return;
|
|
6692
|
+
}
|
|
6629
6693
|
if (lc.giveUpNow) {
|
|
6630
6694
|
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");
|
|
6631
6695
|
notifyExternal(ctx, "Response truncated 3× in a row — giving up auto-continue.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.19",
|
|
4
4
|
"description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. An isolated extension-less auditor re-verifies every completion with raw evidence; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|