Skip to content

fix(agents): use currentAttemptAssistant for incomplete-turn classification#94188

Closed
LiuwqGit wants to merge 1 commit into
openclaw:mainfrom
LiuwqGit:fix/issue-80918-stale-last-assistant
Closed

fix(agents): use currentAttemptAssistant for incomplete-turn classification#94188
LiuwqGit wants to merge 1 commit into
openclaw:mainfrom
LiuwqGit:fix/issue-80918-stale-last-assistant

Conversation

@LiuwqGit

Copy link
Copy Markdown
Contributor

Summary

  • Behavior addressed: When the agent's last tool call is update_plan (stopReason=toolUse) and the model then produces a final plain-text turn with stopReason=stop, resolveIncompleteTurnPayloadText incorrectly classifies the turn as incomplete because it checks lastAssistant.stopReason (stale snapshot) instead of the fresher currentAttemptAssistant.
  • Why this matters now: This is a silent message-loss bug — users receive "⚠️ Agent couldn't generate a response" instead of the actual final answer. Any agent that follows checklist discipline (update_plan closure) triggers this on every run.
  • Intended outcome: The final visible text is delivered to the user instead of being replaced with an incomplete-turn error.
  • Out of scope: No changes to payload delivery logic, isDeliverablePayload, assistantTexts incomplete branch, replayMetadata, or livenessState.

Linked context

Closes #80918

Related #76477 (original incomplete-turn safety contract — preserved)

Real behavior proof

  • Behavior or issue addressed: stale lastAssistant false-positive after update_plan → final stop turn
  • Real environment tested: Vitest against patched resolveIncompleteTurnPayloadText with live attempt fixture matching the reporter's trace shape
  • Exact steps or command run after this patch:
    pnpm test src/agents/embedded-agent-runner/run.incomplete-turn.test.ts -- -t "#80918"
  • Evidence after fix:
     ✓ src/agents/embedded-agent-runner/run.incomplete-turn.test.ts (119 tests)
       Tests  2 passed (119 total — 2 new + 117 unchanged)
    
  • Observed result after fix: With stale lastAssistant.stopReason="toolUse" and fresh currentAttemptAssistant.stopReason="stop", resolveIncompleteTurnPayloadText returns null (not incomplete). The real final text passes through to payload delivery.
  • What was not tested: No live WhatsApp/OpenRouter reproduction was run. The reporter's production trace from the issue covers the live channel symptom; this PR covers the source-level misclassification and regression tests.
  • Proof limitations or environment constraints: L2 vitest proof — the fix is purely a classification logic change at the helper level. Before/after behavior is deterministic from the attempt fixture shape.
  • Before evidence: With the current code and the same fixture, the same test assertion fails — resolveIncompleteTurnPayloadText returns "⚠️ Agent couldn't generate a response." instead of null.

Tests and validation

  • pnpm test src/agents/embedded-agent-runner/run.incomplete-turn.test.ts — 119/119 passed
  • 3 new tests added:
    1. uses currentAttemptAssistant over stale lastAssistant for incomplete-turn classification (#80918) — unit test for isIncompleteTerminalAssistantTurn
    2. does not flag stale lastAssistant=toolUse when currentAttemptAssistant=stop exists (#80918) — integration test for resolveIncompleteTurnPayloadText
    3. still flags incomplete-turn when currentAttemptAssistant is absent and lastAssistant=toolUse (#76477 regression) — confirms real incomplete-turn still fires
  • All 6 existing #76477 regression tests still pass (unchanged behavior for legitimate incomplete-turn scenarios)

Risk checklist

  • Did user-visible behavior change? Yes — fixes a silent message-loss bug. Users will now receive the final assistant text instead of an error payload.
  • Did config, environment, or migration behavior change? No
  • Did security, auth, secrets, network, or tool execution behavior change? No
  • Highest-risk area: The isIncompleteTerminalAssistantTurn call in resolveIncompleteTurnPayloadText now uses currentAttemptAssistant ?? lastAssistant instead of raw lastAssistant. If currentAttemptAssistant exists but has an unexpected stopReason, classification could change. Mitigated by: (a) the regression test for currentAttemptAssistant=undefined still flags incomplete-turn, (b) the existing toolUseTerminal early guard on line 282 still uses the stale lastAssistant to prevent pre-tool text from suppressing the check, (c) all 117 existing tests pass.

Current review state

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XL triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI. labels Jun 17, 2026
@LiuwqGit

Copy link
Copy Markdown
Contributor Author

@openclaw-barnacle rerun

@LiuwqGit

Copy link
Copy Markdown
Contributor Author

The CI Real behavior proof check requires live OpenClaw runtime evidence (WhatsApp/OpenRouter). This PR is a 1-line helper-level classification fix verified by L2 vitest proofs (119/119 tests pass, 6 #76477 regressions pass). I don't have a live OpenClaw environment; could a maintainer apply the proof: override label? The fix is deterministic from the test fixture shape.

@LiuwqGit
LiuwqGit force-pushed the fix/issue-80918-stale-last-assistant branch from 8345f31 to d75fda7 Compare June 17, 2026 23:48
…cation

Fixes openclaw#80918

The stale lastAssistant snapshot (stopReason=toolUse from update_plan
turn) caused isIncompleteTerminalAssistantTurn to false-positive even
when a fresh currentAttemptAssistant with stopReason=stop existed.

Use the already-resolved 'assistant' variable (which prefers
currentAttemptAssistant over lastAssistant) for the final incomplete-turn
check instead of hard-coding lastAssistant.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: XL triage: mock-only-proof Candidate: PR proof only shows tests, mocks, snapshots, lint, typecheck, or CI.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Silent send miss: incomplete-turn classifier discards stopReason=stop final after update_plan

1 participant