Skip to content

fix(polling): require consecutive misses before declaring a window dead - #131

Open
oliver-mee wants to merge 1 commit into
alexei-led:mainfrom
oliver-mee:fix/debounce-dead-window-detection
Open

fix(polling): require consecutive misses before declaring a window dead#131
oliver-mee wants to merge 1 commit into
alexei-led:mainfrom
oliver-mee:fix/debounce-dead-window-detection

Conversation

@oliver-mee

Copy link
Copy Markdown

Summary

status_poll_loop rebuilds its window lookup every cycle from TmuxManager.list_windows().
That method returns an empty list when get_session() hits a transient _TmuxError
(multiplexer/tmux.py:206) and silently drops individual windows whose pane metadata raises
(multiplexer/tmux.py:250, logger.debug only). tick_window treated a single absent poll as
death and went straight to _handle_dead_window_notification, so one bad read could put a
⚠ Session ... ended. banner on a topic whose window and agent were both still alive.

The is_dead_notified guard muted topics that had already been notified, so on a bad tick the
only topic that visibly got a banner was the most recently created one. That is why it presents
as "only new sessions disconnect".

This adds a missing_polls counter to WindowPollState and only takes the dead path once
MAX_MISSING_POLLS consecutive cycles have missed the window. A window that reappears resets
the counter. It mirrors the MAX_PROBE_FAILURES debounce already used for topic probes, so the
shape should be familiar.

Genuine deaths are unaffected: the window_died event stream calls
_handle_dead_window_notification directly and does not route through this path, so it still
fires instantly.

At the default 1s poll interval this delays a real tmux-backend death banner by about 2 seconds.

I kept the change out of polling_coordinator.py deliberately. It is already at its 120 line
ceiling, and a module-level counter there would have needed state the coordinator is not
supposed to own.

Linked issue

Fixes #130

Type of change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • Refactor or cleanup (no behaviour change)
  • Documentation
  • CI, tooling, or config

How to test

  1. uv run pytest tests/ccgram/handlers/polling/test_window_tick.py -k Missing -v covers the new
    behaviour: a single miss and a sub-threshold run of misses do not notify, a window that
    reappears resets the counter, and nothing fires before the threshold is crossed.
  2. The two existing TestTickWindowDeadWindow cases now tick MAX_MISSING_POLLS times before
    asserting, since one tick is no longer enough to declare death. That is the intended
    behaviour change.
  3. To see the original bug, patch TmuxManager.list_windows to return [] on a single call
    while a bound window is alive. On main that produces a recovery banner on a live topic; with
    this change it does not.

Ran locally on Python 3.14: ruff format --check, ruff check, pyright src/ccgram/, deptry src all clean. Unit suite 5931 passed, integration suite 304 passed. Three failures in
test_status_polling.py::TestMaybeDiscoverTranscript also fail on a clean checkout of main
at a475e0f (a KeyError: 'shell' in a provider registry mock), so they are unrelated to this
change.

Checklist

  • One concern only, scope limited to the summary above
  • I ran it and verified end to end (or explained below why that does not apply)

I hit this in daily use on ccgram 4.3.11 and have been running the equivalent debounce on my own
box since, with no recurrence of the spurious banner and no delay I have noticed on real window
deaths.

Model used

Claude Opus 4.8 via Claude Code. It did the code reading, wrote the patch and the tests, and ran
the suites. I found the bug in daily use, directed the investigation, chose to put the debounce
in window_tick rather than the coordinator, and reviewed the result.


🤖 Generated with Claude Code (Claude Opus 4.8)
🧑‍💻 Ideated, directed and reviewed by a human, @oliver-mee

status_poll_loop rebuilds its window lookup each cycle from
TmuxManager.list_windows(), which returns an empty list when get_session()
hits a transient _TmuxError and silently drops individual windows whose pane
metadata raises. tick_window treated a single absent poll as death and fired
the recovery banner, so one bad read could kill a live session's topic. The
is_dead_notified guard muted already-notified topics, which made it look like
only freshly created sessions were affected.

Track consecutive misses per window on WindowPollState and only take the dead
path once MAX_MISSING_POLLS cycles in a row have missed, mirroring the
existing MAX_PROBE_FAILURES debounce. A window that reappears resets the
counter. Genuine deaths still surface instantly through the window_died event
stream, which does not go through this path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant