Make tmux operations literal, bounded, and identity-safe - #6
Merged
Conversation
why: Process and control streams could block indefinitely, lose dispatch certainty, or leak workers and descendants during timeout and close races. what: - Bound admission, output, deadlines, and process reclamation - Parse control replies on dedicated workers with bounded subscriptions - Cover interruption, close barriers, malformed bytes, and stream loss
why: Public filter nodes could change after construction, valid relations were missing from built-in models, and malformed wire documents escaped the schema error boundary. what: - Give built-in fields stable handles and immutable value semantics - Complete cyclic relation models and operator coverage - Fail closed on malformed JSON and document Java regex semantics
why: Handles could act on a replacement tmux process, linked windows lost their session context, and generic execution carriers could change command meaning after tmux alias expansion. what: - Bind snapshots and handles to the live tmux process identity - Validate hierarchy slots, targets, layouts, and hydration scalars - Remove execution modes and their benchmark surface
why: Invalid or unsupported workspace input could create a partial session, and an uncertain creation reply left no exact rollback target. what: - Split parsing from application - Validate names, layouts, topology, and tmux version before creation - Create under a unique staging name and roll back that exact session
why: Teardown treated a failed probe as proof that tmux had exited, while abandoned-server recovery left dead owner directories behind. what: - Keep abandoned process and directory ownership together - Delete owner directories only after process exit is confirmed - Run leak regressions under the Java test root with unconditional cleanup
why: Watch notifications could race or grow without bound, attachment gaps lost invalidations, and malformed protocol input could leave the launcher alive after its session ended. what: - Serialize bounded invalidations and reconcile every session attachment - Track pane generations, retry outages with backoff, and hide watcher clients - Canonicalize resource input, discover live sockets, and couple launcher exit to protocol-session closure
why: The documented testTmuxMatrix command did not exist even though every module registered per-release lane tasks. what: - Create one root compatibility task - Let each matrix-enabled module contribute all supported tmux lanes
why: Safety tiers can be mistaken for confinement even though mutating tools can run arbitrary commands in a pane. what: - State that the ceiling filters tools and protocol annotations - Name the OS, socket and container boundaries that constrain effects
Snapshot the server identity around hydration and retry one complete capture when the server is replaced. Preserve real listing and parse failures when the incarnation remains unchanged. Run version gates from the captured snapshot and guard winlink operations with one target-scoped tmux command so a stale session:index cannot act on its replacement.
Share one bounded process owner between subprocess and control carriers. Snapshot descendants before failure paths can close a pipe, keep later daemonized servers outside cleanup, and report incomplete reclamation without replacing the primary failure. Start pump workers lazily and redact rejected control argv.
why: Snapshot consistency now samples server identity on both sides of hydration, so the whoami command-count guard was stale. what: - Expect four listings, two identity probes, and one socket lookup - Keep the guard exact so handle-by-handle traversal still fails
why: The mutating ceiling includes arbitrary commands and input, but its tools advertised destructiveHint=false, which promises additive-only updates and can suppress client confirmation. what: - Declare each tool's update effect separately from its availability ceiling - Mark all 16 non-additive tools destructive, including mutating-tier commands - Verify direct and stdio hints and correct the safety documentation
Fail closed on tmux 3.2a and 3.3a because deleting an absent named buffer removes the top buffer. Protect trailing semicolons from tmux's command-group parser.
Use one buffer per call and require tmux 3.4 so failures can clean up the exact buffer. Move typing coverage out of the aggregate real-tmux test.
Reject NUL in PaneSpec before workspace topology can change. Keep cleanup-failure coverage on an independent post-creation failure.
Send literal text plus carriage return in one tmux operation. Protect option-shaped and semicolon-terminated lines across direct pane and command-chain delivery.
Reuse Pane.sendLine so a payload and its carriage return reach tmux in one operation. Prevent concurrent tmux_run calls on one pane from merging their shell input.
Use stable session IDs for rename and kill, leaving literal server as the only non-ID destructive target. Keep session-name lookup only on arguments that explicitly request names.
Carry exit status in the framed end marker so late completion cannot leave a pane option behind. Keep the shell independent of Java cleanup after timeout or uncertain delivery.
Treat failed protocol output as a disconnect, including broken-pipe errors hidden by PrintStream. Centralize exactly-once session completion across output and transport shutdown while preserving primary failures.
Upgrade to MCP SDK 2.0.1 so unterminated stdio messages have a hard size limit. Pin the bounded constructor with a focused session-lifecycle regression.
Close the watcher, transport, and input when either stdio side ends or startup fails. Signal the launcher before cleanup and preserve primary failures across immediate and graceful shutdown.
Own the tmux Server lexically so failed watcher or protocol setup cannot strand its non-daemon transport threads. Cover the empty-server watch failure through a real child process.
Limit externally signalled waits to all but one process so signal and observation calls remain dispatchable without increasing the transport bound. Fail excess waits before dispatch and preserve dispatch certainty across close races.
Stop hierarchy capture after an empty session listing because tmux refuses child listings without a current target. Keep the observed process identity and let MCP watch startup report its session requirement.
Run later watcher, attachment, transport, and SDK cleanup even when an earlier owner fails. Preserve the first failure and suppress later ones through one package-private cleanup boundary.
Transfer caller-supplied transport ownership on entry and roll it back when SDK construction fails. Close the built server after acceptance, preserve the startup failure, and detach any prepared watcher.
why: SDK 2.0.1 rejects concurrent emissions to its unicast stdio sink, so a response can be lost when progress or watcher output races it. what: - Queue each session's sends without blocking a worker thread - Prove ordering and failure release with deterministic tests
why: The five-minute override governs server-to-client requests, not tool calls. SDK 2.0.1 can issue roots/list implicitly, turning a ten-second upstream wait into five minutes. what: - Remove the misdirected five-minute request timeout - Record the effective ten-second bound without wall-clock delay
why: SDK 2.0.1 does not cancel running synchronous handlers, and workspace rollback cannot undo commands already started. Existing prose promised both. what: - Distinguish client abandonment from handler termination - Describe workspace validation and cleanup as best effort - Trim duplicated wait rationale
why: The case starts a replacement server on the socket the one it killed was using, and tmux answers a client reaching a server that is still exiting with "server exited unexpectedly" rather than "no server running", from 3.3a onwards. Under full-matrix load the replacement's new-session reached the dying server and the case failed for the teardown rather than for what it is about. It failed on the 3.3a lane and passed alone on both this commit and the one before it. what: - Wait for tmux to unlink the socket before opening the replacement
why: The case makes the shell acknowledge the function definition, but not the clear between that and the marker it asserts on, so under full-matrix load the assertion could run against a screen the shell had not caught up with. It failed that way on the 3.2a lane and passed alone, which is the second time this case has been load-only. what: - Acknowledge the clear the same way the definition is acknowledged
why: Thirteen files each carried the same poll loop with the same five-second bound, and a lane of the matrix runs the whole suite on a machine the other lanes are also using. That bound is only ever spent when something is already wrong, so a tight one reports the load rather than the library — which is how the literal-line case failed on two different lanes on two consecutive runs. what: - Await.until and Await.output replace thirteen copies, 147 lines fewer - Raise the bound to fifteen seconds, in the one place it now lives - Keep the subscription waiter in ExamplesTest: a stream is drained rather than polled
why: A reader could not tell a current spike from one describing a design that has since been removed, and four of them describe types that are gone: the execution modes, their seam, the metamodel base class, and the six-command capture. what: - Head each with what survived and what did not - State in the snapshots guide what a capture now costs, and where the number comes from
why: HandleTest grew its own parser for the fenced group a capture now sends, and GroupedTmux was extracted for exactly that a few commits later. Two readings of tmux's group semantics can disagree, which is the failure the shared one exists to prevent. what: - HandleTest answers through GroupedTmux like the other doubles
why: The second identity probe this replaced compared a pid and a version; the fence that replaced it compared only the pid. A pid is reusable, so a different tmux landing on the one just probed would have answered as the server the rows were being read from — which the form it replaced would have caught, and which its own contract claimed. what: - Fence a capture on pid and version together - Refuse a double answering as 3.7 on a pid probed as 3.6 - Fold the socket wait into Await, which the last commit missed - Say in the grouping table that every row pays the same setup, so the ratio between them understates what grouping saves
why: The case waits ten seconds for a pane's shell to print, and a matrix lane shares its machine with the other eight. It failed that way on the 3.6 lane, which is the same budget problem the integration suite had one commit earlier. what: - Wait thirty seconds, since these cases wait on a shell and on marker scanning rather than on tmux alone
why: mode() handed back tmux's own string in a library whose case is that tmux state is typed, so a caller compared against "tree-mode" by hand and a typo failed at runtime rather than at compile time. what: - PaneMode names the seven modes tmux has, each present from 3.2a - mode() answers with one, and raises on a name outside the range - Kotlin's modeOrNull follows
why: waitFor, waitForWithSignalCapacity, signal and drain each took the channel name again, so a caller repeated it at every call site and a typo waited on a channel nothing would ever signal. The two traps tmux's wait-for carries were documented four times over. what: - Channel binds the name once and carries signal, drain and the two waits, the way options(), hooks() and buffers() already do - Server keeps channel(name) in place of the four
why: Three methods covered name, content and both, and tmux also takes a title, a case-insensitive match and a regular expression. Naming every combination would have meant a method each; what was there was an incomplete surface rather than a crowded one. what: - FindSpec collects what to match and where, the shape CaptureSpec uses - findWindow takes a spec, a builder, or the text alone - findWindowByName and findWindowByContent go: name it on the spec - Every flag exists from 3.2a, so nothing here is version-gated
why: The close test subscribed before tmux had acknowledged the control client, leaving the waiting thread exposed to an attach race. what: - Send one checked command before subscribing
why: A large paste could arrive while the inherited shell was still starting, so the test observed startup timing instead of paste capacity. what: - Start a fixed noncanonical reader pane - Wait for its readiness marker before pasting
Why: The live tmux test assumed an attached shell would remain silent long enough to prove its subscriber was blocked. JDK 25 exposed legitimate pane output winning that race. What: Exercise the same client-close wakeup contract against a controlled fake control process that emits no notifications.
why: Caller comments and parentheses could alter completion framing and escape the subshell that protects the pane's interactive shell. what: - Source exact caller text from an owner-only temporary file - Remove staged text on success, timeout, cancellation, and failure - Exercise exit, comment, and parenthesis isolation against real tmux
why: A child that stopped reading could trap its caller in a synchronous write past the request deadline and retain admission forever. what: - Pump stdin on a reserved platform worker beside both output drains - Reclaim the input worker before returning process admission - Exercise a blocked real child, its deadline, cleanup, and permit reuse
why: A stalled client retained every queued message, while cancellation and close left queued sends live or unresolved. what: - Bound admitted sends to 256 messages and 16 MiB of encoded JSON - Remove cancelled queued sends and return their admission - Fail admitted sends on close without starting concurrent delegate sends
why: Temporary command staging was deleted as soon as send-keys reported uncertain delivery, before a busy pane shell necessarily opened the file. An accepted caller command could disappear while the framing still ran. what: - pass caller text as one quoted eval operand inside the framing subshell - gate ambiguous delivery and prove the accepted command still executes
why: A send promoted before the previous caller was notified still reached the delegate when that callback cancelled it. what: - Distinguish promoted sends from ones already started - Release cancelled promotion admission and continue in order - Cover callback cancellation before delegate start
why: Destructive tools treated an unprovable caller identity as a different server and could kill the pane hosting the conversation. what: - Track outside, different, self, and unknown caller relationships - Require both the server PID and socket identity before trusting a pane - Fail closed on destructive calls when caller identity is uncertain - Cover the refusal against a real isolated tmux server
why: Retained history size is not a lifetime offset, and forged or stale cursors could be accepted across history compaction or server replacement. what: - Authenticate cursors and bind them to the tmux server process - Resume from strong trailing-line context instead of history offsets - Fence screen batches to the pane snapshot and require complete replies - Cover compaction, forged cursors, replacement servers, and failed reads
why: Java character counts understated UTF-8 command sizes, and newline in a final format field ended a row before its content was complete. what: - Measure encoded batch commands in UTF-8 bytes - Append an explicit randomized record terminator to row formats - Parse multiline final and single-field rows through that terminator - Preserve legacy unframed test transports
why: The run protocol uses POSIX shell syntax and otherwise typed its plumbing into an arbitrary foreground program without completing. what: - Refuse tmux_run unless the pane reports a supported POSIX shell - Keep rejected payloads out of non-shell panes - Increase framing and channel nonces from 40 to 128 bits - State the shell precondition in the tool description
why: The branch fixes must satisfy the repository formatting gate. what: - Apply the pinned formatter to the touched Java sources and tests
why: The delivery-uncertainty fixture must satisfy the tmux_run POSIX-shell precondition while exercising the post-delivery failure. what: - Remove the foreground tmux wait gate from the fixture - Retain proof that the accepted send-keys payload still executes
why: The wire test must not race an interactive shell startup while proving tmux_run under its POSIX-shell contract. what: - Create a dedicated /bin/sh pane for the command tool call
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.
Summary
Changes by area
Breaking changes
pasteBuffer, whilepasteaccepts literal text.CommandRequestcarries command groups and optional input instead of one flat argv.Test plan
git diff --check../gradlew checkpasses all 70 tasks on JDK 21 and JDK 25.