Skip to content

Require tmux 3.3a across MCP operations - #139

Draft
tony wants to merge 19 commits into
mainfrom
bump-min-tmux-000
Draft

tony wants to merge 19 commits into
mainfrom
bump-min-tmux-000

Conversation

@tony

@tony tony commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Require tmux 3.3a for ordinary MCP operations. The version check uses the selected daemon, or freshly checks the configured executable when that endpoint is absent. A newer installed client cannot authorize an older daemon.

The owned version probe is hook-free: -N run-shell -C '<marker>#{version}' replaces display-message, so a user's after-display-message and command-error hooks do not run for the version check; the version is read from the unknown command: <marker> line. Owned asynchronous and synchronous subprocesses, buffer/input helpers and generated completion commands carry the same version check and native version guard. Metadata and effects share one remaining process deadline; cancellation kills and reaps the owned asynchronous child. Startup checks the backend and selected endpoint before tool traffic. Cached server handles retain configuration, while each operation checks the current daemon.

The package requires libtmux>=0.63.0, the first release with the libtmux._internal.support module this PR imports; the previous >=0.62.0,<1.0 range was satisfied by the released 0.62.0, which has no backend, so pip, pipx and uvx would have installed a libtmux this package refuses. The development lock pins the implementation from Python core PR779 until the release exists; uv's source override is ignored by ordinary installers.

Merge and release order: libtmux 0.63.0 is released first, from #779 and #780. This PR requires libtmux>=0.63.0, which no installer can satisfy until then, so it merges only after that release, in the same change that drops the development [tool.uv.sources] pin. Nothing in this PR tags or releases.

Validation:

  • Full local suites without reruns: 924 passed and 12 skipped on 3.3a; 930 passed and six skipped on 3.7d. Strict Sphinx, Ruff format/lint and mypy pass.
  • Native version check tests cover live older daemons, cold older executables, unpatched 3.3, endpoint replacement, renamed 3.3a executables, cancellation and the shared deadline. Incompatible backend checks refuse before spawning.
  • A native hook test sets after-display-message and, where accepted, command-error hooks and runs both lanes; it passes on 3.3a and 3.7d and fails with b'xxx' where b'x' is expected when a display-message call is put back into the probe. The pin moves to libtmux f6e4630a, which carries the same probe and publishes the version check as libtmux._internal.support. Hosted CI passes at a2b4b3af on the unsupported-versions job and 3.3a, 3.4, 3.5, 3.6, 3.7a, 3.8 and master (run).
  • Review follow-ups. Probe, -V and backend failures raise LibTmuxException, so the tool boundary reports them as expected errors; a probe that fails with permission denied raises it on both lanes and a RuntimeError fails the test. An unsupported default endpoint logs a warning and startup continues, because tools check each endpoint per call; re-raising fails the lifespan test. A run_command whose typed fence is refused after a daemon replacement now reports a version error instead of timing out; dropping the check fails its test.
  • Restoring the prior uninterruptible wait implementation leaves an observed native child alive after cancellation and fails the permanent regression. The restored owned implementation passes. The child probe also remains effective when terminal width would truncate ordinary process listings.
  • Independent reviews cover the final production paths and fixture corrections. Every replayed file matches the reviewed, gated source; the changelog is isolated. Workflow YAML and lockfile metadata parse, and other locked package versions are unchanged.

Local native evidence covers Linux/WSL. The earlier full run on a mounted-drive virtual environment exceeded its bound; the fresh locked Linux environment passes with the same source. Parent libtmux operations delegated to worker threads retain their existing cancellation behavior. Library-owned metadata/guard commands assume native built-in semantics; configured alias bodies and user shell programs retain native semantics. Optional feature boundaries are the separate stacked PR, while baseline polling stays on 3.3a.

Closes #137.

tony added 5 commits October 17, 2026 02:11
why: The released dependency range alone does not identify the new
daemon version-check contract.

what:
- Select the reviewed immutable libtmux core source
- Keep the existing package range and other locked versions
why: Direct subprocesses and completion helpers could bypass backend
version checks, while a live probe alone permitted endpoint replacement.

what:
- Check the actual daemon or freshly checked cold executable
- Fence owned commands and share the remaining process deadline
- Carry synchronous and asynchronous callers with cancellation ownership
- Retain baseline polling and standalone configuration caching
- Exercise native old-server refusal and actual child cleanup
why: The supported matrix must retain the 3.3a floor and separately prove
rejection below it.

what:
- Keep supported lanes from 3.3a and add stable 3.7a
- Build native 3.2a and exact 3.3 for negative version checks
- Isolate test sockets without suppressing the refusal cases
why: Operators need the selected-daemon rule and a concrete installation
route for the unreleased coordinated backend.

what:
- Document the 3.3a core minimum and source-pinned development pair
- State worker-thread cancellation and native alias boundaries
- Keep optional requirements separate from baseline polling
why: The tmux support floor and owned command behavior change.

what:
- Record daemon version checks and bounded subprocess cleanup
- Describe the coordinated backend requirement
@codecov-commenter

codecov-commenter commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.97727% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.52%. Comparing base (3e72eae) to head (a2b4b3a).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
src/libtmux_mcp/_tmux_proc.py 79.10% 18 Missing and 10 partials ⚠️
src/libtmux_mcp/tools/wait_for_tools.py 14.28% 5 Missing and 1 partial ⚠️
src/libtmux_mcp/tools/pane_tools/io.py 85.71% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #139      +/-   ##
==========================================
- Coverage   86.81%   86.52%   -0.29%     
==========================================
  Files          46       46              
  Lines        3899     4053     +154     
  Branches      587      606      +19     
==========================================
+ Hits         3385     3507     +122     
- Misses        360      381      +21     
- Partials      154      165      +11     

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

tony added 3 commits October 17, 2026 03:46
Move the coordinated libtmux source pin to the revision whose version probe
uses run-shell -C instead of display-message, so no user hook fires before a
refusal. uv.lock changes only the two rev references; the unrelated set
reordering uv lock emits is not committed.
The owned-subprocess version probe ran display-message -p '#{version}' in
both the async and the synchronous lane, which fires a user's
after-display-message hook before any refusal. Probe with
`-N run-shell -C '<marker>#{version}'` as libtmux does: run-shell has no
after-hook, and with -C its failed parse reports the expanded text without a
command-error hook. The version is read from the `unknown command: <marker>`
line; any other failure stays a version-check error, and the absent-endpoint
detection and `-V` fallback are unchanged.

Tests: a native test sets after-display-message and, where set-hook accepts
it, command-error hooks, runs both lanes, then fires a control display-message
and a failing command. Hooks run in order, so each sentinel holding exactly
one letter shows the probes fired none. It runs on the 3.3a lane and the path
tmux. The stub tmux in the wait-tool test answers the new probe.

Verification: the new test and the other version check tests pass on 3.3a with
3.2a and unpatched 3.3 refusals, and on 3.7d. Putting a display-message
call back into the probe fails the test with b'xxx' where b'x' is expected,
on both binaries. Ruff and mypy are clean.
Note that owned subprocess version checks run no user hooks. Keep the release note separate from runtime changes.
tony added 7 commits October 17, 2026 07:25
The range `libtmux>=0.62.0,<1.0` was satisfied by the released 0.62.0, which
has no version-check backend, so an installer that ignores uv's source override
(pip, pipx, uvx) would pick a libtmux this package then refuses to use. libtmux
0.63.x is the series that carries libtmux._internal.support, so require 0.63.0 or
newer. The `[tool.uv.sources]` git pin stays as the development path until
libtmux 0.63.0 is released and moves to libtmux's hook-free, cached,
version-check head (73f82c716). uv.lock changes only the two rev
references; `uv lock --check` passes.

Release order: libtmux 0.63.0 first, then drop the source override here.
Probe, -V and backend failures raised bare RuntimeError, which the tool error
mapper reports as "Unexpected error" at ERROR level instead of an expected
tool error. Raise LibTmuxException from them, and import the daemon-first
check from the libtmux._internal.support module (require_version, check_fence,
MINIMUM). A backend that lacks the module
or its names is refused before any subprocess starts.

Tests: the incompatible-backend test stubs libtmux._internal.support and expects
LibTmuxException on both lanes. Verification: version check module passes on 3.3a
with the 3.2a and unpatched 3.3 lanes; ruff and mypy are clean.
Startup probed only the default endpoint and stopped the whole server when
that daemon was below 3.3a, even though every tool takes its own socket and
checks it on each call. Log a warning for an unsupported, unreachable or
slow default endpoint and keep serving; a missing backend or tmux binary still
stops startup.

Test: a stubbed VersionTooLow from the startup probe enters the lifespan and
logs the warning. Replacing the handler with a re-raise fails it.
run_command types version-fenced tmux commands into the pane. If the daemon
is replaced before the user's command finishes, tmux prints `unknown command:
<marker>` in the pane and runs neither, so no status is stored and the channel
is never signalled: the caller saw an ordinary timeout. Keep both fence markers
and, on a timeout or an unreadable status, look for those exact lines in the
pane and raise VersionTooLow instead. The tool boundary reports it as an
expected tool error.

Tests: a typed fence forced to fail its version condition surfaces
"changed before run_command" instead of timed_out, and the marker helper
ignores other lines and matches only this call's markers.
Name libtmux 0.63.0 as the first release with libtmux._internal.support, describe the
source override as the development path only, and say to release libtmux first
and drop the override before merging this change.
…vior

Note the new libtmux floor, expected tool errors for version check failures, and
that an unsupported default endpoint no longer stops startup. Keep the release
note separate from runtime changes.
Cover the async and synchronous version check lanes with a probe that fails with
permission denied: both raise LibTmuxException, which the tool boundary reports
as an expected error. Making the helper raise RuntimeError again fails the new
test.
@tony
tony force-pushed the bump-min-tmux-000 branch from 39cc1a7 to 918119a Compare October 17, 2026 12:40
tony added 4 commits October 17, 2026 11:24
Move the development source pin to the libtmux revision with the ranked version
grammar, the per-endpoint cache, the control and name-guard fixes (f6e4630a), which publishes the version check as
libtmux._internal.support.
uv.lock changes only the two rev references.
Every owned call spawned a version probe before the command, and each polling
iteration of wait_for_text paid it again. The fence in the dispatched command
already catches a replaced daemon, so remember the daemon's release per live
endpoint (binary, selectors, TMUX_TMPDIR, TMUX): a fence refusal forgets it,
and a call that finds the cached daemon gone forgets it and checks again
once. An absent endpoint is never cached.

Tests: three sync calls and one async call probe once; a daemon killed from
outside is checked again; a stale cached release is refused with no effect and
forgotten. Never storing the release fails the probe-once test.
Run the matrix against the released 3.8 tag before master; only master stays
continue-on-error.
Note that owned subprocesses remember the daemon's release per endpoint. Keep the release note separate from runtime changes.
@tony
tony force-pushed the bump-min-tmux-000 branch 2 times, most recently from 653f6d5 to a2b4b3a Compare October 17, 2026 17:57

This branch has not been deployed

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

Require tmux 3.3a for core operations

2 participants