pi-goal-list-loop-audit 0.28.5 → 0.28.6
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.
|
@@ -498,10 +498,60 @@ export function ensureDirs(cwd: string): void {
|
|
|
498
498
|
fs.mkdirSync(archiveDir(cwd), { recursive: true });
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
// =================================================================
|
|
502
|
+
// Persistence degradation (v0.28.6, audit E1)
|
|
503
|
+
// =================================================================
|
|
504
|
+
// A disk failure (ENOSPC, EACCES, a wedged mount) used to THROW out of
|
|
505
|
+
// appendLedger/writeGoalMd mid-handler — killing the orchestrator turn and
|
|
506
|
+
// silently diverging RAM from disk. Now every persistence step runs
|
|
507
|
+
// through runPersistStep: failures are caught, the session-wide degraded
|
|
508
|
+
// flag latches (the TUI shows it; the first failure notifies loudly), RAM
|
|
509
|
+
// state stays authoritative, and the next SUCCESSFUL step auto-clears the
|
|
510
|
+
// flag (self-healing — the "dirty" marker that write-then-mutate ordering
|
|
511
|
+
// cannot otherwise provide).
|
|
512
|
+
|
|
513
|
+
export interface PersistenceFailure {
|
|
514
|
+
what: string;
|
|
515
|
+
error: string;
|
|
516
|
+
at: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let persistenceDegraded = false;
|
|
520
|
+
let lastFailure: PersistenceFailure | null = null;
|
|
521
|
+
|
|
522
|
+
export function isPersistenceDegraded(): boolean {
|
|
523
|
+
return persistenceDegraded;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function lastPersistenceFailure(): PersistenceFailure | null {
|
|
527
|
+
return lastFailure;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Run one persistence step. On failure: latch the degraded flag, remember
|
|
531
|
+
* the error, return undefined (NEVER throw into an orchestrator handler).
|
|
532
|
+
* On success: clear the flag — a landing write means the disk is back. */
|
|
533
|
+
export function runPersistStep<T>(what: string, fn: () => T): T | undefined {
|
|
534
|
+
try {
|
|
535
|
+
const out = fn();
|
|
536
|
+
if (persistenceDegraded) {
|
|
537
|
+
persistenceDegraded = false;
|
|
538
|
+
lastFailure = null;
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
} catch (err) {
|
|
542
|
+
persistenceDegraded = true;
|
|
543
|
+
lastFailure = { what, error: err instanceof Error ? err.message : String(err), at: new Date().toISOString() };
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
501
548
|
export function readState(cwd: string): State {
|
|
502
549
|
const file = ledgerPath(cwd);
|
|
503
|
-
|
|
504
|
-
|
|
550
|
+
// v0.28.6 (E1): an unreadable ledger (EACCES, EIO) degrades loudly
|
|
551
|
+
// instead of throwing out of session_start.
|
|
552
|
+
const raw = runPersistStep("readState", () => (fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : ""));
|
|
553
|
+
if (raw === undefined || raw === "") return { ...DEFAULT_STATE };
|
|
554
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
505
555
|
if (lines.length === 0) return { ...DEFAULT_STATE };
|
|
506
556
|
let parsed: Partial<State> = {};
|
|
507
557
|
for (const line of lines) {
|
|
@@ -509,7 +559,8 @@ export function readState(cwd: string): State {
|
|
|
509
559
|
const evt = JSON.parse(line);
|
|
510
560
|
if (evt.type === "state") parsed = { ...parsed, ...evt.value };
|
|
511
561
|
} catch {
|
|
512
|
-
// skip malformed lines
|
|
562
|
+
// skip malformed lines — a truncated trailing line (mid-write kill)
|
|
563
|
+
// must not lose the rest of the state
|
|
513
564
|
}
|
|
514
565
|
}
|
|
515
566
|
return {
|
|
@@ -520,16 +571,23 @@ export function readState(cwd: string): State {
|
|
|
520
571
|
}
|
|
521
572
|
|
|
522
573
|
export function appendLedger(cwd: string, type: string, value: unknown): void {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
574
|
+
// v0.28.6 (E1): guarded — a disk failure degrades loudly, never throws
|
|
575
|
+
// into an orchestrator handler.
|
|
576
|
+
runPersistStep("appendLedger", () => {
|
|
577
|
+
ensureDirs(cwd);
|
|
578
|
+
const line = JSON.stringify({ type, value, at: new Date().toISOString() });
|
|
579
|
+
fs.appendFileSync(ledgerPath(cwd), line + "\n");
|
|
580
|
+
});
|
|
526
581
|
}
|
|
527
582
|
|
|
528
583
|
export function writeGoalMd(cwd: string, goal: Goal): string {
|
|
529
|
-
ensureDirs(cwd);
|
|
530
584
|
const file = goalMdPath(cwd, goal.id);
|
|
531
|
-
|
|
532
|
-
|
|
585
|
+
runPersistStep("writeGoalMd", () => {
|
|
586
|
+
ensureDirs(cwd);
|
|
587
|
+
fs.writeFileSync(file, renderGoalMarkdown(goal));
|
|
588
|
+
});
|
|
589
|
+
// Return the intended path even on failure so activePath stays sane —
|
|
590
|
+
// the degraded flag carries the truth that the write did not land.
|
|
533
591
|
return file;
|
|
534
592
|
}
|
|
535
593
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Goal, State } from "./goal-loop-core.js";
|
|
14
|
+
import { isPersistenceDegraded, lastPersistenceFailure } from "./goal-loop-core.js";
|
|
14
15
|
import type { LoopState } from "./goal-loop-forever.js";
|
|
15
16
|
|
|
16
17
|
// ---- formatters ----
|
|
@@ -185,6 +186,17 @@ function countTotal(g: Goal): number {
|
|
|
185
186
|
* Returns undefined when nothing is worth showing.
|
|
186
187
|
*/
|
|
187
188
|
export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
|
|
189
|
+
const inner = buildWidgetLinesInner(state, audit, now, theme, width, extras);
|
|
190
|
+
// v0.28.6 (E1): a persistence failure outranks everything — first line,
|
|
191
|
+
// on every render, until a write lands again.
|
|
192
|
+
if (inner && isPersistenceDegraded()) {
|
|
193
|
+
const err = lastPersistenceFailure();
|
|
194
|
+
return [paint(theme, "error", `⚠ persistence degraded — .pi-glla writes failing (${truncate(err?.error ?? "disk error", 40)}); state in RAM`), ...inner];
|
|
195
|
+
}
|
|
196
|
+
return inner;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
|
|
188
200
|
if (state.loop?.active) return loopLines(state.loop, now, theme, width, extras);
|
|
189
201
|
const g = state.goal;
|
|
190
202
|
if (!g) return undefined;
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -85,6 +85,9 @@ import {
|
|
|
85
85
|
statusLabel,
|
|
86
86
|
writeGoalMd,
|
|
87
87
|
missingGllaTools,
|
|
88
|
+
runPersistStep,
|
|
89
|
+
isPersistenceDegraded,
|
|
90
|
+
lastPersistenceFailure,
|
|
88
91
|
} from "../goal-loop-core.js";
|
|
89
92
|
import {
|
|
90
93
|
LENGTH_CONTINUE_MAX,
|
|
@@ -740,9 +743,29 @@ function createGoal(objective: string, ctx: ExtensionContext, policy: "goal" | "
|
|
|
740
743
|
|
|
741
744
|
function persistState(ctx: ExtensionContext): void {
|
|
742
745
|
appendLedger(ctx.cwd, "state", { goal: state.goal, list: state.list ?? [], loop: state.loop ?? null });
|
|
746
|
+
notifyPersistenceState(ctx); // v0.28.6 (E1): loud on the first failure, all-clear on recovery
|
|
743
747
|
refreshUI(ctx); // every state transition flows through here → the TUI is always current
|
|
744
748
|
}
|
|
745
749
|
|
|
750
|
+
// v0.28.6 (E1): persistence-degradation notify — once per failure streak,
|
|
751
|
+
// once per recovery. The TUI flag (buildWidgetLines) carries the standing
|
|
752
|
+
// state; these notifies are the LOUD part.
|
|
753
|
+
let persistenceDegradedNotified = false;
|
|
754
|
+
function notifyPersistenceState(ctx: ExtensionContext): void {
|
|
755
|
+
if (isPersistenceDegraded() && !persistenceDegradedNotified) {
|
|
756
|
+
persistenceDegradedNotified = true;
|
|
757
|
+
const err = lastPersistenceFailure();
|
|
758
|
+
ctx.ui.notify(
|
|
759
|
+
`⚠ Persistence degraded: ${err?.what ?? "disk write"} failed (${(err?.error ?? "unknown").slice(0, 80)}). State lives in RAM and re-syncs on the next successful write — .pi-glla may be missing recent entries. Fix the disk (space/permissions) and it self-heals.`,
|
|
760
|
+
"warning",
|
|
761
|
+
);
|
|
762
|
+
notifyExternal(ctx, "pi-goal-list-loop-audit: persistence degraded — .pi-glla writes failing.");
|
|
763
|
+
} else if (!isPersistenceDegraded() && persistenceDegradedNotified) {
|
|
764
|
+
persistenceDegradedNotified = false;
|
|
765
|
+
ctx.ui.notify("Persistence recovered — .pi-glla writes are landing again.", "info");
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
746
769
|
function setGoal(goal: Goal, ctx: ExtensionContext): void {
|
|
747
770
|
state = { goal, list: state.list ?? [] }; // preserve the list!
|
|
748
771
|
const file = writeGoalMd(ctx.cwd, goal);
|
|
@@ -765,9 +788,16 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
|
|
|
765
788
|
ensureDirs(ctx.cwd);
|
|
766
789
|
const target = archivedGoalPath(ctx.cwd, goal.id);
|
|
767
790
|
const md = renderGoalMarkdown({ ...goal, status, stopReason });
|
|
768
|
-
|
|
769
|
-
//
|
|
770
|
-
|
|
791
|
+
// v0.28.6 (E1): guarded — and the active md is only removed when the
|
|
792
|
+
// archive actually LANDED (degraded mode must not destroy the only copy).
|
|
793
|
+
const archived = runPersistStep("archiveCurrentGoal", () => {
|
|
794
|
+
ensureDirs(ctx.cwd);
|
|
795
|
+
fs.writeFileSync(target, md);
|
|
796
|
+
return true;
|
|
797
|
+
}) === true;
|
|
798
|
+
if (archived) {
|
|
799
|
+
try { fs.unlinkSync(goalMdPath(ctx.cwd, goal.id)); } catch {}
|
|
800
|
+
}
|
|
771
801
|
state = { goal: { ...goal, status, archivedPath: path.relative(ctx.cwd, target) || target, stopReason }, list: state.list ?? [] };
|
|
772
802
|
appendLedger(ctx.cwd, "goal_archived", { goalId: goal.id, status, stopReason });
|
|
773
803
|
persistState(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.6",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 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 \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|