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
Open
fix(agent): cut degenerate thinking-stream runs, quote litter and word loops before storage and echo#117756maebahesioru wants to merge 3 commits into
maebahesioru wants to merge 3 commits into
Conversation
Author
|
Local run note from the author (updated):
|
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
force-pushed
the
fix/reasoning-stream-repetition-guard
branch
from
September 25, 2026 03:28
a5864cd to
9da08be
Compare
maebahesioru
force-pushed
the
fix/reasoning-stream-repetition-guard
branch
from
September 25, 2026 04:59
23d87b4 to
34ad026
Compare
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
force-pushed
the
fix/reasoning-stream-repetition-guard
branch
from
September 25, 2026 05:00
34ad026 to
aa5802b
Compare
Author
|
Third shape pushed: word-level loops (
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 puretheappears 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:xxhex dumps,Down Downcommand runs,[False, False]arrays, code discussion around repeated variable names (pc), and HTMLdivlistings.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_contentecho 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
Changes Made
agent/repetition_guard.py— newReasoningLoopGuard(incremental, O(chars)) +sanitize_degenerate_reasoning()+THINKING_LOOP_TRUNCATEDmarker. 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 fromreasoning_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
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).sanitize_degenerate_reasoning("手順。\n" + "「" * 30 + "実行" + "」" * 30)→"手順。" + "\n\n" + "[thinking truncated: repetition loop detected]".Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — N/Acli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A