fix: mark truncated assistant replies as incomplete instead of complete - #2147
fix: mark truncated assistant replies as incomplete instead of complete#2147xielixing wants to merge 1 commit into
Conversation
…te (GCWing#1980) When the model output stream ends prematurely (provider-side interruption, timeout, connection drop, or max_tokens cut), the execution engine now: - Sets has_final_response=false for partial_truncated (was true) - Includes partial_truncated in the success matches (turn produced partial output) - Sets finalization_reason=Some(partial_truncated) in the cancellation path (was None, which defaulted to complete) The DialogTurnCompleted event now carries finish_reason=partial_truncated and has_final_response=false for truncated turns, allowing the UI to distinguish incomplete turns from complete ones.
limityan
left a comment
There was a problem hiding this comment.
Request changes for commit e0f50ecb3ad562b073c0b573f2be3c22e83640ba.
The live DialogTurnCompleted event is improved, but the incomplete outcome is not yet carried through the durable runtime contract.
1. Persist the incomplete outcome in core
Problem: ExecutionEngine emits finish_reason=partial_truncated and has_final_response=false, but the returned ExecutionResult.finish_reason is still hard-coded to FinishReason::Complete. persist_completed_dialog_turn / SessionManager::complete_dialog_turn do not receive or persist either terminal field.
Risk: A connected Desktop UI may later write the event fields back, but CLI, server, SDK, cron, disconnected clients, and shutdown-before-UI-save paths persist the turn without the incomplete marker. Reloading that history still cannot distinguish the truncated reply from a completed one.
Recommendation: Carry the effective finish reason and has_final_response in ExecutionResult, persist them atomically in core, and add a regression test that reloads the turn without any frontend write-back.
2. Do not notify users that a truncated background task completed successfully
Problem: partial_truncated is explicitly included in success=true, while the completion notification policy only treats success === false as a failure. The background notification therefore still says the task finished, and the in-chat notice falls back to the generic abnormal-ending copy instead of saying that the reply is incomplete.
Risk: The main user-facing symptom remains misleading for background work even though has_final_response=false is available.
Recommendation: Make notification and turn-notice policy consider finishReason=partial_truncated / hasFinalResponse=false, provide explicit incomplete-response copy, and cover both policies with tests.
3. Remove the PR-body scratch file
pr-body-1980.md duplicates this PR description and is an unrelated generated scratch artifact. Please remove it from the commit.
Verification status
git diff --check, repository hygiene, and changed-filerustfmt --checkpassed locally.- The exact-head CI run is
action_requiredand contains no jobs/check runs, so there is no remote passing evidence yet. - Targeted Cargo tests did not complete locally because of shared compilation/lock contention; I am not treating the timeout as either a pass or a failure.
Fix: Mark truncated assistant replies as incomplete instead of complete
Closes #1980
Problem
When the model output stream ends prematurely (provider-side interruption, timeout, connection drop, or
max_tokenscut), the execution engine marks the dialog turn ascomplete/hasFinalResponse=trueinstead ofinterrupted/incomplete. This causes the frontend to persist and display an incomplete assistant reply as if it were a complete, final response.Root Cause
In
execution_engine.rs, three code paths contribute to the bug:partial_truncatedmarks as final (line ~4464): When continuation attempts are exhausted after a partial recovery,finalization_reasonis set to"partial_truncated", but the code then setshas_final_response = true— treating the truncated text as a complete final response.Success calculation excludes
partial_truncated(line ~4480): Thesuccessvariable ishas_final_response || matches!(effective_finish_reason, "max_rounds" | "repeated_tool_failures"). Since"partial_truncated"is not in the matches, and after fix docs: add product screenshot #1has_final_responseisfalse, the turn would be marked as failed (success=false), which is too harsh — the turn did produce partial output.Cancellation path leaves
finalization_reason = None(line ~4194): Whenshould_continue_after_partial_response(reason)returnsfalse(reason contains "cancelled"), the code breaks immediately without settingfinalization_reason, so it defaults toNone→effective_finish_reason = "complete"→has_final_response = true. A cancelled stream with partial text is reported as complete.Fix
Three surgical changes:
Line ~4465:
has_final_response = true→has_final_response = falsefor thepartial_truncatedbranch. A truncated response is not a complete final response.Line ~4483: Add
"partial_truncated"to thematches!list forsuccess. The turn produced partial output, sosuccess=truewithhas_final_response=falsetells the UI: "the turn produced output, but it is incomplete."Line ~4199: Set
finalization_reason = Some("partial_truncated")beforebreakin the cancellation path. This ensures a cancelled stream with partial text is reported aspartial_truncated(incomplete) rather thancomplete.Event Flow After Fix
The
DialogTurnCompletedevent now carries for truncated turns:finish_reason: "partial_truncated"(was"complete")has_final_response: false(wastrue)success: true(wastruefor path 1,falsewould have been too harsh)partial_recovery_reason: <actual reason>(unchanged — e.g., "idle_timeout", "cancelled")The frontend projection (
frontend_projection.rs) forwards these fields asfinishReason,hasFinalResponse,success, andpartialRecoveryReason, allowing the UI to distinguish incomplete turns from complete ones.Validation
cargo build -p bitfun-core --lib— compiles clean (1 pre-existing warning, unrelated)cargo test -p bitfun-core --lib execution::execution_engine— all 38 existing tests pass