Skip to content

fix: mark truncated assistant replies as incomplete instead of complete - #2147

Open
xielixing wants to merge 1 commit into
GCWing:mainfrom
xielixing:fix/issue-1980-incomplete-reply
Open

fix: mark truncated assistant replies as incomplete instead of complete#2147
xielixing wants to merge 1 commit into
GCWing:mainfrom
xielixing:fix/issue-1980-incomplete-reply

Conversation

@xielixing

Copy link
Copy Markdown

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_tokens cut), the execution engine marks the dialog turn as complete / hasFinalResponse=true instead of interrupted/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:

  1. partial_truncated marks as final (line ~4464): When continuation attempts are exhausted after a partial recovery, finalization_reason is set to "partial_truncated", but the code then sets has_final_response = true — treating the truncated text as a complete final response.

  2. Success calculation excludes partial_truncated (line ~4480): The success variable is has_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 #1 has_final_response is false, the turn would be marked as failed (success=false), which is too harsh — the turn did produce partial output.

  3. Cancellation path leaves finalization_reason = None (line ~4194): When should_continue_after_partial_response(reason) returns false (reason contains "cancelled"), the code breaks immediately without setting finalization_reason, so it defaults to Noneeffective_finish_reason = "complete"has_final_response = true. A cancelled stream with partial text is reported as complete.

Fix

Three surgical changes:

  1. Line ~4465: has_final_response = truehas_final_response = false for the partial_truncated branch. A truncated response is not a complete final response.

  2. Line ~4483: Add "partial_truncated" to the matches! list for success. The turn produced partial output, so success=true with has_final_response=false tells the UI: "the turn produced output, but it is incomplete."

  3. Line ~4199: Set finalization_reason = Some("partial_truncated") before break in the cancellation path. This ensures a cancelled stream with partial text is reported as partial_truncated (incomplete) rather than complete.

Event Flow After Fix

The DialogTurnCompleted event now carries for truncated turns:

  • finish_reason: "partial_truncated" (was "complete")
  • has_final_response: false (was true)
  • success: true (was true for path 1, false would have been too harsh)
  • partial_recovery_reason: <actual reason> (unchanged — e.g., "idle_timeout", "cancelled")

The frontend projection (frontend_projection.rs) forwards these fields as finishReason, hasFinalResponse, success, and partialRecoveryReason, 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

…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.
@bobleer
bobleer requested a review from limityan August 7, 2026 02:25

@limityan limityan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-file rustfmt --check passed locally.
  • The exact-head CI run is action_required and 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Incomplete assistant reply is persisted as complete when the model output stream ends prematurely

2 participants