Skip to content

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

@pwh9882

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
    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