Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue). Three related defects in turn finalization (session/turn/core.rs + tinyagents/mod.rs).
A. EmptyProviderResponse leaves a blank assistant message in history
core.rs:940 extends self.history with outcome.conversation before the empty-response check at core.rs:986-994. The #4093 branch pops the dangling blank assistant row — but only in the tool_calls > 0 branch (core.rs:1032-1043). The tool_calls == 0 branch returns Err(EmptyProviderResponse) with the empty assistant row left in history. Legacy returned the error before history.push (v0.58.7 engine/core.rs:702-730).
Scenario: provider hiccups with an empty completion → user retries → next request carries an empty-content assistant message → strict providers (Anthropic: "text content blocks must be non-empty") 400 — wedging the thread, not just one turn.
Fix: on the EmptyProviderResponse path, pop the trailing empty assistant message before returning (same treatment as the #4093 branch), or move the history extend after the validity checks.
B. Stale provider error masks the real run failure
mod.rs:643 reads error_slot before the LimitExceeded → MaxIterationsExceeded mapping (mod.rs:651). ProviderModel writes the slot on every provider error (model.rs:483,597) including ones recovered by the fallback chain, and never clears it on later success. A turn that failed over successfully and then failed for an unrelated reason returns the stale provider error — wrong classification, wrong Sentry suppression, wrong user message.
Fix: clear error_slot on each successful model call; only consult it when the run error is actually a model error.
C. Duplicate TurnCompleted, emitted before the wrap-up streams
mod.rs:731-735 emits TurnCompleted per observed parent turn and the chat session emits it again (core.rs:1132-1135). The web bridge (web/progress_bridge.rs:1020-1056) records two ledger events + two Completed upserts, and turn_active = false lands at the first emit — on the cap/#4093 paths that's before summarize_turn_wrapup streams the checkpoint text, so the UI is told the turn ended while a model call is still streaming.
Fix: single emission point — suppress the seam-level emit on the chat path (or the session-level one) and move it after the wrap-up completes.
Acceptance criteria
- Empty-completion error leaves history free of empty assistant rows (assert retry request validates against Anthropic-strict rules).
- Fallback-recovered provider errors never surface as the turn's failure reason.
- Exactly one
TurnCompleted per turn, after all streamed output.
Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue). Three related defects in turn finalization (
session/turn/core.rs+tinyagents/mod.rs).A.
EmptyProviderResponseleaves a blank assistant message in historycore.rs:940extendsself.historywithoutcome.conversationbefore the empty-response check atcore.rs:986-994. The #4093 branch pops the dangling blank assistant row — but only in thetool_calls > 0branch (core.rs:1032-1043). Thetool_calls == 0branch returnsErr(EmptyProviderResponse)with the empty assistant row left in history. Legacy returned the error beforehistory.push(v0.58.7engine/core.rs:702-730).Scenario: provider hiccups with an empty completion → user retries → next request carries an empty-content assistant message → strict providers (Anthropic: "text content blocks must be non-empty") 400 — wedging the thread, not just one turn.
Fix: on the
EmptyProviderResponsepath, pop the trailing empty assistant message before returning (same treatment as the #4093 branch), or move the history extend after the validity checks.B. Stale provider error masks the real run failure
mod.rs:643readserror_slotbefore theLimitExceeded → MaxIterationsExceededmapping (mod.rs:651).ProviderModelwrites the slot on every provider error (model.rs:483,597) including ones recovered by the fallback chain, and never clears it on later success. A turn that failed over successfully and then failed for an unrelated reason returns the stale provider error — wrong classification, wrong Sentry suppression, wrong user message.Fix: clear
error_sloton each successful model call; only consult it when the run error is actually a model error.C. Duplicate
TurnCompleted, emitted before the wrap-up streamsmod.rs:731-735emitsTurnCompletedper observed parent turn and the chat session emits it again (core.rs:1132-1135). The web bridge (web/progress_bridge.rs:1020-1056) records two ledger events + two Completed upserts, andturn_active = falselands at the first emit — on the cap/#4093 paths that's beforesummarize_turn_wrapupstreams the checkpoint text, so the UI is told the turn ended while a model call is still streaming.Fix: single emission point — suppress the seam-level emit on the chat path (or the session-level one) and move it after the wrap-up completes.
Acceptance criteria
TurnCompletedper turn, after all streamed output.