fix(adapter): silence handling — sentinel, empty-reply config, diagnostic logging (#836)#842
fix(adapter): silence handling — sentinel, empty-reply config, diagnostic logging (#836)#842howie wants to merge 5 commits into
Conversation
…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>
|
All PRs must reference a prior Discord discussion to ensure community alignment before implementation. Please edit the PR description to include a link like: This PR will be automatically closed in 3 days if the link is not added. |
OpenAB PR ScreeningThis is auto-generated by the OpenAB project-screening flow for context collection and reviewer handoff.
Screening reportscreened PR #842, posted the marker comment, and moved project item `PVTI_lADOEFbZWM4BUUALzgtAjzk` from `Incoming` to `PR-Screening`.GitHub comment: #842 (comment) IntentThis PR tries to make agent silence explicit and operationally debuggable. The visible problem is that agents can currently produce confusing empty replies like FeatFix work. It adds a shared adapter-level Who It ServesPrimary 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 PromptImplement shared silence handling in the OpenAB adapter router. When a complete streamed response, after trimming, is exactly Merge PitchThis 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 Best-Practice ComparisonOpenClaw 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: Implementation OptionsConservative: 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 Comparison Table
RecommendationUse 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. |
- 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>
|
Discord Discussion URL: https://discord.com/channels/1491295327620169908/1491969620754567270/1505591332457152724 |
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 filterSteering 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.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 —
emptyReplyPlaceholderconfigNew
[reactions] empty_reply_placeholderfield (defaulttrue, fully backward-compatible). Whenfalse, empty replies with no error are silently suppressed instead of posting_(no response)_.Helm chart:
configmap.yamltemplate andvalues.yamlcomment updated.Fix 3 — diagnostic logging
Three
tracing::warn!lines added at the threeresponse_error = Some(...)sites:agent process died mid-promptagent hard timeout exceeded+elapsed_sfieldagent JSON-RPC error+code+messagefieldsEach log carries
platformfield 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
discord.rs)slack.rs)AdapterRouter::handle_messageNo changes to
discord.rs,slack.rs,gateway.rs, or anygateway/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 parsersilent_sentinel_with_whitespace_passes_through— whitespace/newline variants survive intactempty_reply_placeholder_defaults_to_true— backward compat regression guardempty_reply_placeholder_can_be_set_to_false— opt-in suppression round-trips through TOML🤖 Generated with Claude Code