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:
- User sends a normal Discord message
- Assistant requests exec approval
- Approval is granted from Discord
- Background exec finishes
- Assistant generates a follow-up reply
- 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:
- In Discord, ask the assistant to run an exec that requires approval
- Approve the exec from Discord
- Make the exec run in background and finish a few seconds later
- Let the assistant produce a follow-up reply after completion
- 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:
- 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:
- In the heartbeat async-completion path, if delivery is skipped or fails after reply generation, roll back/prune the transcript mutation.
- Add explicit instrumentation for:
- resolved delivery target
- skip reason before outbound delivery
- whether transcript was retained or pruned
- Consider a distinct internal event such as
message:delivery_skipped for cases that never enter outbound send.
- 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
Summary
On Discord, a follow-up assistant reply generated after a background/async
execcompletion can be persisted to session history/transcript, but never delivered to the originating Discord channel.The pattern appears especially reproducible when the flow is:
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:
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:
We also reproduced variants where:
message.sendfallback worksWhy 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:
execexitsmaybeNotifyOnExit(...)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:
Relevant code areas
These were the most relevant areas in the installed build:
maybeNotifyOnExit(...)dispatchReplyFromConfig(...)routeReply(...)buildOutboundSessionContext(...)runHeartbeatOnce(...)resolveHeartbeatDeliveryTarget(...)deliverOutboundPayloads(...)createMessageSentEmitter(...)scheduleRestartSentinelWake(...)Important clue
There appear to be heartbeat branches where a reply has already been generated, but delivery may return early for reasons like:
In those cases, the transcript may remain mutated even though no Discord message was actually sent.
That would exactly match the observed symptom:
Relation to other issues
This may be related in architecture to:
message:sentinternal hook never fires for agent replies (all channels) #31293 —message:sentinternal hook never fires for agent repliesThose suggest delayed / non-same-turn outbound delivery paths may already be weaker than direct same-turn reply dispatch.
Suggested fixes
Most likely fix candidates:
message:delivery_skippedfor cases that never enter outbound send.Expected behavior
If a follow-up assistant reply is persisted as a user-visible assistant response, it should either: