pi-goal-list-loop-audit 0.34.6 → 0.34.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.
@@ -411,6 +411,26 @@ function probeExtensionApiStale(): boolean {
411
411
  return extensionApiStale;
412
412
  }
413
413
 
414
+ /** v0.34.7: orchestrator-path sendUserMessage that can NEVER crash the
415
+ * process. Darklord 2026-08-01: fanOutListAuditFindings ran on a stale
416
+ * handle (a /reload landed mid-collect), assertActive threw, the floating
417
+ * promise from sync archiveCurrentGoal turned it into an uncaughtException
418
+ * and pi EXITED mid-audit. Probe first, catch anyway, ledger the skip. */
419
+ function safeSteerUser(ctx: ExtensionContext, text: string): boolean {
420
+ if (probeExtensionApiStale()) {
421
+ appendLedger(ctx.cwd, "steer_skipped_stale", { chars: text.length });
422
+ return false;
423
+ }
424
+ try {
425
+ extensionApi?.sendUserMessage(text, { deliverAs: ctx.isIdle() ? "followUp" : "steer" });
426
+ return true;
427
+ } catch (err) {
428
+ if (isStaleApiError(err)) extensionApiStale = true;
429
+ appendLedger(ctx.cwd, "steer_skipped_stale", { chars: text.length, threw: true });
430
+ return false;
431
+ }
432
+ }
433
+
414
434
  /** v0.28.1 (S3): command-entry staleness probe + honest warning. Returns
415
435
  * true when the handle is stale — callers must skip send-dependent paths
416
436
  * and must NOT claim work started (S3's "created — starting now" lie). */
@@ -1407,11 +1427,9 @@ async function fanOutListAuditFindings(ctx: ExtensionContext): Promise<void> {
1407
1427
  // queued or the fan-out was declined.
1408
1428
  if (decisions.length > 0) {
1409
1429
  const decList = decisions.slice(0, 8).map((d, i) => `${i + 1}. ${d.slice(0, 500)}`).join("\n");
1410
- extensionApi?.sendUserMessage(
1411
- `[DECIDE FINDINGS — user decisions needed] The audit surfaced ${decisions.length} DECIDE finding(s) — direction calls only the user can make (a decision is not a task, so they were NOT queued):\n${decList}\nRaise them to the user NOW with ask_user_question — one question per finding, options from the finding's own two sides plus "Defer" (prose numbered list if ask_user_question is unavailable; Esc = Defer). Then record every answer in ${AUDIT_FINDINGS_REL}: replace the "- [?]" line with "- [x] DECIDED: <what was chosen> (<date>)" (or "- [x] DEFERRED") so it stops re-surfacing, and queue any chosen work with list_add — do NOT start the work inline.`,
1412
- { deliverAs: ctx.isIdle() ? "followUp" : "steer" },
1413
- );
1414
- appendLedger(ctx.cwd, "list_audit_decisions_raised", { decisions: decisions.length });
1430
+ if (safeSteerUser(ctx,
1431
+ `[DECIDE FINDINGS — user decisions needed] The audit surfaced ${decisions.length} DECIDE finding(s) — direction calls only the user can make (a decision is not a task, so they were NOT queued):\n${decList}\nRaise them to the user NOW with ask_user_question — one question per finding, options from the finding's own two sides plus "Defer" (prose numbered list if ask_user_question is unavailable; Esc = Defer). Then record every answer in ${AUDIT_FINDINGS_REL}: replace the "- [?]" line with "- [x] DECIDED: <what was chosen> (<date>)" (or "- [x] DEFERRED") so it stops re-surfacing, and queue any chosen work with list_add — do NOT start the work inline.`))
1432
+ appendLedger(ctx.cwd, "list_audit_decisions_raised", { decisions: decisions.length });
1415
1433
  }
1416
1434
  const decideNote =
1417
1435
  decisions.length > 0
@@ -1481,7 +1499,12 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
1481
1499
  // was empty, enqueueItems activates the first fix itself, so the
1482
1500
  // list-complete / reviewer noise below must NOT fire for this item.
1483
1501
  const isListAuditCollect = goal.objective.includes(LIST_AUDIT_COLLECT_MARKER);
1484
- if (isListAuditCollect) void fanOutListAuditFindings(ctx);
1502
+ // v0.34.7: the float gets a catch — ANY rejection here used to become
1503
+ // an uncaughtException and kill pi (darklord 2026-08-01).
1504
+ if (isListAuditCollect)
1505
+ void fanOutListAuditFindings(ctx).catch((err) => {
1506
+ appendLedger(ctx.cwd, "list_audit_fanout_error", { error: String(err).slice(0, 200) });
1507
+ });
1485
1508
  const advanced = activateNextListItem(ctx);
1486
1509
  // v0.26.0: the queue just EMPTIED on a completion → list-complete.
1487
1510
  if (!advanced && !isListAuditCollect) {
@@ -1699,10 +1722,8 @@ function fireReviewer(
1699
1722
  enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer", { autoActivate: loadGlobalSettings().autoResume === true }),
1700
1723
  proposeGoal: (objective, reason) => {
1701
1724
  try {
1702
- extensionApi?.sendUserMessage(
1703
- `[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`,
1704
- { deliverAs: ctx.isIdle() ? "followUp" : "steer" },
1705
- );
1725
+ safeSteerUser(ctx,
1726
+ `[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`);
1706
1727
  return true;
1707
1728
  } catch (err) {
1708
1729
  // v0.28.8 (E4): the phantom-reviewer hole — a swallowed throw used
@@ -1833,7 +1854,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
1833
1854
  }
1834
1855
  }
1835
1856
  try {
1836
- extensionApi?.sendUserMessage(tmpl, { deliverAs: ctx.isIdle() ? "followUp" : "steer" });
1857
+ safeSteerUser(ctx, tmpl);
1837
1858
  draftingUserReplies = 0;
1838
1859
  draftingBlockedProposals = 0;
1839
1860
  draftingSeedInFlight = true; // our injected prompt also arrives as a user message — don't count it
@@ -2025,6 +2046,7 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
2025
2046
  return;
2026
2047
  }
2027
2048
  appendLedger(ctx.cwd, "resume_rekick", { goalId: state.goal.id, policy: state.goal.policy, via: "/goal resume" });
2049
+ if (state.goal.interruptedAt) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx); // v0.34.7: same marker law here
2028
2050
  ctx.ui.notify(
2029
2051
  `The ${state.goal.policy === "list" ? "list item" : "goal"} is ACTIVE but idle — re-firing its continuation: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}`,
2030
2052
  "info",
@@ -2134,13 +2156,13 @@ async function showDecisionPrompt(ctx: ExtensionContext): Promise<boolean> {
2134
2156
  else if (group === "loop" && verb === "stop") await cmdLoop("stop", ctx);
2135
2157
  else if (group === "loop" && verb === "resume") await cmdLoop("resume", ctx);
2136
2158
  else {
2137
- extensionApi?.sendUserMessage(`Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2159
+ safeSteerUser(ctx, `Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2138
2160
  await cmdResume(ctx);
2139
2161
  }
2140
2162
  return true;
2141
2163
  }
2142
2164
  // Content choice — deliver to the agent, then resume.
2143
- extensionApi?.sendUserMessage(`Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2165
+ safeSteerUser(ctx, `Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2144
2166
  await cmdResume(ctx);
2145
2167
  return true;
2146
2168
  } finally {
@@ -5352,6 +5374,12 @@ async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
5352
5374
  // resume"). Re-kick the continuation instead of shrugging.
5353
5375
  if (g && g.status === "active") {
5354
5376
  appendLedger(ctx.cwd, "resume_rekick", { goalId: g.id, policy: g.policy });
5377
+ // v0.34.7: the re-kick fulfills the stale-handle marker's promise too
5378
+ // (junk-runner/polis/neonbreak 2026-08-01: actively working with the
5379
+ // ⚠ interrupted banner still screaming — the v0.34.2 clear only lived
5380
+ // in the paused-resume path; the staleness entry-guard above already
5381
+ // filtered out a stale session reaching this branch).
5382
+ if (g.interruptedAt) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
5355
5383
  ctx.ui.notify(
5356
5384
  `The ${g.policy === "list" ? "list item" : "goal"} is ACTIVE but idle — re-firing its continuation: ${g.objective.replace(/\s+/g, " ").slice(0, 70)}`,
5357
5385
  "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.34.6",
3
+ "version": "0.34.7",
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. An isolated extension-less auditor re-verifies every completion with raw evidence; confirmed drafts, decision pauses and consent gates keep you in charge.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",