Both round-1 findings were withdrawn once the reviewer had the plan-mode facts. Comment the state-flip order, which is the part that reads like a bug and is not. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
4.9 KiB
Let me get the diff to see what changed:
Now let me check the git history to see what changedNow I have full context. Let me verify the pi API behavior by checking the relevant source files to confirm the claims about sendMessage, compact, and event orderingNow I have all the context needed. Let me verify the sendMessage path more carefully:
I now have a complete picture. Here is the review.
Code Review: ready menu — print plan + add "Ready + compact"
Summary
The agent_end handler now prints the plan working set into the transcript before the menu, and adds a 4th option ("Ready + compact") that compacts the planning chatter away before the work turn. The logic is correct and well-considered, with one structural race and one stale-widget gap.
Important (should fix)
-
src/index.ts:~248(theagent_endhandler) — race: plan mode is turned off before compaction completes.
state = { ...state, isPlanMode: false }executes beforectx.compact()is called (let alone finished). The compaction is fire-and-forget. If the human types and sends a message in the window between the state flip and the compaction callback firing, the agent starts a turn in normal mode without the work instruction. The work instruction arrives later (viasendUserMessageinonComplete/onError), triggering a second, redundant turn.
The window is small (compaction is fast) but the behavior is undefined — the agent could start executing before the work instruction lands.
Fix: flipisPlanModeinside the callbacks, not before. Movestate = { ...state, isPlanMode: false }; persist(); updateWidget(ctx);into bothonCompleteandonError, and also into the non-compact branch (where it already is, effectively). Theworkstring can be defined before the branch.if (!choice.includes("compact")) { state = { ...state, isPlanMode: false }; persist(); updateWidget(ctx); pi.sendUserMessage(work, { deliverAs: "followUp" }); return; } ctx.compact({ customInstructions: `...`, onComplete: () => { state = { ...state, isPlanMode: false }; persist(); updateWidget(ctx); pi.sendUserMessage(work, { deliverAs: "followUp" }); }, onError: (e) => { ctx.ui.notify(`Compaction failed (${e.message}); starting work anyway.`, "warning"); state = { ...state, isPlanMode: false }; persist(); updateWidget(ctx); pi.sendUserMessage(work, { deliverAs: "followUp" }); }, });This also means the widget stays in "planning" mode during compaction, which is truthful — compaction hasn't finished yet.
Suggestions
-
src/index.ts:~248— widget not refreshed after$EDITOR.
When the human chooses "Open in $EDITOR",spawnSyncblocks, thencontinuere-enters the loop. The plan is re-read and potentially re-printed, butupdateWidgetis not called. If the human changed goal statuses (e.g. ticked a checkbox), the widget stays stale until the nextturn_end.
AddupdateWidget(ctx);after thespawnSyncline (or inside thecontinuebranch before the continue). -
src/index.ts:~248—spawnSyncblocks the event loop.
spawnSync(process.env.EDITOR || ...)is a synchronous blocking call. While the editor is open, no async work (including compaction from a previous iteration, timers, etc.) can proceed. This is fine for a local TUI tool, but worth noting — if the editor hangs or the human walks away, the entire pi process is frozen.
Positive
- De-duplication is correct.
printedis a local variable, fresh peragent_endcall, and correctly suppresses re-printing when the working set hasn't changed across editor passes. Thewhileloop exit condition (scanGoals(...).length > 0) correctly handles the human deleting all goals in the editor. - String matching is safe.
choice?.startsWith("Ready")gates both Ready options, thenchoice.includes("compact")distinguishes them. The word "compact" appears only in the "Ready + compact" string. No ambiguity. - Both compaction callbacks queue the work turn.
onCompleteandonErrorboth callpi.sendUserMessage(work, ...). A failed compaction does not strand the session — work starts anyway, with a notification. session_compact→resyncReason→ injection chain is correct. The pi source confirmssession_compactfires (and is awaited) beforethis.compact()resolves andonCompletefires. SoresyncReasonis set before the next LLM call, and the full plan file is re-injected. The compaction summarizes away the exploration; the plan itself survives.
Verdict
REQUEST CHANGES — the race between isPlanMode = false and compaction completion is a real timing bug that can cause the agent to start a turn without the work instruction. The fix is straightforward: move the state flip into the callbacks.