Skip to content

Add native Clojure facade - #20

Draft
tony wants to merge 64 commits into
bump-min-tmux-001from
clojure
Draft

tony wants to merge 64 commits into
bump-min-tmux-001from
clojure

Conversation

@tony

@tony tony commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on the Java tmux-support PRs

This draft now targets bump-min-tmux-001 and cannot merge before the core minimum change (tmux 3.3a floor, hook-free version check) and the optional-feature change (control needs tmux 3.7 or newer, exact-3.7 name rule, pinned control socket). Its diff is Clojure-only: no merges and no duplicates of those PRs' commits. After they land, retarget it to master.

Summary

  • Add native Clojure facade: Publish libtmux-clojure with captured values, explicit tmux effects, local collection processing, bounded tasks, and owned control streams.
  • Add optional core.async support: Provide libtmux-clojure-core-async without adding core.async to the core module.
  • Extend captured Java support: Server.captureServer (named so because capture() is master's package-private snapshot reader) and reserved-capacity batches that go through the checked waiting path.
  • Close runtimes deterministically, protect owned tmux fixtures, add executable Clojure examples and verify the public boundary, as in the original draft; its commits are replayed here with their authors and dates.
  • Follow the Java core: master's exception types (:tmux/unsupported-feature for a refused floor, reported as not dispatched), version probes and fences in the unit-test transports, Server.shell.

Contract

  • Floor: every call is checked against the connected daemon's version, so the facade supports tmux 3.3a through 3.7c and refuses older daemons before any effect. JDK 25 is required, the core's baseline (the draft's JDK 21 claim could not load the core).
  • Control: control/attach! goes through Server.control: tmux 3.7 or newer, decided before any client attaches (next-3.7 and 3.7-rc2 do not qualify; 3.8-rc2 and next-3.8 do). An older daemon raises :tmux/unsupported-feature, not dispatched, with no client attached.
  • Streams: subscribe! keeps :encoding (:bytes or :utf-8) and :capacity. :max-bytes, :malformed and :max-panes are gone: master's Java client decodes per pane (a byte that is not UTF-8 is written \xHH) and the subscription is bounded by event count only, so the old memory bound in bytes no longer exists. next! returns a :gap ahead of the events that remain and a terminal map whose :reason is :closed or :ended with the original :cause.

Verification (Linux/WSL)

  • Clojure namespace load, namespace guard and unit tests for both modules; :docs:test, clojureDependencyMetadata, platformCoversEveryPublishedModule, spotlessCheck, :libtmux:test: pass.
  • :integration-tests:clojureIntegrationTest: 41 tests, 0 failures on tmux 3.3a, 3.5, 3.7 and master (next-3.9). Control scenarios run on 3.7 and newer; older daemons assert the refusal with no attached client. Reporting the refusal as :tmux/transport fails every control case on 3.3a.
  • The Java/Kotlin/Scala checks of the other PRs do not qualify this facade.

Test plan

  • Clojure unit, namespace-guard and real-tmux suites on 3.3a, 3.5, 3.7 and master
  • Documentation examples run on the same lanes
  • Hosted JDK, tmux-version, and macOS matrix (pending when this was written)

tony added 30 commits September 20, 2026 14:41
why: Task completion can publish before the virtual worker stops.

what:
- Join the owned fixture worker before asserting its cleanup.
why: macOS resolves fixture socket paths through /private/tmp while tmux
preserves the original /tmp spelling in its command arguments.

what:
- Match sockets by their resolved paths during ownership checks.
- Cover ancestor-link aliases and control-process ownership.
why: Captured values can outlive a client, while capture I/O must finish
within the server's resource scope.

what:
- Add an executable lazy traversal and eager capture example.
- Check the capture count in the documentation test.
why: Cancellation publishes the terminal outcome asynchronously.

what:
- Observe the cancelled task before resubmission
- Remove the JDK 25 admission timing race
why: Fixture scanning conflated a tmux client with its daemon.

what:
- Read the authenticated daemon PID from tmux
- Cover concurrent client ownership
why: Client operations need live evidence that preserves the captured target.

what:
- Attach an owned control client in the Clojure integration fixture
- Assert switch and detach through refreshed client observations
why: Closing a runtime could test completion-executor quiescence before
its queued callback cancellations ran.

what:
- Snapshot callback stages and await their scheduled cancellation
- Keep the wait within the runtime's existing cleanup deadline
- Record the caller-visible close behavior in the changelog
why: A labeled matrix cannot establish Clojure compatibility unless the
selected runtime and tmux binary are verified by the process under test.

what:
- Add four Clojure/JDK check and package cells
- Add forty Linux and eight macOS Clojure real-tmux cells
- Assert the Clojure runtime selected for each integration run
why: A newer client can reach an older daemon, and the socket can change
after a version probe. Refuse unsupported ordinary Server operations
before requested effects while retaining existing incarnation and
ownership fences.

what:
- Check the daemon's complete patch-aware version; consult a fresh
  executable report only on an unused endpoint. Preserve standalone -V
  and -h diagnostics.
- Prepend a native version guard without rewriting raw argv, batches,
  existing guards or stdin. Use -N after a live version check and on absent
  non-starting paths.
- Share bounded request execution with carrier probes without exposing a
  remote-carrier ProcessTransport instance. Retain request budgets,
  borrowed ownership and dispatch certainty.
- Remove detached-size and prompt-inventory adaptations made obsolete by
  the 3.3a floor. Keep compatibility still needed inside the supported
  range.
- Add version check, replacement, disappearance, malformed-version and
  carrier regressions. Update recording fixtures without weakening data
  batching, input cohort or native PID guard checks.
- Regenerate the operations reference and Kotlin ABI for call-time
  compatibility preflight. Preserve JDK process-argument inspection with
  an ASCII-space native no-op.
why: Python builders and shells execute outside the Java dispatcher. A
Java preflight alone cannot enforce the core floor through an obsolete
installed Python libtmux backend.

what:
- Check the selected daemon, or the executable only when absent, before
  external builder and shell entry.
- Require the Python backend's declared core floor to be at least 3.3a
  before loading builder or shell code. Repeat the handshake in every
  child process.
- Report the selected interpreter and compatible backend requirement
  without inventing a release pin. The coordinated Python core change is
  PR779.
- Remove the CLI's obsolete retry without detached session dimensions.
- Cover old-client refusal, obsolete backend refusal before effects,
  qualified backend success and real tmuxp 1.74.0 extensions using the
  coordinated Python source.
why: Facade transports must distinguish the version check from requested work
so their lifecycle, cancellation and cohort assertions keep measuring
the operation under test.

what:
- Answer exact daemon-version probes in Kotlin, Cats and workspace
  fixtures without counting them as application effects.
- Inspect original commands after the separate native guard in MCP and
  fixture recording transports.
- Retain the native wait-process readiness and cancellation assertions,
  staging-session cleanup and PID incarnation checks.
why: The supported matrix and user-facing requirements must agree with
the core version check. The extra version probe also changes process-count
guidance.

what:
- Remove 3.2a and unpatched 3.3 from required CI and local matrix lanes;
  keep the remaining supported and preview lanes.
- Update JVM facade, example, workspace CLI and contributor requirements
  to 3.3a.
- Describe version-probe reports and the extra process before ordinary
  command groups, captures and filtered reads.
- Update benchmark generator explanations while leaving historical
  generated measurements at their recorded commit.
why: Users must upgrade the running daemon as well as the client, and
external Python backends need their own command-level version checks.

what:
- Describe daemon-first refusal, unused endpoints, diagnostics and full
  version-token handling.
- Document call-time compatibility preflight and the bounded control-
  startup check without claiming endpoint pinning.
- Link the unreleased Python core PR and distinguish its dispatcher
  guarantee from Java preflight.
- State the requirement that configured command aliases preserve the
  native built-ins used for the version check.
why: The raised daemon minimum changes which installations and Python
backends can run JVM products.

what:
- Record the core 3.3a requirement under Unreleased, reference issue 67
  and link the migration instructions.
why: tmux 3.3a prints its executable basename before the version. A
renamed or symlinked supported binary must still pass the cold version check.

what:
- Accept the exact configured basename or canonical tmux banner
- Keep full version-token checks and bare daemon reports unchanged
- Cover mismatched names, malformed releases and daemon authority
why: A delivered marker can cross terminal rows and disappear from a
literal substring assertion even though both intended panes received it.

what:
- Join soft-wrapped rows in the shared typing capture helper
- Reproduce wrapping with ready cat consumers at a fixed pane width
- Retain effective-recipient and disabled-pane assertions
why: Released tmuxp pins Python libtmux before the core version check, so the
Python bridge correctly refuses the existing CI installation.

what:
- Install released tmuxp normally and overlay reviewed Python source
- Pin the source commit and assert its import path and 3.3a floor
- Apply the same qualification to JDK and supported tmux lanes
why: The cold version check and delivered-text assertions depend on native
behavior that differs from a simple version or substring assumption.

what:
- Record renamed tmux 3.3a version banners from copies and symlinks
- Record soft-wrap capture results on native 3.3a and 3.7d
why: The reviewed source overlay qualifies the command version check without
claiming a released or coherently installed tmuxp and libtmux pair.

what:
- Link the exact Python source used by the CI overlay
- State the import-path check and pending tmuxp dependency update
why: A user may configure a renamed or symlinked tmux 3.3a executable
when creating the first session on an unused endpoint.

what:
- Clarify that the cold version check accepts configured executable names
why: A wrapped completion marker or output arriving before a wait's
entry capture made JDK checks fail for unrelated fixture timing.

what:
- Join soft-wrapped completion capture and force that regression.
- Release shell output after the real wait entry capture.
- Preserve cold and warm echo assertions without a fixed delay.
why: The version check asked the daemon for `display-message -p '#{version}'`.
display-message fires a user's `after-display-message` hook on every
tmux release, so the probe itself had effects before a refusal could
stop anything.

what:
- Probe with `run-shell -C '<marker>#{version}'`, marker
  `libtmux-version-<random hex>-`. The daemon expands the format and
  fails to parse the result as a command, so the version is read from
  `unknown command: <marker><version>` on stderr. run-shell has no
  after-hook, and a parse failure inside its -C callback never reaches
  command-error. The live endpoint still gets -N.
- Another request's marker, a plain success, or any other stderr is not
  an answer: absent-endpoint wording still means an unused endpoint,
  anything else is a rejected probe.
- Test doubles (core, MCP, workspace, junit5, integration, Kotlin and
  Cats fake binaries) answer the new probe through VersionProbes;
  FakeTmux answers it for fixtures and carriers.
- VersionCheckHooksIntegrationTest arms after-display-message and, on
  3.5+, command-error hooks, runs checked calls and requires neither
  to fire, with a control that fires each hook the ordinary way.
- Unit cases pin the probe shape, a foreign marker, `master` and other
  unknown suffixes, and `next-3.9`.

Verified: VersionCheckHooksIntegrationTest passes on 3.3a (command-error
case skipped, no such hook), 3.5, 3.6b and master (next-3.9). Restoring
display-message as the probe fails the after-display-message case on 3.3a
and 3.5 ("the version check ran the user's after-display-message hook").
:libtmux, :libtmux-junit5, :libtmux-workspace and :libtmux-scala-cats
test tasks pass.
why: The operations guide said the version check emits a display-message probe
report, which is no longer the command sent.

what:
- Name run-shell and state that the version check fires none of a user's
  after-* or command-error hooks.
why: JDK 25/27 and the 3.5 and master lanes failed four
WaitingForTextTest cases with PRESENT_AT_ENTRY. Keys sent to a shell
that is still starting are echoed by the terminal and the shell draws
its prompt straight after that echo on the same row, so the echo is
glued to the prompt and no longer stands apart as a typed line. That is
not the case these tests pin. The previous follow-up replaced the
fixtures with a capture-gated release that still sent keys into the
starting shell and failed the same way.

what:
- Restore the trunk fixtures of WaitingForTextTest.
- Wait for the pane to show something and hold still for half a second
  before typeAndSubmit and send write to it.

Verified with 20 busy loops on a 10-core host, bash with a long prompt,
tmux 3.5 lane. The trunk fixtures fail sendThenWaitDoesNotMatchTheEchoedCommandLine
(PRESENT_AT_ENTRY) and the previous follow-up fails four cases with the
same outcome; with the wait, RunningCommandsTest, TypingTest and
WaitingForTextTest pass under the same load. Trunk's libtmux-mcp:test is
restored FROM-CACHE in CI, so its own run never exercised these cases.
Records that the version check reads the daemon version with run-shell -C and fires none of a user's hooks.
Applies the project formatter to the capture call added by the settle wait; no behaviour change.
why: The stalled-control fixture wrapped tmux and blocked on a FIFO for
any argument equal to `-C`. The version probe is now
`run-shell -C '<marker>#{version}'`, so the wrapper blocked the probe
and Server.cmd timed out before the control attach was ever reached
("cancelling control ends promptly and kills the stalled process"
failed with DispatchException$TimedOut). The process scan matched the
transient probe process the same way.

what:
- Block on `attach-session`, which only the control attach carries.
- Look for the stalled client by `-C` together with `attach-session`.

Verified: the case failed on this branch before the change and passes
after it (`:libtmux-kotlin:test --tests '*WaitsTest*'`).
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.
tony added 7 commits October 17, 2026 09:40
why: The facade is two published modules with a Clojure runner of their
own, and the repository's gates (platform coverage, release attestation,
dependency verification, documentation facts) have to know them.

what:
- Include libtmux-clojure and libtmux-clojure-core-async, put them in
  the BOM, and add the Clojure, core.async and Manifold versions.
- The clojure-library convention plugin compiles and runs the namespaces
  under a load guard; it defaults to JDK 25, the core's baseline.
- integration-tests depend on the modules and run the Clojure suite as
  clojureIntegrationTest; docs scans .clj sources.
- Checksums of the Clojure artifacts join the dependency verification
  metadata, and the release attests their jars.
why: Provide captured tmux values, explicit effects, bounded async
work, and stream adaptation without duplicating the Java engine.

what:
- Publish core and optional core.async Clojure modules through the BOM.
- Add captured snapshots, bounded tasks, control streams, and byte events.
- Test owned real-tmux fixtures, documentation fences, artifacts, and
  Clojure boundary contracts.
…sion

why: The facade was written before master moved its exception types to
io.github.libtmux.exception, renamed the version refusal to
UnsupportedFeatureException, took `capture()` for a package-private
reader, and put a version probe and a fence in front of every call.

what:
- Translate TargetGoneException, ServerUnavailableException,
  UnsupportedFeatureException, ServerClosedException and DispatchException
  (with its TimedOut subtype); the refusal is now
  :tmux/unsupported-feature and is reported as not dispatched.
- Read the capture view through Server.captureServer and run shell
  commands through Server.shell.
- The fake transports of the unit tests answer the admission probe and
  count only the requests a call makes; grouped replies skip the leading
  version fence, so a group is five requests instead of four.
…criptions

why: Master's control client now decodes pane output per pane, reports
overflow as a gap ahead of the events that remain, and refuses a daemon
below a stable tmux 3.7 before any client attaches. The facade carried
its own byte-limited subscriptions, UTF-8 decoder and termination
reasons for an older client.

what:
- attach! goes through Server.control, so the incarnation check, the
  3.7 floor and the pinned socket are the Java core's; an older daemon
  raises :tmux/unsupported-feature, not dispatched, with no client.
- subscribe! keeps :encoding (:bytes or :utf-8) and :capacity. The
  :max-bytes, :malformed and :max-panes options go: the Java client
  decodes per pane and writes a byte that is not UTF-8 as \xHH.
- next! returns a :gap with :gap/events ahead of the events that remain
  and a terminal map whose :reason is :closed after a close and :ended,
  with the original :cause, when the client ended the stream. Events the
  client delivered stay readable after it ends; a close discards them.
- core.async observe! drops the byte and decoder options and forwards
  gaps.
- Integration tests run control scenarios on stable 3.7+ and assert the
  refusal, with no attached client, on older daemons; documentation
  snippets that attach use a `control` directive with the same rule.
- Guide and READMEs state the control floor and the stream contract.
why: The Java core compiles for JDK 25, so the JDK 21 runtime the
facade named loads none of it; the tmux lanes still ran releases the
core now refuses; the macOS tmux build lacked a utf8proc choice that
3.3a and later require; and master's checks want actions pinned to
commits and deps.edn on the current development version.

what:
- Run the compatibility workflow on JDK 25 and 27, start the tmux matrix
  at 3.3a (the core's own lanes refuse 3.2a and 3.3) and pass
  --disable-utf8proc to the macOS tmux build.
- Pin the workflow's actions to the commits the other workflows use.
- Move deps.edn to 0.0.1-alpha.17-SNAPSHOT.
Notes Server.captureServer and the Clojure close-runtime! fix.
why: A known daemon is admitted once, so the capture case's two data
requests sit behind one probe, not two.

what:
- Expect three requests: the identity read, the grouped listings and the
  one probe.
@tony
tony changed the base branch from master to bump-min-tmux-001 October 17, 2026 14:44
…check class

why: `test-tmux-3.2a` and `test-tmux-3.3` exist in every module that
applies the matrix plugin, and Gradle failed the ones whose test set,
filtered to the version check class, is empty (":libtmux-junit5:test-tmux-3.2a").

what:
- The rejection lanes do not fail on a module with no matching test.

Verified: both lanes pass across all modules, running the version check case
only in integration-tests.
tony added 2 commits October 17, 2026 10:44
why: The supported range starts at 3.3a, but the streaming guide said a
feature needed "3.2 and later", the parity map labelled its rows "3.2a
through 3.7b", and several test and source comments gave 3.2a as the
start of the range. Historical measurements ("measured on 3.2a") stay.

what:
- Say "every supported tmux" in the streaming guide, "3.3a through 3.7c"
  in the parity map, and "across the supported range" in the comments
  that stated a range; make the WRITING.md example comment not name a
  release.

Verified with :docs:test.
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.
tony added 3 commits October 17, 2026 11:24
…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.
…e capacity

why: The Clojure facade reads a whole server as one consistent value and
releases waits through a transport slot reserved for them. The Java
core offered neither: a capture was a snapshot the caller had to wrap,
and a wait-for release shared admission with ordinary commands.

what:
- Server.captureServer returns a CapturedServer: sessions, window links,
  pane occurrences and clients from one observed graph, read-only lists,
  no command after acquisition. (`capture()` is master's package-private
  snapshot reader, so the public name differs.)
- Server.batchReservingCapacity and chainReservingCapacity dispatch
  through the admitted waiting path, so the release command is admitted
  like any other and a daemon below the floor is refused before it runs.
- Unit and real-tmux cases for the view and the reserved-capacity
  groups.
why: On the JDK 27 lanes Gradle itself ran on 27 and the build-logic
Kotlin compile failed ("Inconsistent JVM-target compatibility ... (27)
and (26)").

what:
- Install the lane's JDK and 25 together with 25 last, as ci.yml does;
  -PlibtmuxJavaVersion still selects the JDK that runs the Clojure.

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.

1 participant