Skip to content

fix(ai): abort degenerate generation instead of streaming it to the cap - #1099

Closed
r0h1tb wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
r0h1tb:fix/repetition-guard
Closed

fix(ai): abort degenerate generation instead of streaming it to the cap#1099
r0h1tb wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
r0h1tb:fix/repetition-guard

Conversation

@r0h1tb

@r0h1tb r0h1tb commented Aug 9, 2026

Copy link
Copy Markdown

A thinking stream settled into emitting "The the the the the the the " back to
back 2,366 times — 79,222 characters — and stopped only when the user aborted by
hand. No layer owned this: provider streamers are faithful assemblers and never
inspect content, request construction attaches no repetition penalties, and the
agent loop treats a completed stream as progress. A runaway response could burn
the whole 32k output cap.

fixes #1029

Detection

A stream-level guard on the reasoning channel of openai-completions, with two
detectors:

  • Periodic tail — the tail is one short unit repeated many times. Found with
    the KMP failure function, which yields the smallest period of a string in
    O(n), so there is no scan over candidate period lengths.
  • Novelty stall — distinct word trigrams over total, for loops that drift
    enough that verbatim periodicity misses them.

The novelty floor is calibrated, not guessed

I first implemented near-duplicate paragraph clustering by word-trigram Jaccard,
following the prior art named in the issue. Measured, it does not work here:
drifting loops scored 0.38–0.58, and structurally similar legitimate paragraphs
scored in the same band. No threshold separates them, so I removed it.

Distinct-trigram novelty does separate. Measured over the corpus in the tests:

novelty
loop, verbatim 0.001
loop, drifting 0.136
enumerated analysis 0.286
markdown table 0.485
JSON payload 0.513
source code 0.611
numbered list 0.671
prose reasoning 1.000

0.20 sits in the gap with margin on both sides. The table is reproduced in the
module docstring so the number is not mistaken for a magic constant.

False positives are the real risk

A wrong abort destroys real work, which is worse than the loop it prevents. The
tests assert the guard does not fire on source code, markdown tables,
numbered lists, JSON, prose, enumerated analysis, whitespace padding, short
sub-threshold stutters, or anything under the minimum inspection length.

One of those tests caught a mistake of mine: my first "recovery text" repeated a
single sentence 40 times and the guard fired. It was right to — that shape is
degenerate. The test corpus was wrong, not the detector, and the comment now
says so.

The text channel is deliberately left unguarded. A long legitimate answer can
contain generated tables or code, and the cost asymmetry there is the wrong way
round. This is one of the two deviations the issue flagged as needing sign-off —
the other, default-on for all models, is how this ships. Kill switch:
PRIME_AGENT_NO_REPETITION_GUARD=1.

A second gap found on the way

openai-completions.ts was the only major provider that never called
recordStreamFailure — anthropic, google, google-vertex, amazon-bedrock,
mistral, openai-responses and azure-openai-responses all do. So failures on the
most widely used path (OpenRouter, llama.cpp, any OpenAI-compatible endpoint)
reached the session with no classification at all.

The new degenerate_output kind depends on it, but wiring it in fixes
classification for every existing failure kind on that provider too. Flagging it
because it is scope beyond the issue and easy to miss in review — happy to split
it out.

Verification

25 tests across two files. Unit tests cover detection and the false-positive
corpus; the end-to-end test drives the real streamOpenAICompletions against a
local SSE server that replays the incident (2,366 reasoning deltas of the loop
unit), following the existing pattern in
openai-completions-thinking-as-text.test.ts.

test/repetition-guard.test.ts                     21 passed
test/openai-completions-repetition-guard.test.ts   4 passed

End to end the stream now terminates as an error, classifies as
degenerate_output, and stops after under 20,000 characters against the
79,222 the incident produced. With the kill switch set, all 600 deltas stream
and the turn ends done.

All 9 existing openai-completions-* test files still pass (81 tests).
npm run check exits 0; the pre-commit hook ran it again independently.

Not done

No sampling penalties (frequency_penalty / presence_penalty) and no
cross-turn tool-call loop guard. Both are mentioned in the issue, both are
separate decisions, and neither is needed to stop this failure.

Note

Abort degenerate AI reasoning streams instead of forwarding repetitive output

  • Adds a RepetitionGuard in repetition-guard.ts that detects two degenerate patterns in streaming text: periodic tail repetition (via KMP failure function) and novelty stall (low trigram novelty ratio).
  • Wires the guard into the OpenAI completions stream handler to monitor the reasoning_content channel and throw a StreamFailureError early when degeneracy is detected.
  • Classifies the failure as degenerate_output with a repetition:... providerErrorType and attaches structured diagnostics via recordStreamFailure.
  • An env-based kill switch (REPETITION_GUARD_DISABLED_ENV) disables the guard and restores the original pass-through behavior.
  • Behavioral Change: streams that previously completed with repetitive reasoning content now abort with an error.

Macroscope summarized a17252d.

A thinking stream settled into emitting "The the the the the the the " back to
back 2,366 times, 79,222 characters, and stopped only when the user aborted by
hand. No layer owned this: provider streamers are faithful assemblers and never
inspect content, request construction attaches no repetition penalties, and the
agent loop treats a completed stream as progress. A runaway response could burn
the whole 32k output cap.

Adds a stream-level guard on the reasoning channel of openai-completions. Two
detectors:

- Periodic tail. The tail is one short unit repeated many times, found with the
  KMP failure function, which gives the smallest period in O(n) with no scan
  over candidate lengths.
- Novelty stall. Distinct word trigrams over total, for loops that drift enough
  that verbatim periodicity misses them.

The novelty floor is calibrated rather than guessed. Measured over the corpus in
the tests:

    loops   verbatim 0.001   drifting 0.136
    legit   enumerated analysis 0.286   markdown table 0.485   JSON 0.513
            source code 0.611   numbered list 0.671   prose 1.000

0.20 sits in that gap. Word-trigram Jaccard between paragraphs was tried first
and rejected: drifting loops scored 0.38-0.58 and structurally similar
legitimate paragraphs scored in the same band, so no threshold separated them.

False positives are the real risk, since a wrong abort destroys real work, so
the tests assert that code, markdown tables, numbered lists, JSON, prose and
enumerated analysis do not trip it. The text channel is deliberately left
unguarded: a long legitimate answer can contain generated tables or code.

On a hit the stream fails with a new degenerate_output failure kind rather than
committing the garbage. Kill switch: PRIME_AGENT_NO_REPETITION_GUARD=1.

Also wires recordStreamFailure into the openai-completions terminal catch.
Every other provider already called it; this one did not, so failures on the
most widely used path (OpenRouter, llama.cpp, any OpenAI-compatible endpoint)
reached the session with no classification at all. The new kind depends on it,
but this fixes classification for every existing kind on that provider too.

fixes PrimeIntellect-ai#1029
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@r0h1tb

r0h1tb commented Aug 10, 2026

Copy link
Copy Markdown
Author

Full packages/ai suite result, closing out the verification note above.

Test Files  50 passed | 21 skipped (71)
     Tests  340 passed | 493 skipped (833)
  Duration  37.77s
  exit 0

Two files are excluded from that run: test/stream.test.ts and
test/context-overflow.test.ts. Both do

execSync("which ollama");
describe.skipIf(!ollamaInstalled)(...)

so they skip on CI, which has no Ollama, but activate on a developer machine
that does and pull gpt-oss-20b (13 GB). Not related to this change — flagging
it only so the exclusion is not mistaken for avoiding a failure. Everything else
in the package runs, and the skipped tests are the usual credential-gated ones.

Together with the earlier runs: npm run check exits 0 (biome, tsgo --noEmit
over 903 files, installer render, browser smoke), all 9 existing
openai-completions-* files pass (81 tests), and the 25 new tests pass.

@sethkarten

Copy link
Copy Markdown
Contributor

Thank you for the report and proposed work. This root cause is now covered by maintainer-owned stacked PR #1165, authored independently from upstream/main.

We did not inspect or reuse this PR's diff, branch, commits, implementation code, or tests; its public description/comments were used only as a bug report. To keep one review surface, this PR is superseded by #1165 and is being closed.

The complete review stack is #1158#1165. It is being left unmerged for human review after CI and review-bot findings are cleared.

@sethkarten sethkarten closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No safeguard against degenerate model repetition: "The the the..." thinking stream until manual abort

2 participants