Skip to content

Typing indicator refresh can land after the stop, leaving a sticky iMessage bubble #1495

Description

@njbrake

Note: this issue was drafted by Claude (Opus 5) via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is the model's.

Summary

ChannelManager._spawn_typing_task (channels/manager.py:190) dispatches typing sends as detached tasks with no ordering guarantee. A typing refresh published shortly before a stop can therefore reach the channel after the stop's DELETE. On iMessage the bubble is sticky, so the user is left with a permanent "typing..." that never resolves, which is the exact failure stop_typing_indicator exists to prevent.

Where it bites

Heartbeat turns that decide not to reply. The heartbeat builds a full ClawboltAgent with publish_outbound and chat_id (agent/heartbeat.py:644), so the keepalive added in #1494 refreshes the indicator during heartbeat turns, and _publish_heartbeat_typing(..., stop=True) (agent/heartbeat.py:191) cancels it on the no-reply path.

Before #1494 the last indicator was published at the start of the final LLM call, separated from the stop by the whole call, so inversion was effectively impossible. The 4s keepalive narrows that gap to at most one refresh interval, so a refresh published milliseconds before teardown now races the stop. Rough odds are POST latency divided by the refresh interval, so single-digit percent of long heartbeat no-reply turns.

Proposed fix

Serialize typing operations per chat in ChannelManager: keep at most one in-flight typing task per chat_id and have a stop supersede any pending refresh rather than running concurrently with it. Kept out of #1494 deliberately, since it means touching shared outbound dispatch rather than the agent loop.

Note the fire-and-forget dispatch itself is intentional (#1083): typing sends must not block reply delivery, so the fix needs to preserve that.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions