Skip to content

Execution progress ([DONE:n]) resets to 0/N after compaction or session fork — plan file checkboxes never written back #1376

Description

@huangxinsteam-oss

Summary

During plan execution, [DONE:n] progress is tracked only in memory. The plan file's own checkboxes are never updated, and progress is rebuilt from sources that compaction destroys — so after a /compact, session fork, or resume, the per-turn injection reports 0/N steps complete and re-lists every step as remaining, even though they were already executed.

Root cause

In apps/pi-extension/index.ts:

  1. turn_end handler calls markCompletedSteps(text, checklistItems), which only mutates the in-memory array. Nothing ever writes the completion back to the plan file (- [ ]- [x]).
  2. before_agent_start re-parses the plan file from disk every turn (checklistItems = parseChecklist(planContent)), discarding the in-memory marks before the status snapshot is built — so the injected "Todo status" is stale even without compaction.
  3. resyncPhaseFromSession() (session_start / session_tree) rebuilds progress by re-reading the plan file (all still - [ ]) plus scanning branch messages after the last plannotator-execute entry for [DONE:n]. Compaction removes those assistant messages from the active branch, so the rebuild finds nothing and progress resets to zero.

Net effect: any compaction (or fork onto a compacted branch) revives the full [PLANNOTATOR - EXECUTING PLAN] Remaining steps: ... injection mid-execution, and the agent is told to redo finished work.

Repro

  1. Enter plan mode, submit a plan containing a - [ ] checklist, approve it.
  2. Execute some steps; the agent emits [DONE:1] etc. (progress widget shows partial completion).
  3. Run /compact (or fork the session and then compact).
  4. Next turn: injection claims 0/N steps complete with all steps listed as remaining.

Suggested fix

Persist completion into the plan file itself — the file is the only state that reliably survives compaction, forks, resumes, and restarts. Concretely, upgrade the n-th checkbox line (same ordinal mapping as parseChecklist) whenever marks are detected, and once more when resync rebuilds state from messages (self-healing for pre-fix sessions):

// persist-checklist.ts
import { readFileSync, writeFileSync } from "node:fs";

/** Upgrade `- [ ]` -> `- [x]` for the given 1-based checkbox ordinals.
 *  Upgrade-only, no-op when nothing changes; all failures silent. */
export function persistDoneStepsToPlanFile(
  fullPath: string,
  doneSteps: Iterable<number>,
): boolean {
  const done = doneSteps instanceof Set ? doneSteps : new Set(doneSteps);
  if (done.size === 0) return false;
  let content: string;
  try {
    content = readFileSync(fullPath, "utf-8");
  } catch {
    return false;
  }
  // Same line shape as parseChecklist(): /^[-*]\s*\[([ xX])\]\s+(.+)$/gm
  let ordinal = 0;
  let changed = false;
  const updated = content.replace(
    /^([-*]\s*)\[([ xX])\](\s+.+)$/gm,
    (line: string, bullet: string, mark: string, rest: string) => {
      ordinal += 1;
      if (mark === " " && done.has(ordinal)) {
        changed = true;
        return `${bullet}[x]${rest}`;
      }
      return line;
    },
  );
  if (!changed) return false;
  try {
    writeFileSync(fullPath, updated, "utf-8");
  } catch {
    return false;
  }
  return true;
}

Call sites in index.ts:

  • turn_end, inside the existing markCompletedSteps(...) > 0 branch:
    if (lastSubmittedPath) {
      persistDoneStepsToPlanFile(resolve(ctx.cwd, lastSubmittedPath), extractDoneSteps(text));
    }
  • resyncPhaseFromSession, after the message-scan loop (writes back whatever was rebuilt):
    persistDoneStepsToPlanFile(
      fullPath,
      checklistItems.filter((i) => i.completed).map((i) => i.step),
    );

I've been running this locally as a patched copy of the extension: unit tests for the helper pass (13/13: ordinal mapping, idempotency, CRLF preservation, upgrade-only, missing-file silence), and an end-to-end run (approve → execute → [DONE:1]/[DONE:2] → full session reload) shows the file checkboxes auto-checked and the Plan Complete! state surviving reload with no regressed injection. Happy to open a PR with this change if you'd like.

Environment

  • @plannotator/pi-extension 0.27.4
  • pi 0.84.2 (hosted via pi-web 0.8.9, multiple sessions per process)
  • Windows 11, Chrome 151

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions