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:
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]).
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.
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
- Enter plan mode, submit a plan containing a
- [ ] checklist, approve it.
- Execute some steps; the agent emits
[DONE:1] etc. (progress widget shows partial completion).
- Run
/compact (or fork the session and then compact).
- 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
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 reports0/N steps completeand re-lists every step as remaining, even though they were already executed.Root cause
In
apps/pi-extension/index.ts:turn_endhandler callsmarkCompletedSteps(text, checklistItems), which only mutates the in-memory array. Nothing ever writes the completion back to the plan file (- [ ]→- [x]).before_agent_startre-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.resyncPhaseFromSession()(session_start / session_tree) rebuilds progress by re-reading the plan file (all still- [ ]) plus scanning branch messages after the lastplannotator-executeentry 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
- [ ]checklist, approve it.[DONE:1]etc. (progress widget shows partial completion)./compact(or fork the session and then compact).0/N steps completewith 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):Call sites in
index.ts:turn_end, inside the existingmarkCompletedSteps(...) > 0branch:resyncPhaseFromSession, after the message-scan loop (writes back whatever was rebuilt):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-extension0.27.4