Repository navigation
Conversation
why: A newer client can connect to an older daemon, while normalized version comparisons lose the patch letter that distinguishes 3.3a. what: - Share a bounded daemon-first version check across ordinary raw dispatch - Fence the original native command batch on the checked release - Preserve standalone binary diagnostics without authorizing effects - Route liveness through the version check and make absent cleanup a no-op - Cover patch ordering, raw batches and replacement before mutation
why: A supported daemon can differ from its client executable. Numeric format introduction checks must also keep tmux patch letters out of Python package prerelease ordering. what: - Select format fields from fresh endpoint reports - Preserve native introduction metadata for offline format utilities - Remove obsolete popup, hook and prompt fallbacks below 3.3a - Restore the session dimensions test after retiring its 3.2 defect
why: Installation guidance and tests must name the same baseline that ordinary commands enforce on their connected daemon. what: - Retire 3.2a from the ordinary CI matrix and state the 3.3a floor - Explain daemon authority, diagnostics and absent cleanup - Document development reports and unversioned OpenBSD refusal - Describe fresh format selection separately from cached diagnostics
why: Bare `master` names no release. The legacy helpers map it to a synthetic maximum, which would let an undated development daemon pass the 3.3a check. what: - Require an identifiable release before dispatching to a daemon - Keep the legacy master normalization for diagnostics only - Test that a daemon reporting master is refused The master case fails on the previous check path; the focused version check suites pass (105 tests), as do ruff and mypy.
why: After a live daemon passes the version check, a command that finds it gone would let the configured executable start a new daemon, which may be a different release, before any check runs. what: - Pass -N when dispatch follows a successful live-daemon check - Keep cold startup validating the executable and creating natively - Test a daemon that disappears between the check and the command Without the fix the disappearance test restarts a daemon and fails. The focused suites pass (221 tests), as do ruff, mypy and the strict docs build.
why: A socket name or config path given to -L, -S or -f can itself be the literal -N, which must not count as the no-start flag. what: - Read -N from the parsed global options, not from any argv word - Test -N as the value of -L, -S and -f The regression fails on the previous code and the three value cases pass with the fix; ruff format and checks pass.
why: A numbered development report such as 3.3a-master fell through to the bare-master offline convenience, which selects the newest format catalog, so the daemon was asked for fields later releases introduced. what: - Map a numbered development report to its numeric format catalog before the bare-master convenience applies - Test that a 3.3a-master daemon requests no later fields The 3.3a-master regression fails on the previous code. The version check, format and break-pane checks pass (66 tests), as do strict Sphinx, ruff and mypy.
why: A POSIX shell can stay the pane's foreground process while it waits for a child, so neither the split reply nor a fixed delay shows that sleep is running, and the automatic-name assertion raced. what: - Run exec sleep and wait for its foreground name before asserting the window's automatic name Forcing the exact-3.7 placeholder on a newer daemon fails the intended name assertion; with the release guard restored the focused tests pass without reruns.
why: List accessors, winlink discovery and query lookup turned command errors into empty results, so an unsupported daemon looked like a server with no sessions instead of raising VersionTooLow. what: - Re-raise VersionTooLow from list-shaped accessors, winlink discovery and query lookup - Keep other command errors under the existing lenient list contract - Test each public traversal route against an unsupported daemon Every route fails on the previous code and raises the refusal with the fix. The full suite and the executed documentation examples pass (1499 tests, no reruns), as do strict Sphinx, ruff and mypy.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #779 +/- ##
==========================================
+ Coverage 52.37% 54.24% +1.87%
==========================================
Files 26 30 +4
Lines 3729 3969 +240
Branches 747 767 +20
==========================================
+ Hits 1953 2153 +200
- Misses 1472 1519 +47
+ Partials 304 297 -7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: The completion-marker example typed a nonexistent command and slept 200 ms before reading the pane. It failed in the 3.3a, 3.6 and 3.7b CI lanes and could also match the echo of the typed input. what: - Start a real command with the window and keep the pane alive - Wait with retry_until for the marker as a complete output line The doctest passes without retries on 3.3a and 3.7d; changing only the printed marker makes the unchanged predicate fail with WaitTimeout. Strict docs pass.
This was referenced Oct 17, 2026
why: tmux 3.3a prints its executable's basename in the -V banner, so a 3.3a build installed under another name was refused at the floor. what: - Match the configured executable's basename before the tmux prefix, including names with spaces and non-UTF-8 bytes - Keep full release validation and daemon framing unchanged The version check cases pass on 3.3a (52 tests); the previous parser fails four banner cases, three of them real renamed executables. Ruff, mypy and strict Sphinx pass.
why: The version probe ran display-message -p '#{version}', which fires
a user's after-display-message hook on every release before the check
can refuse anything.
what:
- Probe with -N run-shell -C '<marker>#{version}'. run-shell has no
after-hook, and with -C the expanded string is parsed in a timer
callback after the command returned, so no command-error fires. tmux
exits 1 with `unknown command: <marker><version>` on stderr and the
version is read from that line; any other failure stays an error
- Answer the mocked probes in test_versions.py in run-shell format
- Add a native test that arms after-display-message and, from 3.5 where
set-hook accepts it, command-error, then runs the probe, a prepared
command, a dispatched command, a control display-message and a
failing command; each sentinel holding exactly one letter shows no
probe fired a hook
The native test passes on 3.3a, 3.4, 3.5, 3.6b, 3.7d, 3.8 and master
(next-3.9). Restoring display-message as the probe fails it on 3.3a,
3.5, 3.8 and master. test_versions, test_server, test_common and
test_version pass on 3.7d (236 tests); ruff and mypy are clean.
why: The compatibility page still called the version query read-only, which says nothing about the user's hooks. what: - Say the check queries with run-shell -C, which fires no after-* or command-error hook - Name the one case where command-error fires: a replaced daemon failing the fence on tmux 3.5 or newer
why: The check accepted 3.3a-master and 3.3a-dev at the floor while it refused next-3.3a, rejected the bare 3.8-rc that tmux itself reports, and let 03.3a through. tmux never reports those suffixes; library wrappers made them up. what: - Parse only what tmux has reported: X.Y with an optional patch letter, X.Y-rc and X.Y-rcN, and next-X.Y, with ASCII digits and no leading zeros - Order them next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya, so a candidate or development build passes only above the floor - Replace the cases with one table of supported, below-floor and unparseable vectors, plus the ordering chains and the 3.7 feature floor Widening the pattern to accept -master/-dev and to require rc digits fails seven cases, including 3.8-rc and 3.3a-master. Ruff and mypy are clean.
why: Pane, Window, Session and Server chose optional flags such as -E, -k and -C from the client executable's -V, so a current client talking to a 3.3a daemon sent flags the daemon rejects with a native error instead of taking the warn-and-ignore path. what: - Add Server._has_gte_version, which compares the connected daemon's release, and use it at every optional-flag gate - Read the daemon's release for exact-3.7 break-pane naming too - Patch that decision in the warn-on-old-tmux tests - Test a client that reports the newest release against a 3.3a daemon Making _has_gte_version consult the client again fails the new test. The server, pane, capture, window and session modules pass on 3.7d.
why: Every command spawned a version probe plus the command, and each
optional-flag decision spawned another, so a lookup took two to four
tmux processes. The in-band if-shell -F fence already detects a
replaced daemon, so the release only needs probing again after the
fence fires or the daemon disappears.
what:
- Give each Server a VersionCache holding the release last checked for
its endpoint (binary, selectors, TMUX_TMPDIR and TMUX)
- Read it in prepare() and Server._get_version(), and share it with
list accessors through fetch_objs
- Clear it when the fence refuses. When no daemon answers, clear it and
check once more, which validates the executable for an absent
endpoint exactly as the first call does and cannot have had an effect
- Read the endpoint directly on the first call, for an absent endpoint
and in Server.kill(); never cache an absent endpoint
- Test one probe across three commands, a list accessor and a flag
decision; a stale cached release refused with no effect, forgotten
and re-read; a daemon killed from outside detected and a new session
started after a fresh check; kill() probing afresh
- Clear the cache in the existing disappearance and changed-report
tests so they still exercise the probe
tmux spawns per call, before and after, on 3.7d with config /dev/null:
Server.cmd("list-sessions") 2 and 1, Server.sessions 3 and 1, and
Pane.capture_pane with two optional flags 4 and 1. Wall time, best of
two runs on a shared WSL2 host (it varies by 30 percent between runs;
spawn counts do not): 100 list-sessions 3.02s and 1.94s, 100
Server.sessions 4.11s and 1.72s, 50 capture_pane 0.83s and 0.88s.
Breaking each guard fails its tests: not clearing on a fence refusal,
skipping the gone-daemon retry, and never storing the release. The
version check module passes on 3.7d (83 tests).
why: The compatibility page said the version check never caches, which stopped being true once each Server reused its checked release. what: - Say a Server probes its endpoint once and reuses the release - Say the fence refuses a replaced daemon with no effect - Say kill() and an absent endpoint read the endpoint directly
why: libtmux-mcp and tmuxp reuse the daemon-first version check for their own tmux subprocesses, and had to read private Server methods to do it. what: - Add libtmux._internal.support with MINIMUM, VersionTooLow, require_version, is_at_least, server_version(server) and check_fence, wrapping the existing helpers with no behaviour change - Test the exported names, an older daemon refused through server_version, a checked release returned, and a fence marker turned into VersionTooLow while an unrelated line is ignored - Run the module doctest
why: libtmux-mcp and tmuxp import libtmux._internal.support, so its names need a reference page beside the other internals. what: - Add an internals API page for libtmux._internal.support - Add its card to the internals index
why: Refusal below the floor was covered only with a monkeypatched probe. what: - Start a real 3.2a or 3.3 daemon and drive it with a 3.3a client: a write, a new session, a raw tmux_cmd kill-server and a list accessor all raise VersionTooLow, and a sentinel option and the session list stay unchanged - Check that a cold 3.2a or 3.3 executable cannot create its socket, and that a real 3.3a daemon passes as the control - Skip unless the three LIBTMUX_TEST_*_TMUX variables name native builds, the convention libtmux-mcp and tmuxp use The seven native cases pass against 3.2a, 3.3 and 3.3a builds. Making require_version accept everything fails the four refusal cases.
why: The native refusal tests skip unless real 3.2a, 3.3 and 3.3a builds are available, so CI needs a job that provides them. what: - Add an unsupported-versions job that builds tmux 3.2a, 3.3 and 3.3a from source, exports the three native paths and runs the version check module - Leave the supported matrix starting at 3.3a
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 17, 2026 12:25
6d1be63 to
73f82c7
Compare
This was referenced Oct 17, 2026
why: tmux 3.8 is a released tag, so the matrix should cover it as well as master. what: - Add 3.8 to the tmux matrix as a gating lane; only master stays continue-on-error Focused runs of the version check, control and optional-feature modules pass locally against a 3.8 build.
why: tmux 3.8 adds after-swap-window, client-closed, client-created, marked-pane-changed, session-added-to-group and session-removed-from-group, and drops after-queue. Hooks.from_stdout raised TypeError on the first hook its dataclass lacked, so show-hooks on 3.8 broke parsing. what: - Add the six 3.8 hooks as Hooks fields - Skip hooks the table does not know instead of failing the parse - Cover the 3.8 hooks in the set/show/unset grid, gated on 3.8 - Gate the after-queue case to tmux up to 3.7
why: From 3.8, new-pane -x/-y/-X/-Y give the on-screen footprint including the default border (regress/floating-pane-geometry.sh), so a pane asked for 80 columns reads pane_width 78. The test and the floating_panes doctest assumed content-box sizing and failed on 3.8. what: - Document the border offset on Pane.new_pane and Window.new_pane - Expect width/height minus 2 and x/y plus 1 on 3.8+, in the test and doctest
why: tmux 3.8 display-popup returns early for control-mode clients (cmd-display-menu.c: CLIENT_CONTROL check), so the command never ran and the popup tests timed out waiting on their marker files. what: - Add a pty_client fixture that attaches a terminal-backed client - Use it in the display_popup flag, close_on_success and target_client tests - Note the 3.8 control-client behaviour on Pane.display_popup
why: The per-Server version cache holds a threading.Lock, so copy.deepcopy(Server(...)) and pickle.dumps(Server(...)) raised "cannot pickle '_thread.lock' object". Both work on master. what: - Give VersionCache __getstate__/__setstate__ that drop the cached release and create a fresh lock, so a copy checks its daemon again rather than trusting a release checked by another object - Test deepcopy and pickle round trips keep the endpoint and start without a checked release Removing __getstate__ makes the round-trip test fail with the original TypeError.
why: server_version(server) returned the release the Server had cached, so after a 3.3a daemon was replaced by 3.2a on the same socket it still answered "3.3a". check_fence had no way to tell a Server that its cached version was stale, so a consumer dispatching its own fenced subprocess could not invalidate it either. what: - Clear the Server's cached version before server_version probes - Accept an optional server in check_fence and clear its cached version when the fence fires, before re-raising VersionTooLow - Test a real 3.3a daemon replaced by 3.2a: server_version refuses it and leaves nothing cached - Test that a fired fence clears the cache and a foreign marker does not - Import libtmux._internal.support at module level in the tests Each guard has a deliberate break that fails its test: server_version reading the cache, and check_fence keeping it.
why: get_version and the has_*_version helpers read the tmux client executable, which can be a different release from the daemon a Server talks to. libtmux checks the daemon through libtmux._internal.support, so the executable helpers are legacy and belong out of common. what: - Add libtmux/_internal/version.py holding get_version, get_version_str, has_version, has_gt/gte/lte/lt_version, has_minimum_version, their private helpers, TMUX_MIN_VERSION and TMUX_MAX_VERSION - Each public helper emits a DeprecationWarning (stacklevel 2) saying it reads the client executable and naming the libtmux._internal.support replacement, e.g. is_at_least(server_version(server), "3.4") - Keep cache_clear() on get_version and get_version_str, typed so mypy accepts it - Re-export every helper from libtmux.common, and serve the two constants through a module __getattr__ that warns, so old imports keep working - Resolve tmux_cmd through libtmux.common at call time so tests that replace it keep working - Point the tests that patch TMUX_MIN_VERSION and _query_version at the module that now owns them
why: The tests that exercise get_version and the has_*_version helpers test the legacy executable API, so they belong beside the other legacy tests and not among the suite that libtmux's own behaviour is built on. what: - Move the helper tests from tests/test_common.py to tests/legacy_api/test_version_helpers.py unchanged: memoization, cache keying, comparisons, version parsing and validation - Silence DeprecationWarning for that module only, since it deliberately calls the deprecated helpers - Leave tmux_cmd, session_check_name and the other common tests in place
why: The helpers now read like deprecated API but nothing proved they warn, point at the caller, or name the replacement, so dropping a warning would go unnoticed. what: - Parametrize over all eight public helpers: each emits a DeprecationWarning that names the client executable, its own libtmux.common name and libtmux._internal.support, and points at the caller's file - Check TMUX_MIN_VERSION and TMUX_MAX_VERSION warn through libtmux.common and still return their values - Check an unknown attribute still raises AttributeError - Check get_version.cache_clear and get_version_str.cache_clear survive
why: The doctests called has_gte_version, which reads the client
executable and now warns. They run against the daemon the server fixture
talks to, so that is the release to gate on.
what:
- Replace has_gte_version("X") with is_at_least(server_version(server),
"X") in the Pane, Server and Window doctests, importing both from
libtmux._internal.support
why: The floating-pane examples called has_gte_version, which reads the client executable and now warns, and the format-token page described the legacy helpers as if they were the way to read a version. what: - Use is_at_least(server_version(server), "X") from libtmux._internal.support in the floating-pane examples - Say on the format-token page that get_version and get_version_str are deprecated and point at server_version for the daemon's release
why: The suite gates tests on the release of the tmux it launches, which is the executable. The deprecated libtmux.common helpers warn on every call, so the suite needs its own small gate to keep libtmux's tests free of deprecation warnings. what: - Add tests/tmux_client.py with client_gte, client_lte and clear_caches, built on the private helpers in libtmux._internal.version so the letter-stripping comparison stays what the tests were written against - Flush the caches through clear_caches in the autouse conftest fixture - Read the daemon's release in the non-UTF-8 locale test through Server._get_version - Make the newer-client-executable test fake the executable's release where the helper now lives
why: These tests called has_gte_version and has_lte_version from libtmux.common, which now warn that they read the client executable. what: - Replace the helper calls in the hooks, options, pane, capture-pane, server, session and window tests with client_gte and client_lte from tests/tmux_client.py, with the same version comparisons - Reword the window-name test docstring that cited get_version
why: libtmux must not call the deprecated executable version helpers itself, but nothing stopped a doctest or a test from reintroducing one; the warning would only scroll past in the summary. what: - Turn a DeprecationWarning whose message starts with libtmux.common. into an error when it is raised from libtmux or from tests outside tests/legacy_api, which keeps exercising the helpers on purpose Each break fails and the restored tree passes: a doctest calling has_gte_version and a test outside legacy_api calling it.
tony
force-pushed
the
bump-min-tmux-000
branch
2 times, most recently
from
October 17, 2026 17:39
71f1bb5 to
f6e4630
Compare
why: Users upgrading need to know that tmux servers below 3.3a are now refused, which version helpers warn and what replaces them, and what was fixed for newer tmux releases. what: - Record the tmux 3.3a minimum, checked against the running server - Record the deprecated executable version helpers with a migration - Record flags following the server's release and the tmux 3.8 hooks - Record the completion example that waits for real output
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 17, 2026 18:10
f6e4630 to
c27dd92
Compare
This branch has not been deployed
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.
Require tmux 3.3a before ordinary reads, writes, batches and public raw command dispatch. Existing endpoints are checked against the connected daemon's version, preserving patch letters; a newer client executable cannot vouch for an older daemon. Cold creation validates the executable. A native command prefix checks the daemon's release before the requested commands, and live dispatch uses
-Nso losing a daemon cannot autostart an older executable or load its configuration.The version query is hook-free. It runs
-N run-shell -C '<marker>#{version}', which exits 1 and reports the version inunknown command: <marker><version>;run-shellhas no after-hook and, with-C, nocommand-errorhook fires, so a user'safter-display-messagehook (whichdisplay-messagefires on every release) never runs for the version check. A failure that does not carry the marker line stays an error, and onlyno server running, a missing socket or a refused connection mean an absent endpoint. The fence keepsif-shell -F; on tmux 3.5 and newer a replaced-daemon refusal firescommand-erroronce, which is an accurate error report.Each
Serverprobes its endpoint once and reuses the checked release for later commands and optional-flag decisions: theif-shell -Ffence in every command already catches a replaced daemon, so a refusal clears the cached release and the next command probes again. A command that finds the cached daemon gone clears it and checks again (validating the executable for an absent endpoint, as the first call does). The first call, an absent endpoint andServer.kill()always read the endpoint directly, and an absent endpoint is never cached.The version check parses only the forms tmux has emitted:
X.Ywith an optional patch letter,X.Y-rcandX.Y-rcN, andnext-X.Y, orderednext-X.Y<X.Y-rc<X.Y-rcN<X.Y<X.Ya. Bare3.8-rcis accepted;3.3a-master,3.3a-dev,next-3.3a, leading zeros andmasterare refused.Optional flags (
-E,-k,-Cand the rest) follow the connected daemon's release throughServer._has_gte_version, so a newer client cannot send a flag an older daemon rejects. Programs built on libtmux, such as libtmux-mcp and tmuxp, reuse the decision through the smalllibtmux._internal.supportmodule (MINIMUM,VersionTooLow,require_version,is_at_least,server_version,check_fence) instead of private names. There is no publiclibtmux.supportyet.Choose format fields from the daemon's numeric release, including numbered development reports, and remove obsolete pre-3.3 hook, popup and prompt adaptations. List accessors and query traversal propagate unsupported-version refusals while retaining ordinary command-error leniency. An absent
Server.kill()stays a no-op; standalone version/help diagnostics remain available. Undatedmaster, synthetic OpenBSD diagnostic values and malformed reports cannot pass the version check. The hook table tracks tmux 3.8 (six added hooks,after-queueremoved) and skips hooks it does not know instead of failing the parse, andnew_panedocuments the bordered footprint tmux 3.8 reports.The executable-version helpers in
libtmux.common(get_version,get_version_str,has_version,has_gt_version,has_gte_version,has_lte_version,has_lt_version,has_minimum_version, and theTMUX_MIN_VERSIONandTMUX_MAX_VERSIONconstants) now live inlibtmux._internal.version. They read the client executable, not the daemon, so each emits aDeprecationWarningthat names the replacement, e.g.is_at_least(server_version(server), "3.4"). They stay importable fromlibtmux.common. libtmux's own doctests, docs and tests no longer call them (a pytestfilterwarningserror enforces this outsidetests/legacy_api, which keeps testing them and asserts the warnings).CI, installation and compatibility documentation use the same minimum. The unreleased changelog is isolated in its own commit. Upgrade the executable and restart existing older daemons after saving their sessions.
Validation:
unsupported-versionsjob that builds tmux 3.2a, 3.3 and 3.3a and refuses real 3.2a and unpatched 3.3 daemons with no effects.after-display-messageand, whereset-hookaccepts it (3.5+),command-errorhooks, runs the probe, a prepared command and a dispatched command, then fires a controldisplay-messageand a failing command; hooks run in order, so each sentinel holding exactly one letter shows the version check fired neither. It passes on 3.3a, 3.4, 3.5, 3.6b, 3.7d, 3.8 and master (next-3.9). Restoringdisplay-messageas the probe fails it on 3.3a, 3.5, 3.8 and master. The 236 version-check, server, common and version tests pass on 3.7d; Ruff and mypy pass.-master/-devand require rc digits fails seven cases. Moving the flag decisions to the daemon is pinned by a test where the client helper says "newest" and the daemon reports 3.3a; asking the client again fails it. The per-endpoint reuse is covered by tests for one probe across commands, a stale cached release refused with no effect, a daemon killed from outside, andkill()probing afresh; not clearing on a fence refusal fails two tests, skipping the gone-daemon retry fails one, and never storing the release fails four. Seven native cases drive real 3.2a, 3.3 and 3.3a daemons; accepting everything fails the four refusal cases.Server.cmd("list-sessions")2 and 1,Server.sessions3 and 1,Pane.capture_panewith two optional flags 4 and 1. Wall time varied about 30 percent under shared load; best of two runs for 100list-sessionswas 3.02s and 1.94s and for 100Server.sessions4.11s and 1.72s.tests/test_versions.pypasses on real 3.3a, with Ruff, Ruff format and mypy clean. Removing the warning fromhas_lt_versionfails its test, and putting a deprecated call back in a doctest or in a test outsidetests/legacy_apifails the suite.Merge and release order: this PR and #780 land first and libtmux 0.63.0 is released from them. libtmux-mcp (#139, #140) and tmuxp (#1092, #1093) require
libtmux>=0.63.0and~=0.63.0, so they merge only after that release; each then drops its development[tool.uv.sources]pin. Nothing here tags or releases.Other Python versions and platforms remain outside this qualification. Optional engine and feature restrictions are covered by the paired feature change.
The version check assumes that library-owned metadata and guard commands retain native tmux built-in semantics. Deliberate server
command-aliasredefinitions of those commands are outside this guarantee.Closes #777.