pi-goal-list-loop-audit 0.26.5 → 0.26.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/extensions/goal-loop-core.ts +15 -6
- package/extensions/loops/goal.ts +61 -24
- package/package.json +1 -1
|
@@ -917,6 +917,8 @@ export function isAutoCommitPaused(cwd: string): boolean {
|
|
|
917
917
|
/** Suppress the heartbeat when work shipped very recently — a session
|
|
918
918
|
* that just committed is transitioning, not stalled. Pure; the tick
|
|
919
919
|
* gathers the timestamps. */
|
|
920
|
+
/** @deprecated v0.26.6: no longer called by the heartbeat (self-sustaining
|
|
921
|
+
* under ledger writes / auto-commit daemons). Kept for API compatibility. */
|
|
920
922
|
export function shouldSuppressHeartbeatForRecentShip(args: {
|
|
921
923
|
nowMs: number;
|
|
922
924
|
lastShippedAtMs: number | null;
|
|
@@ -929,7 +931,20 @@ export function shouldSuppressHeartbeatForRecentShip(args: {
|
|
|
929
931
|
|
|
930
932
|
/** Best-effort "when did work last ship" for a repo: newest of the HEAD
|
|
931
933
|
* commit time and the .pi-glla state file mtime. Null when unknown. */
|
|
934
|
+
/** v0.26.7: pi's exact stale-runtime error signature — thrown by every
|
|
935
|
+
* runtime-bound method after pi invalidates the extension on session
|
|
936
|
+
* replacement (newSession/fork/switchSession/reload; compaction reaches
|
|
937
|
+
* the same teardown in pi 0.82.x). See dist/core/extensions/loader.js
|
|
938
|
+
* createExtensionRuntime().invalidate. */
|
|
939
|
+
export function isStaleApiError(err: unknown): boolean {
|
|
940
|
+
return err instanceof Error && err.message.includes("stale after session replacement");
|
|
941
|
+
}
|
|
942
|
+
|
|
932
943
|
export function lastShippedAtMs(cwd: string): number | null {
|
|
944
|
+
// v0.26.6: the .pi-glla/active.jsonl MTIME term was REMOVED — the
|
|
945
|
+
// heartbeat's own ledger writes refreshed it every 15s, which made the
|
|
946
|
+
// 0.25.0 ship-suppression self-sustaining (darklord: 9.1h / 2,184
|
|
947
|
+
// suppressed ticks). Only a real git commit counts as a ship now.
|
|
933
948
|
let best: number | null = null;
|
|
934
949
|
try {
|
|
935
950
|
const out = execSync("git log -1 --format=%ct", { cwd, stdio: ["ignore", "pipe", "ignore"] })
|
|
@@ -940,12 +955,6 @@ export function lastShippedAtMs(cwd: string): number | null {
|
|
|
940
955
|
} catch {
|
|
941
956
|
/* not a git repo or no commits */
|
|
942
957
|
}
|
|
943
|
-
try {
|
|
944
|
-
const mtime = fs.statSync(path.join(cwd, ".pi-glla", "active.jsonl")).mtimeMs;
|
|
945
|
-
if (best === null || mtime > best) best = mtime;
|
|
946
|
-
} catch {
|
|
947
|
-
/* no state file yet */
|
|
948
|
-
}
|
|
949
958
|
return best;
|
|
950
959
|
}
|
|
951
960
|
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
classifyImpossibleReason,
|
|
39
39
|
extractPendingTasks,
|
|
40
40
|
isFullAuditObjective,
|
|
41
|
-
lastShippedAtMs,
|
|
42
41
|
resolveEffectiveAggressiveSettings,
|
|
43
42
|
appendAuditLog,
|
|
44
43
|
computeListDepth,
|
|
@@ -52,7 +51,7 @@ import {
|
|
|
52
51
|
crossRecommendMode,
|
|
53
52
|
formatListDepth,
|
|
54
53
|
shouldEscalateStall,
|
|
55
|
-
|
|
54
|
+
isStaleApiError,
|
|
56
55
|
mergeSettings,
|
|
57
56
|
parseListImport,
|
|
58
57
|
|
|
@@ -179,6 +178,30 @@ const HELD_ON_RESTORE = "held: restored in a fresh session";
|
|
|
179
178
|
// The ExtensionAPI captured in the factory. sendMessage lives on the API,
|
|
180
179
|
// not on ExtensionContext, so continuation sends need it at module scope.
|
|
181
180
|
let extensionApi: ExtensionAPI | null = null;
|
|
181
|
+
// v0.26.7: pi invalidates the extension runtime on session replacement
|
|
182
|
+
// (newSession/fork/switchSession/reload — and the compaction path reaches
|
|
183
|
+
// it via teardownCurrent in pi 0.82.x). Once stale, every sendMessage
|
|
184
|
+
// throws FOREVER in this process — retrying for hours is the hegemon
|
|
185
|
+
// failure shape. Detect the stale signature once and go terminally loud.
|
|
186
|
+
let extensionApiStale = false;
|
|
187
|
+
|
|
188
|
+
/** v0.26.7: a stale api is terminal for this process — pause/stop loudly
|
|
189
|
+
* with restart guidance instead of retrying sends that can never land. */
|
|
190
|
+
function goStaleTerminal(ctx: ExtensionContext, where: string): void {
|
|
191
|
+
if (extensionApiStale) return; // already terminal — don't re-spam
|
|
192
|
+
extensionApiStale = true;
|
|
193
|
+
appendLedger(ctx.cwd, "extension_api_stale", { where, kind: isLoopActive() ? "loop" : "goal" });
|
|
194
|
+
const guidance = "pi invalidated this session's extension handle (session replacement — compaction triggers it in pi 0.82.x). Sends can never land in this process. Restart pi (or reload extensions), then /goal resume / /loop start.";
|
|
195
|
+
if (isLoopActive()) {
|
|
196
|
+
clearLoopTimer();
|
|
197
|
+
state.loop = { ...state.loop!, active: false, stopReason: `extension api stale: ${guidance}` };
|
|
198
|
+
persistState(ctx);
|
|
199
|
+
} else if (state.goal && state.goal.status === "active") {
|
|
200
|
+
updateGoal({ status: "paused", pauseReason: "extension api stale (pi session replacement)", pauseSuggestedAction: guidance }, ctx);
|
|
201
|
+
}
|
|
202
|
+
ctx.ui.notify(`glla: ${guidance}`, "warning");
|
|
203
|
+
notifyExternal(ctx, `glla: extension api stale — restart pi. (${where})`);
|
|
204
|
+
}
|
|
182
205
|
|
|
183
206
|
// The most recent ExtensionContext seen from any event or command handler.
|
|
184
207
|
// pi replaces sessions (newSession/fork/reload) and stale ctx throws on use,
|
|
@@ -240,6 +263,10 @@ let heartbeatNudges = 0;
|
|
|
240
263
|
// refire's own noteActivity, which is what made the hegemon zombie spin
|
|
241
264
|
// self-sustaining (619 refires / 23.5h / zero turns).
|
|
242
265
|
let consecutiveStalls = 0;
|
|
266
|
+
// v0.26.6: precise replacement for the removed ship-recency suppression —
|
|
267
|
+
// set while complete_goal's isolated audit runs, so the heartbeat never
|
|
268
|
+
// refires into an in-flight completion.
|
|
269
|
+
let completionAuditInFlight = false;
|
|
243
270
|
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
244
271
|
|
|
245
272
|
function noteActivity(real = false): void {
|
|
@@ -381,18 +408,16 @@ function heartbeatTick(): void {
|
|
|
381
408
|
notifyExternal(ctx, msg);
|
|
382
409
|
}
|
|
383
410
|
if (!fire) return;
|
|
384
|
-
// v0.25.0
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
411
|
+
// v0.26.6: the 0.25.0 "recent ship (<5m)" suppression was REMOVED. It fed
|
|
412
|
+
// lastShippedAtMs, which read the state-file MTIME — and the heartbeat's
|
|
413
|
+
// own suppressed-tick ledger writes refreshed that mtime every 15s,
|
|
414
|
+
// making the suppression self-sustaining forever (field-observed in
|
|
415
|
+
// darklord: 2,184 suppressed ticks over 9.1h after a post-compaction
|
|
416
|
+
// send failure; the completed list item never closed). Under an
|
|
417
|
+
// auto-committing daemon the git-head term self-sustains too. The legit
|
|
418
|
+
// windows are already covered precisely — busy mid-turn, pending
|
|
419
|
+
// messages, scheduled timers — plus the audit-in-flight flag below.
|
|
420
|
+
if (completionAuditInFlight) return;
|
|
396
421
|
noteActivity();
|
|
397
422
|
consecutiveStalls++;
|
|
398
423
|
appendLedger(ctx.cwd, "heartbeat_refire", { nudgesSoFar: heartbeatNudges, consecutiveStalls });
|
|
@@ -486,7 +511,7 @@ function sendContinuation(goalId: string): void {
|
|
|
486
511
|
continuationTimer.unref?.();
|
|
487
512
|
return;
|
|
488
513
|
}
|
|
489
|
-
if (!extensionApi) return;
|
|
514
|
+
if (!extensionApi || extensionApiStale) return;
|
|
490
515
|
try {
|
|
491
516
|
extensionApi.sendMessage({
|
|
492
517
|
customType: GOAL_EVENT_ENTRY,
|
|
@@ -496,7 +521,9 @@ function sendContinuation(goalId: string): void {
|
|
|
496
521
|
appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
|
|
497
522
|
} catch (err) {
|
|
498
523
|
appendLedger(ctx.cwd, "goal_continuation_send_failed", { goalId, error: err instanceof Error ? err.message : String(err) });
|
|
499
|
-
//
|
|
524
|
+
// v0.26.7: stale runtime = terminal (sends can never land); anything
|
|
525
|
+
// else is transient — next agent_end/session_start reschedules.
|
|
526
|
+
if (isStaleApiError(err)) goStaleTerminal(ctx, "sendContinuation");
|
|
500
527
|
}
|
|
501
528
|
}
|
|
502
529
|
|
|
@@ -1268,7 +1295,7 @@ function isLoopActive(): boolean {
|
|
|
1268
1295
|
|
|
1269
1296
|
/** Run the user's measure command. Orchestrator-side, never agent-side. */
|
|
1270
1297
|
async function runMeasure(ctx: ExtensionContext, cmd: string): Promise<number | null> {
|
|
1271
|
-
if (!extensionApi) return null;
|
|
1298
|
+
if (!extensionApi || extensionApiStale) return null;
|
|
1272
1299
|
try {
|
|
1273
1300
|
const result = await extensionApi.exec("bash", ["-c", cmd], { cwd: ctx.cwd, timeout: MEASURE_TIMEOUT_MS });
|
|
1274
1301
|
const stdout = (result as any)?.stdout ?? "";
|
|
@@ -1392,6 +1419,8 @@ function sendLoopTurn(): void {
|
|
|
1392
1419
|
// stale API — next agent_end reschedules (but if none comes, the
|
|
1393
1420
|
// heartbeat's stall escalation stops the spin — v0.26.1).
|
|
1394
1421
|
appendLedger(ctx.cwd, "loop_turn_send_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
1422
|
+
// v0.26.7: stale runtime is terminal, not transient — go loud now.
|
|
1423
|
+
if (isStaleApiError(err)) goStaleTerminal(ctx, "sendLoopTurn");
|
|
1395
1424
|
}
|
|
1396
1425
|
}
|
|
1397
1426
|
|
|
@@ -1894,13 +1923,20 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1894
1923
|
// retry with backoff before we report "auditor infrastructure error
|
|
1895
1924
|
// (retried once)". Neither attempt is a verdict on the work.
|
|
1896
1925
|
const auditStartMs = Date.now();
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1926
|
+
completionAuditInFlight = true;
|
|
1927
|
+
let result: Awaited<ReturnType<typeof runAudit>>;
|
|
1928
|
+
let retriedOnce = false;
|
|
1929
|
+
try {
|
|
1930
|
+
({ result, retriedOnce } = await runWithInfraRetry(runAudit, {
|
|
1931
|
+
onRetry: (err) => {
|
|
1932
|
+
latestAuditProgress = { label: `infra error (${err.slice(0, 40)}) — retrying once`, lastEventAt: Date.now() };
|
|
1933
|
+
refreshUI(ctx);
|
|
1934
|
+
appendLedger(ctx.cwd, "audit_infra_retry", { goalId: state.goal?.id, error: err.slice(0, 200) });
|
|
1935
|
+
},
|
|
1936
|
+
}));
|
|
1937
|
+
} finally {
|
|
1938
|
+
completionAuditInFlight = false;
|
|
1939
|
+
}
|
|
1904
1940
|
const auditDurationMs = Date.now() - auditStartMs;
|
|
1905
1941
|
latestAuditProgress = null;
|
|
1906
1942
|
// Audit history: record REAL verdicts only — a non-empty report is the
|
|
@@ -3373,6 +3409,7 @@ function warnOnCommandCollision(ctx: ExtensionContext): void {
|
|
|
3373
3409
|
|
|
3374
3410
|
export default function (pi: ExtensionAPI): void {
|
|
3375
3411
|
extensionApi = pi;
|
|
3412
|
+
extensionApiStale = false; // a fresh factory run means a fresh runtime (reload path)
|
|
3376
3413
|
startHeartbeat();
|
|
3377
3414
|
startUITicker();
|
|
3378
3415
|
// Four top-level commands, that's all (v0.8.0 consolidation):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.7",
|
|
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",
|