Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #718 +/- ##
==========================================
+ Coverage 52.14% 52.29% +0.15%
==========================================
Files 26 26
Lines 3688 3725 +37
Branches 741 747 +6
==========================================
+ Hits 1923 1948 +25
- Misses 1461 1472 +11
- Partials 304 305 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Code review — resolvedThis review originally covered The four valid findings are addressed:
The Maintainer sign-off: keep |
Code review — addendumAn adversarial bug-scan pass found no correctness bugs in the change, and independently verified the part I most wanted checked: no That downgrades issue 5 in my earlier comment from a live bug to a latent one — worth a contract line, not a fix. One more real issue, though:
Lines 245 to 262 in 89396ba Also worth knowing, pre-existing and not introduced here: 🤖 Generated with Claude Code |
why: Both #716 and #717 come from one gap: a window_id names a window, but a row in list-windows names a winlink -- the (session, index, window) edge -- and one window can own several. The word has no entry, so neither issue can be explained without inventing vocabulary. what: - Add a winlink glossary term, tying it to Window and Session
why: A window linked twice into one session has two winlinks, so list-windows -t @id returns two rows for it. The survivor loop kept the last, reporting the highest index; tmux reports the winlink it would act on. Users of 0.61.0 met this as a wrong window_index. what: - Port cmd_find_best_winlink_with_window: the current window if it holds the target, otherwise the first. #{window_active} is tmux's own wl == s->curw test, and a listing walks the winlinks in index order, so both halves read off the rows already fetched - Keep the single-row path untouched: panes, sessions, and clients have no winlink edge and never reach the tie-break
why: A failed QueryList.get() raised MultipleObjectsReturned or ObjectDoesNotExist -- both outside LibTmuxException and both raised bare, so `except LibTmuxException` walked past them and str(e) was empty. The caller saw nothing, caught nothing, learned nothing. BREAKING: TmuxObjectDoesNotExist inherits ObjectDoesNotExist, so it moves under LibTmuxException too. Handlers that catch the base before the specific one now leave the specific clause unreachable, and code that retries on LibTmuxException should exclude ObjectDoesNotExist -- an object that is not there will not appear on a retry. what: - Move both exceptions into exc.py, where they subclass LibTmuxException and render in autodoc; query_list re-exports them, so the old import path keeps working - exc.py imports nothing from libtmux at runtime, so no cycle is possible; _internal already depends on exc (see _internal/env.py) - Give get() failures a message naming the count and the query - Revive two asserts that named the intended messages but never ran
why: A shared window is genuinely in several sessions at once, but Window.session answers with only the session recorded on that window. Asking which sessions hold a window meant reading duplicate rows out of a server-wide listing -- inferring the answer from an artifact instead of asking. what: - Add Window.linked_sessions, listing each holding session once however many indexes it links the window at
why: The winlink shapes had no coverage. The same-session double link had no helper at all, and nothing asserted libtmux against tmux's own answer for it. what: - Assert against the display-message oracle, never a hardcoded index, so a tmux behaviour change breaks CI loudly instead of drifting - Cover all three shapes: same-session double link, cross-session link, grouped session -- and all three current-window states, since master agrees with tmux in exactly one of them by accident
why: A server-wide listing enumerates winlinks, not windows, so a shared window appears once per session holding it. Nothing said so, and a reader who hit it had no way to tell a bug from the model. what: - Say what a server-wide listing enumerates, and when an id can match more than one row - Point a contested lookup at Pane.from_pane_id and Window.from_window_id, which ask tmux and always have exactly one answer
why: Upgraders need to account for lookup errors joining the common libtmux exception hierarchy in 0.62. what: - Document the new exception relationships and handler order - Exclude deterministic lookup outcomes from blanket retries
why: Readers need the forthcoming release framed around the breaking lookup hierarchy, linked-window behavior, and public accessors. what: - Add release lead and exception migration guidance - Document linked sessions, winlink resolution, and lookup messages
Fixes #716. Fixes #717.
Both issues are one modelling gap: a
window_idnames a window, but a row inlist-windows/list-panesnames a winlink — the(session, index, window)edge — and one window can own several. libtmux's object layer assumed rows were unique per id. #717 is that gap surfacing as wrong attribution; #716 is the same gap surfacing as wrong arity.This is not an inference. tmux enumerates winlinks in its own format tokens — link a window into
aaatwice and intozzzonce, and#{window_linked_sessions_list}answersaaa,zzz,aaa.#717 — the wrong window index
A window linked twice into one session has two winlinks, so
list-windows -t @0returns two rows with the samewindow_idand differentwindow_index.neo.fetch_obj's survivor loop kept the last, so libtmux reported the highest index where tmux reports the one it would act on.The fix ports tmux's own rule, whose comment reads "the current if it contains the window, otherwise the first" (
cmd-find.c:204-231):Both halves are read off rows we already fetched.
#{window_active}is not a proxy for tmux's test —format_cb_window_activeisft->wl == ft->wl->session->curw, the identical predicate; and a listing walks thewinlinksRB-tree, whichwinlink_cmpkeys by index, so "the first" is the lowest index. Unchanged 3.2a→3.7b.Note this is not "libtmux inventing a tie-break" — keeping the last row was an invented tie-break, just an unnamed and incorrect one. Master happens to agree with tmux in exactly one of the three current-window states, by accident. The tests cover all three.
The
-tscoping from #713 already delegates the cross-session question to tmux, so the port only has to be right within a session — precisely where the format tokens are exact. Across sessions they are not: tmux comparesstruct timevalwithtimercmp()while#{session_activity}renderstv_seconly, and on a headless serveractivity_timenever advances pastcreation_time. That half is left to tmux, where it belongs.#716 — the ambiguous lookup
The collections are unchanged.
Server.panes/Server.windowsstill enumerate winlinks, because that is what tmux stores and what-ameans. A shared pane really is reachable two ways, anddocs/topics/self_location.mdalready relies on that signal.What was broken is that the ambiguity was reported illegibly.
MultipleObjectsReturnedsat outsideLibTmuxExceptionand raised bare, soexcept LibTmuxExceptionwalked past it andstr(e)was empty. The user saw nothing, caught nothing, learned nothing. Now:…catchable with everything else libtmux raises. The point lookups (
Pane.from_pane_id,Window.from_window_id) always had exactly one answer, because they ask tmux — the docs now say so.Window.linked_sessionsis the supported way to ask "which sessions hold this window?", listing each holder once however many indexes it links the window at.MultipleObjectsReturnedandObjectDoesNotExistnow subclassLibTmuxException.TmuxObjectDoesNotExistinherits fromObjectDoesNotExist, so it moves underLibTmuxExceptiontoo.Two consequences, both in
CHANGES:LibTmuxExceptionshould excludeObjectDoesNotExist— an object that is not there will not appear on a retry.libtmux-mcp'sReadonlyRetryMiddlewaredefaults toretry_exceptions=(LibTmuxException,)and needs a lockstep change.The old import path still works:
from libtmux._internal.query_list import MultipleObjectsReturnedresolves via re-export.No cycle is introduced.
exc.pyimports nothing fromlibtmuxat runtime, and_internalalready depends onexc(_internal/env.py) — the arrow was simply backwards before.The exception commit (
f084df97) is isolated and revertible on its own if the hierarchy change is contested.Verification
ruff check --fix·ruff format·mypy·pytest --reruns 0·just build-docs— all green.Every resolution test asserts against
display-message -p -t <id>, tmux's own answer, never a hardcoded index — so a change in tmux breaks CI loudly instead of drifting silently. Coverage: same-session double link, cross-session link, grouped session (new-session -t), and all three current-window states.Also fixes a shipped copy-paste bug found in passing:
TmuxObjectDoesNotExist's docstring read "The query returned multiple objects when only one was expected."