Skip to content

fix(context_compressor): keep tool-call arguments JSON valid when shrinking#12259

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-e6fb870f
Apr 18, 2026
Merged

fix(context_compressor): keep tool-call arguments JSON valid when shrinking#12259
teknium1 merged 2 commits into
mainfrom
hermes/hermes-e6fb870f

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Context compression Pass 3 now produces valid JSON when shrinking long tool-call arguments, ending the MiniMax HTTP 400 (code 2013) "invalid function arguments json string" loop that poisoned sessions on every subsequent turn.

Root cause: _prune_old_tool_results Pass 3 sliced raw function.arguments at byte 200 and appended ...[truncated], producing unterminated JSON. Strict providers (MiniMax, Anthropic via LiteLLM) rejected it non-retryably; the broken call stayed in session history, so every turn re-sent the same malformed payload → stuck session until /new.

Changes

  • agent/context_compressor.py: add _truncate_tool_call_args_json() — parses args, recursively shrinks long string leaves inside the parsed structure, re-serializes. Non-string leaves (paths, ints, lists) pass through. Non-JSON args returned unchanged.
  • tests/agent/test_context_compressor.py: 8 new tests (7 direct + 1 E2E Pass 3 reproducing the exact failure payload from the incident).
  • scripts/release.py: AUTHOR_MAP entry for @honghua.

Validation

Before After
write_file with 671-char args through Pass 3 Unterminated string at column 71 valid JSON, path preserved, content cleanly truncated
Non-JSON args (MiniMax textual recovery path) would double-corrupt passed through unchanged
CJK / emoji content bloated to \uXXXX preserved as-is (ensure_ascii=False)
Nested structures whole blob sliced walks dicts/lists, shrinks only string leaves
tests/agent/test_context_compressor.py 48/48 passed locally

E2E harness confirmed the pre-fix output fails json.loads() with Unterminated string starting at: line 1 column 71 — exact match for MiniMax's code=2013 rejection observed in the wild.

Issues closed

Credits

All three PRs independently diagnosed the same bug on Apr 17 within 8 hours of each other. Going with honghua's implementation because it preserves arg structure (path/ints/lists stay intact) so the model retains more tool-call context after compression.

honghua and others added 2 commits April 18, 2026 12:34
…inking

Pass 3 of `_prune_old_tool_results` previously shrunk long `function.arguments`
blobs by slicing the raw JSON string at byte 200 and appending the literal
text `...[truncated]`. That routinely produced payloads like::

    {"path": "/foo.md", "content": "# Long markdown
    ...[truncated]

— an unterminated string with no closing brace. Strict providers (observed
on MiniMax) reject this as `invalid function arguments json string` with a
non-retryable 400. Because the broken call survives in the session history,
every subsequent turn re-sends the same malformed payload and gets the same
400, locking the session into a re-send loop until the call falls out of
the window.

Fix: parse the arguments first, shrink long string leaves inside the parsed
structure, and re-serialise. Non-string values (paths, ints, booleans, lists)
pass through intact. Arguments that are not valid JSON to begin with (rare,
some backends use non-JSON tool args) are returned unchanged rather than
replaced with something neither we nor the provider can parse.

Observed in the wild: a `write_file` with ~800 chars of markdown `content`
triggered this on a real session against MiniMax-M2.7; every turn after
compression got rejected until the session was manually reset.

Tests:
- 7 direct tests of `_truncate_tool_call_args_json` covering valid-JSON
  output, non-JSON pass-through, nested structures, non-string leaves,
  scalar JSON, and Unicode preservation
- 1 end-to-end test through `_prune_old_tool_results` Pass 3 that
  reproduces the exact failure payload shape from the incident

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants