Skip to content

fix(typing): call markDispatchIdle in followup runner to prevent stuck indicator#26881

Merged
steipete merged 1 commit intoopenclaw:mainfrom
codexGW:fix/typing-indicator-stuck-on-silent-reply
Feb 26, 2026
Merged

fix(typing): call markDispatchIdle in followup runner to prevent stuck indicator#26881
steipete merged 1 commit intoopenclaw:mainfrom
codexGW:fix/typing-indicator-stuck-on-silent-reply

Conversation

@codexGW
Copy link
Contributor

@codexGW codexGW commented Feb 25, 2026

Problem

After the typing system refactor in 2026.2.24, the typing indicator can get stuck on channels like Telegram when the agent replies with NO_REPLY or produces empty payloads during followup turns (queued messages, inter-agent sends, heartbeat followups).

The typing controller requires both markRunComplete() and markDispatchIdle() to trigger cleanup. The followup runner only called markRunComplete() in its finally block. markDispatchIdle() is normally called by the buffered dispatcher's finally block — but followup turns bypass the dispatcher entirely, so the second signal never fires. The keepalive loop continues sending typing actions indefinitely.

Fix

Add typing.markDispatchIdle() alongside typing.markRunComplete() in the followup runner's finally block. This ensures both signals always fire regardless of whether the followup produced a reply, was silent, or errored.

Tests

Four new test cases in followup-runner.test.ts:

  • NO_REPLY → both signals fire
  • Empty payloads → both signals fire
  • Agent error → both signals fire
  • Successful delivery → both signals fire

All 17 tests pass (13 existing + 4 new).

Context

Complements #26295 which fixed the channel-level callback layer (closed flag in createTypingCallbacks). This PR fixes the controller-level lifecycle gap that #26295 doesn't cover.

Relates to #26595, #8785.

Greptile Summary

Added typing.markDispatchIdle() call in the followup runner's finally block to fix stuck typing indicators on channels like Telegram when followup turns produce NO_REPLY or empty payloads.

  • The typing controller requires both markRunComplete() and markDispatchIdle() signals to trigger cleanup
  • Followup turns bypass the buffered dispatcher, which normally calls markDispatchIdle() in its finally block
  • Without this fix, the typing keepalive loop continues indefinitely until TTL expires
  • Comprehensive test coverage added for all edge cases (NO_REPLY, empty payloads, errors, successful delivery)

Confidence Score: 5/5

  • This PR is safe to merge with no risk
  • The fix is a straightforward addition of a missing cleanup signal that follows the established pattern used elsewhere in the codebase. The change is minimal (one line of code plus comment), addresses a clear bug with a well-documented root cause, and includes comprehensive test coverage for all edge cases. The typing controller's dual-signal requirement is documented in the existing code, and this PR simply ensures both signals fire in the followup path where one was previously missing.
  • No files require special attention

Last reviewed commit: 5ea4712

(5/5) You can turn off certain types of comments like style here!

…k indicator

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595
@steipete steipete merged commit 6fb082e into openclaw:main Feb 26, 2026
27 checks passed
@steipete
Copy link
Contributor

Landed on main.

What was landed:

Status:

  • PR is merged and closed.

Thanks @codexGW for the fix and tests.

@chatgpt-codex-connector
Copy link

Great news — thanks for the update.

Since PR #26881 is already merged to main (squashed as 6fb082e13160cfa7282c587a225d80b0e8c770bd), there’s no follow-up code action needed from me on this branch.

View task →

brianleach pushed a commit to brianleach/openclaw that referenced this pull request Feb 26, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
brianleach pushed a commit to brianleach/openclaw that referenced this pull request Feb 26, 2026
zerone0x added a commit to zerone0x/clawdbot that referenced this pull request Feb 26, 2026
… finally block

PR openclaw#26881 fixed the followup runner's missing typing.markDispatchIdle()
call, but the same gap existed in the main reply pipeline (runReplyAgent).
The main pipeline relied solely on the buffered dispatcher's finally block
(dispatch.ts) to fire markDispatchIdle(). When the reply path exits before
that finally executes — early error, abort, or code path that bypasses the
dispatcher — the typing keepalive loop spins indefinitely (same root cause
as openclaw#26993, openclaw#27053).

Fix: add typing.markDispatchIdle() as a safety-net in runReplyAgent's own
finally block, immediately after typing.markRunComplete().  Calling it
twice is harmless: maybeStopOnIdle() is guarded by both the runComplete
and dispatchIdle flags, so the second invocation is a no-op.

Adds two regression tests to agent-runner.runreplyagent.test.ts:
  - markDispatchIdle is fired on successful run
  - markDispatchIdle is fired even when the agent throws

Fixes openclaw#27172
Related: openclaw#26881 (same pattern, followup runner)

Co-Authored-By: Claude <noreply@anthropic.com>
execute008 pushed a commit to execute008/openclaw that referenced this pull request Feb 27, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
execute008 pushed a commit to execute008/openclaw that referenced this pull request Feb 27, 2026
r4jiv007 pushed a commit to r4jiv007/openclaw that referenced this pull request Feb 28, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
r4jiv007 pushed a commit to r4jiv007/openclaw that referenced this pull request Feb 28, 2026
vincentkoc pushed a commit to Sid-Qin/openclaw that referenced this pull request Feb 28, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
vincentkoc pushed a commit to Sid-Qin/openclaw that referenced this pull request Feb 28, 2026
vincentkoc pushed a commit to rylena/rylen-openclaw that referenced this pull request Feb 28, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
vincentkoc pushed a commit to rylena/rylen-openclaw that referenced this pull request Feb 28, 2026
hughdidit pushed a commit to hughdidit/DAISy-Agency that referenced this pull request Mar 1, 2026
steipete pushed a commit to Sid-Qin/openclaw that referenced this pull request Mar 2, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
steipete added a commit to Sid-Qin/openclaw that referenced this pull request Mar 2, 2026
hughdidit pushed a commit to hughdidit/DAISy-Agency that referenced this pull request Mar 3, 2026
dorgonman pushed a commit to kanohorizonia/openclaw that referenced this pull request Mar 3, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
dorgonman pushed a commit to kanohorizonia/openclaw that referenced this pull request Mar 3, 2026
karmafeast pushed a commit to karmaterminal/openclaw that referenced this pull request Mar 3, 2026
Restores code that was accidentally removed alongside the continuation
feature in the initial commit (e0dc060). These are upstream safety
mechanisms that the feature lich didn't understand:

1. catch(error) block in runReplyAgent — keeps followup queue moving
   when an unexpected exception escapes the run path
2. typing.markDispatchIdle() in finally — stops typing keepalive on
   early exit/error (fix for openclaw#26881)
3. hasSessionRelatedCronJobs check — suppresses false 'I didn't schedule
   a reminder' warnings when existing cron job covers the commitment
   (fix for openclaw#32228)

These regressions were correctly identified by automated review
(chatgpt-codex-connector[bot] and greptile-apps[bot]).
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
thebenjaminlee pushed a commit to escape-velocity-ventures/openclaw that referenced this pull request Mar 7, 2026
…k indicator (openclaw#26881)

The followup runner (used for queued messages, inter-agent sends,
heartbeat followups, etc.) only called typing.markRunComplete() in
its finally block.  The typing controller requires BOTH markRunComplete
AND markDispatchIdle to trigger cleanup — but markDispatchIdle was
only wired through the buffered dispatcher path, which followup turns
bypass entirely.

This caused the typing indicator to persist indefinitely on channels
like Telegram when the agent replied with NO_REPLY or produced empty
payloads, because the keepalive loop was never stopped.

Adds markDispatchIdle() alongside markRunComplete() in the followup
runner's finally block, and four test cases covering NO_REPLY, empty
payloads, agent errors, and successful delivery.

Complements openclaw#26295 which addressed the channel-level callback layer.

Fixes openclaw#26595

Co-authored-by: Samantha <samantha@Samanthas-Mac-mini.local>
thebenjaminlee pushed a commit to escape-velocity-ventures/openclaw that referenced this pull request Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants