Skip to content

fix(agent): cut degenerate thinking-stream runs, quote litter and word loops before storage and echo - #117756

Open
maebahesioru wants to merge 3 commits into
NousResearch:mainfrom
maebahesioru:fix/reasoning-stream-repetition-guard
Open

maebahesioru wants to merge 3 commits into
NousResearch:mainfrom
maebahesioru:fix/reasoning-stream-repetition-guard

Conversation

@maebahesioru

@maebahesioru maebahesioru commented Sep 21, 2026 •

Copy link
Copy Markdown

What does this PR do?

Cuts a thinking/reasoning stream that has degenerated into a growing single-char run (「「「「「「実行」」」」」」…, run length exploding line over line) before the looped bytes reach the live display, the stored transcript, or the next request. The turn itself survives — visible content and tool calls keep streaming normally.

A second shape is covered too: quote litter — openers inserted between tokens and mostly never closed (「の「diff「全体「像), net unmatched openers climbing steadily with no long run anywhere, so the run rules never fire. Chronic in long-lived sessions (1143 rows across 4 sessions in the same corpus scan). Rule: ≥600 chars fed, ≥20 net unmatched 「, ≥1.5% of chars unmatched, and quoting-dense (≥5 openers) in at least half of the 200-char bins — fires on ~93% of the chronic rows, zero fires elsewhere (analysis sessions that quote degenerate excerpts peak at a 1.4% rate and a 0.27 dense-bin fraction, under the thresholds).

A third shape is word-level: one word (nearly always the English article) swells until it fills the channel — the move: the the the crossing: the the … — with no character run anywhere, so both rules above stay silent (a 65k-token half of pure the appears in the corpus). Rule: a word-level pass that trips when ONE word both repeats and dominates — a ≥10-run of an article/filler cue word (the/a/an/hmm) or ≥25 consecutive of any other word, at ≥30% word share over ≥50 words; the spread shape (runs of only 2-4, the article wedged between tokens) is covered by an adjacent-pair gate (≥8 adjacent pairs of a cue word, ≥40% share, ≥60 words). Calibrated on the same 149k-message corpus: fired on 286 degenerate rows across 4 chronic sessions and on zero healthy rows — explicitly audited silent on the shapes that must never trip: xx hex dumps, Down Down command runs, [False, False] arrays, code discussion around repeated variable names (pc), and HTML div listings.

This is the thinking-channel complement to the streaming repetition guards under review (#89294, #78551), not a duplicate of them:

Why the echo matters: providers that require a reasoning_content echo on tool-call replays (DeepSeek V4 / Kimi / MiMo — _needs_thinking_reasoning_pad) replay stored reasoning into the next request. Once a session starts looping, the echoed bytes re-seed the pattern on every following turn; a captured session kept re-degenerating across turns until the stored bytes were cut. Cutting at the storage boundary is therefore part of the fix, not just a display nicety.

Related Issue

No linked issue — root-caused and reproduced from captured session data (real sessions, ~175k stored reasoning messages). Happy to file one if the project prefers issue-first for this class.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/repetition_guard.py — new ReasoningLoopGuard (incremental, O(chars)) + sanitize_degenerate_reasoning() + THINKING_LOOP_TRUNCATED marker. Trips on runs of 「」『』 ≥ 12, any non-formatting char run ≥ 160, the quote-litter rule, or the word-loop rules (≥10-run of a cue word / ≥25 of any word at ≥30% word dominance; adjacent-pair spread gate at ≥8 pairs / ≥40% / ≥60 words). Thresholds calibrated over the full local corpus (~175k reasoning messages): zero fires on the non-degenerate remainder; all 40 captured run-loop messages, ~93% of 1143 chronic litter rows, and 286 word-loop rows across 4 chronic sessions fire. Word runs crossing deltas stay continuous (a pending word is carried across feeds).
  • agent/chat_completion_helpers.py
    • _call_chat_completions (streaming): the reasoning accumulator feeds the guard per delta; on trip it keeps the clean prefix, removes the looped tail from reasoning_parts, stops emitting further reasoning deltas and emits the marker once. The stream keeps being consumed — content/tool calls are unaffected.
    • _assistant_reasoning_text + build_assistant_message: sanitize at the extraction/storage boundary, covering non-streaming intakes and every wire that stores through these paths.
  • tests/agent/test_reasoning_loop_guard.py — behavior contracts: cut-at-run-start semantics, chunked-feeding equivalence (char and word paths), healthy-shape no-fire, quote-litter cut + runway floor + analysis-quoting no-fire, word-loop cut + dominance/volume no-fire + quoting no-fire + non-cue runaway runs.

How to Test

  1. Unit: scripts/run_tests.sh tests/agent/test_reasoning_loop_guard.py → 11 passed (the sibling repetition-guard files and the full suite run in CI).
  2. Degenerate input: sanitize_degenerate_reasoning("手順。\n" + "「" * 30 + "実行" + "」" * 30) → "手順。" + "\n\n" + "[thinking truncated: repetition loop detected]".
  3. Live: a real streaming chat completion on the patched tree is unaffected for healthy turns.

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — pure-Python, no platform-specific code

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/streaming Streaming responses: gateway delivery, provider wire sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 21, 2026
@maebahesioru

maebahesioru commented Sep 21, 2026 •

Copy link
Copy Markdown
Author

Local run note from the author (updated):

  • Focused suites pass: tests/agent/test_reasoning_loop_guard.py + the two existing repetition-guard files → 11 passed.
  • My earlier failure list was an environment gap, not the change: this machine's runtime venv lacked optional provider SDKs (24 of the failures were ImportError: The 'anthropic' package is required; the rest fell back to the wrong client class). After installing the CI extras set into a dedicated dev venv (uv sync --locked --extra all --extra dev --extra anthropic --extra mistral ..., same as tests.yml), all 38 previously-failing files pass: 1060 passed, 0 failed.
  • A local full-suite run reached 48% (25,964 passed) before I stopped it to free the workstation. One failure appeared mid-run (tests/hermes_cli/test_cmd_update.py::TestRepairCurrentCheckoutRuntimeRepair::test_venv_repair_path_refreshes_memory_provider, a subprocess/multiprocessing error); it passes on rerun → flake, unrelated to this change.
  • Leaving the complete run to CI.

Thinking streams can degenerate into growing single-char runs — 「「「「「「実行」」」」」」
with the run length exploding line over line — while the visible reply stays healthy.
The visible-text repetition guard only runs on truncation/interrupt paths and needs
60-char exact repeats, so it misses this shape entirely (17 of 21 messages in a
captured incident set). Providers that require a reasoning_content echo on tool-call
replays (DeepSeek V4 / Kimi / MiMo) then replay the looped bytes into the next request
and re-seed the loop on every following turn.

- ReasoningLoopGuard: incremental detector for the thinking channel; trips on
  「」『』 runs >= 12 or any non-formatting char run >= 160. Calibrated over ~175k real
  reasoning messages: 0 fires on the non-degenerate remainder, all 40 captured
  degenerate messages trip.
- chat_completions streaming: cut reasoning_parts at the run start, stop emitting
  further reasoning deltas, emit a single marker; the stream keeps being consumed and
  content/tool calls are unaffected.
- Non-streaming intakes: _assistant_reasoning_text + build_assistant_message sanitize
  at the extraction/storage boundary so the echo never replays looped bytes.
- Tests: cut semantics, chunked-feeding equivalence, healthy-shape no-fire.
Run growth was only one of two observed shapes. The other: openers inserted
between tokens and mostly never closed (「の「diff「全体「像) — net unmatched
openers climbing steadily with no >=12 run anywhere, so the run rules never fire.

Rule: >=600 chars fed, >=20 net unmatched 「, net >= 1.5% of chars, and quoting-
dense (>=5 openers) for at least half of the 200-char bins. Calibrated on a
122,889-message corpus scan: fires on ~93% of the 1143 chronic-litter rows in
4 long-lived sessions; zero fires elsewhere, including the 22 rows of a session
that quotes degenerate excerpts for analysis (peak 1.4% rate, 0.27 bin fraction).
The cut backs up to where the net openers started piling up.
@maebahesioru
maebahesioru force-pushed the fix/reasoning-stream-repetition-guard branch from a5864cd to 9da08be Compare September 25, 2026 03:28
@maebahesioru maebahesioru changed the title fix(agent): cut degenerate thinking-stream loops before storage and echo fix(agent): cut degenerate thinking-stream loops and quote litter before storage and echo Sep 25, 2026
@maebahesioru
maebahesioru force-pushed the fix/reasoning-stream-repetition-guard branch from 23d87b4 to 34ad026 Compare September 25, 2026 04:59
One word (nearly always the English article) can swell until it fills a
thinking channel - 'the the the ...' - with no character runs at all, so
both the run rules and the quote-litter rule stay silent (a 65k-token
half appears in one corpus incident). Add a word-level pass to
ReasoningLoopGuard: trips when one word both repeats (a >=10 run of an
article/filler cue word, or >=25 of any other word) and dominates the
message (>=30% of words on >=50 words; adjacent-pair shape at >=8 pairs
/ >=40% / >=60 words). Calibrated on a 149k-message corpus: fires on 286
degenerate rows across 4 chronic sessions, never on healthy rows -
audited silent on xx-dumps, Down Down command runs, [False, False]
arrays, variable-name (pc) discussion and HTML div listings.
@maebahesioru
maebahesioru force-pushed the fix/reasoning-stream-repetition-guard branch from 34ad026 to aa5802b Compare September 25, 2026 05:00
@maebahesioru maebahesioru changed the title fix(agent): cut degenerate thinking-stream loops and quote litter before storage and echo fix(agent): cut degenerate thinking-stream runs, quote litter and word loops before storage and echo Sep 25, 2026
@maebahesioru

Copy link
Copy Markdown
Author

Third shape pushed: word-level loops (the the the …), commit aa5802b1.

  • The pass trips when one word both repeats and dominates the message: a ≥10-run of an article/filler cue word (the/a/an/hmm) or ≥25 consecutive of any other word, at ≥30% word share over ≥50 words fed; the spread shape (runs of only 2-4, article wedged between tokens) is covered by an adjacent-pair gate (≥8 pairs of a cue word, ≥40% share, ≥60 words).
  • Corpus calibration (149k stored reasoning messages): fires on 286 degenerate rows across 4 chronic sessions, zero healthy fires. Verified silent on the shapes that must never trip: xx hex dumps, Down Down Down command runs, [False, False] arrays, code discussion around a variable named pc, HTML div listings.
  • Streaming continuity: a short pending word is carried across deltas, so a run crossing chunk boundaries counts as one run (test asserts chunked feed lands on the same trip offset as a single feed). Cost stays incremental.
  • Tests: tests/agent/test_reasoning_loop_guard.py → 11 passed (5 new word-rule tests: cut at the onset, chunked equivalence, dominance/volume no-fire, quoting no-fire, non-cue runaway run).

This branch has not been deployed

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

Labels

area/streaming Streaming responses: gateway delivery, provider wire comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants