Skip to content

fix(channels): strip silence tokens mixed with reply text#164

Merged
williamwa merged 5 commits into
mainfrom
fix/silence-token-strip
May 29, 2026
Merged

fix(channels): strip silence tokens mixed with reply text#164
williamwa merged 5 commits into
mainfrom
fix/silence-token-strip

Conversation

@ayush-that
Copy link
Copy Markdown
Member

@ayush-that ayush-that commented May 28, 2026

Summary by CodeRabbit

  • Refactor
    • Unified silent-response handling across Discord, Signal, Slack, Telegram, WhatsApp and WeChat: silence markers are stripped, silent replies are suppressed, and provisional/partial messages are removed when appropriate.
  • New Features
    • Added shared silence-token utilities for adapters.
  • Tests
    • Added tests validating silence-token detection and stripping.
  • Documentation
    • Clarified adapter and session-model behavior for silent responses.
  • Chores
    • Minor test/script updates in package configs.

Review Change Stack

Every channel bridge gated the "agent stayed silent" path on
`responseText.trim() === NO_REPLY_TAG`. That only matched when the
model emitted the token alone — but models occasionally emit a real
reply AND the token in the same final string (e.g. "ok.\n<NO_REPLY>"),
in which case the literal "<NO_REPLY>" leaked through to Telegram /
Discord / Slack / WeChat / Signal.

Promote the silence-token logic to @openhermit/shared as
stripSilenceTokens: removes every occurrence of <NO_REPLY> /
<EMPTY_RESPONSE> from anywhere in the text and reports both the
remainder and whether the result is effectively silent. Each bridge
now drops the response only when the *stripped* remainder is empty,
and sends/edits with the cleaned text when the model mixed both forms.

Includes a node:test suite for the helper covering the regression case.
Copilot AI review requested due to automatic review settings May 28, 2026 09:20
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 797ad426-9cef-47a5-ac62-0a192cffe162

📥 Commits

Reviewing files that changed from the base of the PR and between bc4fa20 and 06caad5.

📒 Files selected for processing (3)
  • apps/channels/discord/src/bridge.ts
  • apps/channels/slack/src/bridge.ts
  • apps/channels/telegram/src/bridge.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/channels/discord/src/bridge.ts
  • apps/channels/telegram/src/bridge.ts
  • apps/channels/slack/src/bridge.ts

📝 Walkthrough

Walkthrough

Adds a shared stripSilenceTokens utility and updates all channel bridges to use it for streaming edits and final-response suppression, replacing per-bridge NO_REPLY sentinel logic.

Changes

Silence token implementation and bridge migration

Layer / File(s) Summary
Core silence token utility and tests
packages/shared/src/silence-tokens.ts, packages/shared/test/silence-tokens.test.ts, packages/shared/src/index.ts
SILENCE_TOKENS and stripSilenceTokens(text) remove silence tokens, trim, and return { text, hadToken, isSilent }. Tests cover bare tokens, whitespace-wrapped tokens, mixed content, multiple occurrences. Barrel export added.
Channel bridge response suppression migrations
apps/channels/discord/src/bridge.ts, apps/channels/signal/src/bridge.ts, apps/channels/slack/src/bridge.ts, apps/channels/telegram/src/bridge.ts, apps/channels/wechat/src/bridge.ts, apps/channels/whatsapp/src/bridge.ts
All bridges now strip silence tokens for streaming display and final responses, delete provisional/streamed messages when stripped output is silent, and choose stripped text when tokens were present (falling back to raw text otherwise). Old NO_REPLY/NO_REPLY_TAG constants removed.
Docs and package scripts
docs/channel-adapter.md, docs/session-model.md, package.json, packages/shared/package.json
Docs updated to describe stripping behavior and suppression rules; minor test/typecheck script changes in package.json files.

Sequence Diagram

sequenceDiagram
  participant Agent
  participant Bridge
  participant StripFn as stripSilenceTokens
  participant ChannelAPI as Channel API
  
  Agent->>Bridge: final accumulated response
  Bridge->>StripFn: rawResponseText
  StripFn-->>Bridge: {text, hadToken, isSilent}
  alt isSilent == true
    Bridge->>Channel API: delete provisional message
    Bridge-->>Agent: {text: undefined, error: undefined}
  else isSilent == false
    alt hadToken == true
      Bridge->>Channel API: send/edit with stripped text
    else hadToken == false
      Bridge->>Channel API: send/edit with original text
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I nibble tokens hidden in thread,
Quiet crumbs swept from what was said,
Six bridges now hush or relay,
Interim drafts fade clean away,
Rabbit bounces on — silence fed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing silence token stripping logic across multiple channel adapters (Discord, Signal, Slack, Telegram, WeChat, WhatsApp) to remove <NO_REPLY> and <EMPTY_RESPONSE> markers mixed with reply text.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/silence-token-strip

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a production regression where the agent’s “silence” sentinel tokens (e.g. <NO_REPLY>) could be emitted alongside real reply text and end up displayed in downstream channel messages. It centralizes token-stripping logic in @openhermit/shared and updates channel bridges to suppress replies only when the post-stripped remainder is empty, otherwise sending the cleaned text.

Changes:

  • Add stripSilenceTokens (and SILENCE_TOKENS) to @openhermit/shared and export it from the package entrypoint.
  • Update Slack/Discord/Telegram/Signal/WeChat bridges to use the shared stripping logic instead of strict equality checks.
  • Add a new node:test suite covering bare-token silence, mixed-content stripping, multiple occurrences, and legacy token handling.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/shared/src/silence-tokens.ts Introduces the shared token-stripping helper and result contract.
packages/shared/src/index.ts Re-exports the new helper/types from @openhermit/shared.
packages/shared/test/silence-tokens.test.ts Adds regression and behavior tests for stripping/silence detection.
apps/channels/slack/src/bridge.ts Uses stripSilenceTokens to prevent silence tokens leaking into Slack messages.
apps/channels/discord/src/bridge.ts Uses stripSilenceTokens to prevent silence tokens leaking into Discord messages.
apps/channels/telegram/src/bridge.ts Uses stripSilenceTokens to prevent silence tokens leaking into Telegram messages (incl. delete-on-silence).
apps/channels/signal/src/bridge.ts Uses stripSilenceTokens so Signal suppresses token-only responses and strips mixed tokens.
apps/channels/wechat/src/bridge.ts Uses stripSilenceTokens before sending outbound WeChat messages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/channels/slack/src/bridge.ts
Comment thread apps/channels/discord/src/bridge.ts
Comment thread apps/channels/telegram/src/bridge.ts
Comment thread packages/shared/test/silence-tokens.test.ts
Comment thread packages/shared/src/silence-tokens.ts
- Strip silence tokens from streaming text too: slack/discord/telegram
  all edit partial messages with accumulatedText during text_delta. If
  <NO_REPLY> arrived mid-stream the token would flash visibly until the
  final edit. Compute displayText once per delta and route it through
  every streaming send/edit call.
- Wire packages/shared into the root test glob and add a per-package
  test script so the new node:test suite runs in CI.
- Clarify isSilent docstring to mirror the consumer-side definition.
- Update docs/channel-adapter.md and docs/session-model.md so the
  documented NO_REPLY contract reflects strip-anywhere semantics.
Conflicts:
  apps/channels/telegram/src/bridge.ts — kept local removal of
  NO_REPLY_TAG, kept main's new shouldSpeak/VOICE_MAX_TEXT_LENGTH
  helpers.

Also applies the same silence-token-strip fix to apps/channels/whatsapp
(the new Baileys adapter from #155), so the WhatsApp bridge doesn't
ship with the brittle equality check the rest of this PR removes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/session-model.md`:
- Line 86: The docs sentence that currently references only `<NO_REPLY>` should
be updated to reflect runtime behavior by mentioning both `<NO_REPLY>` and the
legacy `<EMPTY_RESPONSE>` markers; change the text that says "`<NO_REPLY>`
markers are stripped..." to something like "Both `<NO_REPLY>` and legacy
`<EMPTY_RESPONSE>` markers are stripped by channel adapters; if the stripped
remainder is empty the reply is suppressed entirely (whether the model emitted
the token alone or alongside real text)" so the documentation matches the
implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6e1732a7-8c9b-4d83-945b-799254860f56

📥 Commits

Reviewing files that changed from the base of the PR and between d86f9a6 and 77de2ff.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • apps/channels/discord/src/bridge.ts
  • apps/channels/slack/src/bridge.ts
  • apps/channels/telegram/src/bridge.ts
  • apps/channels/whatsapp/src/bridge.ts
  • docs/channel-adapter.md
  • docs/session-model.md
  • package.json
  • packages/shared/package.json
  • packages/shared/src/silence-tokens.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/channel-adapter.md
  • package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/channels/slack/src/bridge.ts
  • packages/shared/src/silence-tokens.ts
  • apps/channels/telegram/src/bridge.ts

Comment thread docs/session-model.md Outdated
session-model.md mentioned only <NO_REPLY> but the runtime strips both
<NO_REPLY> and the legacy <EMPTY_RESPONSE>. channel-adapter.md already
called both out — bring session-model.md in line.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

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.

3 participants