pi-goal-list-loop-audit 0.38.5 → 0.38.7
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/CHANGELOG.md +16 -0
- package/docs/INDEX.md +1 -1
- package/extensions/goal-commands.ts +7 -3
- package/extensions/goal-continuation.ts +12 -0
- package/extensions/goal-loop-display.ts +69 -2
- package/extensions/loops/goal-activation.ts +48 -2
- package/extensions/loops/goal-runtime-globals.ts +15 -0
- package/extensions/loops/goal-ui.ts +48 -1
- package/extensions/loops/goal.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.7 — session visibility: recovery banner + durable verdict tally (2026-09-03)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
Load-hold recovery banner: a consent-less cold load that engages the load hold now also paints objective + next pending task + verdict tally + resume command (`/goal`/`/list`/`/loop` by policy), all from durable disk state — the transcript is empty in exactly the sessions that need this. Fires once with the fresh hold; ledger `load_hold_recovery_banner`. Durable verdict tally (`auditorVerdictTally` via `auditVerdictLabel`): disapproval count + last-verdict age on the auditing status-line footer and the `/goal status` `Audits:` line, silent when history is empty — a reloaded session answers "are we progressing?" from stored verdicts.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
Reload invisibility: resumed-but-empty sessions no longer look goal-less, and capped/queued auditor sessions show stored-verdict evidence instead of a dead surface. Hold mechanics, hold text, auditor phase machine, and auditor card untouched. `tests/session-visibility.test.ts` (5 tests) pins tally classification, banner text, status-line tally, and a behavioral reload.
|
|
10
|
+
|
|
11
|
+
## 0.38.6 — over-cap starvation ladder: compact first, then 5 ordered recoveries (2026-09-03)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
Compact-first nudge: at 85% context (below the 90% starvation line) a one-shot-per-episode notify to run `/compact` while summarization still fits (episode resets below 80% or on compaction; ledger `context_compact_first_nudge`). The agent_end starvation yield notify now carries the full over-cap ladder — `/compact` retry after GLLA's deterministic trim, larger-context model (GLLA already auto-rotates its fallback chain after a failed compact-and-retry, v0.34.116), `/new` + `/goal resume` from durable disk state with no summarization needed — and states that automatic turns stay parked.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
Over-cap spin: `sendContinuation` now refuses while `isContextStarvedRefused()` — the single choke point every automatic path funnels through — so a 119–243% session stops queueing truncating turns (ledger `continuation_send_refused_context_starved`, silent; heartbeat one-shot + yield ladder own messaging). The refuse stays sticky while the last-known percent is ≥90% instead of lapsing 90s after the last yield. Heartbeat refuse branch/ledger/text unchanged (pinned). `tests/starvation-ladder.test.ts` (6 tests) pins band, ladder text, nudge episodes, sticky matrix, wiring, and a behavioral starved boot.
|
|
18
|
+
|
|
3
19
|
## 0.38.5 — delta-only goal continuation: marker-only steady-state (2026-09-03)
|
|
4
20
|
|
|
5
21
|
### Fixed
|
package/docs/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ For shipped docs, the relevant entry points are:
|
|
|
18
18
|
failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
|
|
19
19
|
handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
|
|
20
20
|
shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
|
|
21
|
-
v0.35.14–v0.38.
|
|
21
|
+
v0.35.14–v0.38.7 continue through the supervisor freeze (`/glla pause`),
|
|
22
22
|
load hold, auditor picker parity, Windows launch fix, zombie-watchdog
|
|
23
23
|
subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
|
|
24
24
|
durable state-root selection, blank-until-resume auditor context, frozen
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "./goal-loop-core.js";
|
|
24
24
|
import { clearDispatchRecord, dispatchRecordExists } from "./goal-loop-dispatch.js";
|
|
25
25
|
import type { AuditDisplayProgress } from "./goal-loop-display.js";
|
|
26
|
-
import { fmtElapsed } from "./goal-loop-display.js";
|
|
26
|
+
import { auditorVerdictTally, fmtElapsed, formatVerdictTallySegment } from "./goal-loop-display.js";
|
|
27
27
|
import { AUDIT_FINDINGS_REL, HELD_ON_RESTORE, LOOP_AUDIT_MARKER, listAuditCollectTarget, projectAuditTarget } from "./goal-loop-forever.js";
|
|
28
28
|
import { buildLoopCompletionSummary, compactCompletionSummary, compactTerminalCompletionSummary } from "./completion-summary.js";
|
|
29
29
|
import { ProjectRollup, discoverGllaProjects, filterPremature, formatRollupJson, formatRollupTable, rollupProject } from "./goal-loop-stats.js";
|
|
@@ -381,8 +381,12 @@ async function cmdStatus(ctx: ExtensionContext): Promise<void> {
|
|
|
381
381
|
`Tokens: ${(g.usage?.tokensUsed ?? 0).toLocaleString()}${(g.usage?.tokensLimit ?? 0) > 0 ? ` / ${(g.usage!.tokensLimit).toLocaleString()}` : " (no cap — set Token limit in /glla settings)"}`,
|
|
382
382
|
...formatMainModelRecoveryStatus(state.mainModelRecovery, normalizeMainModelFallbackRefs(loadSettings(ctx.cwd).mainModelFallbacks)),
|
|
383
383
|
];
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
// v0.38.7: /goal status names disapprovals + last-verdict age, not just
|
|
385
|
+
// the approval count — a capped/queued session must show what unblocks.
|
|
386
|
+
const statusTally = auditorVerdictTally(g.auditHistory);
|
|
387
|
+
if (statusTally.total > 0) {
|
|
388
|
+
const tallyText = formatVerdictTallySegment(statusTally);
|
|
389
|
+
lines.push(`Audits: ${tallyText} (${statusTally.approvals} approved)`);
|
|
386
390
|
}
|
|
387
391
|
if (g.status === "auditing") {
|
|
388
392
|
lines.push(`Completion audit: ${isCompletionAuditRecoveryPending(g) ? `recovery pending — ${activeGoalSurfaceCommand("resume")} retries the stored claim` : flags.completionAuditInFlight && flags.latestAuditProgress?.label === "queued" ? "detached auditor queued" : flags.completionAuditInFlight ? "detached auditor running" : "awaiting lifecycle recovery"}`);
|
|
@@ -146,6 +146,7 @@ export interface ContinuationDeps {
|
|
|
146
146
|
isForeignCtx(ctx: ExtensionContext): boolean;
|
|
147
147
|
sessionManagerId(ctx: ExtensionContext): string;
|
|
148
148
|
isActionableGoal(): boolean;
|
|
149
|
+
isContextStarvedRefused(): boolean;
|
|
149
150
|
isSupervising(): boolean;
|
|
150
151
|
goalNoun(): string;
|
|
151
152
|
activeGoalSurfaceCommand(command: string): string;
|
|
@@ -170,6 +171,7 @@ let goStaleTerminal: ContinuationDeps["goStaleTerminal"];
|
|
|
170
171
|
let isForeignCtx: ContinuationDeps["isForeignCtx"];
|
|
171
172
|
let sessionManagerId: ContinuationDeps["sessionManagerId"];
|
|
172
173
|
let isActionableGoal: ContinuationDeps["isActionableGoal"];
|
|
174
|
+
let isContextStarvedRefused: ContinuationDeps["isContextStarvedRefused"];
|
|
173
175
|
let isSupervising: ContinuationDeps["isSupervising"];
|
|
174
176
|
let goalNoun: ContinuationDeps["goalNoun"];
|
|
175
177
|
let activeGoalSurfaceCommand: ContinuationDeps["activeGoalSurfaceCommand"];
|
|
@@ -194,6 +196,7 @@ export function createGoalContinuation(flagsArg: ContinuationFlags, d: Continuat
|
|
|
194
196
|
isForeignCtx = d.isForeignCtx;
|
|
195
197
|
sessionManagerId = d.sessionManagerId;
|
|
196
198
|
isActionableGoal = d.isActionableGoal;
|
|
199
|
+
isContextStarvedRefused = d.isContextStarvedRefused;
|
|
197
200
|
isSupervising = d.isSupervising;
|
|
198
201
|
goalNoun = d.goalNoun;
|
|
199
202
|
activeGoalSurfaceCommand = d.activeGoalSurfaceCommand;
|
|
@@ -1140,6 +1143,15 @@ export function sendContinuation(goalId: string): void {
|
|
|
1140
1143
|
continuationTimer = scheduleSessionTimeout(() => sendContinuation(goalId), BACKOFF_IDLE_RETRY_MS);
|
|
1141
1144
|
return;
|
|
1142
1145
|
}
|
|
1146
|
+
// v0.38.6: the starvation choke point — EVERY automatic send path funnels
|
|
1147
|
+
// through here (agent_end, heartbeat rearm, loop tick, recovery). While
|
|
1148
|
+
// the session is starved with no compaction, no turn can land, so refuse
|
|
1149
|
+
// SILENTLY (ledger only): the heartbeat one-shot + agent_end yield ladder
|
|
1150
|
+
// own user messaging, and a refused send must not rearm a timer spin.
|
|
1151
|
+
if (isContextStarvedRefused()) {
|
|
1152
|
+
appendLedger(ctx.cwd, "continuation_send_refused_context_starved", { goalId, generation: flags.sessionGeneration });
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1143
1155
|
if (!guardGoalBeforeContinuation(ctx, "dispatch", goalId)) return;
|
|
1144
1156
|
if (!isActionableGoal()) return;
|
|
1145
1157
|
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { truncateToWidth as tuiTruncateToWidth, visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
15
|
import type { DurableDeferRecommendationInput, Goal, MainModelRecovery, State } from "./goal-loop-core.js";
|
|
16
|
-
import { buildDurableDeferRecommendation, compactDisplayText, formatMainModelRecoveryStatus, isMonitorGoal, isPersistenceDegraded, lastPersistenceFailure, sanitizeDisplayText, sanitizeProviderAuditReport, sanitizeProviderDisplayText, stripThinkBlocks } from "./goal-loop-core.js";
|
|
16
|
+
import { auditVerdictLabel, buildDurableDeferRecommendation, compactDisplayText, formatMainModelRecoveryStatus, isMonitorGoal, isPersistenceDegraded, lastPersistenceFailure, sanitizeDisplayText, sanitizeProviderAuditReport, sanitizeProviderDisplayText, stripThinkBlocks } from "./goal-loop-core.js";
|
|
17
17
|
|
|
18
18
|
export { isMonitorGoal };
|
|
19
19
|
import { HELD_ON_RESTORE, type LoopState } from "./goal-loop-forever.js";
|
|
@@ -432,6 +432,68 @@ function latestAuditFeedback(g: Goal): LatestAuditFeedback | undefined {
|
|
|
432
432
|
};
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/** v0.38.7 (note.md Next: reload recovery + progress signal): durable
|
|
436
|
+
* verdict tally — disapproval count + last-verdict age from auditHistory.
|
|
437
|
+
* After a reload the in-memory auditor progress is gone; the stored
|
|
438
|
+
* verdicts are what answers "are we progressing?". Classification goes
|
|
439
|
+
* through auditVerdictLabel so a shield-blocked approval is never counted
|
|
440
|
+
* as a disapproval and an infra error is never counted as a verdict. */
|
|
441
|
+
export interface AuditorVerdictTally {
|
|
442
|
+
total: number;
|
|
443
|
+
approvals: number;
|
|
444
|
+
disapprovals: number;
|
|
445
|
+
lastAt: number | null;
|
|
446
|
+
lastLabel: string | null;
|
|
447
|
+
}
|
|
448
|
+
export function auditorVerdictTally(history: Goal["auditHistory"], now = Date.now()): AuditorVerdictTally {
|
|
449
|
+
void now;
|
|
450
|
+
const entries = Array.isArray(history) ? history : [];
|
|
451
|
+
let approvals = 0;
|
|
452
|
+
let disapprovals = 0;
|
|
453
|
+
for (const v of entries) {
|
|
454
|
+
const label = auditVerdictLabel(v);
|
|
455
|
+
if (label === "approved") approvals++;
|
|
456
|
+
else if (label === "disapproved") disapprovals++;
|
|
457
|
+
}
|
|
458
|
+
const last = entries[entries.length - 1];
|
|
459
|
+
const lastMs = last ? Date.parse(last.at) : Number.NaN;
|
|
460
|
+
return {
|
|
461
|
+
total: entries.length,
|
|
462
|
+
approvals,
|
|
463
|
+
disapprovals,
|
|
464
|
+
lastAt: last && Number.isFinite(lastMs) ? lastMs : null,
|
|
465
|
+
lastLabel: last ? auditVerdictLabel(last) : null,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/** Compact tally segment for the always-on surfaces. "" when no verdicts. */
|
|
469
|
+
export function formatVerdictTallySegment(t: AuditorVerdictTally, now = Date.now()): string {
|
|
470
|
+
if (t.total <= 0) return "";
|
|
471
|
+
const dis = t.disapprovals > 0 ? ` · ${t.disapprovals} disapproved` : "";
|
|
472
|
+
const age = t.lastAt !== null && t.lastLabel ? ` · last ${t.lastLabel} ${fmtElapsed(now - t.lastAt)} ago` : "";
|
|
473
|
+
return `${t.total} verdict${t.total === 1 ? "" : "s"}${dis}${age}`;
|
|
474
|
+
}
|
|
475
|
+
/** v0.38.7: the load-hold recovery banner — objective + next task + verdict
|
|
476
|
+
* tally + resume command, all from durable disk state (never transcript
|
|
477
|
+
* memory, which is empty in exactly the sessions that need this). */
|
|
478
|
+
export interface LoadHoldRecoverySummary {
|
|
479
|
+
objective?: string | null;
|
|
480
|
+
status?: string;
|
|
481
|
+
nextTask?: string | null;
|
|
482
|
+
tally: AuditorVerdictTally;
|
|
483
|
+
resumeCommand: string;
|
|
484
|
+
listWaiting?: number;
|
|
485
|
+
}
|
|
486
|
+
export function buildLoadHoldRecoveryLines(s: LoadHoldRecoverySummary, now = Date.now()): string[] {
|
|
487
|
+
const lines = [
|
|
488
|
+
`glla: recovered from disk — "${truncate((s.objective ?? "").trim() || "(no objective recorded)", 120)}" (${s.status ?? "held"})`,
|
|
489
|
+
];
|
|
490
|
+
lines.push(s.nextTask ? `next: ${truncate(s.nextTask, 100)}` : `next: no pending tasks recorded`);
|
|
491
|
+
const tally = formatVerdictTallySegment(s.tally, now);
|
|
492
|
+
lines.push(tally ? `audits: ${tally}` : `audits: none yet`);
|
|
493
|
+
if ((s.listWaiting ?? 0) > 0) lines.push(`list: ${s.listWaiting} waiting — /list to manage`);
|
|
494
|
+
lines.push(`run ${s.resumeCommand} to continue`);
|
|
495
|
+
return lines;
|
|
496
|
+
}
|
|
435
497
|
function activeAttention(g: Goal): ActiveAttention | undefined {
|
|
436
498
|
if (g.status !== "active" || !g.pauseReason) return undefined;
|
|
437
499
|
if (/regression shield/i.test(g.pauseReason)) {
|
|
@@ -954,7 +1016,12 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
954
1016
|
const quietSuffix = quietAge !== undefined ? ` · silent ${fmtElapsed(quietAge)}` : "";
|
|
955
1017
|
const next = ` · next: ${auditorNextTransition(phase)}`;
|
|
956
1018
|
const detachedSuffix = live ? "" : " · detached worker";
|
|
957
|
-
|
|
1019
|
+
// v0.38.7: durable verdict tally on the always-on footer — after a
|
|
1020
|
+
// reload there is no live auditor evidence, so the stored verdicts
|
|
1021
|
+
// (disapproval count + last-verdict age) answer "are we progressing?".
|
|
1022
|
+
const tallyText = formatVerdictTallySegment(auditorVerdictTally(g.auditHistory, now), now);
|
|
1023
|
+
const verdictSuffix = tallyText ? ` · ${tallyText}` : "";
|
|
1024
|
+
return `glla: ${host} · ${label}${quietSuffix}${next}${detachedSuffix}${verdictSuffix}${heldSuffix}`;
|
|
958
1025
|
}
|
|
959
1026
|
if (g.status === "paused") {
|
|
960
1027
|
// v0.28.22: the status line names the ACTIONABILITY, not the reason —
|
|
@@ -238,7 +238,7 @@ import {
|
|
|
238
238
|
textFingerprint,
|
|
239
239
|
pushCapped as pushRepetitionCapped,
|
|
240
240
|
} from "../goal-loop-repetition.js";
|
|
241
|
-
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
241
|
+
import { auditorVerdictTally, buildLoadHoldRecoveryLines, buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
242
242
|
import { compactLoopCompletionSummary } from "../completion-summary.js";
|
|
243
243
|
import {
|
|
244
244
|
defaultAgentDir,
|
|
@@ -1864,6 +1864,34 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1864
1864
|
"Loaded without starting: your goal/list/loop state is restored and shown below, but automation is HELD for your decision. /goal resume, /list resume, or /list next starts work; enable Auto-resume in /glla settings to restore load-time automation.",
|
|
1865
1865
|
"warning",
|
|
1866
1866
|
);
|
|
1867
|
+
// v0.38.7 (note.md Next: objectives seemingly lost on reload) —
|
|
1868
|
+
// the recovery banner: objective + next task + verdict tally +
|
|
1869
|
+
// resume command, all from durable disk state. The transcript is
|
|
1870
|
+
// empty in exactly the sessions that need this, so nothing here
|
|
1871
|
+
// may depend on in-memory progress. Fires with the fresh hold
|
|
1872
|
+
// (guarded above), never as a timer re-arm.
|
|
1873
|
+
const tasks = state.goal?.taskList?.tasks ?? [];
|
|
1874
|
+
const pendingTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
1875
|
+
const tally = auditorVerdictTally(state.goal?.auditHistory);
|
|
1876
|
+
const resumeCommand = state.goal
|
|
1877
|
+
? (state.goal.policy === "list" ? "/list resume" : "/goal resume")
|
|
1878
|
+
: (state.list?.length ?? 0) > 0 ? "/list resume" : state.loop ? "/loop resume" : "/goal resume";
|
|
1879
|
+
const banner = buildLoadHoldRecoveryLines({
|
|
1880
|
+
objective: state.goal?.objective ?? ((state.list?.length ?? 0) > 0 ? `${state.list!.length} queued list items` : null),
|
|
1881
|
+
status: state.goal?.status ?? (state.loop ? "loop" : "held"),
|
|
1882
|
+
nextTask: pendingTasks[0]?.title ?? null,
|
|
1883
|
+
tally,
|
|
1884
|
+
resumeCommand,
|
|
1885
|
+
listWaiting: state.goal?.policy === "list" ? undefined : state.list?.length ?? 0,
|
|
1886
|
+
});
|
|
1887
|
+
appendLedger(ctx.cwd, "load_hold_recovery_banner", {
|
|
1888
|
+
goalId: state.goal?.id ?? null,
|
|
1889
|
+
status: state.goal?.status ?? null,
|
|
1890
|
+
pendingTasks: pendingTasks.length,
|
|
1891
|
+
totalVerdicts: tally.total,
|
|
1892
|
+
disapprovals: tally.disapprovals,
|
|
1893
|
+
});
|
|
1894
|
+
ctx.ui.notify(banner.join("\n"), "info");
|
|
1867
1895
|
}
|
|
1868
1896
|
} else if ((autoResume || explicitRecovery) && typeof state.loadHoldAt === "number") {
|
|
1869
1897
|
// v0.35.28 (issue #16): a hold persisted by a PREVIOUS process must
|
|
@@ -1980,7 +2008,11 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
1980
2008
|
return;
|
|
1981
2009
|
}
|
|
1982
2010
|
}
|
|
1983
|
-
|
|
2011
|
+
// v0.38.6: the yield now carries the full over-cap ladder (retry
|
|
2012
|
+
// /compact after GLLA's deterministic trim, larger-context model,
|
|
2013
|
+
// /new + resume from durable state) instead of only the auto-compact
|
|
2014
|
+
// pointer — field 2026-09-03: summarization itself was failing.
|
|
2015
|
+
ctx.ui.notify(buildStarvationLadderMessage({ percent: typeof contextUsage?.percent === "number" ? contextUsage.percent : null, streak: starved.streak }), "info");
|
|
1984
2016
|
return;
|
|
1985
2017
|
}
|
|
1986
2018
|
if (lc.giveUpNow) {
|
|
@@ -2187,6 +2219,20 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
2187
2219
|
if (!state.goal) return;
|
|
2188
2220
|
if (state.goal.status !== "active") return;
|
|
2189
2221
|
clearContinuationTimer();
|
|
2222
|
+
// v0.38.6: feed the sticky refuse gate + compact-first nudge with the
|
|
2223
|
+
// live percent. Placed AFTER the goal gate: the length-continue order
|
|
2224
|
+
// pin requires the length path first, and starved sessions already
|
|
2225
|
+
// returned via the yield ladder above. Recording null clears a stale
|
|
2226
|
+
// reading (host estimate gone) so the refuse can lapse instead of
|
|
2227
|
+
// sticking forever.
|
|
2228
|
+
noteContextPercent(typeof contextUsage?.percent === "number" ? contextUsage.percent : null);
|
|
2229
|
+
if (shouldCompactFirstNudge(contextUsage?.percent)) {
|
|
2230
|
+
appendLedger(ctx.cwd, "context_compact_first_nudge", {
|
|
2231
|
+
goalId: state.goal?.id ?? null,
|
|
2232
|
+
contextPercent: contextUsage?.percent ?? null,
|
|
2233
|
+
});
|
|
2234
|
+
ctx.ui.notify(`glla: context at ${contextUsage!.percent!.toFixed(1)}% — run /compact now while summarization still fits. Below 90% the compact succeeds; past it you get the over-cap ladder (retry /compact, larger model, or /new + resume).`, "info");
|
|
2235
|
+
}
|
|
2190
2236
|
|
|
2191
2237
|
const last = [...(event.messages as any[])].reverse().find((m) => m.role === "assistant");
|
|
2192
2238
|
const text = last && Array.isArray(last.content) ? last.content.filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n") : "";
|
|
@@ -150,6 +150,11 @@ export const GOAL_RUNTIME_GLOBAL_NAMES = [
|
|
|
150
150
|
"contextStarvedStreak",
|
|
151
151
|
"lastContextStarvedAt",
|
|
152
152
|
"noteContextStarvedYield",
|
|
153
|
+
"noteContextPercent",
|
|
154
|
+
"shouldCompactFirstNudge",
|
|
155
|
+
"buildStarvationLadderMessage",
|
|
156
|
+
"COMPACT_FIRST_NUDGE_PERCENT",
|
|
157
|
+
"lastContextPercent",
|
|
153
158
|
"clearContextStarvedStreak",
|
|
154
159
|
"onCompactionLanded",
|
|
155
160
|
"isContextStarvedRefused",
|
|
@@ -310,6 +315,11 @@ interface GoalRuntimeDataTypes {
|
|
|
310
315
|
CONTEXT_STARVATION_RECENT_WINDOW_MS: number;
|
|
311
316
|
contextStarvedStreak: number;
|
|
312
317
|
lastContextStarvedAt: number;
|
|
318
|
+
noteContextPercent: (pct: number | null | undefined) => void;
|
|
319
|
+
shouldCompactFirstNudge: (percent: number | null | undefined) => boolean;
|
|
320
|
+
buildStarvationLadderMessage: (input?: { percent?: number | null; streak?: number }) => string;
|
|
321
|
+
COMPACT_FIRST_NUDGE_PERCENT: number;
|
|
322
|
+
lastContextPercent: number | null;
|
|
313
323
|
postCompactResumeOwed: boolean;
|
|
314
324
|
postCompactResyncPending: boolean;
|
|
315
325
|
COMPACTION_GRACE_MS: number;
|
|
@@ -487,6 +497,11 @@ declare global {
|
|
|
487
497
|
var contextStarvedStreak: GoalRuntimeGlobals["contextStarvedStreak"];
|
|
488
498
|
var lastContextStarvedAt: GoalRuntimeGlobals["lastContextStarvedAt"];
|
|
489
499
|
var noteContextStarvedYield: GoalRuntimeGlobals["noteContextStarvedYield"];
|
|
500
|
+
var noteContextPercent: GoalRuntimeGlobals["noteContextPercent"];
|
|
501
|
+
var shouldCompactFirstNudge: GoalRuntimeGlobals["shouldCompactFirstNudge"];
|
|
502
|
+
var buildStarvationLadderMessage: GoalRuntimeGlobals["buildStarvationLadderMessage"];
|
|
503
|
+
var COMPACT_FIRST_NUDGE_PERCENT: GoalRuntimeGlobals["COMPACT_FIRST_NUDGE_PERCENT"];
|
|
504
|
+
var lastContextPercent: GoalRuntimeGlobals["lastContextPercent"];
|
|
490
505
|
var clearContextStarvedStreak: GoalRuntimeGlobals["clearContextStarvedStreak"];
|
|
491
506
|
var onCompactionLanded: GoalRuntimeGlobals["onCompactionLanded"];
|
|
492
507
|
var isContextStarvedRefused: GoalRuntimeGlobals["isContextStarvedRefused"];
|
|
@@ -171,6 +171,7 @@ import {
|
|
|
171
171
|
} from "../goal-continuation.js";
|
|
172
172
|
export { __testOnlySetContinuationStartTimeout, __testOnlySetContinuationRetryBackoff } from "../goal-continuation.js";
|
|
173
173
|
import {
|
|
174
|
+
LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT,
|
|
174
175
|
LENGTH_CONTINUE_MAX,
|
|
175
176
|
LENGTH_CONTINUE_TEXT,
|
|
176
177
|
isContextStarvedLengthStop,
|
|
@@ -963,8 +964,45 @@ let lastCompactionAt = 0;
|
|
|
963
964
|
// immediately clears the streak.
|
|
964
965
|
const CONTEXT_STARVATION_REFUSE_THRESHOLD = 2;
|
|
965
966
|
const CONTEXT_STARVATION_RECENT_WINDOW_MS = 90_000;
|
|
967
|
+
// v0.38.6: compact-first band — heavy but still compactable. One proactive
|
|
968
|
+
// nudge per episode here avoids the over-cap ladder entirely (summarization
|
|
969
|
+
// still fits below the 90% starvation line).
|
|
970
|
+
export const COMPACT_FIRST_NUDGE_PERCENT = 85;
|
|
971
|
+
export const COMPACT_FIRST_RESET_PERCENT = 80;
|
|
966
972
|
let contextStarvedStreak = 0;
|
|
967
973
|
let lastContextStarvedAt = 0;
|
|
974
|
+
// v0.38.6: last-known context percent (sampled at agent_end). Keeps the
|
|
975
|
+
// refuse sticky while the session is physically over cap even after the 90s
|
|
976
|
+
// recency window lapses — a slow-truncating 120%+ session must not re-arm
|
|
977
|
+
// the spin (field 2026-09-03: 119–243% truncation loops).
|
|
978
|
+
let lastContextPercent: number | null = null;
|
|
979
|
+
let compactFirstNudged = false;
|
|
980
|
+
export function noteContextPercent(pct: number | null | undefined): void {
|
|
981
|
+
lastContextPercent = typeof pct === "number" && Number.isFinite(pct) ? pct : null;
|
|
982
|
+
}
|
|
983
|
+
/** v0.38.6: compact-first decision — pure. True exactly once per episode
|
|
984
|
+
* while percent sits at/above the nudge band; resets below 80% or on
|
|
985
|
+
* compaction. Returns false for missing/non-finite input. */
|
|
986
|
+
export function shouldCompactFirstNudge(percent: number | null | undefined): boolean {
|
|
987
|
+
if (typeof percent !== "number" || !Number.isFinite(percent)) return false;
|
|
988
|
+
if (percent < COMPACT_FIRST_RESET_PERCENT) { compactFirstNudged = false; return false; }
|
|
989
|
+
if (percent < COMPACT_FIRST_NUDGE_PERCENT || compactFirstNudged) return false;
|
|
990
|
+
compactFirstNudged = true;
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
/** v0.38.6: the over-cap ladder, user-facing. Three recoveries in order
|
|
994
|
+
* (retry /compact after GLLA's deterministic trim, larger-context model,
|
|
995
|
+
* /new + resume from durable state) plus the no-LLM backstop: the
|
|
996
|
+
* post-compact resync re-anchors a fresh session with no summarization. */
|
|
997
|
+
export function buildStarvationLadderMessage(input: { percent?: number | null; streak?: number } = {}): string {
|
|
998
|
+
const at = typeof input.percent === "number" && Number.isFinite(input.percent)
|
|
999
|
+
? `${input.percent.toFixed(1)}%` : "nearly full";
|
|
1000
|
+
return `glla: output-token stop was context starvation (tiny output at ${at} context) — yielding to pi auto-compaction instead of re-sending. ` +
|
|
1001
|
+
`If compaction fails or context is already over cap, in order: (1) run /compact again — GLLA already trimmed repeat payloads to a checkpoint, so the retry may now fit; ` +
|
|
1002
|
+
`(2) switch to a larger-context model, then /compact (GLLA auto-rotates its fallback chain after a failed compact-and-retry); ` +
|
|
1003
|
+
`(3) /new, then /goal resume — the goal, tasks, ledger and audits are durable on disk, and the post-compact resync re-anchors the fresh session with no summarization needed. ` +
|
|
1004
|
+
`Automatic turns stay parked until a real compaction lands.`;
|
|
1005
|
+
}
|
|
968
1006
|
function noteContextStarvedYield(): { streak: number; shouldRefuse: boolean } {
|
|
969
1007
|
const now = Date.now();
|
|
970
1008
|
if (now - lastContextStarvedAt > CONTEXT_STARVATION_RECENT_WINDOW_MS) {
|
|
@@ -981,6 +1019,7 @@ function clearContextStarvedStreak(): void {
|
|
|
981
1019
|
/** Public: a session_compact landed — clear the starvation refuse gate. */
|
|
982
1020
|
function onCompactionLanded(): void {
|
|
983
1021
|
clearContextStarvedStreak();
|
|
1022
|
+
compactFirstNudged = false; // v0.38.6: fresh episode after real room
|
|
984
1023
|
}
|
|
985
1024
|
/** Public: has the yield path declared compaction absent? The heartbeat
|
|
986
1025
|
* consults this to refuse to schedule a new continuation while the
|
|
@@ -988,7 +1027,10 @@ function onCompactionLanded(): void {
|
|
|
988
1027
|
function isContextStarvedRefused(): boolean {
|
|
989
1028
|
if (contextStarvedStreak < CONTEXT_STARVATION_REFUSE_THRESHOLD) return false;
|
|
990
1029
|
if (lastCompactionAt > lastContextStarvedAt) return false; // a compact happened after the streak — fresh slate
|
|
991
|
-
|
|
1030
|
+
if (Date.now() - lastContextStarvedAt <= CONTEXT_STARVATION_RECENT_WINDOW_MS) return true;
|
|
1031
|
+
// v0.38.6: sticky while physically over cap — recency lapse must not
|
|
1032
|
+
// re-arm the spin against a session that never got room (field: 243%).
|
|
1033
|
+
return lastContextPercent !== null && lastContextPercent >= LENGTH_CONTINUE_CONTEXT_STARVED_PERCENT;
|
|
992
1034
|
}
|
|
993
1035
|
/** Test-only: simulate a compaction event at a controlled time without firing
|
|
994
1036
|
* the full session_compact plumbing. Pass null to clear. */
|
|
@@ -1132,6 +1174,11 @@ defineGoalRuntimeGlobal("CONTEXT_STARVATION_RECENT_WINDOW_MS", { get: () => CONT
|
|
|
1132
1174
|
defineGoalRuntimeGlobal("contextStarvedStreak", { get: () => contextStarvedStreak, set: (v) => { contextStarvedStreak = v as any; } });
|
|
1133
1175
|
defineGoalRuntimeGlobal("lastContextStarvedAt", { get: () => lastContextStarvedAt, set: (v) => { lastContextStarvedAt = v as any; } });
|
|
1134
1176
|
defineGoalRuntimeGlobal("noteContextStarvedYield", { get: () => noteContextStarvedYield });
|
|
1177
|
+
defineGoalRuntimeGlobal("noteContextPercent", { get: () => noteContextPercent });
|
|
1178
|
+
defineGoalRuntimeGlobal("shouldCompactFirstNudge", { get: () => shouldCompactFirstNudge });
|
|
1179
|
+
defineGoalRuntimeGlobal("buildStarvationLadderMessage", { get: () => buildStarvationLadderMessage });
|
|
1180
|
+
defineGoalRuntimeGlobal("COMPACT_FIRST_NUDGE_PERCENT", { get: () => COMPACT_FIRST_NUDGE_PERCENT });
|
|
1181
|
+
defineGoalRuntimeGlobal("lastContextPercent", { get: () => lastContextPercent, set: (v) => { lastContextPercent = v as any; } });
|
|
1135
1182
|
defineGoalRuntimeGlobal("clearContextStarvedStreak", { get: () => clearContextStarvedStreak });
|
|
1136
1183
|
defineGoalRuntimeGlobal("onCompactionLanded", { get: () => onCompactionLanded });
|
|
1137
1184
|
defineGoalRuntimeGlobal("isContextStarvedRefused", { get: () => isContextStarvedRefused });
|
package/extensions/loops/goal.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.7",
|
|
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. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "dracon",
|