openclaw - ✅(Solved) Fix Gateway should strip trailing assistant messages before sending to provider API [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72556Fetched 2026-04-28 06:34:32
View on GitHub
Comments
2
Participants
2
Timeline
7
Reactions
0
Timeline (top)
referenced ×3commented ×2closed ×1cross-referenced ×1

Fix Action

Fixed

PR fix notes

PR #72666: fix(gateway): strip trailing assistant messages before context-engine assembly returns

Description (problem / solution / changelog)

Fixes #72556.

Problem

`installContextEngineLoopHook` (the per-iteration `afterTurn` + `assemble` wrapper for sessions where the context engine owns compaction) returns whatever the engine's `assemble` produced, falling back to the raw source messages on three paths: short-circuit (`hasNewMessages === false`), engine-failure (caught and swallowed), and assemble-returned-same-reference. None of those paths previously guarded against the transcript ending on an assistant turn.

When that view reaches the Anthropic SDK, the API hard-rejects with:

``` This model does not support assistant message prefill. The conversation must end with a user message. ```

The same defect breaks heartbeat injections, system-event bridge calls, and any session whose last persisted turn happens to be assistant. The reference-equality check (`assembled.messages !== sourceMessages`) the existing code uses to detect "no transform" has the additional sharp edge that plugins which short-circuit by returning the source array unchanged are silently treated as no-op — passing the raw, possibly-assistant-ending transcript straight through.

Fix

Add a small provider-agnostic helper `stripTrailingAssistantMessages` and apply it on all three return paths of the loop hook:

```ts export function stripTrailingAssistantMessages(messages: AgentMessage[]): AgentMessage[] { let lastNonAssistant = messages.length - 1; while (lastNonAssistant >= 0 && messages[lastNonAssistant]?.role === "assistant") { lastNonAssistant -= 1; } if (lastNonAssistant === messages.length - 1) { return messages; } return messages.slice(0, lastNonAssistant + 1); } ```

The helper preserves reference-equality on the no-trim fast path so downstream identity assumptions (`assembled.messages !== sourceMessages`) keep working unchanged. Stripping is provider-agnostic — Anthropic is the loudest case but other providers may also misbehave.

Scope is intentionally narrow: only the context-engine loop hook is touched. `installToolResultContextGuard` is left alone (its outputs always end on the inbound message which is whatever the agent loop just appended, not a separately-assembled view).

Tests

6 new unit tests for the helper:

  • Fast path returns input reference when last is non-assistant
  • Single trailing assistant dropped
  • Multiple consecutive trailing assistants dropped (matches the #72556 "raw assistant prefill" reproducer)
  • All-assistant input → empty array
  • Interleaved (preserves middle assistants, drops only trailing run)
  • Empty input returned unchanged

All 29 pre-existing tests in the file continue to pass (35/35 total). `pnpm tsgo:test` clean.

Diff

+89 / -3 across 1 production file + 1 test file + CHANGELOG (92 lines).

Changed files

  • src/agents/pi-embedded-runner/tool-result-context-guard.test.ts (modified, +48/-0)
  • src/agents/pi-embedded-runner/tool-result-context-guard.ts (modified, +40/-3)

Code Example

This model does not support assistant message prefill. The conversation must end with a user message.
RAW_BUFFERClick to expand / collapse

Feature / Hardening Request

The gateway should ensure conversations never end on an assistant turn before sending to provider APIs. Currently, if a context engine plugin (e.g. lossless-claw) short-circuits or returns messages unmodified, trailing assistant messages can reach the Anthropic API and cause a hard 400:

This model does not support assistant message prefill. The conversation must end with a user message.

Current behavior

installContextEngineLoopHook uses reference equality (assembled.messages !== sourceMessages) to detect whether the plugin transformed the messages. If the plugin returns the same array reference (common in short-circuit / bail-out paths), the gateway treats it as 'no transform' and uses the raw messages — which may end on an assistant turn.

Impact

  • Heartbeat injections fail when the session's last turn is assistant
  • System events and bridge calls fail with FailoverError
  • Any session that ends on an assistant turn is 'stuck' until a real user message arrives
  • This affects ALL users, not just those with context engine plugins

Expected behavior

Before sending to any provider SDK, the gateway should strip trailing assistant-role messages from the conversation. This is a provider-agnostic safety guard — Anthropic rejects them, but other providers may also behave unexpectedly.

Suggested approach

Add a message sanitizer in the SDK transport layer (or in installContextEngineLoopHook's final output) that drops trailing assistant messages. This protects against:

  1. Plugin short-circuits returning raw messages
  2. Sessions where the last turn was genuinely an assistant response
  3. Race conditions between system event injection and conversation state

Version

OpenClaw 4.24, Anthropic claude-opus-4-6, lossless-claw 0.9.2

extent analysis

TL;DR

Implement a message sanitizer to strip trailing assistant-role messages from conversations before sending to provider APIs.

Guidance

  • Identify the installContextEngineLoopHook function and modify it to check the content of the messages array instead of relying on reference equality.
  • Add a message sanitizer in the SDK transport layer to drop trailing assistant messages, protecting against plugin short-circuits and sessions ending on an assistant turn.
  • Verify the fix by testing conversations that previously failed due to trailing assistant messages, ensuring they now succeed.
  • Consider adding logging or monitoring to detect and report any conversations that are modified by the message sanitizer.

Example

// Pseudo-code example of message sanitizer
function sanitizeMessages(messages) {
  while (messages.length > 0 && messages[messages.length - 1].role === 'assistant') {
    messages.pop();
  }
  return messages;
}

Notes

This solution assumes that the installContextEngineLoopHook function has access to the conversation messages and can modify them before sending to provider APIs. Additionally, the message sanitizer should be designed to handle edge cases, such as empty conversations or conversations with only assistant messages.

Recommendation

Apply workaround: Implement the message sanitizer in the SDK transport layer to ensure conversations never end on an assistant turn before sending to provider APIs, as this provides a provider-agnostic safety guard.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Before sending to any provider SDK, the gateway should strip trailing assistant-role messages from the conversation. This is a provider-agnostic safety guard — Anthropic rejects them, but other providers may also behave unexpectedly.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING