fix(Pane,Window): Resolve point lookups through tmux#713
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #713 +/- ##
==========================================
+ Coverage 51.78% 51.93% +0.14%
==========================================
Files 25 25
Lines 3638 3645 +7
Branches 733 735 +2
==========================================
+ Hits 1884 1893 +9
+ Misses 1448 1446 -2
Partials 306 306 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
tony
added a commit
that referenced
this pull request
Jul 11, 2026
why: The answer to "which session is this window in?" changes for anyone with a linked window, and the exception a failed lookup raises is now narrower. Both are visible from outside. what: - Add a Fixes entry for #713
This was referenced Jul 11, 2026
Closed
Open
tony
added a commit
that referenced
this pull request
Jul 12, 2026
why: The answer to "which session is this window in?" changes for anyone with a linked window, and the exception a failed lookup raises is now narrower. Both are visible from outside. what: - Add a Fixes entry for #713
why: A window can be linked into several sessions at once. The four
point lookups -- Pane.refresh, Pane.from_pane_id, Window.refresh,
Window.from_window_id -- listed every object on the server and picked
the last row matching the id. tmux orders that listing by session name,
so libtmux answered "which session is this in?" with whichever session
happened to sort last, and the answer changed if you renamed a session.
It also disagreed with tmux, which resolves the same id to the session
it would itself act on.
Naming the object with -t hands the question to tmux's cmd_find, which
picks the most recently active session holding the window. One row comes
back instead of a server-wide scan, so the lookups also get cheaper.
The catch: a live server rejects an unknown -t target on stderr rather
than by returning an empty listing, so fetch_obj would have stopped
raising TmuxObjectDoesNotExist entirely. Classifying that stderr also
separates "the object is gone" from "the server is gone", which the two
failures had no way to distinguish before.
what:
- Add neo._is_target_not_found_error, mirroring
server._is_daemon_not_up_error, and map tmux's "can't find <kind>"
back to TmuxObjectDoesNotExist inside fetch_obj
- Switch the four point lookups from list_extra_args=("-a",) to
("-t", <id>)
- Cover the classifier with a parametrized NamedTuple fixture, and the
resolution itself against a window linked into two sessions whose
name order and activity order disagree
tony
added a commit
that referenced
this pull request
Jul 12, 2026
why: The answer to "which session is this window in?" changes for anyone with a linked window, and the exception a failed lookup raises is now narrower. Both are visible from outside. what: - Add a Fixes entry for #713
157e016 to
5a424af
Compare
why: A window linked into several sessions resolved to whichever session sorted last by name, so renaming a session changed libtmux's answer. That shipped in 0.61.0, so users of a published release met it. what: - Describe the shipped defect and the answer tmux itself gives - Warn that a linked or grouped window's session now follows activity, so it can change between calls where it used to be stably arbitrary - Note the lookups no longer scan the whole server
tony
added a commit
that referenced
this pull request
Jul 12, 2026
A tmux window can be linked into several sessions at once, and into one session at several indexes -- tmux calls each of these a winlink. libtmux flattened them: a multiply-linked window could report a session or index tmux itself would not act on, a shared window had no way to name every session holding it, and an ambiguous lookup failed with no explanation. This teaches the resolution path, a new accessor, and the lookup errors to model winlinks the way tmux does. - **Resolve**: Window.from_window_id(), Pane.from_pane_id() and both refresh() methods now return the session and index tmux would act on -- the current winlink when it holds the window, otherwise the lowest-indexed one -- instead of whichever row sorted last. This extends #713, which fixed the session, to the window index. - **List**: Window.linked_sessions names every session a window is reachable from, each once even when it holds the window at several indexes, in two list commands regardless of holder count. It returns an empty result on tmux error, like the other list accessors. - **Explain**: QueryList.get() now names both the lookup and its outcome -- "No objects found: pane_id='%99'", "Multiple objects returned (2): pane_id='%0'" -- and the exceptions carry that data (count, query) for callers that branch on it. - **Tests**: end-to-end cases build windows linked across and within sessions and compare resolution against live tmux over the supported version range, so a resolver that reads the wrong row cannot pass by luck. - **Docs**: a winlink glossary entry and a filtering guide for why Server.windows and Server.panes enumerate winlinks rather than deduplicating windows. Breaking change: exc.ObjectDoesNotExist and exc.MultipleObjectsReturned now subclass LibTmuxException, so TmuxObjectDoesNotExist falls under it too. Order exception handlers most-specific-first, and exclude these two deterministic lookup exceptions from blanket LibTmuxException retry policies. The libtmux._internal.query_list import path still works. See MIGRATION for before/after examples. Fixes #717. Addresses #716. Refs #713.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #710.
The bug
A window can be linked into several sessions at once. Ask libtmux which session such a window (or a pane inside it) is in, and the answer depends on what the sessions are called:
Rename
zzz-guesttoaaa-guestand the same call answersaaa-homeinstead. Nothing about the window changed.The four point lookups —
Pane.refresh,Pane.from_pane_id,Window.refresh,Window.from_window_id— passed-atolist-panes/list-windows, listing every object on the server. tmux emits a linked window once per holding session, ordered by session name (its session tree is keyed bystrcmp—session.c:41-44), andneo.fetch_objscans that listing keeping the last row that matches the id. So "which session?" resolved to whichever session sorted last alphabetically.It also disagreed with tmux.
tmux display-message -t @0 '#{session_id}'resolves the same window throughcmd_find, which picks the most recently active session holding it (cmd-find.c:176-202). libtmux and tmux gave different answers for the same id.The fix
Name the object with
-tand let tmux resolve it.tmux's
cmd_find_targetroutes a target to a slot by sigil, not by the command's declared target type (cmd-find.c:1078-1083):$→session,@→window,%→pane. Solist-panes -t %IDroutes%IDto the pane slot even thoughlist-panesdeclares a window target, and resolves upward throughcmd_find_best_session_with_window()— the same function tmux uses for every-tyou type. One row comes back, stamped with the session tmux itself would act on.This is byte-identical in every tmux CI covers (
3.2a→3.7b), and libtmux already ships-ton these commands for same-type targets (Window.panes,Session.windows).Point lookups also get cheaper:
-tscans one window's panes or one session's windows instead of the whole server.Server.panes/Server.windows/Server.sessionskeep-a— a server-wide listing is what they're for.The catch, and what it cost
A live server rejects an unknown
-ttarget on stderr (can't find pane: %99) rather than by returning an empty listing.raise_if_stderrturns that into aLibTmuxException, sofetch_objwould have stopped raisingTmuxObjectDoesNotExistaltogether — andTmuxObjectDoesNotExistextendsObjectDoesNotExist, notLibTmuxException, so nothing downstream would have caught it.neo._is_target_not_found_errorclassifies that stderr, mirroring the existingserver._is_daemon_not_up_error. It buys nothing new: it keeps a distinction that already works, which-twould otherwise have destroyed. A missing object staysTmuxObjectDoesNotExist; a dead daemon, a missing socket or a permission error stayLibTmuxException— exactly as in0.61.0. Only the mechanism behind that contract moved.Behaviour change
For a window in exactly one session — the overwhelmingly common case — nothing changes.
For a linked or grouped window,
refresh()/from_pane_id()/from_window_id()now report tmux's canonical session instead of the alphabetically-last one. That is the fix. Note the consequence: because tmux picks by activity, the answer can now change over time as focus moves between the sessions holding the window, where before it was stable but arbitrary. That is tmux's own semantics, and it is what every-tcommand already does. Grouped sessions (tmux new-session -t) are ordinary usage — tmuxp creates them — so this is worth knowing before you upgrade.Not in this PR
Duplicate rows still crash the server-wide accessors — #716.
Server.panes/Server.windowskeep-a, soserver.panes.get(pane_id=…)still raisesMultipleObjectsReturnedfor a window shared between sessions:tmux list-panes -aemits one row per holding session, andQueryList.get()rejects >1 match before consultingdefault. Untouched here, and it pre-dates this branch. It is also why the fix has to be-t: you cannot dedupe an-alisting without inventing the very tie-break tmux already owns.from_pane_id/from_window_idare the correct migration target for callers hitting it.window_indexis still wrong for a window linked twice into one session — #717.list-windows -t @Nreturns one row per winlink, so a window linked into the same session at two indexes still produces two rows inside the-tscope, and the survivor loop still keeps the last. This PR fixes thePaneside (list-panesemits each pane once) and does not reach theWindowside. The session this PR is about is right either way — both rows carry the samesession_id— butwindow_indexis not, and fixing it means resolving throughdisplay-message -p -t, which is a refactor rather than a patch.Tests
tests/test_resolution.pybuilds a window linked into two sessions whose name order and activity order disagree (zzz-oldsorts last butaaa-recentis active), so a resolver that reads the last row of an-alisting cannot pass by luck. Both linked-window tests fail onmasterand pass here.The others are guards for what the
-tswitch could have broken, and pass onmastertoo:Paneheld across amove-windowstill finds its session (Pane.sessionresolves throughPane.window, which re-queries — answering from the pane's cachedsession_idwould be one subprocess cheaper and would break here, because tmux destroys a session when its last window leaves)refresh()on a killed window/pane still raisesTmuxObjectDoesNotExistTmuxObjectDoesNotExist; a dead server is nottests/test_neo.pycovers the classifier against real tmux stderr for both kinds of failure.Verification
ruff·ruff format·mypy·pytest --reruns 0·just build-docs— all green locally, and CI is green across the full matrix (3.2a→master).