Conversation
…er turn
_ensure_leading_user_turn() prepends a user turn when a compacted window
starts with an assistant message (Anthropic requires messages[0].role ==
user). It used a single space " " as the placeholder text, but Anthropic's
Messages API rejects empty/whitespace-only text content blocks with HTTP
400 "messages: text content blocks must contain non-whitespace text".
Because messages[0] is re-sent on every turn, any conversation whose
compacted window leads with an assistant turn wedges permanently behind
the same 400 — both primary and fallback models fail identically, so the
user only sees repeated 'model provider failed' banners. Observed in a
long-lived gateway (Discord) session after a second context compaction.
Use the module's existing _EMPTY_TEXT_PLACEHOLDER ("(empty)") instead,
mirroring the sanitize paths (_safe_text / _sanitize_replay_block).
Adds TestEnsureLeadingUserTurn regression coverage and updates two
change-detector assertions in test_anthropic_adapter.py that had frozen
the buggy " " value.
Ref NousResearch#69512.
|
Thanks for isolating the synthesis-site gap. The premise remains valid on current The proposed replacement with The branch predates substantial test pruning after base Automated hermes-sweeper review. |
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Four PRs address the same assembly-time Anthropic whitespace failure. #70081, #70159, and #71935 replace only the invalid synthetic leading-user placeholder, while #70910 fixes that root cause with the canonical sentinel and additionally guards pre-shaped message blocks and cache-controlled system blocks in the fully assembled request.
Related pull requests
- #70081
duplicate— (+46/-3) — close as duplicate of #70910: it fixes the synthesized leading-user block but hard-codes "(empty)" instead of using_EMPTY_TEXT_PLACEHOLDER; despite the keep_open review on #70081, #70910 contains the same source fix using the requested canonical constant and covers additional assembly-time blank-block paths. - #70159
duplicate— (+8/-3) — close as duplicate of #70910: it changes the same synthesized block to "." and updates the existing tests to assert non-whitespace behavior; despite the keep_open review on #70159, #70910 fixes the identical site with_EMPTY_TEXT_PLACEHOLDERand adds coverage for blank pre-shaped and system blocks. - #70910
best fix— (+165/-3) — keep open with a salvage path: this is the recorded best fix for #70909, using_EMPTY_TEXT_PLACEHOLDERat the proven synthesis site and adding a final assembled-request guard for the independently reproduced pre-shaped-content and cache-controlled-system cases. Preserve the root-cause hunk, the final text-block-only guard, and focused regressions while reconciling the test additions with current main. - #71935
fixes— (+48/-3) — close as duplicate of #70910: its source hunk correctly uses_EMPTY_TEXT_PLACEHOLDER, but its scope remains limited to the synthesized leading turn. Despite the MAINTAINER-BOT keep_open verdict on #71935, the complete diff of #70910 contains that same fix plus tested request-level protection for two additional blank-block paths, so #71935 has no distinct implementation to salvage.
Duplicates
#70081, #70159, and #71935 duplicate the synthesized-leading-user fix already contained in #70910; #70081 and #71935 are especially close at the source level, while #70159 differs only by choosing "." rather than the canonical placeholder.
Suggested consolidation
Keep #70910 open with a salvage path: retain its canonical _EMPTY_TEXT_PLACEHOLDER root-cause fix, final text-block-only request guard, and focused regressions, rebasing or reconciling the tests against current main as needed. Close #70081, #70159, and #71935 as duplicates of #70910 because their diffs cover only the root-cause subset and add no independent behavior beyond the recorded best fix.
Complex graph
flowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I70909(["issue #70909 (open)"])
subgraph Dup70081 ["PRs duplicating each other"]
P70081["PR #70081 (open)"]
P70159["PR #70159 (open)"]
P71935["PR #71935 (open)"]
end
P71935 -->|fixes| I70909
class I70909 open
class P70081 open
class P70159 open
class P71935 open
class P71935 target
click I70909 "https://github.com/NousResearch/hermes-agent/issues/70909"
click P70081 "https://github.com/NousResearch/hermes-agent/pull/70081"
click P70159 "https://github.com/NousResearch/hermes-agent/pull/70159"
click P71935 "https://github.com/NousResearch/hermes-agent/pull/71935"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).
Cross-PR triage: Reviewed 4 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 21 kB of PR diffs, 13 kB of issue/PR text, 4 kB of discussion (8 comments), 8 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
Problem
_ensure_leading_user_turn()inagent/anthropic_adapter.pyprepends a synthetic user turn when a compacted message window starts with an assistant message (Anthropic requiresmessages[0].role == "user"). The placeholder text was a single space" ".Anthropic's Messages API rejects empty/whitespace-only text content blocks:
Because
messages[0]is re-sent on every turn, any conversation whose compacted window leads with an assistant turn (e.g. after a second context compaction whereprotect_headhas decayed to 0) wedges permanently behind the same 400. Both the primary model and the fallback receive the identical malformed payload and fail identically — a non-retryable client error — so the user only ever sees repeated "model provider failed / switching to fallback" banners with no recovery.Observed live in a long-lived gateway (Discord) session: every inbound message 400'd across restarts until the placeholder was fixed. It never self-heals because the offending block is structural, not content the user can edit away.
Fix
Use the module's existing
_EMPTY_TEXT_PLACEHOLDER("(empty)") — the same non-whitespace placeholder already used by the sanitize paths (_safe_text,_sanitize_replay_block,_convert_assistant_message). This is a one-line behavioral change; the surrounding whitespace-coercion machinery was already in place (ref #69512) but this synthesis site was missed.Tests
TestEnsureLeadingUserTurnintests/agent/test_anthropic_whitespace_text_blocks.py(4 cases): prepends when first is assistant, placeholder is non-whitespace, no-op when already user-led, empty list untouched.tests/agent/test_anthropic_adapter.pythat had frozen the buggy" "value to assert_EMPTY_TEXT_PLACEHOLDERinstead.scripts/run_tests.sh tests/agent/test_anthropic_whitespace_text_blocks.py tests/agent/test_anthropic_adapter.py→ 198 passed. (3 unrelatedTestRunOauthSetupTokenfailures are pre-existing onmain— a pytest-9 MagicMock/json.loadsinteraction — and are untouched by this change.)Ref #69512.