Skip to content

Raise the core tmux minimum to 3.3a - #21

Draft
tony wants to merge 30 commits into
masterfrom
bump-min-tmux-000
Draft

tony wants to merge 30 commits into
masterfrom
bump-min-tmux-000

Conversation

@tony

@tony tony commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Raise the minimum tmux for every cmdlet that talks to tmux to 3.3a, decided from the connected daemon before anything is sent.

The cmdlets are a thin layer over the published LibTmux .NET package, which still lists 3.2a as its own minimum and checks only the client executable's -V. This change does not wait for that package: TmuxCmdlet resolves the server behind each operation's target (server, session, window, pane or client) and checks its version first. Workspace plan, apply and conversion say which server they use. A refusal is a LibTmux.TmuxVersionTooLowException error on the error stream, so Get-* cmdlets cannot turn it into an empty result.

The probe is -N run-shell -C <marker>#{version}: run-shell has no after-hook and -C fails while parsing its own output, so neither an after-* nor a command-error hook fires. Releases compare with patch letters. next-X.Y and X.Y-rcN rank just below X.Y, so next-3.9 and 3.8-rc2 are supported and 3.3, next-3.3a, openbsd-*, bare master and malformed reports are refused. An absent endpoint validates the executable's own -V (tmux <ver> or <basename> <ver>) instead.

The checked version of a live endpoint is cached, keyed by the executable, socket, configuration and socket directory, so a pipeline of records probes once. An absent, refused or name-factory endpoint is never cached. A fenced batch that finds the daemon changed, a failed batch, a transport failure or a stale generation forgets the entry. On native Windows an operation that resolves a server now fails with PlatformNotSupportedException instead of skipping the check and dereferencing a null.

Invoke-TmuxCommand and Invoke-TmuxChain also fence the batch: the first command is if-shell -F '#{==:#{version},<checked>}' '' <marker>, so a daemon replaced after the version check fails before any later command runs and raises the version error. A live endpoint's fence starts with -N, so a vanished daemon is not restarted by the dispatching client. A generation-guarded batch is left to the package's own wrapping and gets no -N. Typed operations are checked per cmdlet but not fenced, because the package dispatches them one command at a time, so a daemon replaced by an older one between two typed operations is seen only when a fenced batch or a failure follows.

The check fails closed. A cmdlet that resolves no server fails its operation unless it carries TmuxVersionCheckExempt; the Select-Tmux* filters, the workspace import, resolve, discovery and conversion cmdlets, Disconnect-TmuxControl, Invoke-TmuxControlCommand and Watch-TmuxEvent do. A connection that Connect-TmuxControl opened carries the version checked before the package attached. A connection opened elsewhere exposes no endpoint, so Invoke-TmuxControlCommand takes -Server and runs the hook-free run-shell -C probe against it; without -Server the command is refused. Nothing is sent over the connection for the check. The narrow race is that the connection could belong to a different daemon if the endpoint was replaced between its attach and the call. Invoke-TmuxCommand kill-server on an absent endpoint is an empty success, and a failed chain's error no longer carries the fence.

A refusal is a non-terminating error like every other operation failure, so -ErrorAction SilentlyContinue hides it; the compatibility page says to use -ErrorAction Stop. The separate MCP tool is outside this change and keeps the package's floor. Once the .NET package checks and fences by itself, typed calls can rely on it and this layer can shrink.

Wait-TmuxPaneText is untouched here. Its polling fallback below tmux 3.7 and the control-mode gating belong to the optional-features PR (#22).

CI tests tmux 3.3a, 3.4, 3.5, 3.6, 3.7a, 3.7b, 3.7c, 3.8 and master on PowerShell 7.4.20 and 7.6.6, and adds a reject job that builds tmux 3.2a and unpatched 3.3 from their pinned commits and runs only the VersionCheck suite against the packed modules. The 3.2a window-size workaround in two fixtures is removed. README, compatibility page, contributing guide, one help example and the release-artifact job set name the same floor.

Each guard has a test that fails when the guard is removed: no check, floors 3.2a and 3.3, no fence, no -N, a display-message probe, a missing exemption, the fail-closed check, a leaked fence, the kill-server no-op, the control-connection check, a list-commands probe, no cache and no invalidation. The version grammar test is one table of vectors plus the ordering next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya. The command-error hook half is reported SKIPPED before tmux 3.5, and the renamed-3.3a-executable case is SKIPPED because the shared package reads only a tmux <version> banner. A 3.4a-rc1 report would be refused where the Python reference accepts it.

macOS: the trial is advisory and does not establish macOS support. The Product suite aborts at ResourceInstall.Tests.ps1 there, so the trial now runs the VersionCheck suite as its own step. The trial run on master is the baseline for that failure.

Not yet qualified: the hosted matrix and the rejection lanes for the latest head. Results are posted on the PR when the runs finish.

Closes #17.

@tony
tony deployed to docs-preview October 17, 2026 09:36 — with GitHub Actions Active
@tony
tony deployed to docs-preview October 17, 2026 09:54 — with GitHub Actions Active
@tony
tony deployed to docs-preview October 17, 2026 11:21 — with GitHub Actions Active
tony added 18 commits October 17, 2026 06:44
why: The module's tmux floor lived only in the shared .NET package,
which still accepts 3.2a and reads the client executable's `-V` instead
of asking the daemon that will run the command. Nothing refused an old
daemon before a cmdlet changed it.

what:
- Check the connected daemon before every cmdlet dispatches: tmux 3.3a
  or newer, comparing patch letters, with `next-X.Y` and release
  candidates ranked below the release they name and undated or unknown
  reports refused.
- Probe with `-N run-shell -C <marker>#{version}`. run-shell has no
  after-hook and `-C` fails while parsing its own output, so no
  `after-*` or `command-error` hook fires; the version comes from the
  `unknown command:` line on stderr. An absent endpoint falls back to
  the executable's `-V`, accepting `tmux <ver>` and `<basename> <ver>`
  because 3.3a prints its own file name.
- Resolve the server from the operation's target in
  `TmuxCmdlet.RunOperation`; cmdlets whose target is not the endpoint
  they use override `ResolveServer`.
- Report a refusal as a non-terminating error carrying
  `TmuxVersionTooLowException`, so `Get-*` cmdlets cannot turn it into
  an empty result.
- Fence `Invoke-TmuxCommand` and `Invoke-TmuxChain` batches with `if-
  shell -F '#{==:#{version},<checked>}' '' <marker>` as the first
  command, so a daemon replaced after the check fails on the unknown
  marker before any later command runs. The fence is stripped from the
  returned arguments.
- Refuse a daemon older than 3.2 by the answer it gives the probe: a
  3.1c daemon prints the `run-shell` usage line, a 3.0a daemon makes the
  client print `server version is too old for client`, and a client
  older than 3.2 rejects `-N`, so the executable's `-V` decides. Each
  refusal says to upgrade tmux and restart the server, or to keep
  libtmux for PowerShell 0.1.0-alpha2.
why: The version check needs tests that fail when it stops refusing,
stops fencing or starts firing tmux hooks.

what:
- Add tests/VersionCheck.Tests.ps1, which runs against the tmux first on
  PATH.
- Below 3.3a, assert that reads, session creation, raw commands and
  chains are refused on a live daemon and on an absent endpoint,
  including through a client wrapper that claims `tmux 9.9`, and that
  the daemon's sessions and the absent socket are unchanged.
- On 3.3a or newer, assert that reads and writes are checked and that
  neither the check nor the fence fires an after-display-message or
  command-error hook, with a native display-message and a failing
  command as positive controls. The command-error half is reported
  SKIPPED before tmux 3.5, where `set-hook` rejects it.
- Assert that a replaced daemon fails the fence for both batch cmdlets
  and leaves a sentinel session uncreated, while an unchanged daemon
  behind the same wrapper is not refused.
- Assert the version grammar: 3.2a, 3.3, next-3.3, 3.3-rc, master and
  malformed text are refused; 3.3a, 3.3b, 3.4, 3.4-rc1, next-3.4 and
  3.7d are accepted.
- Answer the probe with doubles that print the 3.1c usage line, the 3.0a
  protocol refusal and the unknown `-N` error, and require each refusal
  to name its release and the remedy. The 3.1c and 3.0a doubles keep
  stdout open past the client's exit, as those daemons do, and the read
  must fail within 10 s. With LIBTMUX_OLD_DAEMON set, the same bound
  applies to a real 3.0a or 3.1c daemon behind a 3.3a client.
why: Lanes below the floor must assert refusal without running suites
that need a supported daemon.

what:
- Add `eng/Test.ps1 -Suite VersionCheck`, which installs the packed
  modules and runs tests/VersionCheck.Tests.ps1 alone.
- Run the suite with the others in the Product suite.
why: tmux 3.2a sized detached windows from the command client unless
`window-size` was manual, so the create and guide fixtures forced that
option on that release only. The 3.3a floor removes the release.

what:
- Remove the forced option from the create and guide fixtures and the
  note from the create guide.
- Keep reading the daemon version in the guide fixture for its deferral
  check and in Create.Tests.ps1 for tmux 3.4's dollar-sign escaping.
why: The workflow still exercised 3.2a, which the module no longer
supports, and had no lane proving that older daemons are refused.

what:
- Pair PowerShell 7.4.20 and 7.6.6 with tmux 3.3a and 3.7c, and swap
  3.2a for the same 3.3a source commit in the advisory macOS trial. The
  MCP discovery step follows the lower endpoint.
- Add a `reject` job that builds tmux 3.2a and unpatched 3.3 from their
  pinned source commits, checks the tag, and runs only the VersionCheck
  suite against the packed modules. The 3.3 commit is the `3.3` tag of
  tmux/tmux; the shared build script does not know that release, so the
  job builds from source directly.
- Add the job to the aggregate gate and the release artifact binding, so
  a release needs the rejection cells as well as the supported ones. The
  artifact checker's job set and its unit test follow.
- Add a `reject-old-daemon` job that builds tmux 3.3a and a 3.0a or
  3.1c daemon from their pinned tag commits and runs the VersionCheck
  suite with the 3.3a client against that daemon. The job joins the
  gate and the release artifact binding.
why: The install note, the compatibility page and the contributing guide
disagreed about the minimum tmux.

what:
- State the 3.3a minimum, how the connected daemon is checked, and where
  it is tested in the README, the compatibility page and the
  contributing guide.
- Explain on the compatibility page that reads fail with
  `TmuxVersionTooLowException` instead of returning nothing, what the
  batch fence covers and what it does not, and that the shared .NET
  package still lists 3.2a and reads only a `tmux <version>` banner.
- Prepare a 3.3a plan in the Get-TmuxQueryPlan example; regenerate the
  help XML and follow in the help-example and source query tests.
- Describe how daemons older than 3.2 are refused and which CI job
  proves it.
why: If the checked daemon disappeared before dispatch, an older client
executable could start a replacement server and run the batch there.

what:
- Write `-N` as the first word of the native command list that starts a
  live endpoint's fence. The shared package cannot add the flag to a
  chain itself.
- Add tests that kill the daemon between the probe and the dispatch of
  `new-session` and assert the dispatch fails with no server left, plus
  the development-build versions: next-3.9, 3.8-rc2 and 3.8 are
  accepted; next-3.3a, openbsd-7.4 and bare master are refused.
why: The package wraps a batch that carries a server generation in its
own guard and then treats every command name as tmux text, so the
leading `-N` failed the guarded input chain with `unknown command: -N`.

what:
- Add `-N` only to unguarded batches. A guarded batch is already bound
  to one daemon and fails on a replaced generation.
why: Several suites wrap tmux in a script that logs one line per
invocation, then assert that invalid input, previews and offline paths
dispatch nothing. The version check now runs one hook-free probe, or an
executable `-V` for an absent endpoint, ahead of the library call that
rejects the input.

what:
- Make those wrappers skip exactly the probe and `-V` forms and keep
  counting every other invocation, including the fenced batch.
why: tmux 3.3a's configure stops with "must give --enable-utf8proc or
--disable-utf8proc" when it finds utf8proc, which Homebrew installs, so
the shared build script's bare configure failed every 3.3a cell before
any suite ran.

what:
- Configure the already cloned and verified source in the trial with
  `--enable-utf8proc`.
- Write the same source-commit marker the later steps read, and keep the
  3.7c cells on the same path.
why: With `--enable-utf8proc` the arm64 cells stopped at "utf8proc not
found": Homebrew installs under /opt/homebrew, outside the compiler's
default search paths, while the Intel cells find it under /usr/local.

what:
- Pass the include and library directories from `brew --prefix utf8proc`
  to configure.
why: `TmuxCmdlet` skipped the version check whenever `ResolveServer`
returned null, so a cmdlet that forgot to resolve its server shipped
unchecked without any failure.

what:
- Fail an operation that resolves no server with an error naming the
  cmdlet, unless the class carries the new `TmuxVersionCheckExempt`
  attribute, which states why no daemon needs a check.
- Mark the three Select-Tmux* filters, the workspace import, resolve,
  discovery and conversion cmdlets, Disconnect-TmuxControl, Invoke-
  TmuxControlCommand and Watch-TmuxEvent exempt. The last two take a
  connection the caller acquired, and their own server is still checked.
why: A failed `Invoke-TmuxChain` rethrew the package's exception whose
result still began with the fence and its marker. Separately, `Invoke-
TmuxCommand kill-server` against an endpoint where no daemon answers
dispatched a command that can only fail.

what:
- Carry the result the caller's commands produced in the error, without
  the fence.
- Return an empty successful result for `kill-server` when no daemon
  answers.
why: The fail-closed rule, the version grammar and the error contents
had no tests.

what:
- Check by reflection that the exempt cmdlets in both assemblies equal
  an explicit list, that every other cmdlet exposes a Server, Session,
  Window, Pane, Client or plan parameter or overrides `ResolveServer`,
  and that a test cmdlet that does neither is refused at run time.
- Add one table of version vectors (accept 3.3a, 3.10, 4.0, bare and
  numbered release candidates, next-3.9; refuse 3.3, 3.3-rc, next-3.3,
  master, openbsd-7.6, empty, 3.3a-master, 3.3a-dev, next-3.3a,
  next-3.9-master) and the ordering next-X.Y < X.Y-rc < X.Y-rcN < X.Y <
  X.Ya.
- Assert that a failed chain's error carries no fence or marker and that
  `kill-server` on an absent endpoint succeeds with no output.
why: Readers could not tell how a refusal behaves under `-ErrorAction`,
whether the MCP server is covered, or why the benchmark receipts name
3.2a.

what:
- Say on the compatibility page that a refusal is a non-terminating
  error that follows `-ErrorAction`, so `SilentlyContinue` hides it as
  it does any operation failure.
- Say that `kill-server` on an absent endpoint is a no-op, and that the
  separate MCP tool is outside this change and keeps the package's floor
  until the package checks the daemon itself.
- Note in the benchmark README that its 3.2a receipts predate the floor.
why: `Invoke-TmuxControlCommand` sent commands over a connection the
caller opened elsewhere without checking the daemon behind it, and a
connection exposes no server to resolve.

what:
- Ask the connection for `list-commands -F '#{version}'` and refuse a
  sub-floor or undated daemon before the command is sent. Remember the
  answer per connection.
- Record the version `Connect-TmuxControl` acquired under. The package
  refuses `run-shell` on a control connection, so the usual probe is
  unavailable here.
why: The control-connection check needs a test that fails when the check
is removed.

what:
- On tmux 3.7 or newer, use a connection opened with
  `EnterControlModeAsync` with `Invoke-TmuxControlCommand`; the same
  connection recorded as a 3.2a daemon's is refused with the version
  error while the option it would have set stays unset.
- Report the check SKIPPED below 3.7, where control clients are not
  routine.
why: The Product suite stops at its first failing worker, and on macOS
that is ResourceInstall.Tests.ps1, so the version check suite never ran
there.

what:
- Run `-Suite VersionCheck` first in the advisory trial and report it
  beside Product and Documentation in the summary and warning.
- Keep `continue-on-error` on every step: the trial stays advisory.
@tony
tony force-pushed the bump-min-tmux-000 branch from d9994b9 to 8785b3e Compare October 17, 2026 12:16
tony added 7 commits October 17, 2026 08:03
why: A `list-commands` command sent over the connection carries an
after-hook flag, but the version check must trigger no after-hook.

what:
- Rely on the version a connection from `Connect-TmuxControl` already
  carries from before the package attached.
- Add `-Server` to `Invoke-TmuxControlCommand` for a connection opened
  elsewhere, which exposes no endpoint. The cmdlet runs the hook-free
  `run-shell -C` subprocess probe against that server and remembers the
  answer. Without `-Server` the command is refused.
- The remaining race: the connection could belong to a different daemon
  if the endpoint was replaced between its attach and the call.
why: A regression to a `list-commands` probe must fail a test.

what:
- On tmux 3.7 or newer, arm after-list-commands (where the release
  allows it), after-display-message and command-error, and assert that
  connections from `Connect-TmuxControl` and a foreign connection named
  with `-Server` fire no more hooks than the package's own attach, and
  that the armed hooks do fire for ordinary commands.
- Use a counting connection to prove that checking a foreign connection
  sends nothing over it and that an unknown connection without `-Server`
  is refused. after-list-commands is not settable on 3.5, 3.7d, 3.8 or
  master, so the counting connection is the check that catches a `list-
  commands` probe.
why: The compatibility page did not say how a control connection's
daemon is checked.

what:
- Describe the version recorded by `Connect-TmuxControl`, the `-Server`
  probe for a foreign connection, and the refusal without it.
why: Marking the cmdlet exempt let it resolve no server, but its Session
parameter still resolved to the session's server by default. Freezing a
captured session then probed a daemon that may be gone, and after `kill-
server` failed with "tmux did not report a daemon version".

what:
- Restore the `ResolveServer` override that returns null; the exemption
  states why.
why: The `-Server` parameter was missing from the cmdlet's reference
page, so the documentation export failed its check that help and the
native parameter inventory agree.

what:
- Describe `-Server` on the reference page and in the generated help
  XML.
why: Every cmdlet invocation ran its own probe, once per record in a
pipeline. Separately, on native Windows an operation that resolved a
server skipped the check and later dereferenced a null.

what:
- Cache the checked version of a live endpoint, keyed by the executable,
  socket, configuration and socket directory. An absent, refused or
  name-factory endpoint is never cached.
- Forget the entry when a fenced batch finds the daemon changed, a batch
  fails, a transport fails or a generation goes stale, so the next
  operation probes again. Typed operations are unfenced, so a daemon
  replaced by an older one between two of them is still seen only when a
  fenced batch or a failure follows.
- Fail with `PlatformNotSupportedException` on native Windows, since the
  cmdlets are Unix-only.
why: The cache and its invalidation need tests that fail without them.

what:
- Count probes through a wrapper: three typed reads and a raw command on
  one endpoint probe once.
- Make a wrapper answer its first probe as 9.9, so the first fenced
  batch fails on the real daemon; the next batch must probe again (two
  probes in total) and succeed.
- Plant an option and a buffer beside the session in the rejection half
  and assert all three are unchanged.
- Make a rejection lane that sets `LIBTMUX_REQUIRE_REJECTED` fail unless
  the tmux on PATH is exactly that unsupported release.
tony added 4 commits October 17, 2026 10:50
why: The matrix covered only the floor and the newest release, so a
release in between or an upcoming one could break unnoticed.

what:
- Return the Linux matrix to 3.3a, 3.4, 3.5, 3.6, 3.7a, 3.7b and 3.7c,
  each on PowerShell 7.4.20 and 7.6.6, and add a 3.8 lane (tag and
  commit pinned) and a master lane (`next-3.9`, built from the current
  head, logging the commit it built). The shared build script knows
  neither, so the job builds them from source.
- Export `LIBTMUX_REQUIRE_REJECTED` in the rejection job so a lane whose
  tmux is not the unsupported release it names fails instead of passing
  quietly.
- Follow with the release-artifact job set and its unit test.
why: The compatibility page and the contributing guide listed an
outdated matrix.

what:
- List the releases the Linux workflow now exercises.
why: The offline probe cmdlet in Runtime.Tests.ps1 derives from
`TmuxCmdlet` and resolves no server, so the fail-closed check refused it
with "The runtime operation did not start" on every lane.

what:
- Mark it exempt. Its fenced case dispatches nothing, and its native
  case still names its server, which is checked.
why: On PowerShell 7.6 the 7.4-built module made the inline test cmdlet
fail with CS1701. The other suites that compile inline C# reference the
host's reference assemblies and suppress that warning.

what:
- Do the same in the VersionCheck suite.
@tony
tony force-pushed the bump-min-tmux-000 branch 2 times, most recently from a0da4d8 to bf018d3 Compare October 17, 2026 17:19
@tony
tony force-pushed the bump-min-tmux-000 branch from bf018d3 to 3a7f24e Compare October 17, 2026 18:53
why: Callers need to know that older tmux daemons are now refused and
what the refusal looks like.

what:
- Add a breaking-change entry for the 3.3a floor, the version-too-low
  error on reads, the replaced-daemon refusal and the absent-endpoint
  kill-server no-op.
@tony
tony force-pushed the bump-min-tmux-000 branch from 3a7f24e to 831c283 Compare October 17, 2026 19:04
@tony
tony deployed to docs-preview October 18, 2026 02:29 — with GitHub Actions Active
@tony
tony force-pushed the bump-min-tmux-000 branch from 831c283 to 2c5c712 Compare October 18, 2026 04:05

This branch was successfully deployed

1 active (outdated) deployment
docs-preview — 831c2835 Deployed Oct 18, 2026 by tony via publish (pr-21, pr, false, true, powershell) / publish #33
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

1 participant