Skip to content

fix(truncation): combine compressor marker + write guard (#83714) - #83858

Closed
djbclark wants to merge 3 commits into
NousResearch:mainfrom
djbclark:fix/truncation-combined-83714
Closed

fix(truncation): combine compressor marker + write guard (#83714)#83858
djbclark wants to merge 3 commits into
NousResearch:mainfrom
djbclark:fix/truncation-combined-83714

Conversation

@djbclark

Copy link
Copy Markdown

Summary

Combined local-deploy stack for #83714 that merges:

  1. fix(context-compressor): stop priming models to abbreviate their own tool-call arguments #83843 — stop priming models via bare ...[truncated] in compressed assistant tool_call args
  2. fix(tools): refuse patch/write_file when content contains a truncation placeholder #83752 — fail-closed write/patch guard
  3. Clearly-better pieces of fix(file-tools): abort write/patch when AI truncation placeholders detected #68512 — count-based signature comparison + AI "unchanged"/"rest of file" stubs
  4. Review follow-up — shared tools/truncation_markers.py so compressor marker text and write-guard detection cannot drift; refuse ⟪HERMES-CONTEXT-COMPRESSION…⟫ on write

Why a combined PR

The two independent PRs are still valid and MERGEABLE on their own. This PR is the integration branch we deployed locally (plus the shared-constant follow-up requested after review). Maintainers can land the split PRs or this combined one.

Tests

pytest tests/tools/test_file_tools.py tests/agent/test_context_compressor.py tests/test_trajectory_compressor.py -q
216 passed, 2 skipped, 2 pre-existing macOS /tmp vs /private/tmp mock failures (unrelated).

Relationship

Refs: #83714, #83752, #83843, #68512, #11762, #20805

djbclark and others added 3 commits August 11, 2026 07:18
… into replayed tool_calls

Root cause for NousResearch#83714 (write_file/patch_tool writing literal
"...[truncated]" into files, PR NousResearch#83752's guard is the safety net, not
the fix): _truncate_tool_call_args_json() in the compression pass
shrinks long string values inside a PAST assistant message's
tool_calls[].function.arguments — the exact field that represents the
model's own prior generated output, replayed back to it verbatim on
every subsequent turn. The old marker, a bare "...[truncated]" suffix,
is indistinguishable from something the model itself could have
written (it's exactly the kind of terse ellipsis abbreviation models
already produce). A model conditioned on seeing itself "get away with"
that pattern in its own history imitates it in a new tool call,
writing the literal marker instead of real content.

This is the second bug from the same root text. The first (NousResearch#11762,
MiniMax 400s from unterminated JSON) was fixed by shrinking inside the
parsed structure so the JSON stays valid, but kept the same visible
marker text — fixing the syntax problem while leaving the imitation
problem untouched.

Fix: replace the marker with one deliberately NOT shaped like prose a
model would write — distinctive non-ASCII delimiters, an explicit "not
part of the original tool call" disclaimer, and a per-instance
char-count that won't match the next omission point even if copied
verbatim. The shrunk value stays a plain string (not a nested object)
so the NousResearch#11762 valid-JSON/matching-shape contract is unchanged — only
the marker text changed.

Checked context_compressor.py's other "...[truncated]" call sites
(_serialize_for_summary, _compact_fallback_turn, the user-message-only
one near _ACTIVE_TASK_MAX_CHARS) — none of them write into a value
that gets replayed as the main model's own assistant/tool_calls
history, so they don't share this priming risk and were left as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n placeholder

Fixes NousResearch#83714. write_file_tool and patch_tool (both replace and V4A modes)
wrote the model's function-call arguments straight to disk with no check
for AI truncation-placeholder markers like "...[truncated]". When a model
(observed with deepseek-v4-pro via deepseek) abbreviated a long new_string
instead of emitting it in full, the literal marker text landed in the
file, corrupting it.

No length-based truncation of `content`/`new_string`/`patch` exists
anywhere between JSON-parsing the tool call and the write/patch calls, so
this rules out Hermes truncating the parameter in transit. The likely
mechanism: Hermes itself uses this exact marker to signal truncated tool
*output* elsewhere (todo_tool.py, file_operations.py's per-line cap,
mcp_tool.py), and the model is imitating a pattern from its own context.

Adds `_find_truncation_placeholder()` and wires it into:
- write_file_tool: scans `content`
- patch_tool replace mode: scans `new_string`, skipping markers already
  present in `old_string` (avoids false positives on legitimate edits to
  text that mentions the marker)
- patch_tool V4A mode: scans only ADDED content via
  `_extract_v4a_added_content()` (parses the patch and joins '+' hunk
  lines plus Add-File bodies), not the whole patch text, so a patch that
  removes or merely anchors context on a placeholder-like literal isn't
  flagged

Falls back to raw-text scanning if the V4A patch fails to parse, so
detection degrades gracefully instead of silently skipping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…unt-based guard

- Add tools/truncation_markers.py as the single source for the
  HERMES-CONTEXT-COMPRESSION template and write-path detection
- context_compressor imports the shared marker (no drift with the guard)
- file_tools uses count-based comparison (NousResearch#68512) and also refuses the
  new compressor marker plus AI "unchanged"/"rest of file" stubs
- Regression tests for count-based allowance and compression-marker reject

Combines NousResearch#83843 + NousResearch#83752 + the clearly-better pieces of NousResearch#68512 for local deploy.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tools Tool registry, model_tools, toolsets tool/file File tools (read, write, patch, search) P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 11, 2026
@djbclark

Copy link
Copy Markdown
Author

Closing this as redundant noise, not as a rejection of the work.

The canonical upstream path for #83714 is the two focused PRs already open:

  1. fix(context-compressor): stop priming models to abbreviate their own tool-call arguments #83843 — root cause (stop injecting an imitable ...[truncated] into compressed assistant tool_calls args)
  2. fix(tools): refuse patch/write_file when content contains a truncation placeholder #83752 — defense in depth (refuse write/patch when a truncation placeholder would hit disk)

This combined PR was only an operator convenience branch for a local install. It is not needed for review, and leaving three overlapping PRs open is harder on maintainers than two clean ones.

Optional follow-ups (shared marker module, count-based comparison inspired by #68512) can land as small commits on #83752 / #83843 if wanted after those merge — no third PR required.

Sorry for the extra surface area.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/file File tools (read, write, patch, search) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants