Skip to content

Pane(fix[run]): Wait for a new pane's shell - #156

Merged
tony merged 4 commits into
masterfrom
fix/release-rehearsal-shell
Nov 13, 2026
Merged

tony merged 4 commits into
masterfrom
fix/release-rehearsal-shell

Conversation

@tony

@tony tony commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Pane.run no longer refuses a pane that was created a moment ago. It waits, for up to two seconds, until tmux names the pane something other than tmux, then runs the usual POSIX-shell check.

Root cause

The release rehearsal job on master failed in run 43230109896 with:

java.lang.IllegalStateException: running a command to its end needs a POSIX shell in the pane, and it is running 'tmux'

The staged consumer creates a session and calls Pane.run on its pane at once. Between fork and exec the process in a pane's terminal is the server's own copy, so #{pane_current_command} reads tmux. Pane.run read that once and requirePosixShell refused it.

It is a race, not a regression: the previous master run passed, and 3 of 150 reads straight after new-session -d returned tmux on tmux 3.8a. libtmux-rs recorded the same window as a just-forked pane named tmux.

The README's Pane.run example has the same exposure, so the wait is in the library rather than in the consumer. libtmux-mcp's run_shell_command reads the pane command in its own code path and is not changed here.

Test plan

  • PaneRunIntegrationTest.aPaneThatHasNotStartedItsShellYetIsWaitedFor failed before the change with the message above, and passes after it
  • The other eight PaneRunIntegrationTest cases pass, including aPaneNotRunningAShellIsRefused
  • ./gradlew :integration-tests:test --tests '*PaneRunIntegrationTest' :libtmux:spotlessCheck :integration-tests:spotlessCheck and python3 -m tools.ci.check
  • Rerun release rehearsal on this branch several times

tony added 2 commits November 12, 2026 17:54
why: The release rehearsal on master failed with "running a command to
its end needs a POSIX shell in the pane, and it is running 'tmux'". The
staged consumer creates a session and calls Pane.run on its pane at
once. Between fork and exec the process in a pane's terminal is the
server's own copy, so #{pane_current_command} reads "tmux" for a moment,
and Pane.run read it once and refused. The same call passed on the
previous master run, so it is a race, not a change: 3 of 150 reads
straight after new-session -d returned "tmux" on tmux 3.8a.

Alternatives considered:
- Wait in the consumer. Every caller that follows the README, which
  runs a command in a pane it just made, has the same race.
- Accept "tmux" as a shell. A pane that really runs tmux would then be
  typed into as if it read POSIX syntax.
- Poll for the prompt. The library does not know the shell's prompt.

The wait is bounded, so a pane that stays "tmux" is refused as before,
after the wait.

what:
- Make Pane.run await a name other than "tmux" for up to two seconds
  when its first read says "tmux", then read the command and the socket
  again before the shell check
- Test it with a pane whose process is a symlink named tmux that sleeps
  before it execs a shell: refused with the CI message before the
  change, runs the command after it
- Record the measurement in docs/internals/tmux-behaviour.md
why: A caller can observe the change: Pane.run on a pane created a
moment ago no longer throws while its shell starts.

what:
- Add a Fixed entry under Unreleased in libtmux/CHANGELOG.md
@tony
tony force-pushed the fix/release-rehearsal-shell branch from ecbdf09 to 75bb1ed Compare November 12, 2026 23:55
tony added 2 commits November 12, 2026 19:15
why: run_shell_command read the pane's command once, as Pane.run did
before the previous commit, so a pane created a moment before the call
was refused with "run_shell_command requires a POSIX-compatible shell
in the target pane; it is running 'tmux'". tmux names a pane "tmux"
between fork and exec. The wait added to Pane.run belongs in one place
that both callers use, and the MCP server is the outside caller
PaneCommand is public for.

The cohort read that follows also compares the pane's command before
and at dispatch, so a pane that changed from "tmux" to a shell in
between would have been refused as changed under the run. Waiting
before the first read keeps both reads on the shell.

what:
- Move the wait out of Pane.run into PaneCommand.awaitShellStart, which
  waits only when the command it is given is "tmux" and says whether it
  did, so the caller reads the pane again
- Call it from Pane.run and from RunningCommands.run, which resolves the
  input cohort again after a wait
- Add RunningCommandsTest
  aPaneThatHasNotStartedItsShellYetIsWaitedFor: a pane whose process is
  a symlink named tmux that sleeps before it execs a shell. It failed
  before the change with the refusal above, and passes after it
- Name the MCP caller in docs/internals/tmux-behaviour.md
why: A caller can observe two changes: run_shell_command on a pane
created a moment ago no longer refuses it while its shell starts, and
PaneCommand has a new public method both run paths use.

what:
- Add a Fixed entry under Unreleased in libtmux-mcp/CHANGELOG.md
- Add an Added entry for PaneCommand.awaitShellStart under Unreleased in
  libtmux/CHANGELOG.md
@tony
tony merged commit fae00e5 into master Nov 13, 2026
31 of 33 checks passed
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