Skip to content

Add native Clojure facade - #20

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

tony wants to merge 17 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)

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
@tony

tony commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Brings the draft Clojure facade in line with the Java core and feature changes (core minimum change: #69, optional features: #72). The PR stays a draft; its history is kept and this is a fast-forward push to clojure.

What changed

  • Merged the features branch (it carries the core branch and current master). The first merge took master's control line reader, ControlClient and EventSubscription, replacing this branch's byte subscriptions, UTF-8 decoder and termination reasons that master now provides. The history shows two merges because the features branch was rebased between them; the second kept this branch's Server additions. CapturedServer stays, reached through Server.captureServer because master gave capture() to a package-private reader. batchReservingCapacity and chainReservingCapacity now go through the admitted waiting path instead of the raw transport.
  • Facade: master's exception types, :tmux/unsupported-feature for a refused floor (reported not dispatched), Server.shell for run-shell!.
  • Control: attach! uses Server.control, so an older daemon than a stable 3.7 is refused before any client attaches. subscribe! keeps :encoding and :capacity; :max-bytes, :malformed and :max-panes are gone because the Java client decodes per pane. next! returns a :gap ahead of the events that remain and a terminal map with :closed or :ended plus the original cause. The core.async adapter drops the byte options and forwards gaps.
  • Build and CI: JDK 25 (the core's baseline; the facade's JDK 21 claim could not load the core), tmux matrix from 3.3a, dependency verification metadata for the Clojure artifacts, pinned actions, release attestation and version baselines required by master's documentation checks.

Verification (Linux/WSL, not the hosted matrix)

  • Clojure namespace load, namespace guard and unit tests for libtmux-clojure and libtmux-clojure-core-async via Gradle: pass.
  • :integration-tests:clojureIntegrationTest with -PlibtmuxTmuxExpected: 41 tests, 0 failures on tmux 3.3a, 3.5 and 3.7, and on master (next-3.9, which control refuses). Control scenarios run on stable 3.7+; every older daemon asserts the refusal with no attached client.
  • Negative control: reporting the floor refusal as :tmux/transport fails every control case on 3.3a.
  • :docs:test, clojureDependencyMetadata, platformCoversEveryPublishedModule, spotlessCheck, :libtmux:test: pass. The Java/Kotlin/Scala checks of the other PRs do not qualify this facade; hosted Clojure workflow results are not claimed here.

Not done: the diff against master still includes the unmerged core and feature commits until those PRs land. The hosted tmux 3.4/3.6/3.7x lanes and macOS are untested by me.

@tony

tony commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Update: the core branch was rebased onto master and the version grammar now follows the canonical vectors (candidates and development builds rank below their release), so control qualifies on any daemon at or above 3.7 (3.8-rc2 and next-3.8 included, next-3.7 and 3.7-rc2 excluded). The facade's control gate asks the Java core; a third merge of the features branch (rewritten again) is pushed as a fast-forward. Re-verified: Clojure namespace load, namespace guard, unit tests, documentation checks, dependency metadata and spotless pass, and :integration-tests:clojureIntegrationTest passes (41 tests, 0 failures) on tmux 3.3a, 3.5, 3.7 and master (next-3.9, where the control cases now run). Hosted Clojure workflow results are still not claimed.

tony added 8 commits October 17, 2026 09:40
…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: 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
@tony
tony force-pushed the bump-min-tmux-001 branch from 6988620 to e57f5e0 Compare October 17, 2026 14:57
@tony
tony force-pushed the bump-min-tmux-001 branch from e57f5e0 to e295b16 Compare October 17, 2026 15:44
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