Repository navigation
Conversation
why: Control clients now need tmux 3.7, but several integration cases only needed some client attached to a session, and the workspace CLI process test kept a private copy of the PTY helper. Both need the same standard-library pseudo-terminal driver. what: - Move the PTY helper into gradle/test-fixtures and serve it as a test resource of integration-tests and libtmux-workspace-cli, so it never enters a published artifact. - Add NativeClient, an ordinary terminal client that attaches to a session through that driver, owns its process tree and detaches on close. - ProcessTest reads the shared helper. Python 3 is already required by the PTY tests; CONTRIBUTING is updated with the control commit's documentation.
why: The workspace CLI hands builders, plugins and shell code to a Python libtmux. The core version check (3.3a) alone does not tell the CLI that interpreter also refuses the optional operations Java refuses, so a backend with only the floor could run an unsafe literal command. what: - The runtime prelude requires the integer TMUX_OPTIONAL_FEATURE_POLICY_VERSION >= 2 next to the 3.3a floor, in the runtime probe and again in the child, before external code runs. - Cases cover a missing, 0, 1, boolean, string and float marker, and a child that passes the probe but fails the recheck; none enters the backend. - CI and the tmux matrix qualify tmuxp 1.74.0 with the Python libtmux source overlay that carries the marker (tmux-python/libtmux PR 780), and assert the marker besides the import path and floor. Released tmuxp still pins libtmux 0.61, so the overlay is not a coherent installed package pair.
why: At the 3.3a floor a few operations still misbehave on particular releases: named buffer delete and rename act on an unrelated buffer before 3.4, `split-window -p` is unreliable on 3.4, an invalid layout string kills a 3.3a daemon, an unnamed or raw `break-pane` ends or renames on exact 3.7, and a single-pane break prints no id before 3.6. Each must be decided from the connected daemon, for typed calls and for literal commands alike, so it can no longer fail after an earlier command in a batch had its effect. what: - TmuxFeatures inspects the literal commands a request carries (full verbs, aliases and unambiguous prefixes, option clustering, `--`) against the checked daemon version and refuses with UnsupportedFeatureException or IllegalArgumentException before the fenced batch is sent. - Versions.execute applies it after the version check; Server, IncarnationFence and handle batches pass the commands they represent. - Buffers.delete, Pane.paste and its callback form stop checking the version themselves: the dispatch-level guard covers them, and the paste temporary buffer is refused before it is created. - Pane.breakOut() is refused on exact 3.7; breakOut(name) keeps the post-break rename. A break is read back with display-message on the moved pane, so releases that print nothing for a single-pane break still return the new window. - Direct `paste-buffer -d -b`, user-authored if-shell bodies and alias contents keep native behaviour; no second script parser is added. Verified on real tmux with OptionalFeatureIntegrationTest.
why: Control clients are only qualified on stable tmux 3.7 and newer; a short-lived control attach crashes 3.2a and 3.6b servers. Until now the version check ran through the carrier and the later `-C` attach named the socket path again, so a daemon replaced in between could receive the attach, and a long socket path could exceed sun_path. what: - ControlAcquisition decides first, with the hook-free version probe, and refuses a daemon below a stable 3.7 (development builds and release candidates included) before any control client exists. It then reads the daemon identity, pins the socket, rereads the identity through the pin and attaches with `-N` to the pinned alias. One deadline covers qualification and attach. - ControlCarrier gains pinSocket; the default refuses, so custom carriers must retain the socket in their own realm. The process transport and the local carrier use LocalSocketPin: a private 0700 directory on the socket's filesystem holding a hard link, under the 103-byte limit, removed on close, EOF or failed attach. - ControlClient.attach, attachUnfenced and Server.control go through it; Server.control refuses before its identity read. Literal commands sent over a control line pass the same feature guards. - Subprocess and polling paths are untouched below 3.7. - Tests: control-positive cases carry the control tag and the matrix runs them from 3.7; every lane asserts the refusal and that no client attached. Cases that only needed some attached client use NativeClient. Fakes (FakeTmux, the Kotlin and Cats control scripts, ControlClientTest) answer the probe, the identity read and the pin. Verified on real tmux 3.3a and 3.7 with ControlAcquisitionIntegrationTest, ControlFloorIntegrationTest and the control suites.
why: The guides still described one support range and a control client that attaches on any supported daemon, and the documentation checks ran every control example everywhere. what: - Guides, module READMEs and CONTRIBUTING state that managed control needs stable tmux 3.7 and a pinnable socket, name the 3.4 boundary for named buffers and managed paste, the exact-3.7 break-pane rule and the 3.3a layout rule, and keep the 3.3a floor for everything else. - A `control:` directive for Java and Kotlin snippets and `scala-io-control` for Scala run normally from stable 3.7 and assert UnsupportedFeatureException with no attached client below it, so every lane still compiles and exercises the examples. - The Python workspace backend section names the optional-policy marker.
why: Callers need to know which operations keep the 3.3a floor and which are refused earlier, and what a custom ControlCarrier must now provide. what: - Table of the per-feature requirement or repair, the escape-hatch boundary for opaque programs and aliases, and the pinSocket contract for custom carriers. - The Python bridge requires optional-policy marker 2.
Notes the control floor, the 3.4 buffer boundary, the layout, split and break refusals and the Python policy marker, with a link to the migration notes. References issue #68.
why: Control acquisition read the daemon identity with
`display-message -p`, and the attach then confirmed it with another
display-message over the control line, as did Server.control before
it. Each fires a user's after-display-message hook, the first two
before the daemon was known to be eligible. run-shell -C cannot answer
over a control line: its parse error is printed outside any %begin/%end
block, so the block ends empty.
what:
- The identity is read by subprocess with `run-shell -C`, as the
version probe is. tmux names only the first word it cannot parse, so
the fields travel in one word: version, pid and start time joined by
commas, then the socket path in single quotes with embedded quotes
closed, escaped and reopened. More or fewer lines than one are
refused.
- After the attach the identity is read again through the pin, by
subprocess, and a mismatch closes the client; nothing is read over the
control line.
- Server.control no longer reads `#{pid} #{version}`: the pinned
acquisition compares the daemon it reaches with the captured pid, start
time and version. The floor is decided with the hook-free probe when
the capture carries no version.
- Cases: a socket path with a space, a quote and a comma survives; the
hook stays quiet through qualification and recheck while a control
`display-message` fires it.
Verified on tmux 3.7 and master (next-3.9). Reintroducing a
display-message read in the recheck fails the hook case on 3.7
("acquisition ran the hook"). Control replies are framed with
display-message markers once a client is attached; that is the client's
own traffic and unchanged.
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 17, 2026 11:51
e967af3 to
d34e6d0
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 17, 2026 12:35
171da71 to
ad6d92d
Compare
why: The release rehearsal runs a staged consumer that reads a session's live view. A live view is managed control, which a daemon older than 3.7 now refuses before attaching, and the runner's tmux is 3.4, so the rehearsal failed with UnsupportedFeatureException. what: - The consumer treats UnsupportedFeatureException from the attach as the expected answer on an older daemon and still asserts one view where control is available. Verified by staging the release locally and running the consumer with tmux 3.7d (one view read) and tmux 3.4 on PATH (refusal accepted).
why: Exactly tmux 3.7 rejects `.` and `:` in session and window names. The library passed the name through, so a batch or a workspace could run the commands ahead of the rejected one and leave a half-built session. what: - Names.require, for a version or for a server (read hook-free), refuses such a name on exactly 3.7 (a candidate counts, 3.7a and later do not) with IllegalArgumentException. - TmuxFeatures applies it to new-session -s/-n, new-window -n and the new name of rename-session and rename-window, so typed calls, raw commands and batches are all covered, including clustered flags. - The workspace applier checks every window name before it creates the staging session, as it already checks layouts. - Cases: NamesTest over every release family; on the real 3.7 lane a batch whose second command carries the name leaves the first command's option unset and creates no window, while other releases accept it. Verified: making Names.require accept every name fails NamesTest and the 3.7 lane case.
Adds the delimiter rule for session and window names to the optional requirements table.
Notes that names with a dot or colon are refused before dispatch on exactly 3.7. References issue #68.
tony
force-pushed
the
bump-min-tmux-001
branch
3 times, most recently
from
October 17, 2026 15:44
e57f5e0 to
e295b16
Compare
…able why: The watching examples attach a control client, which a daemon older than 3.7 now refuses. The JDK checks run them against the runner's tmux 3.4 and failed (RunnableExamplesSuite, ExamplesRunTest, MainsTest). what: - Those cases assume ControlAcquisition.supports (Scala: the fixture's supportsControl), so they run on 3.7 and newer and are skipped below. Verified with :examples:test under tmux 3.4.
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 17, 2026 18:53
8e59cbe to
32e9f85
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 17, 2026 18:53
f8faa9f to
e77e759
Compare
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 17, 2026 20:02
32e9f85 to
54837b8
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 17, 2026 20:07
e77e759 to
80f1754
Compare
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 17, 2026 22:26
54837b8 to
b569d98
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 17, 2026 22:26
80f1754 to
f7a8e83
Compare
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 18, 2026 03:39
b569d98 to
e251f95
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 18, 2026 03:39
f7a8e83 to
993f791
Compare
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 18, 2026 04:00
e251f95 to
b1f0e99
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 18, 2026 04:00
993f791 to
542daf2
Compare
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 18, 2026 12:49
b1f0e99 to
87e4fcb
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 18, 2026 12:49
542daf2 to
586e66d
Compare
tony
force-pushed
the
bump-min-tmux-000
branch
from
October 18, 2026 12:50
87e4fcb to
ec52020
Compare
tony
force-pushed
the
bump-min-tmux-001
branch
from
October 18, 2026 12:50
586e66d to
bd1e4d8
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.
Keep ordinary operations, workspace builds and MCP startup on tmux 3.3a while refusing unsafe optional operations before their effects. This is stacked on the core minimum change.
run-shelloutput (existing refusal, unchanged)Server.control,ControlClient.attach, watches, mirrors, streams, live viewsExact 3.7 also refuses raw
break-paneand unnamedPane.breakOut();breakOut(name)keeps its post-break rename. Literalsplit-window -pis refused on 3.4 (typedpercentkeeps-l N%). Names with.or:are refused on exactly 3.7 before dispatch. Literalselect-layoutbelow 3.4 must name a layout or carry tmux's checksum, because 3.3a ends the server on anything else. A single-pane break that prints no id before 3.6 is read back from the moved pane. Directpaste-buffer -d -bstays available on 3.3a.Control acquisition decides on the daemon before any control client exists, with the hook-free probe and the release-train order
next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya:next-3.7and3.7-rc2do not qualify,3.8-rc2andnext-3.8do. It then reads the identity by subprocess (run-shell -C, one word: version, pid and start time joined by commas, the socket path last in single quotes with its quotes reopened), pins the socket through a hard link in a private 0700 directory on the socket's filesystem (alias under 103 bytes, removed on close, EOF or failed attach), rereads the identity through the pin, attaches with-Nto the alias, and rereads it again by subprocess after the attach. Nothing is read over the control line:run-shell -Creports through stderr, which a control reply does not carry. A customControlCarriermust implementpinSocket; the default refuses.Subprocess and polling paths are untouched below 3.7. The guards live in one place,
TmuxFeatures, applied after the version check to the literal commands a request carries (full verbs, aliases and unambiguous prefixes, flag clustering,--), so typed calls, raw commands, flat batches and handle batches all pass through it and a refused batch runs none of its commands. Opaque programs (if-shellbodies,source-file, shell text, alias contents) keep native semantics, and no second script parser is added.The workspace CLI requires
TMUX_OPTIONAL_FEATURE_POLICY_VERSION >= 2of the Python libtmux next to its 3.3a floor, in the probe and again in the child, before external code runs. CI qualifies tmuxp 1.74.0 with the Python libtmux source of the Python libtmux feature change at54b277bb673e208820fc9c795abe4444e2ded272; that overlay is not a released package pair.Validation, on this Linux/WSL host:
next-3.9, which now runs the control cases); control-positive cases carry thecontroltag and run on every daemon that qualifies, and every lane asserts the refusal and that no client attached. The unit suites of:libtmux,:libtmux-junit5,:libtmux-kotlin,:libtmux-scala-catsand:libtmux-workspace, and the documentation checks, pass; the workspace CLIExecutionTestpasses with the source overlay.TmuxFeatures.requirefrom the version check fails threeOptionalFeatureIntegrationTestcases on 3.3a (named buffers, managed paste, layouts). Making the control floor a no-op failsControlFloorIntegrationTeston 3.5 ("Expected UnsupportedFeatureException to be thrown, but nothing was thrown"). A table-drivenControlFloorVectorsTestholds the floor to the canonical version vectors; making the floor refuse development and candidate builds again fails it ("3.8-rc ==> expected: true"). Reading the daemon withdisplay-messageagain in the acquisition recheck fails the hook case on 3.7 ("acquisition ran the hook").ControlAcquisitionIntegrationTestalso covers a socket path with a space, a quote and a comma, a long socket path through the short alias, replacement of the original pathname after pinning, and the hook staying quiet through qualification and recheck while a controldisplay-messagefires it.Exact 3.7 rejects
.and:in session and window names;Names.requirerefuses them before dispatch for new-session, new-window, rename-session and rename-window (and for workspace window names before staging), with a real-lane case and a recorded break. Not done or not qualified: On a local tmux 3.8 buildServerMirrorIntegrationTest.aRebuildThatFindsNothingNewPublishesNothingfails (a control-mirror case that the preview lane now reaches); it was not diagnosed. Tmux 3.4, 3.6 and the 3.7 patch releases were not run locally and are CI qualification, as is macOS. Once a client is attached, control replies are framed withdisplay-messagemarkers; that is the client's own traffic and still fires a user'safter-display-messagehook. Reads that print a version withdisplay-messageinside a checked call are unchanged. Linux evidence does not establish pinning on macOS. Clojure draft PR20 is a separate follow-up.Closes #68.