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
Summary
Three related failure modes in the
reactions/ adapter slice, all observed in production on 2026-05-12 and 2026-05-17. They cluster aroundsrc/adapter.rs:530-670and 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.
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 silenceCode:
src/adapter.rs:659-663Observed: 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.
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_errorassignments have no diagnostic logCode:
src/adapter.rs:542 / 547 / 567-- all threeresponse_error = Some(...)sites have no adjacenttracing::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.
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:
<silent />string -- existing agents unaffected until steering is updatedemptyReplyPlaceholder: false: opt-in suppress, defaulttruepreserves current behaviourTests suggested
emptyReplyPlaceholder: false<silent />response_errorlog emission: asserttracing::warn!fires for each of the three error branches