Skip to content

[Bug]: Discord async exec/approval follow-up replies can persist in transcript but never reach the channel #39978

Description

@pwh9882

Summary

On Discord, a follow-up assistant reply generated after a background/async exec completion can be persisted to session history/transcript, but never delivered to the originating Discord channel.

The pattern appears especially reproducible when the flow is:

  1. User sends a normal Discord message
  2. Assistant requests exec approval
  3. Approval is granted from Discord
  4. Background exec finishes
  5. Assistant generates a follow-up reply
  6. The reply exists in session history/transcript, but no Discord message appears

Normal same-turn Discord replies work fine. The problem seems specific to delayed/system-resume paths after async completion / approval.

Observed behavior

What we repeatedly observe:

  • Direct Discord user input -> assistant reply: works
  • Discord approval prompt shown -> approval granted -> async exec completes -> follow-up assistant reply: often missing from channel
  • The missing follow-up reply is still present in:
    • session history
    • transcript JSONL

So this does not look like "model produced no answer".
It looks like "answer was generated/persisted, but outbound channel delivery never happened".

Reproduction sketch

A minimal sketch:

  1. In Discord, ask the assistant to run an exec that requires approval
  2. Approve the exec from Discord
  3. Make the exec run in background and finish a few seconds later
  4. Let the assistant produce a follow-up reply after completion
  5. Observe:
    • system completion event is visible
    • follow-up assistant reply may appear in session history/transcript
    • no Discord channel message is delivered

We also reproduced variants where:

  • direct message.send fallback works
  • same-turn replies work
  • async completion follow-up does not

Why this looks like a core delivery-path issue

Code-level tracing suggests background exec completion does not use the normal same-turn Discord reply path.

Instead it appears to go through a delayed path roughly like:

  • background exec exits
  • maybeNotifyOnExit(...)
  • enqueue system event
  • request heartbeat wake
  • heartbeat path generates reply
  • delivery target is resolved later
  • outbound delivery may be skipped/fail after reply generation

This matters because the reply may already be written to transcript/history before delivery is finalized.

Strong hypothesis

The bug seems to be in the async completion / heartbeat delivery path, not primarily in the Discord send adapter itself.

A likely sequence is:

  • reply is generated during heartbeat/system-resume flow
  • transcript/history is updated
  • delivery is skipped or fails in a later branch
  • transcript is not pruned/rolled back in those branches
  • result: "reply exists internally, but never reached Discord"

Relevant code areas

These were the most relevant areas in the installed build:

  • background exec completion:
    • maybeNotifyOnExit(...)
  • normal reply routing:
    • dispatchReplyFromConfig(...)
    • routeReply(...)
    • buildOutboundSessionContext(...)
  • heartbeat path:
    • runHeartbeatOnce(...)
    • heartbeat wake handler
    • resolveHeartbeatDeliveryTarget(...)
  • outbound delivery:
    • deliverOutboundPayloads(...)
    • createMessageSentEmitter(...)
  • restart-style delayed wake:
    • scheduleRestartSentinelWake(...)

Important clue

There appear to be heartbeat branches where a reply has already been generated, but delivery may return early for reasons like:

  • no target / unresolved delivery target
  • alerts visibility disabled
  • channel readiness failure
  • pre-send skip branches

In those cases, the transcript may remain mutated even though no Discord message was actually sent.

That would exactly match the observed symptom:

  • session/transcript says reply exists
  • Discord channel never received it

Relation to other issues

This may be related in architecture to:

Those suggest delayed / non-same-turn outbound delivery paths may already be weaker than direct same-turn reply dispatch.

Suggested fixes

Most likely fix candidates:

  1. In the heartbeat async-completion path, if delivery is skipped or fails after reply generation, roll back/prune the transcript mutation.
  2. Add explicit instrumentation for:
    • resolved delivery target
    • skip reason before outbound delivery
    • whether transcript was retained or pruned
  3. Consider a distinct internal event such as message:delivery_skipped for cases that never enter outbound send.
  4. Audit whether delivery context from the original Discord session is preserved correctly across approval / async-completion wake flows.

Expected behavior

If a follow-up assistant reply is persisted as a user-visible assistant response, it should either:

  • be delivered to the originating Discord channel, or
  • be rolled back / marked as undelivered rather than silently existing only in transcript/history

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions