Skip to content

fix(Pane,Window): Resolve point lookups through tmux#713

Open
tony wants to merge 2 commits into
masterfrom
targeted-lookups
Open

fix(Pane,Window): Resolve point lookups through tmux#713
tony wants to merge 2 commits into
masterfrom
targeted-lookups

Conversation

@tony

@tony tony commented Jul 11, 2026

Copy link
Copy Markdown
Member

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:

>>> window = home.active_window                       # 'aaa-home'
>>> server.cmd("link-window", "-s", window.window_id, "-t", "zzz-guest")
>>> Window.from_window_id(server, window.window_id).session_name
'zzz-guest'

Rename zzz-guest to aaa-guest and the same call answers aaa-home instead. Nothing about the window changed.

The four point lookups — Pane.refresh, Pane.from_pane_id, Window.refresh, Window.from_window_id — passed -a to list-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 by strcmp), and neo.fetch_obj scans 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 through cmd_find, which picks the most recently active session holding it. libtmux and tmux gave different answers for the same id.

The fix

Name the object with -t and let tmux resolve it.

tmux's cmd_find_target routes a target to a slot by sigil, not by the command's declared target type (cmd-find.c:1078-1083): $→session, @→window, %→pane. So list-panes -t %ID routes %ID to the pane slot even though list-panes declares a window target, and resolves upward through cmd_find_best_session_with_window() — the same function tmux uses for every -t you type. One row comes back, stamped with the session tmux itself would act on.

This is byte-identical in every tmux CI covers (3.2a3.7b), and libtmux already ships -t on these commands for same-type targets (Window.panes, Session.windows).

Point lookups also get cheaper: -t scans one window's panes or one session's windows instead of the whole server.

Server.panes / Server.windows / Server.sessions keep -a — a server-wide listing is what they're for.

The catch, and what it bought

A live server rejects an unknown -t target on stderr (can't find pane: %99) rather than by returning an empty listing. raise_if_stderr turns that into a LibTmuxException, so fetch_obj would have stopped raising TmuxObjectDoesNotExist altogether — and TmuxObjectDoesNotExist extends ObjectDoesNotExist, not LibTmuxException, so nothing downstream would have caught it.

neo._is_target_not_found_error classifies that stderr, mirroring the existing server._is_daemon_not_up_error. Between them they answer a question the two failures previously had no way to distinguish: is the object gone, or is the server gone? A missing object stays TmuxObjectDoesNotExist; a dead daemon, a missing socket or a permission error stay LibTmuxException.

Behaviour change

For a window in exactly one session — the overwhelmingly common case — nothing changes.

For a linked 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. That is tmux's own semantics, and it is what every -t command already does.

Tests

tests/test_resolution.py builds a window linked into two sessions whose name order and activity order disagree (zzz-old sorts last but aaa-recent is active), so a resolver that reads the last row of an -a listing cannot pass by luck. Both linked-window tests fail on master and pass here.

The other five are guards for what the -t switch could have broken, and pass on master too:

  • a Pane held across a move-window still finds its session (Pane.session resolves through Pane.window, which re-queries — answering from the pane's cached session_id would 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 raises TmuxObjectDoesNotExist
  • a missing pane on a live server is TmuxObjectDoesNotExist; a dead server is not

tests/test_neo.py covers the classifier against real tmux stderr for both kinds of failure.

Verification

ruff · ruff format · mypy · pytest --reruns 0 (1361 passed) · just build-docs — all green.

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
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 51.85%. Comparing base (e67075a) to head (674206f).

Files with missing lines Patch % Lines
src/libtmux/neo.py 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #713      +/-   ##
==========================================
- Coverage   51.86%   51.85%   -0.02%     
==========================================
  Files          25       25              
  Lines        3638     3645       +7     
  Branches      733      735       +2     
==========================================
+ Hits         1887     1890       +3     
- Misses       1445     1449       +4     
  Partials      306      306              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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
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.

Pane.session / Window.session return an arbitrary session for a linked window

1 participant