Skip to content

fix(assistant): send post-compaction summary as user message (GLM/Z.ai 400 after compaction) - #218

Draft
juacker wants to merge 1 commit into
mainfrom
fix/glm-post-compaction-no-user-message
Draft

juacker wants to merge 1 commit into
mainfrom
fix/glm-post-compaction-no-user-message

Conversation

@juacker

@juacker juacker commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

With GLM (Z.ai, OpenAI-compatible) models only, the first request after an automatic conversation compaction fails with HTTP 400 "The messages parameter is illegal". Anthropic and OpenAI models are unaffected. Notably, this even killed a long-running agent task mid-flight (the task's own session compacted and the next request 400'd).

Root cause

When a compaction cut lands after the session's last user message (the norm in long agentic turns — user message, then a long assistant/tool chain), the provider-bound history is:

[System(prompt), System(summary), Assistant, Tool, ...]   ← no User message at all

Z.ai rejects user-less message arrays; the other providers tolerate them — which is exactly why the bug was GLM-only and only right after a compaction.

Reproduced twice from this app's own history DB (runs ee2ff7de, b4ca9e36 in session b884a31a, after compactions 1216b39e and 5c1176ef): the message immediately after the compaction summary row had role assistant, and the failing provider request carried no user message.

Fix

At the provider boundary only, map the persisted System-role compaction summary through a new provider_bound_summary() that emits role User:

  • Outbound request becomes [System(prompt), User(summary), Assistant, Tool, ...] — user-led on every provider.
  • Persisted rows are unchanged (still System with the compaction metadata); the flip applies only to the provider-bound copy.
  • normalize_history_for_provider already merges consecutive user messages, so Anthropic never sees adjacent user turns; OpenAI passes roles 1:1; the CLI path filters summaries by metadata and never sees the flipped copy.
  • The persisted SUMMARY_MESSAGE_PREAMBLE stays the single framing text — no double framing.

Tests

All fail on revert (trace-verified):

  • compacted_history_with_only_tool_tail_leads_with_user_summary — user-less assistant/tool tail → history leads with User(summary).
  • compacted_history_with_empty_tail_is_summary_only — cut at the final session message → history is exactly [User(summary)], the minimal GLM-failing shape.
  • compacted_history_with_mixed_tail_keeps_tail_after_user_summary — mixed tail ordering preserved.
  • normalize_keeps_post_compaction_history_user_led (engine.rs) — goes through the real provider_history_messages_with_compaction + normalize_history_for_provider with a persisted-shape fixture, asserting [System, User, Assistant, Tool] after the system prompt is prepended.

Validation

cargo check, cargo clippy --all-targets -- -D warnings, cargo fmt — all clean. (cargo test intentionally not run locally — it corrupts generated TS bindings; CI will run the suite.)

Review

Three independent passes (two reviewer rounds + domain-expert final gate), verdicts recorded in the workspace: production_quality, no open findings.

When an automatic compaction cut lands after the session's last user
message (long agentic turns), the provider-bound history was
[System(prompt), System(summary), Assistant, Tool, ...] with no user
message. Z.ai/GLM rejects user-less message arrays with HTTP 400
"The messages parameter is illegal"; Anthropic and OpenAI tolerate
them, which is why the failure was GLM-only and only right after a
compaction.

Map the persisted System-role summary through a new
provider_bound_summary() role flip at the provider boundary so the
outbound request is user-led. Persisted rows keep role System; the
flipped copy is provider-bound only. The persisted
SUMMARY_MESSAGE_PREAMBLE remains the single framing text.

Tests: compaction.rs covers tool-only tail, mixed tail, and the empty
tail (history is exactly [User(summary)] -- the minimal GLM-failing
shape); engine.rs pins the full path through
provider_history_messages_with_compaction +
normalize_history_for_provider (made pub(crate) for the test) and
asserts [System, User, Assistant, Tool] after the system prompt is
prepended. All tests fail on revert.
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.

1 participant