Merged
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: 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
why: The fallback returned rows[0], correct only while the caller handed rows in ascending window_index order -- a precondition that was documented but never enforced. A future reorder would silently resolve the wrong winlink. what: - Return the lowest-window_index row via min(), not rows[0], so the fallback matches tmux's "first" regardless of input order - Drop the ascending-order precondition from the docstring - Add a doctest proving a high-index-first listing still resolves to tmux's first
why: get() guarded the empty-match branch with `default == no_arg`. A default whose __eq__ is non-identity (a Mock, a numpy array) makes that comparison truthy or non-bool, so get(default=x) wrongly raised ObjectDoesNotExist instead of returning x. what: - Compare the no_arg sentinel with `is`, not `==` - Add a regression test with a broad-__eq__ default
why: The constructor accepts `query=` and renders it into the message but discarded it, so `except ObjectDoesNotExist as e: e.query` raised AttributeError -- unlike its sibling MultipleObjectsReturned, which exposes the same data. what: - Store `self.query` in ObjectDoesNotExist.__init__, matching MultipleObjectsReturned - Add a regression test asserting the attribute
tony deleted the winlinks branch
July 12, 2026 21:23tony added a commit to tmux-python/libtmux-mcp that referenced this pull request
Jul 12, 2026why: tmux-python/libtmux#718 shipped in 0.62.0, moving MultipleObjectsReturned and ObjectDoesNotExist -- and TmuxObjectDoesNotExist with them -- under LibTmuxException. The retry and error-mapping fixes that follow depend on that hierarchy, so the floor must require the release that provides it. what: - Raise the libtmux floor from >=0.61.0 to >=0.62.0 and relock against the PyPI release
tony added a commit to tmux-python/libtmux-mcp that referenced this pull request
Jul 13, 2026… libtmux Rebasing onto main pulled in #98, whose retry-skip set and error mapping key on libtmux 0.62.0's re-parented query errors (tmux-python/libtmux#718 moved ObjectDoesNotExist and MultipleObjectsReturned under LibTmuxException). The chainable-commands experiment pins libtmux to an older branch that lacks MultipleObjectsReturned and where TmuxObjectDoesNotExist is not yet a LibTmuxException, so the merged code would not import there. Resolve the libtmux exception types by name in NON_RETRYABLE_EXCEPTIONS and in _map_exception_to_tool_error, falling back to a never-matched sentinel for any type this libtmux build does not define. Behavior is identical on the libtmux>=0.62.0 floor the package targets; on the older pin the missing arms fall through to the generic LibTmuxException handler and the package stays importable. Gate the #98 test cases that assert the 0.62.0 hierarchy so they run on that floor and are skipped on the older pin. Relock uv.lock against the merged pyproject.