fix(discord): stop typing keepalive on message_tool_only delivery (#84276)#84300
fix(discord): stop typing keepalive on message_tool_only delivery (#84276)#84300Rohang2005 wants to merge 4 commits into
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 4:23 PM ET / 20:23 UTC. Summary PR surface: Source +145, Tests +311, Docs +1. Total +457 across 19 files. Reproducibility: yes. at source level: current main still lacks a source-delivery terminal hook and still honors explicit instant typing config for Discord message_tool_only runs. I did not run a live Discord client reproduction. Review metrics: 1 noteworthy metric.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Choose one canonical lifecycle owner for configured message_tool_only Discord typing, refresh that implementation against current embedded/Codex runtime paths, drop the changelog edit, and verify with redacted live transport proof. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main still lacks a source-delivery terminal hook and still honors explicit instant typing config for Discord message_tool_only runs. I did not run a live Discord client reproduction. Is this the best way to solve the issue? No as submitted: the shared lifecycle idea is plausible, but the branch is stale/conflicting and must be refreshed against current runtime paths and maintainer owner-boundary choice. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 640258d7b31d. Label changesLabel justifications:
Evidence reviewedPR surface: Source +145, Tests +311, Docs +1. Total +457 across 19 files. View PR surface stats
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Addressed all three required changes. |
|
@mcaxtr could you please take a look at this PR? Thank you for your time! |
|
This pull request has been automatically marked as stale due to inactivity. |
|
@Rohang2005 thanks for the PR. ClawSweeper is still waiting on real behavior proof before this can move forward. Useful proof can be a screenshot, short video, terminal output, copied live output, linked artifact, or redacted logs that show the changed behavior after the fix. Please redact private tokens, phone numbers, private endpoints, customer data, and anything else sensitive. Once proof is added to the PR body or a comment, ClawSweeper or a maintainer can re-check it. |
Summary
message_tool_onlysource-reply mode the Discord typing bubble keeps showing for ~10s after the reply has already landed in the channel.sendresolves, instead of waiting for the dispatcher's idle grace window. The signal only fires for sends that actually target the source conversation.TypingController.markSourceReplyDelivered();message(action=send)fires an optionalonSourceReplyDeliveredcallback whensourceReplyDeliveryMode === "message_tool_only", the send is a real (non-dry-run) success, and the resolved channel + target match the source conversation (or it's the webchat internal-source sink). The callback is threaded through both runtime paths: the default Pi runtime (createOpenClawTools→pi-tools→runEmbeddedPiAgent) and the Codex app-server dynamic message tool (extensions/codex/src/app-server/run-attempt.ts → buildDynamicTools). Wired inagent-runner.ts/followup-runner.tsto calltyping.markSourceReplyDelivered().DISPATCH_IDLE_GRACE_MSis left at 10s (shortening it makes the bug worse — see the linked issue), and theautomaticsource-reply path is unaffected. Amessage.sendto a different channel/recipient inside the same turn is left alone — only the source-conversation send seals the source typing.Motivation
Users perceive the bot as "still typing" for ~10s after a reply has already arrived in DM. It's small but it makes the agent feel laggy or stuck. Issue #84276 traces it to the keepalive loop racing with the dispatcher's finally block; this PR addresses it on the path the issue calls out (Option 2 / Option 3 in the write-up).
Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Real behavior proof (required for external PRs)
message_tool_onlyreply lands, and only the source conversation's typing is sealed when the message tool delivers.node. Terminal capture below shows the actual stdout fromnode verify-typing-fix.mjsafter the patch.TypingControllerlifecycle and the newsendTargetIsSourceConversationguard fromsrc/agents/tools/message-tool.ts, then drives 8 scenarios against them.)message(action=send)reports a successful non-dry-run send to the source conversation. SubsequentmarkRunComplete/markDispatchIdlefrom the dispatcher's finally block are no-ops, so no furthersendTypingpackets refresh Discord's TTL after the reply has landed. Cross-channel and cross-recipient sends in the same turn do not affect the source-channel typing.@openclaw-mantiscan capture the visible bubble disappearance for the redacted live recording the reviewer suggested, that closes the loop.Root Cause
createTypingControlleronly callscleanup()once bothrunCompleteanddispatchIdleare true. Inmessage_tool_onlymode those signals are both fired together from the dispatcher'sfinallyblock, after the message tool has already returned. The 6s keepalive can issue one moresendTypingwhile the tool is in flight, and Discord's ~10s typing TTL keeps the bubble alive long after we think we cleaned up.DISPATCH_IDLE_GRACE_MSmakes things worse — it seals the controller before the legitimatemarkDispatchIdlearrives, but Discord's TTL is independent of our cleanup so the bubble still hangs.Regression Test Plan
src/auto-reply/reply/typing-persistence.test.ts,src/agents/tools/message-tool.test.ts.sourceReplyDeliveryMode === "message_tool_only"to the source conversation, the keepalive stops immediately,onCleanupfires once, and any latemarkRunComplete/markDispatchIdlefrom the dispatcher's finally block are no-ops. Amessage.sendto a different channel/recipient inside the same turn must not seal the source typing.markRunComplete/markDispatchIdleseparately, but nothing for themessage_tool_onlydelivery path.User-visible / Behavior Changes
Discord (and any other channel using
message_tool_only) clears the typing indicator in lockstep with delivery instead of holding it for the channel-side TTL. No config flag. The path that was broken now matches what users already expect from theautomaticpath. Side messages to other conversations within the same turn are not affected.Diagram
Security Impact (required)
sendTypingcalls per turn.Repro + Verification
Environment
chat_type=direct,sourceReplyDeliveryMode === "message_tool_only".Steps
message_tool_onlyconfigured.Expected
Actual
markSourceReplyDelivered()the moment the message tool reports a successful non-dry-run send to the source conversation, so no furthersendTypingpackets refresh the TTL.Human Verification (required)
markSourceReplyDelivered()stops the keepalive immediately and firesonCleanuponce.markRunComplete/markDispatchIdle/cleanupcalls are no-ops once sealed.markRunComplete.action === "send",sourceReplyDeliveryMode === "message_tool_only", the result is asend, anddryRun !== true. It does not fire for dry runs,automatic, or non-send actions.user:123vs123), and webchathandledBy === "internal-source"sends. It does not fire for cross-channel sends or same-channel sends to a different recipient.onSourceReplyDeliveredcallback does not fail the tool call.message.sendinside the same turn (source typing untouched).Review Conversations
Compatibility / Migration
onSourceReplyDeliveredoption andmarkSourceReplyDeliveredmethod are both optional. Callers that don't pass the callback get exactly the previous behavior.Risks and Mitigations
onSourceReplyDeliveredcallback throws and we swallow the error.send. The rest of the tool result path is untouched.handledBy === "internal-source"sink short-circuits to "always source". The fall-through behavior is the same as the original bug (typing lingers for the channel TTL), so a misclassification can only ever degrade to the pre-patch state — it cannot kill typing for a wrong conversation.message(action=send)to the source conversation more than once in a single turn won't get a typing indicator for the 2nd/3rd call.message_tool_onlymodedisableBlockStreamingis alreadytrue, so block streaming wasn't driving typing for those follow-up sends anyway. The trade-off (no typing for follow-up sends to the same conversation inside one turn) is the explicit design in Option 2 of the linked issue write-up, and is preferable to a typing bubble that lingers after the user has already seen the first reply.