Skip to content

Spurious "Session ended" recovery banner: one failed tmux poll marks a live window dead #130

Description

@oliver-mee

Prerequisites

  • I searched existing issues and discussions, this is not a duplicate.
  • This reproduces on a clean, up to date checkout.

Environment

  • Install method: uv tool install ccgram
  • OS, browser, device: Ubuntu 24.04 aarch64 (Oracle ARM VPS), tmux backend
  • Version or commit: ccgram 4.3.11, behaviour still present on main at a475e0f

Steps to reproduce

This is a race against a transient multiplexer read, so it is intermittent rather than
deterministic. It shows up most often on a freshly created topic.

  1. Create a new Telegram topic and send a message so ccgram spawns a window and starts Claude.
  2. Let the poll loop run while the tmux server is under some load.
  3. Within a minute or so the topic can show ⚠ Session <name> ended. plus the recovery banner,
    while the tmux window and the agent process are both still alive and the session keeps working.

Expected behaviour

The recovery banner should only appear when the window is actually gone.

Actual behaviour

A single bad poll is enough to declare a live window dead.

status_poll_loop rebuilds its window lookup every cycle from
TmuxManager.list_windows() (handlers/polling/polling_coordinator.py). That method has two
paths where a live window silently disappears from the result:

  • multiplexer/tmux.py:206 returns an empty list when get_session() hits a transient
    _TmuxError (get_session catches, resets the server, returns None).
  • multiplexer/tmux.py:250 drops any individual window whose pane metadata raises, logging at
    debug only, so it is invisible at the default log level.

tick_window then treats window is None as death immediately, with no grace period, and calls
_handle_dead_window_notification.

The is_dead_notified guard is what makes the symptom look so specific. On the bad tick every
bound window looks dead, but topics that have already been notified stay quiet, so the only topic
that visibly gets a banner is the one created seconds ago. From the user's side it reads as
"only new sessions disconnect", which sent me looking in entirely the wrong place.

Worth noting the codebase already has the right primitive for this:
list_windows_for_reconciliation returns None to mean "cannot confirm" rather than [] to
mean "nothing exists", and TopicLifecycleStrategy already debounces topic probes with
MAX_PROBE_FAILURES. The poll loop's dead-window path just does not have an equivalent.

Logs or screenshots

From my box, window @11 created and the banner arriving under a minute later, with the tmux
window and the claude process still alive at the time of writing:

01:52:07 [info] Created window 'reference' (id=@11) at /home/ubuntu/magbrain/01-Projects/alibaba-token-plan/reference
01:52:07 [info] Bound thread 6247 -> window_id @11 (reference) for user ...
01:52:12 [info] Accepted bypass permissions prompt for window @11
01:52:12 [info] Session map: window_id @11 updated sid=f44e4aa0-..., cwd=/home/ubuntu/magbrain/01-Projects/alibaba-token-plan/reference
(banner appears in the topic ~01:53, nothing logged for it)
$ tmux list-panes -a -F '#{window_id} #{pane_pid} #{pane_current_command}'
...
@11 3448973 claude

Nothing is logged when this happens, because the drop path is logger.debug and the dead
notification itself does not log, which makes it awkward to diagnose from the log file alone.

Happy to send a PR that adds a consecutive-miss threshold before the dead path fires, mirroring
the existing MAX_PROBE_FAILURES pattern. Genuine deaths would still surface instantly through
the window_died event stream, which is unaffected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions