When the Reply Exists but the Thread Stayed Silent: An Agent-Ops Visibility Lesson
The bug looked like an agent that had stopped working.
A request had been handled. The internal answer existed. But the Discord thread where the user was waiting did not receive the final result. From the user's point of view, nothing happened. From the agent's internal task state, the language work had already completed.
That split is exactly why this kind of failure is so annoying. The internal state can be consistent with task completion while still violating the user-visible completion contract.
Unlike a saved-report delivery miss, this was a final-answer visibility miss: the answer existed only on the private/internal side of the chat boundary.
A reply that is not visible to the user is not a completed interaction.
The Failure Shape
The important split was between three states that are easy to collapse into one:
| State | What it means | Why it matters |
|---|---|---|
| Work completed | The agent did the requested investigation, synthesis, or closeout. | Rerunning the work may be wasteful or even confusing. |
| Internal reply produced | The assistant generated a final answer inside the orchestration session. | This proves the language step happened, not that the chat saw it. |
| Visible message delivered | The answer actually appeared in the user-facing Discord thread. | This is the state the human experiences as completion. |
The failure lived in the gap between the second and third states. A normal final answer was generated, but that runtime/surface configuration required an explicit visible-send action for user-visible output. The answer already existed in the author’s observed case, pointing to a visibility-layer gap. That observation does not categorically rule out transport or model failures in other incidents.
Why This Is Not Just “Delivery Failed”
I have written before about jobs where the generated report exists but the final delivery side effect fails. This is related, but slightly sharper.
In a report workflow, there is usually a durable artifact: a file, digest, rendered page, or saved summary. In a chat workflow, the artifact can be the final answer itself, and the system may treat that answer as private unless a specific channel tool publishes it.
That means the recovery path is not “rerun the agent.” It is:
- confirm whether a substantive internal answer exists;
- confirm the current surface delivery contract and reconcile any earlier send outcome;
- if reconciliation confirms delivery is still owed, use the supported delivery path to the original thread;
- record the miss so the same completion shape gets audited next time.
The distinction matters because rerunning can create duplicate summaries, conflicting status, or a second worker that races the first. If the answer already exists, recover the visibility layer first.
The Guardrail I Want
The useful guardrail is not complicated. Before ending a turn on a private-by-default chat surface that was directly addressed, reports status, or completes requested work, the agent should ask one operational question:
Does this user-facing answer need to be visible in the source thread?
If yes, the agent should use normal final delivery when the surface supports it, and use an explicit message-delivery tool only when that surface contract requires one, then reconcile that delivery outcome before suppressing an additional visible copy. That second half matters: a visibility fix should not become a duplicate-message bug.
For background workers, the guard needs one more field: the original source thread. A worker that finishes in a helper thread or internal session should not guess where the user is waiting. It should carry a bridge-back target from launch time and use that exact destination when reporting completion.
A Small State Machine
The pattern is small enough to write as a state machine:
work completes → preserve the answer and original destination
→ use normal final delivery when the current surface supports it
→ use an explicit send only when the surface contract requires it
→ reconcile the delivery outcome:
confirmed delivered → record evidence and suppress duplicate delivery
confirmed not delivered → retain the answer for an authorized retry
unknown → retain pending state; inspect/reconcile, do not blindly resend
→ if another worker finds the answer:
suppress duplicate generation, not unresolved delivery
→ one completion owner reports to the recorded source destination
The important phrase is exact visible thread. In multi-thread chat systems, “current” can mean the worker's current context, the parent context, or a stale default. If the user is waiting in the original thread, the bridge-back target must be explicit.
What the Audit Should Catch
A good audit for this class of failure should not simply ask whether the agent produced text. It should ask whether the right surface received the right text.
| Audit question | Bad answer | Better action |
|---|---|---|
| Was the request directly addressed or status-like? | “Probably.” | Classify it explicitly before finalizing. |
| Is the source surface private-by-default, requiring an explicit visible-send action rather than automatic reply visibility? | “The final answer exists in session history.” | Check the current surface contract and visible delivery, not only internal history; do not add a second send when normal final delivery already works. |
| Did a child worker finish somewhere else? | “It posted in its own context.” | Bridge the concise result back to the source thread. |
| Was the explicit target preserved? | “The tool default should pick the right channel.” | Use the recorded source destination for bridge-back sends. |
This is not just polish. In agent workflows, visible completion is part of correctness. If a user has to ask whether the work continued, the system already lost some trust.
How I Would Design It From Scratch
OpenClaw already has source surfaces, delivery targets, and explicit message tools. If I were making the final-delivery contract even more explicit for chat-native agent runtimes, I would put these fields directly in the task envelope:
- source surface: where the request came from;
- work surface: where long-running execution should live;
- final surface: where the user expects the result;
- visibility requirement: whether the final reply should remain private to the session, appear in the source thread, or be sent to an external surface such as email or another notification channel;
- dedupe key: a request-scoped marker that prevents retries or competing workers from sending the same final result twice;
- failure policy: how to distinguish confirmed failure from an unknown outcome, reconcile first, and use only an authorized retry or approved fallback when appropriate.
That sounds bureaucratic until the first missed reply. Then it feels like basic distributed-systems hygiene.
A Practical Checklist
For a self-hosted agent on Discord or another chat surface where internal replies and visible messages can diverge, I would use this checklist. The pattern here is drawn from a Discord-backed workflow; Slack, Matrix, Teams, and other platforms have their own visibility models and may need different guardrails.
- Separate answer generation from answer delivery. A completed internal response is not the same as a visible user reply.
- Resolve the current delivery contract. Prefer the supported normal-final path; use explicit sending only where required.
- Carry the original source target through delegation. Background workers should not infer where the final answer belongs.
- Separate generation dedupe from delivery dedupe. An existing answer prevents repeated work; only delivery evidence settles whether a visible copy is still owed.
- Audit misses as workflow failures. If a user asks “did it continue?”, inspect visibility before rerunning the work.
- Keep unknown sends unresolved. Preserve the answer and target, inspect available receipts, and use an authorized retry or fallback only after reconciliation; do not infer failure merely from an absent local receipt.
Relationship disclosure: anyech, this site’s GitHub identity, authored PR #141621 with AI assistance; PR #159416 was authored by steipete. The public changes do not establish the cause of the historical incident.
Later changes concern different delivery boundaries
| Case | Missing boundary | Scope of evidence |
|---|---|---|
| Historical story | Produced final answer versus the original surface’s visible delivery | Author’s sanitized observation, not a general causal exclusion |
| PR #141621 | Already-admitted commentary versus a second channel filter | Public source-reported paired controls; no rerun or production rate |
| PR #159416 | Intentional side answer while the longer task remains active | Public native-harness path; still-pending answers retained for final delivery |
A later failure: commentary passed one filter and vanished at the next
The original incident described above involved a surface whose final reply was private. That is a historical configuration, not a rule that all present-day Discord integrations require a separate send call. Check the current surface's delivery contract before changing the delivery path.
A later public OpenClaw repair exposed a different reason for silence: commentary had already been admitted upstream, but Discord's second progress filter dropped it. PR #141621 preserves the already-admitted commentary signal at that boundary. The public description reports baseline-fail/repaired-pass controls and an isolated bot-driven Discord testbed; these are source-reported results, not tests rerun here or a production reliability rate.
Diagnose the missing stage before rerunning the work: was text generated, admitted, handed to the channel, accepted by the transport, and made visible? A missing callback upstream is a different fault from a downstream filter dropping an admitted event. Likewise, a failed send must not erase the pending progress state as though delivery succeeded. Preserve the unresolved outcome; do not blindly duplicate an uncertain send.
The practical correction is to follow the first missing fact. Do not add an explicit-send workaround when normal final delivery is already the supported path.
A side answer is not progress commentary
A question can arrive while a longer task is still running. Merged OpenClaw PR #159416 adds an independent non-final delivery path for that side answer in the native harness, while preserving still-pending side answers for the task’s final response. The PR states that commentary visibility settings remain unchanged.
This is different from the September 15 filter fix: that preserved commentary already admitted upstream; this path delivers an intentional side answer during active work. The public PR description reports deterministic regressions for independently delivered, retained for recovery, and failed-before-send outcomes. That is scoped path evidence, not a platform-wide reliability estimate or proof that any earlier one-off incident was resolved.
Conclusion
The lesson is not that Discord is weird, or that agents are flaky. The lesson is that chat agents have two outputs: the internal answer and the visible message. Some surfaces make those the same thing. Others do not.
Preserve the answer, then follow the current surface's supported delivery path. A confirmed delivery, a confirmed failure and an unknown outcome require different next steps. Keep uncertain delivery pending without blindly resending, and keep one completion owner responsible for the original destination.
An agent that finishes work but leaves the thread silent is not done. It is holding the answer on the wrong side of the delivery boundary.