Skip to content

fix(adapter): silence handling — sentinel, empty-reply config, diagnostic logging (#836)#842

Open
howie wants to merge 5 commits into
openabdev:mainfrom
howie:fix/836-silence-handling
Open

fix(adapter): silence handling — sentinel, empty-reply config, diagnostic logging (#836)#842
howie wants to merge 5 commits into
openabdev:mainfrom
howie:fix/836-silence-handling

Conversation

@howie
Copy link
Copy Markdown
Contributor

@howie howie commented May 18, 2026

Closes #836.

What

Three fixes for silence/empty-reply handling. All land in AdapterRouter::stream_prompt_blocks (src/adapter.rs) — the single funnel for every adapter — so Discord, Slack, and all gateway-relayed adapters (Feishu, Telegram, LINE, Teams, WeChat) inherit them automatically. No per-adapter changes.

Fix 1 — <silent /> sentinel filter

Steering can instruct agents to output exactly <silent /> when staying silent. The gateway detects this post-loop (not per-chunk, to avoid partial-buffer false positives), deletes any streaming placeholder, and returns without posting.

if text_buf.trim() == "<silent />" {
    info!(platform = %adapter.platform(), "agent emitted <silent /> sentinel -- suppressing reply");
    if let Some(msg) = placeholder_msg.as_ref() {
        let _ = adapter.delete_message(msg).await;
    }
    return Ok(());
}

Addresses Bug 1: the agent now has a positive "output X" instruction instead of a negative "don't output Y", which LLMs follow far more reliably.

Fix 2 — emptyReplyPlaceholder config

New [reactions] empty_reply_placeholder field (default true, fully backward-compatible). When false, empty replies with no error are silently suppressed instead of posting _(no response)_.

# per-agent config.toml (or values.yaml -> reactions.emptyReplyPlaceholder: false)
[reactions]
empty_reply_placeholder = false

Helm chart: configmap.yaml template and values.yaml comment updated.

Fix 3 — diagnostic logging

Three tracing::warn! lines added at the three response_error = Some(...) sites:

Site Log message
Agent process died agent process died mid-prompt
Hard timeout exceeded agent hard timeout exceeded + elapsed_s field
JSON-RPC error agent JSON-RPC error + code + message fields

Each log carries platform field for cross-adapter filtering. Zero behaviour change. Resolves the "two agents returned _(no response)_ with no server-side evidence" case from 2026-05-17.

Adapter scope

Adapter Benefits?
Discord native (discord.rs) Yes
Slack native (slack.rs) Yes
Gateway-relayed: Feishu, Telegram, LINE, Teams, WeChat Yes — all re-enter via AdapterRouter::handle_message
Future adapters Yes — automatically

No changes to discord.rs, slack.rs, gateway.rs, or any gateway/src/adapters/*.rs. Adapters remain pure transport.

Also adds

docs/steering/silence.md — agent steering template explaining <silent /> usage and the multi-mention sub-case (Bug 1B: agent must read the full mention list before deciding silence, not pattern-match on "other bot present").

Tests added

  • silent_sentinel_passes_through_directive_parser<silent /> is NOT consumed by the [[key:value]] directive parser
  • silent_sentinel_with_whitespace_passes_through — whitespace/newline variants survive intact
  • empty_reply_placeholder_defaults_to_true — backward compat regression guard
  • empty_reply_placeholder_can_be_set_to_false — opt-in suppression round-trips through TOML

🤖 Generated with Claude Code

…stic logging (openabdev#836)

Three related fixes, all in the platform-agnostic AdapterRouter layer.
All current adapters (Discord, Slack, gateway-relayed Feishu/Telegram/LINE/Teams/WeChat)
inherit the fixes automatically — no per-adapter changes needed.

**Fix 1 — <silent /> sentinel**
Steering can now instruct agents to output `<silent />` to stay silent.
AdapterRouter detects the sentinel post-loop (not per-chunk, to avoid partial-buffer
false positives), deletes any streaming placeholder, and returns Ok without posting.

**Fix 2 — emptyReplyPlaceholder config**
New `[reactions] empty_reply_placeholder` field (default true, backward-compatible).
When false, empty replies with no error are suppressed instead of posting "_(no response)_".
Rendered via Helm configmap template; commented example added to values.yaml.
Pairs with the sentinel: both share the same "delete placeholder + early return" path.

**Fix 3 — diagnostic logging**
Three tracing::warn! lines added at the response_error assignment sites:
agent process died, hard timeout exceeded, JSON-RPC error. Zero behaviour change.
Each log includes `platform` so entries are filterable across adapter types.
Addresses the "two agents returned _(no response)_ with no server-side evidence" case.

Also adds docs/steering/silence.md — agent steering template explaining <silent />
usage and the multi-mention sub-case (Bug 1B in the issue).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@howie howie requested a review from thepagent as a code owner May 18, 2026 02:44
@github-actions github-actions Bot added pending-screening PR awaiting automated screening closing-soon PR missing Discord Discussion URL — will auto-close in 3 days labels May 18, 2026
@github-actions
Copy link
Copy Markdown

⚠️ This PR is missing a Discord Discussion URL in the body.

All PRs must reference a prior Discord discussion to ensure community alignment before implementation.

Please edit the PR description to include a link like:

Discord Discussion URL: https://discord.com/channels/...

This PR will be automatically closed in 3 days if the link is not added.

@shaun-agent
Copy link
Copy Markdown
Contributor

shaun-agent commented May 18, 2026

OpenAB PR Screening

This is auto-generated by the OpenAB project-screening flow for context collection and reviewer handoff.
Click 👍 if you find this useful. Human review will be done within 24 hours. We appreciate your support and contribution 🙏

Screening report screened PR #842, posted the marker comment, and moved project item `PVTI_lADOEFbZWM4BUUALzgtAjzk` from `Incoming` to `PR-Screening`.

GitHub comment: #842 (comment)
Project action: https://github.com/orgs/openabdev/projects/1

Intent

This PR tries to make agent silence explicit and operationally debuggable. The visible problem is that agents can currently produce confusing empty replies like _(no response)_, or fail silently enough that Discord, Slack, and gateway operators have little evidence about whether the agent chose silence, died, timed out, or hit JSON-RPC failure.

Feat

Fix work. It adds a shared adapter-level <silent /> sentinel suppression path, a backwards-compatible empty_reply_placeholder reactions config option, and warning logs for agent process death, hard timeout, and JSON-RPC errors. Because the changes land in AdapterRouter::stream_prompt_blocks, the behavior should apply to Discord, Slack, and gateway-relayed adapters without transport-specific patches.

Who It Serves

Primary beneficiaries are Discord users and Slack users who should stop seeing unwanted empty placeholders when silence is intentional. Secondary beneficiaries are deployers and agent runtime operators who need logs that distinguish intended silence from broken agent execution.

Rewritten Prompt

Implement shared silence handling in the OpenAB adapter router. When a complete streamed response, after trimming, is exactly <silent />, delete any streaming placeholder and return without posting a reply. Add a [reactions] empty_reply_placeholder config field that defaults to true; when set to false, suppress empty successful replies instead of posting _(no response)_. Add structured warning logs with platform for agent process death, hard timeout, and JSON-RPC error paths. Update Helm values/templates and add focused tests for sentinel parsing and config default/override behavior. Keep transport adapters unchanged unless a shared-router integration issue requires otherwise.

Merge Pitch

This is a narrow, high-leverage adapter fix: one shared funnel improves all current and future adapters. The risk profile is moderate-low because the default empty-reply behavior remains backwards-compatible and the sentinel requires an exact trimmed match. The likely reviewer concern is whether <silent /> can accidentally suppress legitimate user-facing output, and whether deleting placeholders is reliable across adapter implementations.

Best-Practice Comparison

OpenClaw relevant practices are explicit delivery routing, run logs, and isolated execution evidence. This PR moves in that direction by making silence an explicit output contract and adding failure logs, but it does not add durable run logs or retry/backoff.

Hermes Agent relevant practices are fresh scheduled runs, atomic persisted state, and self-contained scheduled prompts. Those mostly do not apply because this PR is about live adapter response handling, not scheduling. The self-contained prompt idea applies lightly: docs/steering/silence.md makes the silence contract clearer for agent steering.

Implementation Options

Conservative: merge the shared sentinel and config path as-is after focused review of placeholder deletion behavior and tests. This preserves the current architecture and keeps the blast radius small.

Balanced: merge this PR, then follow with a small observability PR that attaches a stable request/session id to the new warning logs and documents expected log queries for Discord, Slack, and gateway deployments.

Ambitious: replace ad hoc empty response handling with a typed response outcome model such as Reply, Silent, Empty, and Error, then route delivery, logging, placeholder cleanup, and metrics through that model. This would be cleaner long-term but larger than the current bug fix.

Comparison Table

Option Speed Complexity Reliability Maintainability User Impact Fit for OpenAB now
Conservative Fast Low Good if exact sentinel tests pass Good, shared router only Immediate reduction in unwanted replies Strong
Balanced Medium Medium Better operator diagnosis with request correlation Good, adds observability without redesign Same user fix plus easier incident triage Best
Ambitious Slow High Best if completed well Best long-term model, but more invasive Cleaner semantics across adapters Premature for this PR

Recommendation

Use the balanced path. Advance this PR through review as the behavior fix, with reviewer attention on exact sentinel matching, placeholder deletion, and the default-preserving config path. Split request/session correlation or richer run-log work into a follow-up so this fix can land without turning a targeted silence bug into a broader runtime redesign.

howie and others added 4 commits May 18, 2026 11:12
- Trim verbose sentinel comment to single-line WHY
- Fix stringly-typed boolean in debug log message
- Spawn placeholder delete to release connection slot immediately
  (mirrors reactions.clear() spawn pattern at lines 412-416)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Default

E0063 compile error: struct update/Default impl was missing the new field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lack delete

Four fixes on top of the initial silence-handling commit:

1. Session reset + sentinel (Codex P2): text_buf was pre-populated with the
   reset prelude before agent output, so text_buf.trim() == "<silent />" never
   matched after a session reset. Track reset_prelude separately and prepend
   it to streaming updates and final_content after sentinel/suppress checks.

2. reactions.suppress() (Claude Important): early-return suppress paths
   returned Ok(()), causing dispatch.rs to call reactions.set_done() and add
   a done emoji to the user message even though no reply was posted. Added
   suppress() to StatusReactionController (clear + finished=true) so the
   subsequent set_done() is a no-op.

3. delete_message error logging (Claude Important): tokio::spawn blocks for
   placeholder deletion were silently discarding errors. Now emit warn! so
   failures appear in telemetry.

4. Slack delete_message (Codex P2): SlackAdapter lacked a delete_message
   override, falling back to edit_message with U+200B. Added chat.delete so
   the placeholder is truly removed rather than left as a blank message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…shed, fix mood face

Five fixes found by group review R1:

1. Streaming sentinel flash (Critical): the edit loop fires every 1500ms and
   would briefly show "<silent />" text in the placeholder before delete_message
   ran. Now skip buf_tx.send() when text_buf.trim() == "<silent />".

2. clear() missing finished guard (Important): after suppress() sets
   finished=true, a subsequent clear() call (e.g. from dispatch's
   remove_after_reply path) would fire an extra remove_reaction API call on an
   already-cleared controller. Added if inner.finished { return; } guard.

3. suppress() remove_reaction error logging (Important): silent discard of the
   remove_reaction failure meant a stuck emoji would leave no trace in telemetry.
   Now emits tracing::warn! on failure, consistent with delete_message logging.

4. set_done() mood face fires after suppress() (Critical test finding): the mood
   face addition code ran unconditionally after finish(), which is a no-op when
   already finished. Added guard: only add face if inner.current == emoji (i.e.
   finish() actually ran, not already suppressed).

5. reactions tests (Critical gap): added #[cfg(test)] module with MockAdapter
   recording add_reaction/remove_reaction calls. Two tests verify:
   - suppress() removes current reaction and blocks subsequent set_done()
   - clear() is a no-op after suppress()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@howie
Copy link
Copy Markdown
Contributor Author

howie commented May 18, 2026

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

Labels

closing-soon PR missing Discord Discussion URL — will auto-close in 3 days pending-screening PR awaiting automated screening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

silence handling: agent meta-replies, (no response) placeholder, and missing diagnostic logging

2 participants