Skip to content

Gateway: suppress NO_REPLY lead-fragment chat leaks#32116

Merged
steipete merged 1 commit intoopenclaw:mainfrom
liuxiaopai-ai:codex/webchat-no-reply-prefix-fragment-32073
Mar 2, 2026
Merged

Gateway: suppress NO_REPLY lead-fragment chat leaks#32116
steipete merged 1 commit intoopenclaw:mainfrom
liuxiaopai-ai:codex/webchat-no-reply-prefix-fragment-32073

Conversation

@liuxiaopai-ai
Copy link

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: silent-response streaming can emit early assistant deltas like NO before later chunks complete to NO_REPLY, and the cached final buffer can then surface NO as a visible assistant message on lifecycle end/refresh (NO_REPLY control token partially stripped — renders as "NO" in webchat after v2026.3.1 upgrade #32073).
  • Why it matters: users see confusing stray NO replies from control-token flows.
  • What changed: added NO_REPLY lead-fragment detection in gateway chat event handling; lead fragments are still buffered but no delta is emitted until content disambiguates.
  • What did NOT change (scope boundary): no changes to model output, routing, or token semantics; this is chat event/display suppression logic only.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • Webchat/TUI no longer leak NO from NO_REPLY lead fragments in silent-response runs.
  • Legitimate short assistant replies like No are still preserved in final messages.

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: node + vitest
  • Model/provider: N/A (event-handler unit path)
  • Integration/channel (if any): gateway chat stream
  • Relevant config (redacted): N/A

Steps

  1. Emit assistant stream chunks in order: NO, NO_, NO_RE, NO_REPLY.
  2. End lifecycle for the run.
  3. Check chat final payload.

Expected

  • No visible assistant message for the silent token sequence.

Actual

  • With this patch, final chat payload has message: undefined; no stray NO is emitted.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • pnpm exec vitest run src/gateway/server-chat.agent-events.test.ts
    • pnpm lint src/gateway/server-chat.ts src/gateway/server-chat.agent-events.test.ts CHANGELOG.md
    • pnpm exec oxfmt --check src/gateway/server-chat.ts src/gateway/server-chat.agent-events.test.ts CHANGELOG.md
  • Edge cases checked:
    • NO/NO_/NO_RE/NO_REPLY progression suppresses leak
    • final one-word No remains visible
  • What you did not verify:
    • full e2e chat UI run in browser

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert this PR commit.
  • Files/config to restore: src/gateway/server-chat.ts, src/gateway/server-chat.agent-events.test.ts, CHANGELOG.md
  • Known bad symptoms reviewers should watch for: missing streamed deltas for very short replies that are exact early prefixes of NO_REPLY.

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: short replies beginning with N/NO may defer delta emission until disambiguated.
    • Mitigation: final payload buffering is preserved and covered by regression test (No still renders in final).

@aisle-research-bot
Copy link

aisle-research-bot bot commented Mar 2, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Overbroad NO_REPLY lead-fragment suppression hides legitimate assistant streaming deltas (e.g., "No")

1. 🔵 Overbroad NO_REPLY lead-fragment suppression hides legitimate assistant streaming deltas (e.g., "No")

Property Value
Severity Low
CWE CWE-840
Location src/gateway/server-chat.ts:78-90

Description

isSilentReplyLeadFragment() suppresses any assistant delta whose trimmed, uppercased content is an ASCII [A-Z_]+ prefix of SILENT_REPLY_TOKEN ("NO_REPLY").

This creates unintended content filtering / output integrity issues for legitimate short replies because:

  • Case-insensitive normalization (toUpperCase()) makes "No" match "NO".
  • The prefix check matches very short/common strings like "N", "NO", and "NO_".
  • In emitChatDelta, matching texts are returned early and not broadcast to clients.

Concrete examples of legitimate assistant outputs whose streaming deltas will be suppressed:

  • "No" (common refusal opener)
  • "NO" (legitimate all-caps reply)
  • "NO_" (can appear in code/identifiers)
  • "N" (single-letter response)

While the final message may still be delivered on lifecycle end, any client behavior that relies on deltas for timely display/logging/audit (or where final events are delayed/lost) will experience a denial-of-service style degradation and unexpected filtering triggered purely by prompting the assistant to begin with these strings.

Vulnerable code:

function isSilentReplyLeadFragment(text: string): boolean {
  const normalized = text.trim().toUpperCase();
  ...
  return SILENT_REPLY_TOKEN.startsWith(normalized);
}

And the suppression is applied here:

if (isSilentReplyLeadFragment(cleaned)) {
  return;
}

Recommendation

Narrow the matching so only actual silent-token lead fragments produced by the system are suppressed, without catching common legitimate text.

Options (choose one):

  1. Require an underscore (or a minimum length) so that "No"/"NO" are not suppressed:
function isSilentReplyLeadFragment(text: string): boolean {
  const normalized = text.trim().toUpperCase();
  if (!normalized) return false;// Only suppress fragments that look like the control token (contain '_')
  if (!normalized.includes("_")) return false;
  if (!/^[A-Z_]+$/.test(normalized)) return false;
  if (normalized === SILENT_REPLY_TOKEN) return false;

  return SILENT_REPLY_TOKEN.startsWith(normalized);
}
  1. Alternatively, only suppress when the original text is already all-caps/underscore (no toUpperCase()), so mixed-case natural language like "No" is not affected.

Also add tests asserting that delta events for common replies like "No"/"NO" are (or are not) emitted according to the desired behavior.


Analyzed PR: #32116 at commit b9c7e65

Last updated on: 2026-03-02T20:12:39Z

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR fixes a bug where streaming NO_REPLY control tokens would leak early fragments like NO to users during silent-response flows. The fix introduces a isSilentReplyLeadFragment() function that detects prefixes of NO_REPLY (N, NO, NO_, NO_RE, etc.) and suppresses delta emission until content disambiguates.

Key changes:

  • Added lead-fragment detection using regex pattern /^[A-Z_]+$/ and prefix matching against NO_REPLY
  • Moved buffer update before silent-reply checks (critical: ensures final message logic has correct buffered value)
  • Suppresses streaming deltas for lead fragments while preserving legitimate short replies like No in final messages
  • Added comprehensive test coverage for both the bug fix and edge case preservation

Technical approach: The solution buffers all text regardless of whether it's a lead fragment, then suppresses only the delta emission. This ensures the final message at lifecycle end has the complete text to determine if it's truly NO_REPLY (suppressed) or a legitimate reply like No (preserved).

Confidence Score: 5/5

  • This PR is safe to merge with no identified risks
  • Score reflects well-scoped bug fix with clear logic, comprehensive test coverage (including edge case for legitimate 'No' replies), no breaking changes, and effective solution to user-visible issue. The buffer-first approach elegantly handles both streaming suppression and final message correctness.
  • No files require special attention

Last reviewed commit: b9c7e65

@steipete steipete merged commit bf06538 into openclaw:main Mar 2, 2026
30 checks passed
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm test src/gateway/server-chat.agent-events.test.ts
  • Land commit: LAND_SHA_PLACEHOLDER
  • Merge commit: MERGE_SHA_PLACEHOLDER

Thanks @CONTRIB_PLACEHOLDER!

@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Correction: landed details.

  • Gate: pnpm test src/gateway/server-chat.agent-events.test.ts
  • Land commit: bf06538
  • Merge commit: bf06538

Thanks @liuxiaopai-ai!

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

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NO_REPLY control token partially stripped — renders as "NO" in webchat after v2026.3.1 upgrade

2 participants