pi-goal-list-loop-audit 0.34.17 → 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.
- package/extensions/goal-loop-display.ts +19 -6
- package/extensions/length-continue.ts +42 -0
- package/extensions/loops/goal.ts +137 -20
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -583,6 +593,11 @@ let lastCtx: ExtensionContext | null = null;
|
|
|
583
593
|
// timer or late event that reaches the old module must stand down until the
|
|
584
594
|
// fresh session_start rebinds it.
|
|
585
595
|
let sessionHandoffPending = false;
|
|
596
|
+
// v0.34.18: pi's initial `session_start` fires before interactive mode
|
|
597
|
+
// renders the selected transcript. A plain `pi` startup is also a fresh,
|
|
598
|
+
// empty session; do not let project-scoped autoResume launch work into that
|
|
599
|
+
// placeholder before the user has loaded a real session.
|
|
600
|
+
let initialSessionLoadPending = false;
|
|
586
601
|
const sessionTimeouts = new Set<NodeJS.Timeout>();
|
|
587
602
|
// v0.23.8: the session that OWNS the loop (its sessionManager). Subagent
|
|
588
603
|
// sessions (pi-subagents binds extensions there too) fire our handlers
|
|
@@ -590,7 +605,37 @@ const sessionTimeouts = new Set<NodeJS.Timeout>();
|
|
|
590
605
|
// subagent ctx would silently kill the heartbeat/wedge machinery).
|
|
591
606
|
let ownerSession: unknown = null;
|
|
592
607
|
|
|
608
|
+
function sessionHasConversation(ctx: ExtensionContext): boolean | undefined {
|
|
609
|
+
try {
|
|
610
|
+
const manager = ctx.sessionManager as unknown as {
|
|
611
|
+
buildSessionContext?: () => { messages?: unknown[] };
|
|
612
|
+
};
|
|
613
|
+
if (typeof manager.buildSessionContext !== "function") return undefined;
|
|
614
|
+
return (manager.buildSessionContext().messages?.length ?? 0) > 0;
|
|
615
|
+
} catch {
|
|
616
|
+
// Older pi contexts and test doubles may not expose the session reader;
|
|
617
|
+
// preserve their existing behavior rather than guessing that they are blank.
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function isBlankInitialStartup(ctx: ExtensionContext, reason: string): boolean {
|
|
623
|
+
if (reason !== "startup" && reason !== "unknown") return false;
|
|
624
|
+
return sessionHasConversation(ctx) === false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function releaseInitialSessionLoadBarrier(): void {
|
|
628
|
+
initialSessionLoadPending = false;
|
|
629
|
+
// The factory starts the heartbeat, but a future pi/context may not. This
|
|
630
|
+
// is idempotent and makes an explicit /goal resume or /loop start usable.
|
|
631
|
+
startHeartbeat();
|
|
632
|
+
}
|
|
633
|
+
|
|
593
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;
|
|
594
639
|
let ownerLive = false;
|
|
595
640
|
if (ownerSession && lastCtx) {
|
|
596
641
|
try { lastCtx.isIdle(); ownerLive = true; } catch { /* owner went stale (session replaced) */ }
|
|
@@ -927,7 +972,7 @@ function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
|
|
|
927
972
|
}
|
|
928
973
|
|
|
929
974
|
function heartbeatTick(): void {
|
|
930
|
-
if (zombieStoodDown) return; //
|
|
975
|
+
if (zombieStoodDown || initialSessionLoadPending) return; // blank startup waits for pi to bind a real session
|
|
931
976
|
const ctx = freshCtx();
|
|
932
977
|
if (!ctx) return;
|
|
933
978
|
let idle = false;
|
|
@@ -1179,9 +1224,19 @@ function clearContinuationTimer(): void {
|
|
|
1179
1224
|
}
|
|
1180
1225
|
|
|
1181
1226
|
function scheduleSessionTimeout(callback: () => void, delayMs: number): NodeJS.Timeout {
|
|
1227
|
+
const generation = sessionGeneration;
|
|
1182
1228
|
let timer: NodeJS.Timeout;
|
|
1183
1229
|
timer = setTimeout(() => {
|
|
1184
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;
|
|
1185
1240
|
callback();
|
|
1186
1241
|
}, delayMs);
|
|
1187
1242
|
sessionTimeouts.add(timer);
|
|
@@ -1191,6 +1246,8 @@ function scheduleSessionTimeout(callback: () => void, delayMs: number): NodeJS.T
|
|
|
1191
1246
|
|
|
1192
1247
|
function clearSessionOwnedTimers(): void {
|
|
1193
1248
|
sessionHandoffPending = true;
|
|
1249
|
+
sessionGeneration++;
|
|
1250
|
+
initialSessionLoadPending = false;
|
|
1194
1251
|
clearContinuationTimer();
|
|
1195
1252
|
clearLoopTimer();
|
|
1196
1253
|
if (queueStuckProbe) { clearTimeout(queueStuckProbe); queueStuckProbe = null; }
|
|
@@ -1208,7 +1265,7 @@ function isActionableGoal(): boolean {
|
|
|
1208
1265
|
}
|
|
1209
1266
|
|
|
1210
1267
|
function freshCtx(): ExtensionContext | null {
|
|
1211
|
-
if (sessionHandoffPending) return null;
|
|
1268
|
+
if (sessionHandoffPending || initialSessionLoadPending) return null;
|
|
1212
1269
|
// A captured ctx throws "stale" after session replacement. Probe cheaply;
|
|
1213
1270
|
// on stale, drop it and wait for the next event to hand us a fresh one.
|
|
1214
1271
|
if (!lastCtx) return null;
|
|
@@ -1253,7 +1310,7 @@ function armQueueStuckProbe(sentAt: number): void {
|
|
|
1253
1310
|
}
|
|
1254
1311
|
|
|
1255
1312
|
function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: number): void {
|
|
1256
|
-
if (sessionHandoffPending) return;
|
|
1313
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
1257
1314
|
abortedStandDown = false; // v0.29.5: any explicit schedule ends the stand-down
|
|
1258
1315
|
if (!isActionableGoal()) return;
|
|
1259
1316
|
rememberCtx(ctx);
|
|
@@ -1271,7 +1328,7 @@ function scheduleContinuation(ctx: ExtensionContext, force = false, delayMs?: nu
|
|
|
1271
1328
|
}
|
|
1272
1329
|
|
|
1273
1330
|
function sendContinuation(goalId: string): void {
|
|
1274
|
-
if (sessionHandoffPending) return;
|
|
1331
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
1275
1332
|
continuationTimer = null;
|
|
1276
1333
|
continuationScheduledFor = null;
|
|
1277
1334
|
if (!isActionableGoal()) return;
|
|
@@ -1321,7 +1378,7 @@ function sendContinuation(goalId: string): void {
|
|
|
1321
1378
|
// what closes the turn: complete_goal if done, pause_goal if blocked, a tool
|
|
1322
1379
|
// call otherwise. display: true — the user should see the warning too.
|
|
1323
1380
|
function sendStallEscalation(ctx: ExtensionContext, nudges: number): void {
|
|
1324
|
-
if (sessionHandoffPending || !extensionApi || extensionApiStale) return;
|
|
1381
|
+
if (sessionHandoffPending || initialSessionLoadPending || !extensionApi || extensionApiStale) return;
|
|
1325
1382
|
const remaining = HEARTBEAT_MAX_NUDGES - nudges;
|
|
1326
1383
|
const text = [
|
|
1327
1384
|
`[STALL WARNING ${nudges}/${HEARTBEAT_MAX_NUDGES}] The last turn produced no tool calls.`,
|
|
@@ -1343,7 +1400,7 @@ function sendStallEscalation(ctx: ExtensionContext, nudges: number): void {
|
|
|
1343
1400
|
// sendContinuation (stale api = terminal), independent of goal state —
|
|
1344
1401
|
// plain sessions truncate too.
|
|
1345
1402
|
function sendLengthContinue(ctx: ExtensionContext, consecutive: number): void {
|
|
1346
|
-
if (sessionHandoffPending || !extensionApi || extensionApiStale) return;
|
|
1403
|
+
if (sessionHandoffPending || initialSessionLoadPending || !extensionApi || extensionApiStale) return;
|
|
1347
1404
|
try {
|
|
1348
1405
|
extensionApi.sendMessage({
|
|
1349
1406
|
customType: GOAL_EVENT_ENTRY,
|
|
@@ -1913,6 +1970,9 @@ function listQueue(): NonNullable<State["list"]> {
|
|
|
1913
1970
|
}
|
|
1914
1971
|
|
|
1915
1972
|
function activateNextListItem(ctx: ExtensionContext, n = 1): boolean {
|
|
1973
|
+
// An explicit list activation is user consent even when pi initially
|
|
1974
|
+
// reported a blank startup context; automatic restore never reaches here.
|
|
1975
|
+
releaseInitialSessionLoadBarrier();
|
|
1916
1976
|
// v0.28.14: one-active-thing choke point — NO call site (session_start,
|
|
1917
1977
|
// completion cascade, /list next, list_activate, list-draft auto-activate)
|
|
1918
1978
|
// may activate a list item over a live loop, present or future.
|
|
@@ -2100,6 +2160,7 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2100
2160
|
// =================================================================
|
|
2101
2161
|
|
|
2102
2162
|
async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): Promise<void> {
|
|
2163
|
+
releaseInitialSessionLoadBarrier();
|
|
2103
2164
|
// v0.28.1 (S3): probe at the creation entry — no "created — starting now"
|
|
2104
2165
|
// lie in a doomed process. (The draft path's seed send has its own loud
|
|
2105
2166
|
// stale handling — E6.)
|
|
@@ -2180,6 +2241,7 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
|
2180
2241
|
}
|
|
2181
2242
|
|
|
2182
2243
|
async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
2244
|
+
releaseInitialSessionLoadBarrier();
|
|
2183
2245
|
// v0.34.3: /goal resume on an ACTIVE-but-idle goal re-kicks its
|
|
2184
2246
|
// continuation (was: silent return — the user got NOTHING while the
|
|
2185
2247
|
// widget said "active"). One-active-thing still holds: an active loop
|
|
@@ -2873,7 +2935,7 @@ function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: strin
|
|
|
2873
2935
|
}
|
|
2874
2936
|
|
|
2875
2937
|
function scheduleLoopTick(ctx: ExtensionContext): void {
|
|
2876
|
-
if (sessionHandoffPending || !isLoopActive()) return;
|
|
2938
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown || !isLoopActive()) return;
|
|
2877
2939
|
rememberCtx(ctx);
|
|
2878
2940
|
clearLoopTimer();
|
|
2879
2941
|
let delay = 0;
|
|
@@ -2886,7 +2948,7 @@ function scheduleLoopTick(ctx: ExtensionContext): void {
|
|
|
2886
2948
|
}
|
|
2887
2949
|
|
|
2888
2950
|
function sendLoopTurn(): void {
|
|
2889
|
-
if (sessionHandoffPending) return;
|
|
2951
|
+
if (sessionHandoffPending || initialSessionLoadPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
2890
2952
|
loopTimer = null;
|
|
2891
2953
|
if (!isLoopActive() || !extensionApi) return;
|
|
2892
2954
|
const ctx = freshCtx();
|
|
@@ -3255,6 +3317,7 @@ interface LoopConfig {
|
|
|
3255
3317
|
|
|
3256
3318
|
/** Shared loop-start path: /loop start AND propose_loop_draft (after Confirm). */
|
|
3257
3319
|
async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Promise<boolean> {
|
|
3320
|
+
releaseInitialSessionLoadBarrier();
|
|
3258
3321
|
// branch=1 mode: scratch branch ONLY. Refuse on non-git or dirty tree —
|
|
3259
3322
|
// we never mix uncommitted user work into the loop's branch.
|
|
3260
3323
|
let branchName: string | undefined;
|
|
@@ -3343,6 +3406,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3343
3406
|
const rest = args.trim().slice(sub.length).trim();
|
|
3344
3407
|
|
|
3345
3408
|
if (!sub || sub === "resume") {
|
|
3409
|
+
releaseInitialSessionLoadBarrier();
|
|
3346
3410
|
// /loop with no args (or /loop resume, v0.28.22) → resume a held loop
|
|
3347
3411
|
// if one is waiting; otherwise draft the loop config (metric design is
|
|
3348
3412
|
// the whole game for a long-running loop; never start one blind).
|
|
@@ -4282,6 +4346,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
4282
4346
|
const confirmedTarget = draftingTarget;
|
|
4283
4347
|
draftingTarget = null;
|
|
4284
4348
|
const full = p.objective.trim() + (normContract ? `\nDone when:\n${normContract}` : "");
|
|
4349
|
+
// The user has just confirmed this activation; release the blank-start
|
|
4350
|
+
// barrier before the direct goal path schedules its first continuation.
|
|
4351
|
+
releaseInitialSessionLoadBarrier();
|
|
4285
4352
|
// v0.28.14: one-active-thing — no goal/list activation over a live loop.
|
|
4286
4353
|
if (isLoopActive()) {
|
|
4287
4354
|
return { content: [{ type: "text", text: "A loop is active — one active thing at a time. The user must /loop stop it before a goal or list item can activate; do not re-propose until then." }], details: {} };
|
|
@@ -5488,6 +5555,7 @@ async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
|
|
|
5488
5555
|
// used to answer "Nothing to resume" — the resume path must name the
|
|
5489
5556
|
// real recovery (/reload rebuilds extensions in place), not mislead.
|
|
5490
5557
|
if (warnIfStaleAtEntry(ctx, "/glla resume")) return;
|
|
5558
|
+
releaseInitialSessionLoadBarrier();
|
|
5491
5559
|
const g = state.goal;
|
|
5492
5560
|
const goalResumable = g && g.status === "paused";
|
|
5493
5561
|
const loopResumable = state.loop && !state.loop.active && state.loop.stopReason === HELD_ON_RESTORE;
|
|
@@ -6191,6 +6259,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6191
6259
|
// 60s heartbeat notices. Re-arm it as soon as pi settles post-compact.
|
|
6192
6260
|
pi.on("session_compact", async (_event: any, ctx: ExtensionContext) => {
|
|
6193
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;
|
|
6194
6265
|
rememberCtx(ctx);
|
|
6195
6266
|
if (!isSupervising()) return;
|
|
6196
6267
|
appendLedger(ctx.cwd, "session_compact", {});
|
|
@@ -6256,6 +6327,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6256
6327
|
// v0.15.1: ask_user_question answers arrive as tool results, not chat
|
|
6257
6328
|
// messages — count answered (non-cancelled) questionnaires as replies too.
|
|
6258
6329
|
pi.on("tool_result", async (event: any) => {
|
|
6330
|
+
if (sessionHandoffPending || extensionApiStale || staleTerminalDone || zombieStoodDown) return;
|
|
6259
6331
|
noteToolResult(event); // v0.33.0: slim widget "last action" feed
|
|
6260
6332
|
// v0.24.0: roll loop tool-result fingerprints (same-tool-same-result
|
|
6261
6333
|
// detection) — recorded for ANY tool result while a loop is active.
|
|
@@ -6328,6 +6400,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6328
6400
|
if (isForeignCtx(ctx)) return;
|
|
6329
6401
|
extensionApi = pi;
|
|
6330
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;
|
|
6413
|
+
const startReason = typeof event?.reason === "string" ? event.reason : "unknown";
|
|
6414
|
+
initialSessionLoadPending = isBlankInitialStartup(ctx, startReason);
|
|
6331
6415
|
rememberCtx(ctx);
|
|
6332
6416
|
startHeartbeat();
|
|
6333
6417
|
startUITicker();
|
|
@@ -6339,11 +6423,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6339
6423
|
// confirm the new handle actually works.
|
|
6340
6424
|
writeOwnerFile(ctx.cwd);
|
|
6341
6425
|
sessionReplacementUntil = 0;
|
|
6342
|
-
zombieStoodDown = false;
|
|
6343
|
-
staleTerminalDone = false; // v0.33.1: a rebound session must be able to go terminal AGAIN (was: one-shot for the process lifetime)
|
|
6344
6426
|
postCompactResumeOwed = false; // v0.33.1: a compact from a previous session must not resync THIS one
|
|
6345
6427
|
postCompactResyncPending = false;
|
|
6346
|
-
const startReason = typeof event?.reason === "string" ? event.reason : "unknown";
|
|
6347
6428
|
appendLedger(ctx.cwd, "session_rebound", { reason: startReason });
|
|
6348
6429
|
if (extensionApiStale) {
|
|
6349
6430
|
extensionApiStale = false; // fresh ctx delivered — re-probe
|
|
@@ -6397,6 +6478,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6397
6478
|
} catch (err) {
|
|
6398
6479
|
ctx.ui.notify(`glla subagent override sync failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
6399
6480
|
}
|
|
6481
|
+
// Consume ownership markers before the startup barrier so a later loaded
|
|
6482
|
+
// session cannot mistake this placeholder runtime for a rebind.
|
|
6483
|
+
const recoveryResume = consumeRecoveryResume(ctx.cwd);
|
|
6484
|
+
const rebindResume = claimSessionOwnerAndDetectRebind(ctx.cwd);
|
|
6485
|
+
if (rebindResume) appendLedger(ctx.cwd, "rebind_resume", { pid: process.pid });
|
|
6486
|
+
const explicitRecovery = handoffResume || recoveryResume || rebindResume;
|
|
6487
|
+
if (initialSessionLoadPending && !explicitRecovery) {
|
|
6488
|
+
appendLedger(ctx.cwd, "session_waiting_for_load", { reason: startReason });
|
|
6489
|
+
ctx.ui.notify("glla: pi has not loaded a conversation yet — waiting before auto-resume. Load/resume the session, or explicitly run /goal resume or /loop start.", "info");
|
|
6490
|
+
return;
|
|
6491
|
+
}
|
|
6492
|
+
// An explicit lifecycle handoff/rebind is continuation consent even if
|
|
6493
|
+
// pi reported a blank startup context. Release the barrier before any
|
|
6494
|
+
// scheduling path below can observe it.
|
|
6495
|
+
initialSessionLoadPending = false;
|
|
6400
6496
|
// v0.29.6: stacked-state auto-arbitration FIRST — one live artifact
|
|
6401
6497
|
// survives before the restore gate decides hold-vs-resume for it.
|
|
6402
6498
|
autoArbitrateStackedState(ctx);
|
|
@@ -6453,14 +6549,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6453
6549
|
persistState(ctx);
|
|
6454
6550
|
appendLedger(ctx.cwd, "audit_loop_target_migrated", { from: "audit-every-iteration", to: "fix-first" });
|
|
6455
6551
|
}
|
|
6456
|
-
// v0.34.15 compatibility:
|
|
6457
|
-
//
|
|
6458
|
-
const recoveryResume = consumeRecoveryResume(ctx.cwd);
|
|
6459
|
-
// v0.34.16: a same-process lifecycle handoff is explicit continuation
|
|
6460
|
-
// debt, so it resumes independently of the cold-boot autoResume setting.
|
|
6461
|
-
// A same-pid owner rebind is the second same-process signal.
|
|
6462
|
-
const rebindResume = claimSessionOwnerAndDetectRebind(ctx.cwd);
|
|
6463
|
-
if (rebindResume) appendLedger(ctx.cwd, "rebind_resume", { pid: process.pid });
|
|
6552
|
+
// v0.34.15 compatibility: the legacy recovery marker and v0.34.16
|
|
6553
|
+
// rebind marker were consumed before the startup barrier above.
|
|
6464
6554
|
if (isLoopActive()) {
|
|
6465
6555
|
const l = state.loop!;
|
|
6466
6556
|
if (autoResume || recoveryResume || rebindResume || handoffResume) {
|
|
@@ -6552,6 +6642,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6552
6642
|
});
|
|
6553
6643
|
|
|
6554
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;
|
|
6555
6649
|
rememberCtx(ctx);
|
|
6556
6650
|
// v0.23.8: a subagent finishing must not drive the main session's
|
|
6557
6651
|
// continuation loop.
|
|
@@ -6572,7 +6666,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
6572
6666
|
const rawPriorA = assistants.length >= 2 ? assistants[assistants.length - 2] : null;
|
|
6573
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") : "";
|
|
6574
6668
|
const lastA = rawLastA ? { stopReason: rawLastA.stopReason, text: extractText(rawLastA), priorText: extractText(rawPriorA) } : null;
|
|
6575
|
-
|
|
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
|
+
}
|
|
6576
6693
|
if (lc.giveUpNow) {
|
|
6577
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");
|
|
6578
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",
|