pi-goal-list-loop-audit 0.38.18 → 0.38.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.19 — disapproval response: dispatch stall root-cause + state clear (2026-09-04)
4
+
5
+ ### Fixed
6
+ - Track 2 root cause (post-answer stall): the wait-for-idle send gate deferred an owed continuation forever when the session went phantom-busy — busy, nothing pending, zero stream (neonbreak: 35 re-arms, zero sends, 45m to the zombie abort). A busy session with nothing pending and no real stream for `GLLA_BUSY_SILENT_SEND_MS` (default 5m) is wedged, not working: the marker is now sent into pi's followUp queue instead (`goal_continuation_send_busy_bypass`, `busyBypass` on the sent event). Genuinely working sessions and loaded queues keep the old wait path.
7
+ - Track 3 state clear (stale waiting-verdict): `auditorDisplayPhase` projects `awaiting-verdict` only while the goal is still `auditing`. Closed goals (complete/aborted) and pre-archive snapshots carrying a stale running claim fall through to the quiet gate — no stale progress object can resurrect the wait after `goal_archived`.
8
+ - Both fixes carry behavioral MockPi regression tests with empirical fail-before (old code fails the new assertions, guards pass throughout): `tests/answered-question-dispatch.test.ts` (3), `tests/closed-goal-clears-waiting.test.ts` (2).
9
+
3
10
  ## 0.38.18 — completion/lifecycle field trilogy (2026-09-04)
4
11
 
5
12
  ### 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.18 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.19 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
@@ -330,6 +330,26 @@ export function sendRearmDelayMs(streak: number): number {
330
330
  return 30_000;
331
331
  }
332
332
 
333
+ /** v0.38.19 (track 2): how long a busy session with an owed send may go
334
+ * without real stream output before the marker is sent into pi's followUp
335
+ * queue anyway. 5m matches SEND_REARM_ESCALATE_SILENT_MS's definition of
336
+ * wedged silence, far below the ~45m zombie abort that used to be the only
337
+ * exit from a phantom-busy session. */
338
+ export function busySilentSendMs(): number {
339
+ const raw = Number(process.env.GLLA_BUSY_SILENT_SEND_MS ?? 5 * 60_000);
340
+ return Number.isFinite(raw) && raw >= 0 ? raw : 5 * 60_000;
341
+ }
342
+
343
+ /** v0.38.19 (track 2): true when the session's business is stale enough
344
+ * that waiting for idle is the stall, not patience. Requires observed
345
+ * stream history (lastRealActivityAt > 0): a session that never streamed
346
+ * in this process is ignorance, not evidence of wedging — it keeps the old
347
+ * wait path (storm → escalation → zombie abort). */
348
+ export function busySilentBypassDue(): boolean {
349
+ if (flags.lastRealActivityAt <= 0) return false;
350
+ return Date.now() - flags.lastRealActivityAt >= busySilentSendMs();
351
+ }
352
+
333
353
  export function accountSendRearm(ctx: ExtensionContext, kind: "continuation" | "loop"): void {
334
354
  const streak = kind === "continuation" ? ++continuationRearmStreak : ++flags.loopRearmStreak;
335
355
  if (streak === 1) {
@@ -1225,12 +1245,35 @@ export function sendContinuation(goalId: string): void {
1225
1245
  }
1226
1246
  if (!guardGoalBeforeContinuation(ctx, "dispatch", goalId)) return;
1227
1247
  if (!isActionableGoal()) return;
1248
+ // v0.38.19 (track 2): set when the busy-but-silent bypass below fires so
1249
+ // the sent ledger event carries the marker. The test hook for the stream
1250
+ // clock lives in loops/goal-ui.ts (__testOnlySetLastRealActivityAt).
1251
+ let busyBypass = false;
1228
1252
  if (!ctx.isIdle() || ctx.hasPendingMessages()) {
1229
1253
  accountSendRearm(ctx, "continuation");
1230
- continuationScheduledFor = goalId;
1231
- // v0.28.29: backing-off cadence (was flat 50ms — 6,000 spins in 5m).
1232
- continuationTimer = scheduleSessionTimeout(() => sendContinuation(goalId), sendRearmDelayMs(continuationRearmStreak));
1233
- return;
1254
+ // v0.38.19 (track 2, auditor-required): busy-but-silent bypass. A
1255
+ // session that is busy with nothing pending and zero real stream for
1256
+ // busySilentSendMs is not doing work a followUp could corrupt — it is
1257
+ // wedged (neonbreak: answer in, 45m of phantom-busy, zero sends because
1258
+ // wait-for-idle never cleared). pi's followUp queue is the designed
1259
+ // vehicle for exactly this: the marker queues behind the stuck turn
1260
+ // instead of rearming into the void until the zombie abort. Genuinely
1261
+ // working sessions (fresh stream) and loaded queues (pending messages)
1262
+ // keep the old wait path.
1263
+ if (!ctx.isIdle() && !ctx.hasPendingMessages() && busySilentBypassDue()) {
1264
+ busyBypass = true;
1265
+ appendLedger(ctx.cwd, "goal_continuation_send_busy_bypass", {
1266
+ goalId,
1267
+ generation: flags.sessionGeneration,
1268
+ silentMs: Date.now() - flags.lastRealActivityAt,
1269
+ rearmStreak: continuationRearmStreak,
1270
+ });
1271
+ } else {
1272
+ continuationScheduledFor = goalId;
1273
+ // v0.28.29: backing-off cadence (was flat 50ms — 6,000 spins in 5m).
1274
+ continuationTimer = scheduleSessionTimeout(() => sendContinuation(goalId), sendRearmDelayMs(continuationRearmStreak));
1275
+ return;
1276
+ }
1234
1277
  }
1235
1278
  if (!flags.extensionApi || flags.extensionApiStale) return;
1236
1279
  try {
@@ -1264,7 +1307,7 @@ export function sendContinuation(goalId: string): void {
1264
1307
  lastContinuationSentPayload = { content, display: false }; // v0.34.88: verbatim retry payload
1265
1308
  if (!dispatchAccepted(ctx, attempt)) return;
1266
1309
  continuationRearmStreak = 0; continuationRearmSince = 0; // v0.28.5 (E3): an accepted dispatch clears the storm
1267
- appendLedger(ctx.cwd, "goal_continuation_sent", { goalId, attemptId: attempt.id, generation: attempt.generation, kind, payloadChars: content.length });
1310
+ appendLedger(ctx.cwd, "goal_continuation_sent", { goalId, attemptId: attempt.id, generation: attempt.generation, kind, payloadChars: content.length, ...(busyBypass ? { busyBypass: true } : {}) });
1268
1311
  // v0.35.37 (audit finding): the welcome-back recovery notice must fire
1269
1312
  // EXACTLY ONCE per auto-resume. The payload above was built with the
1270
1313
  // notice in it; once the dispatch is ACCEPTED the message has landed in
@@ -668,7 +668,15 @@ export function auditorDisplayPhase(g: Goal, audit: AuditDisplayProgress | null
668
668
  const label = audit?.label?.toLowerCase() ?? "";
669
669
  if (label === "queued") return "queued";
670
670
  if (/infra|error|failed|blocked|no verdict/.test(label)) return "blocked";
671
- if (audit?.phase === "complete") return "awaiting-verdict";
671
+ // v0.38.19 (track 3, auditor-required): awaiting-verdict is a LIVE claim
672
+ // — a worker done, its verdict not yet applied. A closed goal (complete /
673
+ // aborted, or anything past auditing) can never be waiting: the verdict
674
+ // landed or the claim died with the archive. A stale progress object
675
+ // handed in for a closed goal must not resurrect the wait (junk-runner:
676
+ // session narrating "waiting on the auditor's verdict" after
677
+ // goal_archived complete). Stale timestamps fall through to the quiet
678
+ // gate below, which is true — no worker will ever speak again.
679
+ if (audit?.phase === "complete" && g.status === "auditing") return "awaiting-verdict";
672
680
  const age = auditorActivityAge(audit, now);
673
681
  if (age !== undefined && age > AUDITOR_QUIET_MS) {
674
682
  // v0.37.0: progress-aware quiet gate. A tool that is still running INSIDE
@@ -691,7 +699,12 @@ export function auditorDisplayPhase(g: Goal, audit: AuditDisplayProgress | null
691
699
  return "running";
692
700
  return "quiet";
693
701
  }
694
- if (!audit && g.pendingCompletion?.phase === "running") return "awaiting-verdict";
702
+ // Same lifecycle scope as above: a stale goal snapshot that still
703
+ // carries a running pendingCompletion must not project a wait either.
704
+ // (The archive strips pendingCompletion; this guards pre-archive
705
+ // snapshots read after the close.)
706
+ if (!audit && g.status === "auditing" && g.pendingCompletion?.phase === "running") return "awaiting-verdict";
707
+ if (!audit && g.pendingCompletion?.phase === "running") return "quiet";
695
708
  return "running";
696
709
  }
697
710
 
@@ -1044,6 +1044,12 @@ function isContextStarvedRefused(): boolean {
1044
1044
  export function __testOnlySetLastCompactionAt(at: number | null): void {
1045
1045
  lastCompactionAt = at ?? 0;
1046
1046
  }
1047
+ /** Test-only: set the real-stream clock directly so busy-silence scenarios
1048
+ * (answered question, phantom-busy host, zero stream) are drivable without
1049
+ * wall-clock waits. Exported through loops/goal.js like the other hooks. */
1050
+ export function __testOnlySetLastRealActivityAt(at: number): void {
1051
+ lastRealActivityAt = at;
1052
+ }
1047
1053
 
1048
1054
  /** Test-only: expose the live activity projection without requiring a TUI
1049
1055
  * renderer. This drives the same scope/timestamp checks used by refreshUI. */
@@ -74,7 +74,7 @@ export {
74
74
  __testOnlyResetOwnerSession,
75
75
  __testOnlyRunFanOutListAuditFindings,
76
76
  } from "./goal-session.js";
77
- export { __testOnlySetLastCompactionAt, __testOnlyLoadState, __testOnlyRegisterAgentTools, __testOnlyRememberCtx, __testOnlyDisplayActivityFor, __testOnlyResetToolActivity } from "./goal-ui.js";
77
+ export { __testOnlySetLastCompactionAt, __testOnlyLoadState, __testOnlyRegisterAgentTools, __testOnlyRememberCtx, __testOnlyDisplayActivityFor, __testOnlyResetToolActivity, __testOnlySetLastRealActivityAt } from "./goal-ui.js";
78
78
  export { auditorRetryPlan, runDetachedCompletionWithFallback, __testOnlySetAuditorRecoveryRetryDelay, __testOnlyResetAuditorRecoveryRuntime, type AuditorModelCandidate } from "./goal-auditor-hooks.js";
79
79
  export { handleSettingChoice } from "./goal-settings-ui.js";
80
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.18",
3
+ "version": "0.38.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. 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",