pi-goal-list-loop-audit 0.38.30 → 0.38.32

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.32 — migrate-on-read backfill for pre-v0.38.21 audit histories (2026-09-08)
4
+
5
+ ### Fixed
6
+ - Pre-v0.38.21 audit histories lack `superseded` flags, so one post-upgrade retry could argue already-settled objections as live (DECIDED 2026-09-08: migrate-on-read). `liveDisapproval` now replays the `appendAuditVerdict` scope rules over the stored array first (`backfillSupersededObjections`): a later verdict-bearing disapproval or clean approval retires older live rounds in place. Add-only (never clears a flag) and idempotent — post-v0.38.21 histories pass through untouched, and the flags ride the next normal state write to disk.
7
+ - Regression coverage in `tests/migrate-on-read.test.ts` (5 pins: settled-by-approval, settled-by-later-disapproval, lone legacy stays live, error transparency, idempotence); existing `tests/objection-pinning.test.ts` green; `tsc` clean.
8
+
9
+ ## 0.38.31 — overdue wait-pause stops promising imminent resume (2026-09-08)
10
+
11
+ ### Fixed
12
+ - A recovery-timer wait whose retry time passed long ago on a held/idle host no longer claims `next: resuming now` / `auto-retrying · now` forever while also reading `paused` + `safely parked` (field screenshot 20260908_180721). `resuming now` / `resuming…` / `now` are grace-bounded (90s); past that the card and status bar name the timer (`next: recovery timer`, `auto-retrying · overdue — waiting on recovery timer`, `retry overdue`).
13
+ - Recovery-timer waits end at the auto-retry row: the stock provider-failure `pauseSuggestedAction` boilerplate and the generic `awaiting first turn` / `saved` tail are gone (owner + next are already on the rows above); the previous row closes the tree with `└─`. Decision/error/blocked pauses are unchanged.
14
+ - Regression coverage in `tests/paused-overdue-display.test.ts` (6 pins); two v0.34-era pins that encoded the old permanent-`resuming…` wording updated to the grace contract; `tsc` clean.
15
+
3
16
  ## 0.38.30 — fresh audit pass: nine low-severity fixes (2026-09-08)
4
17
 
5
18
  ### 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.30 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.32 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
@@ -1054,12 +1054,46 @@ export function countTrailingDisapprovals(history: AuditVerdict[]): number {
1054
1054
  * signal. Infrastructure entries are transparent, but a changed contract
1055
1055
  * revision breaks the comparison because the auditor may now be judging new
1056
1056
  * work. */
1057
+ /** v0.38.32 (DECIDED 2026-09-08 migrate-on-read): replay the
1058
+ * appendAuditVerdict scope-transition rules chronologically over a stored
1059
+ * history, backfilling superseded/supersededBy on pre-v0.38.21 entries that
1060
+ * a later verdict settled. Add-only (never clears a flag) and idempotent:
1061
+ * a second run over the same array marks nothing, and replaying over
1062
+ * post-v0.38.21 histories that live code already flagged is a no-op.
1063
+ * Returns the count newly marked. Durability: callers mutate the live state
1064
+ * object in place; the flags ride the next normal state write to disk, and
1065
+ * the replay re-runs harmlessly until then. Never call this from a
1066
+ * persistence fence — it is a read-path migration, not a write. */
1067
+ export function backfillSupersededObjections(history: AuditVerdict[]): number {
1068
+ let n = 0;
1069
+ for (let i = 0; i < history.length; i++) {
1070
+ const trigger = history[i]!;
1071
+ if (!((trigger.disapproved && !trigger.error) || (trigger.approved && !trigger.error))) continue;
1072
+ const byRef = trigger.disapproved ? `disapproval:${trigger.at}` : `approval:${trigger.at}`;
1073
+ for (let j = 0; j < i; j++) {
1074
+ const older = history[j]!;
1075
+ if (older.disapproved && !older.superseded) {
1076
+ older.superseded = true;
1077
+ older.supersededBy = byRef;
1078
+ n++;
1079
+ }
1080
+ }
1081
+ }
1082
+ return n;
1083
+ }
1084
+
1057
1085
  /** v0.38.21 (objection pinning): the latest still-live disapproval — the
1058
1086
  * objection set the next retry must argue. Superseded rounds and
1059
1087
  * verdictless infrastructure entries are never live. A later clean
1060
1088
  * approval clears the pin, so a live entry is always the newest
1061
1089
  * verdict-bearing disapproval. */
1062
1090
  export function liveDisapproval(history: AuditVerdict[]): AuditVerdict | undefined {
1091
+ // v0.38.32: migrate-on-read — legacy entries predate the pinning flags.
1092
+ // Backfill what later verdicts settled before selecting, so a settled
1093
+ // objection is never argued as live again. Add-only + idempotent (see
1094
+ // backfillSupersededObjections); post-v0.38.21 histories pass through
1095
+ // untouched.
1096
+ backfillSupersededObjections(history);
1063
1097
  for (let i = history.length - 1; i >= 0; i--) {
1064
1098
  const v = history[i]!;
1065
1099
  if (v.superseded) continue;
@@ -1011,6 +1011,14 @@ function goalTotalText(g: Goal, now: number): string {
1011
1011
  return Number.isFinite(startedAt) ? `total ${fmtElapsed(bucketSilentMs(Math.max(0, now - startedAt)))}` : "";
1012
1012
  }
1013
1013
 
1014
+ /** v0.38.31 (field 2026-09-08 180721): "resuming now" is a transient
1015
+ * truth, not a state. A wait-pause whose retry time passed an hour ago on
1016
+ * a held/idle host is overdue — claiming an imminent resume forever while
1017
+ * the card also says "safely parked" is the same contradiction class as
1018
+ * f8d1c2f. Past the grace window the transition falls through to the
1019
+ * pause-kind label ("recovery timer" for waits) instead. */
1020
+ export const PAUSED_RESUME_GRACE_MS = 90_000;
1021
+
1014
1022
  function pausedNextTransition(g: Goal, state: State, now: number): string {
1015
1023
  const resume = g.policy === "list" ? "/list resume" : "/goal resume";
1016
1024
  const retryAt = state.mainModelRecovery?.retryAt ? Date.parse(state.mainModelRecovery.retryAt) : Number.NaN;
@@ -1026,7 +1034,11 @@ function pausedNextTransition(g: Goal, state: State, now: number): string {
1026
1034
  }
1027
1035
  const resumeAt = g.pauseResumeAt ? Date.parse(g.pauseResumeAt) : Number.NaN;
1028
1036
  if (Number.isFinite(resumeAt)) {
1029
- return resumeAt <= now ? "resuming now" : `auto-retry in ${fmtElapsed(resumeAt - now)}`;
1037
+ if (resumeAt > now) return `auto-retry in ${fmtElapsed(resumeAt - now)}`;
1038
+ // Inside the grace window the retry is genuinely imminent. Past it the
1039
+ // timer never fired (held host, idle session) — fall through to the
1040
+ // kind label below instead of promising "resuming now" forever.
1041
+ if (now - resumeAt < PAUSED_RESUME_GRACE_MS) return "resuming now";
1030
1042
  }
1031
1043
  if (isCompletionAuditNoVerdict(g)) {
1032
1044
  const retryAt = g.pendingCompletion?.recoveryRetryAt
@@ -1207,14 +1219,16 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1207
1219
  if (kind === "wait" || kind === "blocked") {
1208
1220
  // v0.34.12: live countdown (the UI ticker keeps rendering through a
1209
1221
  // timed wait) — "auto-retry in 23m" beats a static clock time, and a
1210
- // passed resumeAt says "resuming…" instead of lying about the past.
1222
+ // freshly-passed resumeAt says "resuming…" instead of lying about the
1223
+ // past. v0.38.31: "resuming…" shares the transition grace window — a
1224
+ // retry time long past with no dispatch reads "retry overdue".
1211
1225
  // Every retry-class pause renders the same ⏳ auto-retrying… line +
1212
1226
  // countdown; blocked pauses without a recovery timer render as
1213
1227
  // ⏸ action needed. A main-model manual hold names its recovery owner.
1214
1228
  const rms = g.pauseResumeAt ? Date.parse(g.pauseResumeAt) - now : Number.NaN;
1215
- const when = Number.isFinite(rms)
1216
- ? rms <= 0 ? " · resuming…" : ` · auto-retry in ${fmtElapsed(rms)}`
1217
- : "";
1229
+ const when = !Number.isFinite(rms) ? ""
1230
+ : rms > 0 ? ` · auto-retry in ${fmtElapsed(rms)}`
1231
+ : -rms >= PAUSED_RESUME_GRACE_MS ? " · retry overdue" : " · resuming…";
1218
1232
  if (kind === "blocked") {
1219
1233
  const label = state.mainModelRecovery?.manualResumeRequired === true
1220
1234
  ? "⏸ manual recovery hold"
@@ -1887,7 +1901,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1887
1901
  recoverySummary.forEach((line) => lines.push(`│ ${paint(theme, "dim", line.replace(/^Main-model recovery: /, ""))}`));
1888
1902
  }
1889
1903
  else if (Number.isFinite(retryMs)) {
1890
- const when = retryMs <= 0 ? "now" : `next probe in ${fmtElapsed(retryMs)}`;
1904
+ // v0.38.31: "now" shares the transition grace window — a retry time
1905
+ // that passed long ago with no dispatch is overdue, not imminent.
1906
+ const overdue = -retryMs >= PAUSED_RESUME_GRACE_MS;
1907
+ const when = retryMs > 0 ? `next probe in ${fmtElapsed(retryMs)}`
1908
+ : overdue ? "overdue — waiting on recovery timer"
1909
+ : "now";
1891
1910
  lines.push(`├─ ${paint(theme, "dim", `auto-retrying · ${when}`)}`);
1892
1911
  } else if (kind === "blocked" && state.mainModelRecovery?.manualResumeRequired === true) {
1893
1912
  lines.push(`├─ ${paint(theme, "warning", "manual recovery hold — automatic probes stopped")}`);
@@ -1928,6 +1947,16 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1928
1947
  // before the first turn), render "awaiting first turn" instead of "saved"
1929
1948
  // — the latter was misleading because no work was ever "saved" before the
1930
1949
  // session ended.
1950
+ if (kind === "wait") {
1951
+ // v0.38.31 (field 2026-09-08 180721): a recovery-timer wait carries no
1952
+ // objective-specific tail — owner + next are already on the lifecycle /
1953
+ // transition / auto-retry rows, and the parked suggestedAction is stock
1954
+ // provider-failure boilerplate identical on every recovery wait. End
1955
+ // the card here, closing the previous row as the footer.
1956
+ const tail = lines[lines.length - 1];
1957
+ if (tail !== undefined) lines[lines.length - 1] = tail.replace(/^├─|^│\s*/, "└─ ");
1958
+ return lines;
1959
+ }
1931
1960
  const spent: string[] = [];
1932
1961
  const tokUsed = g.usage?.tokensUsed ?? 0;
1933
1962
  const audits = g.auditHistory?.length ?? 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.30",
3
+ "version": "0.38.32",
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",