Skip to content

fix(telegram): suppress bare Reasoning: prefix leak during Gemini streaming#37892

Open
okuyam2y wants to merge 2 commits intoopenclaw:mainfrom
okuyam2y:fix/gemini-reasoning-leak
Open

fix(telegram): suppress bare Reasoning: prefix leak during Gemini streaming#37892
okuyam2y wants to merge 2 commits intoopenclaw:mainfrom
okuyam2y:fix/gemini-reasoning-leak

Conversation

@okuyam2y
Copy link
Copy Markdown

@okuyam2y okuyam2y commented Mar 6, 2026

Summary

Fixes #37890

  • Add guard for bare Reasoning: / Reasoning:\n during streaming partials
  • trim() collapses "Reasoning:\n" to "Reasoning:" which doesn't match REASONING_MESSAGE_PREFIX and falls through to answerText, leaking the prefix into user-visible output
  • The new regex /^Reasoning:?\s*$/ catches these partial chunks and returns {} (suppress) before they reach the answer path

Test plan

  • Added test cases for "Reasoning:", "Reasoning:\n", and "Reasoning: \n"
  • Added test confirming multi-paragraph raw Gemini reasoning stays classified as reasoningText
  • pnpm exec vitest run src/telegram/reasoning-lane-coordinator.test.ts — 6 tests pass
  • pnpm check passes

cc @AyaanZaidi — you recently worked on draft materialize in this area (#36746)

@openclaw-barnacle openclaw-barnacle bot added channel: telegram Channel integration: telegram size: XS labels Mar 6, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR fixes a prefix leak in the Telegram Gemini streaming path where bare Reasoning:\n partial chunks were being surfaced as user-visible answer text. The root cause was that text.trim() collapses "Reasoning:\n" to "Reasoning:", which no longer matches the REASONING_MESSAGE_PREFIX constant ("Reasoning:\n"), causing it to fall through to the answerText path.

Changes:

  • A new regex guard /^Reasoning:?\s*$/ is inserted in splitTelegramReasoningText to suppress these bare prefix chunks before they can reach the answerText fallback
  • Two new test cases are added: one verifying suppression of "Reasoning:" / "Reasoning:\n" / "Reasoning: \n", and one confirming that "Reasoning:\n" followed by actual content is still correctly classified as reasoningText

Issue found:

  • The colon in the regex is optional (:?), meaning the pattern also matches the plain string "Reasoning" (no colon). This is over-broad — if a streaming chunk contains exactly the word "Reasoning" (without colon), it will be silently suppressed rather than emitted as answer text. The fix should use /^Reasoning:\s*$/ (colon required) to match only the intended case. Additionally, \s* before $ is redundant since the check is applied to trimmed.

Confidence Score: 4/5

  • Safe to merge after fixing the over-broad :? in the regex on line 74, which could silently suppress legitimate "Reasoning" (no colon) answer chunks.
  • The fix is targeted and the tests are thorough and correct. The only issue is the optional colon (:?) in the guard regex making it slightly over-broad — suppressing bare "Reasoning" in addition to "Reasoning:". This is a low-probability edge case (a real answer of just "Reasoning" is rare) but is still an unintended behavior change worth correcting before merge.
  • src/telegram/reasoning-lane-coordinator.ts line 74 — the regex :? should be changed to require the colon.

Last reviewed commit: e71a7ec

// Suppress partial "Reasoning:" arriving during streaming before any
// actual reasoning content follows (trim() collapses "Reasoning:\n" to
// "Reasoning:" which would otherwise fall through to answerText).
if (/^Reasoning:?\s*$/.test(trimmed)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex /^Reasoning:?\s*$/ makes the colon optional, so it also suppresses the bare string "Reasoning" (no colon). This is broader than the stated intent ("suppress partial Reasoning: chunks") and would silently drop any streaming chunk that is exactly the word "Reasoning".

Since REASONING_MESSAGE_PREFIX is "Reasoning:\n" (colon required), the guard should only match the colon form:

Suggested change
if (/^Reasoning:?\s*$/.test(trimmed)) {
if (/^Reasoning:\s*$/.test(trimmed)) {

Additionally, \s* before $ is redundant here because this is applied to trimmed (the result of text.trim()), which already has no trailing whitespace. The guard still works correctly because \s* matches zero characters, but /^Reasoning:$/.test(trimmed) is equally correct and more self-documenting for this specific case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/reasoning-lane-coordinator.ts
Line: 74

Comment:
The regex `/^Reasoning:?\s*$/` makes the colon optional, so it also suppresses the bare string `"Reasoning"` (no colon). This is broader than the stated intent ("suppress partial `Reasoning:` chunks") and would silently drop any streaming chunk that is exactly the word `"Reasoning"`.

Since `REASONING_MESSAGE_PREFIX` is `"Reasoning:\n"` (colon required), the guard should only match the colon form:

```suggestion
  if (/^Reasoning:\s*$/.test(trimmed)) {
```

Additionally, `\s*` before `$` is redundant here because this is applied to `trimmed` (the result of `text.trim()`), which already has no trailing whitespace. The guard still works correctly because `\s*` matches zero characters, but `/^Reasoning:$/.test(trimmed)` is equally correct and more self-documenting for this specific case.

How can I resolve this? If you propose a fix, please make it concise.

@okuyam2y
Copy link
Copy Markdown
Author

okuyam2y commented Mar 6, 2026

Fixed in 6d1bbba — changed :? to require the colon: /^Reasoning:\s*$/

Good catch, thanks.

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

Labels

channel: telegram Channel integration: telegram size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gemini raw reasoning text leaks into answer during Telegram DM streaming

1 participant