Skip to content

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

@howie

Description

@howie

Summary

Three related failure modes in the reactions / adapter slice, all observed in production on 2026-05-12 and 2026-05-17. They cluster around src/adapter.rs:530-670 and Slack silence semantics. All three are independently fixable; they are filed together because they share a root area and a coherent fix story.


Bug 1 -- Agent outputs silence rationale instead of staying silent

Observed: An agent not addressed by a message is dispatched, reasons correctly in internal thought that it should stay silent, then posts a user-visible message explaining that it is staying silent -- the exact anti-pattern the steering doc says to avoid.

Sub-case B (2026-05-17): When multiple bots are named in a single message, the LLM misjudges its own addressee status -- applying "stay silent" even when its own UID is present in the mention list, because it pattern-matches "I see other bot mentions -> stay silent" before reading the full mention list.

Root cause: Prompt-instruction shape. A "don't post 'I'm staying silent because...'" rule activates the model's explain-what-I-am-doing reflex. The reasoning is correct; the action contradicts it.

Proposed fix -- Sentinel pattern:

Steering instructs: "if you decide to stay silent, output exactly <silent /> and nothing else."
Gateway detects the sentinel and suppresses the message before posting.

// adapter.rs (sketch)
if final_content.trim() == "<silent />" {
    return Ok(()); // suppress -- do not post
}

This gives the model a positive "output X" instruction instead of a negative "don't output Y", which LLMs follow far more reliably.


Bug 2 -- _(no response)_ placeholder fires on legitimate silence

Code: src/adapter.rs:659-663

let final_content = if final_content.is_empty() {
    if let Some(err) = response_error {
        format!("⚠️ {err}")
    } else {
        "_(no response)_".to_string()   // hardcoded, no config
    }

Observed: An agent correctly decides it has nothing to say (standing-by task, not-addressed message) -> emits empty string -> gateway posts _(no response)_ -> looks like the agent broke.

Proposed fix: make empty-reply behaviour configurable.

# values.yaml
agents:
  codex:
    reactions:
      emptyReplyPlaceholder: false   # default true for backward compat

When false, suppress the message entirely instead of posting the placeholder. Pairs cleanly with the sentinel fix: both take the same "skip send" branch.


Bug 3 -- response_error assignments have no diagnostic log

Code: src/adapter.rs:542 / 547 / 567 -- all three response_error = Some(...) sites have no adjacent tracing::warn!.

Observed (2026-05-17): Two agents were dispatched for a PR review task and both returned _(no response)_. Without server-side logs there is no way to determine whether they hit a tool failure, a timeout, or a JSON-RPC error. The user-facing placeholder is the only artifact.

Proposed fix: three log lines, zero behaviour change.

response_error = Some("Agent process died".into());
tracing::warn!(agent = %agent_cmd, "agent process died mid-prompt");

response_error = Some(format!("Agent exceeded hard timeout ({}s)", hard_timeout.as_secs()));
tracing::warn!(agent = %agent_cmd, elapsed_s = ?elapsed, "agent hard timeout exceeded");

response_error = Some(format_coded_error(err.code, &err.message));
tracing::warn!(agent = %agent_cmd, code = err.code, message = %err.message, "agent JSON-RPC error");

Also observed: gemini emitting []

On 2026-05-17, a gemini-cli-backed agent posted [] to Slack instead of a reply or _(no response)_. Not yet attributed to a specific code path. Mentioned here as a fourth symptom -- without Bug 3's logging it is undiagnosable from the outside.


Backward compatibility

All proposed changes are default-off or additive:

  • Sentinel: new gateway check, only fires on exact <silent /> string -- existing agents unaffected until steering is updated
  • emptyReplyPlaceholder: false: opt-in suppress, default true preserves current behaviour
  • Log lines: additive only, zero behaviour change

Tests suggested

  • Empty-reply path: assert no message posted when emptyReplyPlaceholder: false
  • Sentinel-reply path: assert no message posted when agent outputs <silent />
  • response_error log emission: assert tracing::warn! fires for each of the three error branches

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions