Prerequisites
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.
- Create a new Telegram topic and send a message so ccgram spawns a window and starts Claude.
- Let the poll loop run while the tmux server is under some load.
- 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.
Prerequisites
Environment
uv tool install ccgrammainat a475e0fSteps 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.
⚠ 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_looprebuilds its window lookup every cycle fromTmuxManager.list_windows()(handlers/polling/polling_coordinator.py). That method has twopaths where a live window silently disappears from the result:
multiplexer/tmux.py:206returns an empty list whenget_session()hits a transient_TmuxError(get_sessioncatches, resets the server, returnsNone).multiplexer/tmux.py:250drops any individual window whose pane metadata raises, logging atdebugonly, so it is invisible at the default log level.tick_windowthen treatswindow is Noneas death immediately, with no grace period, and calls_handle_dead_window_notification.The
is_dead_notifiedguard is what makes the symptom look so specific. On the bad tick everybound 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_reconciliationreturnsNoneto mean "cannot confirm" rather than[]tomean "nothing exists", and
TopicLifecycleStrategyalready debounces topic probes withMAX_PROBE_FAILURES. The poll loop's dead-window path just does not have an equivalent.Logs or screenshots
From my box, window
@11created and the banner arriving under a minute later, with the tmuxwindow and the
claudeprocess still alive at the time of writing:Nothing is logged when this happens, because the drop path is
logger.debugand the deadnotification 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_FAILURESpattern. Genuine deaths would still surface instantly throughthe
window_diedevent stream, which is unaffected.