pi-goal-list-loop-audit 0.32.0 → 0.32.1

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.
@@ -668,6 +668,16 @@ let loopRearmStreak = 0;
668
668
  // whose turn trigger was still dead — pausing a resumable goal 4 minutes
669
669
  // after the compact instead of giving pi room to recover.
670
670
  let compactionGraceUntil = 0;
671
+ // v0.32.1 (pi-goal-x's lesson — "recover from compacts smarter"): a compact
672
+ // leaves a RESUME DEBT, not just two fixed-offset settle probes that can both
673
+ // lose (field: hellhunter 4-min dangle 2026-07-31; polis stall same day).
674
+ // postCompactResumeOwed discharges only when a real turn starts (agent_start);
675
+ // every heartbeat tick past grace retries it. postCompactResyncPending arms a
676
+ // deterministic [POST-COMPACTION RESYNC] block on the next continuation/loop
677
+ // message (pi-goal-x's #5) so the compacted agent re-anchors on artifact
678
+ // state instead of lost chat history.
679
+ let postCompactResumeOwed = false;
680
+ let postCompactResyncPending = false;
671
681
  const COMPACTION_GRACE_MS = 3 * 60_000;
672
682
  // v0.28.25: provider-error retry cadence. Field-observed in dracon-utilities
673
683
  // (kimi, 19-session fleet on one provider account): a "concurrent request
@@ -827,6 +837,24 @@ function heartbeatTick(): void {
827
837
  if (!absorbStaleIfSuperseded(ctx)) goStaleTerminal(ctx, "heartbeat probe");
828
838
  return;
829
839
  }
840
+ // v0.32.1: post-compaction resume debt — retry on every heartbeat tick
841
+ // past grace until a turn actually starts. Fixed-offset settles alone
842
+ // can both lose (pi busy at 2s AND at grace+2s = a dangling chain).
843
+ if (postCompactResumeOwed && isSupervising() && !abortedStandDown) {
844
+ try {
845
+ if (ctx.isIdle() && !ctx.hasPendingMessages() && continuationTimer === null && loopTimer === null) {
846
+ if (isLoopActive()) {
847
+ appendLedger(ctx.cwd, "compaction_resume_owed_refire", { kind: "loop" });
848
+ scheduleLoopTick(ctx);
849
+ } else if (isActionableGoal()) {
850
+ appendLedger(ctx.cwd, "compaction_resume_owed_refire", { kind: "goal" });
851
+ scheduleContinuation(ctx, true);
852
+ } else {
853
+ postCompactResumeOwed = false; // nothing to resume — discharge
854
+ }
855
+ }
856
+ } catch { /* next tick */ }
857
+ }
830
858
  // v0.29.16: zombie-run watchdog. pi reports BUSY (a run is "active") but
831
859
  // zero stream events for 20 min = the provider stream hung silently —
832
860
  // queued continuations can't land, and every other watchdog stays quiet
@@ -1048,11 +1076,13 @@ function sendContinuation(goalId: string): void {
1048
1076
  }
1049
1077
  if (!extensionApi || extensionApiStale) return;
1050
1078
  try {
1079
+ const resync = postCompactResyncPending ? buildPostCompactResync() : "";
1051
1080
  extensionApi.sendMessage({
1052
1081
  customType: GOAL_EVENT_ENTRY,
1053
- content: continuationPrompt(state.goal!),
1082
+ content: resync + continuationPrompt(state.goal!),
1054
1083
  display: false,
1055
1084
  }, { triggerTurn: true, deliverAs: "followUp" });
1085
+ if (resync) postCompactResyncPending = false; // consumed only by a landed send
1056
1086
  continuationRearmStreak = 0; continuationRearmSince = 0; // v0.28.5 (E3): a landed send clears the storm
1057
1087
  appendLedger(ctx.cwd, "goal_continuation_sent", { goalId });
1058
1088
  } catch (err) {
@@ -1105,6 +1135,25 @@ function sendLengthContinue(ctx: ExtensionContext, consecutive: number): void {
1105
1135
  }
1106
1136
  }
1107
1137
 
1138
+ /** v0.32.1: deterministic post-compaction re-anchor (pi-goal-x's #5) —
1139
+ * prepended to the first continuation/loop message after a compact. */
1140
+ function buildPostCompactResync(): string {
1141
+ const lines: string[] = [
1142
+ "[POST-COMPACTION RESYNC] The transcript was just compacted. Trust the artifacts on disk and .pi-glla/ state — NOT your memory of the prior chat. Re-read files before editing them.",
1143
+ ];
1144
+ if (state.goal) {
1145
+ lines.push(`Goal ${state.goal.id} — status ${state.goal.status}`);
1146
+ lines.push(`Objective: ${state.goal.objective.slice(0, 200)}`);
1147
+ const next = findNextPendingTask(state.goal.taskList?.tasks ?? []);
1148
+ if (next) lines.push(`Next pending task: \`${next.id}\` — ${next.title}`);
1149
+ const lastAudit = state.goal.auditHistory?.[state.goal.auditHistory.length - 1];
1150
+ if (lastAudit) lines.push(`Last audit: ${lastAudit.approved ? "APPROVED" : lastAudit.impossible ? "IMPOSSIBLE" : "disapproved"} (${lastAudit.at})`);
1151
+ } else if (state.loop?.active) {
1152
+ lines.push(`Loop: ${state.loop.target.slice(0, 160)} — iteration ${state.loop.iteration}`);
1153
+ }
1154
+ return lines.join("\n") + "\n\n";
1155
+ }
1156
+
1108
1157
  function continuationPrompt(goal: Goal): string {
1109
1158
  // Read the .md file as the template, then substitute {{tokens}}.
1110
1159
  // For v0.1.0 we inline-substitute so we don't need fs at runtime.
@@ -2617,11 +2666,13 @@ function sendLoopTurn(): void {
2617
2666
  // instruction (metricless loops; metric loops already vary via values).
2618
2667
  const variantNote = metricless ? continueVariant(loop.iteration) : "";
2619
2668
  try {
2669
+ const loopResync = postCompactResyncPending ? buildPostCompactResync() : "";
2620
2670
  extensionApi.sendMessage({
2621
2671
  customType: GOAL_EVENT_ENTRY,
2622
- content: loopPrompt(loop, regressionNote, strategyNote, boundsNote, interventionNote, variantNote),
2672
+ content: loopResync + loopPrompt(loop, regressionNote, strategyNote, boundsNote, interventionNote, variantNote),
2623
2673
  display: false,
2624
2674
  }, { triggerTurn: true, deliverAs: "followUp" });
2675
+ if (loopResync) postCompactResyncPending = false; // consumed only by a landed send
2625
2676
  // v0.26.1: the send path is ledgered — the hegemon zombie spun 619
2626
2677
  // refires with zero visibility into whether sends were landing.
2627
2678
  loopRearmStreak = 0; loopRearmSince = 0; // v0.28.5 (E3): a landed turn clears the storm
@@ -5708,6 +5759,11 @@ export default function (pi: ExtensionAPI): void {
5708
5759
  continuationRearmStreak = 0; continuationRearmSince = 0;
5709
5760
  loopRearmStreak = 0; loopRearmSince = 0;
5710
5761
  compactionGraceUntil = Date.now() + COMPACTION_GRACE_MS;
5762
+ // v0.32.1: arm the resume debt + the resync block (the settle probes
5763
+ // below stay as the fast path; the heartbeat now retries the debt on
5764
+ // EVERY post-grace tick until agent_start discharges it).
5765
+ postCompactResumeOwed = true;
5766
+ postCompactResyncPending = true;
5711
5767
  const settle = setTimeout(() => {
5712
5768
  const c = freshCtx();
5713
5769
  if (!c) return;
@@ -6351,6 +6407,9 @@ export default function (pi: ExtensionAPI): void {
6351
6407
  });
6352
6408
  pi.on("agent_start", () => {
6353
6409
  lastStreamActivityAt = Date.now();
6410
+ // v0.32.1: a real turn started — the post-compaction resume debt is
6411
+ // discharged (the heartbeat stops retrying it).
6412
+ postCompactResumeOwed = false;
6354
6413
  });
6355
6414
  pi.on("turn_start", () => {
6356
6415
  lastStreamActivityAt = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
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",