tony · GitHub

@tony mentioned this pull request

Jul 12, 2026

Merged

@tony

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

@tony

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

@tony

@tony

tony deleted the winlinks branch

July 12, 2026 21:23

tony added a commit to tmux-python/libtmux-mcp that referenced this pull request

Jul 12, 2026
why: 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.

Read the original on github.com ↗