fix(pi): finalize turns on the run terminal so a reused process does not replay the previous answer #83
No reviewers
Labels
No labels
epic
in-review
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
weiwen/evie!83
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fm/evie-followup-repeat-diagnosis"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
A follow-up turn made Evie replay the previous turn's answer, instantly (microseconds, no LLM call) instead of answering the new message. Reproduced live on
matsutake(buildmm47w0w…, pi 0.80.10,provider=opencode): a follow-up returned the prior answer in ~0.4ms, and itsPi tool calldebug lines (emitted only bylast_assistant_text, reachable only from theagent_endbranch) proved it had parsed a leftoveragent_end.Regression from #81.
Mechanism
#81added aturn_end{stop}return path toread_agent_response. In pi 0.80.10's stream aturn_end{stop}is followed by the run's own terminal events —agent_end, thenagent_settled. Returning atturn_end{stop}handed control back whileagent_endwas still unread in the process's stdout.The pi process is reused across turns, and
PiProcess::send_messagedoesreader.into_inner()between turns, which discards only the userspaceBufReaderbuffer — not the kernel pipe. So the trailingagent_end(which restates the just-finished turn's assistant text) survived into the next turn, whose fresh reader consumed it first and returned it vialast_assistant_text. The pre-#81 parser returned onagent_end, so it consumed the run fully and never stranded a text-bearing event — hence no repeat.Reproduced end-to-end against the real pi 0.80.10 binary: the #81 parser delivered a stale answer on every follow-up; the pre-#81 parser did not.
Fix
Align the turn boundary with pi's run boundary so nothing is stranded:
turn_endnow only records its text intoTurnState(last terminal wins, so anauto_retryre-run overwrites the earlier attempt); it no longer finalizes the turn.agent_endin the native schema (delivers directly, consuming itself so only the benign text-lessagent_settledtrails) oragent_settledin the turn schema (delivers the recorded response).agent_settledstill surfaces a recorded provider error and still skips a stray leading marker (so a run that produced neither doesn't finalize empty).agent_settled.This also removes a latent early-return on the first
agent_endof anauto_retryrun. #81's turn-schema/error handling is kept intact; this does not revert to returning onagent_endonly (which would reintroduce #81's "turns never finalize").Note on approach
The diagnosis proposed making
agent_settledthe sole terminal (withagent_endrecording). That stalls two existing fixtures that emitagent_endon an open stream with noagent_settled(test_abort_after_agent_end_returns_response,fake_multi_responder) — they would hang to the idle timeout, and the drain-to-settle fallback hits the same wall. Keepingagent_endas the native terminal fixes the bug identically (it is the proven-correct pre-#81 native behavior) while touching no existing fixture and staying robust to anagent_end-without-agent_settledprovider.Regression test
reused_reader_does_not_replay_previous_turndrivesread_agent_responsetwice over one continuous reader holding turn 1's fullturn_end{stop}→agent_end→agent_settledtail, and asserts turn 2 delivers its own answer. This is the exact gap in #81's tests (they fed a single stream to one call and never ran a second turn over the leftover tail). Verified it fails on the pre-fix behavior (turn 2 returns"FIRST") and passes with the fix.Validation
just checkgreen:cargo fmt --check,cargo clippy -- -D warnings,cargo test(486 passed, incl. the new test and the existing pi/native + turn-schema tests).