Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

TUI launch: two launches that create the control session at the same moment race, and the loser fails with "duplicate session" (tmux 3.4, psmux 3.4.0)

Đã đóng
#952 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Một pull request liên quan đã được merge.

  • #955 của @dracic — đã merge

Đánh giá

Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức phù hợp với người mới
25/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Đình trệ
Công nghệ
python
Lĩnh vực
cli

Hướng nghiên cứu

The fix belongs in _ensure_ctl_session in src/bmad_loop/tui/launch.py, where a MultiplexerError from mux.new_session is turned into a LaunchError without re-checking has_session. Read adapters/tmux_base.py new_session to see the duplicate session path. A change is done when a failed create re-checks the session and continues if it now exists. Linked PR #955 is already open for this, so check it before starting.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Summary

tui.launch._ensure_ctl_session checks has_session and then calls new_session, with nothing between them. When two processes both observe that the control session is absent, a competing create can fail after another process has created the session. The launcher converts that create failure into LaunchError: multiplexer ctl-session setup failed: ... without re-checking whether the desired session now exists. The control session exists a moment later, so the launch was refused for nothing. On tmux the control session is one fixed name on the default socket, shared by every project (source-derived, runs.ctl_session_for; not measured across two projects), so two TUIs in two different projects that start their first run together should be enough. It should re-check after a failed create and use the session that now exists.

Environment

psmux 3.4.0 (20aa1d7), Windows 11 Enterprise 10.0.26200, PowerShell 7.6.6 (launching shell; SHELL removed), PSMUX_NO_WARM=1; tmux 3.4 in WSL2 Ubuntu 24.04.4 (bash 5.2.21) with TMUX_TMPDIR, HOME and XDG_CONFIG_HOME set to a throwaway dir; bmad-loop 88c1879c

Steps to reproduce

No coding CLI is started. The psmux steps run from PowerShell 7 with SHELL removed; the tmux steps run from bash in WSL. Save the script below as ctl_race.py in the directory you run the commands from (for the tmux steps, /tmp/ctlrace/proj, after the mkdir), and run it with the bmad-loop checkout's interpreter. It starts N processes that each call the TUI launcher's own control-session setup (_ensure_ctl_session, called before start_detached creates the run's window) at the same instant, on a project whose control session does not exist yet, and prints what each one got.

# usage: python ctl_race.py <project> <N> [fixed]        (driver)
import json, subprocess, sys, time
from pathlib import Path
from bmad_loop import cli
from bmad_loop.tui import launch

project = Path(sys.argv[1])
cli._configure_mux(project)
if sys.argv[2] == "worker":
    if sys.argv[4:] == ["fixed"]:  # candidate fix, in this process only: a create that lost the race re-checks
        m = launch.get_multiplexer(); orig = m.new_session
        def new(name, cwd, *a, **k):
            try:
                return orig(name, cwd, *a, **k)
            except launch.MultiplexerError:
                if m.has_session(name):
                    return None
                raise
        m.new_session = new
    while time.time() < float(sys.argv[3]):
        time.sleep(0.002)
    try:
        print("OK", launch._ensure_ctl_session(project), flush=True)
    except launch.LaunchError as e:
        print("LaunchError", e, flush=True)
    sys.exit(0)
mux = launch.get_multiplexer(); ctl = launch.ctl_session(project)
print("backend", type(mux).__name__, "ctl", ctl, "exists before:", mux.has_session(ctl))
go = time.time() + 3
ps = [subprocess.Popen([sys.executable, __file__, str(project), "worker", str(go)] + sys.argv[3:4],
                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) for _ in range(int(sys.argv[2]))]
outs = [p.communicate(timeout=120)[0].strip() for p in ps]
for o in outs:
    print("  worker:", o)
print("exists after:", mux.has_session(ctl), "launch_errors:", sum("LaunchError" in o for o in outs))

tmux 3.4 (WSL, bash). A private tmux server through TMUX_TMPDIR, and a project whose policy selects tmux; python is the checkout's Linux venv interpreter:

export TMUX_TMPDIR=/tmp/ctlrace/tmux HOME=/tmp/ctlrace XDG_CONFIG_HOME=/tmp/ctlrace BMAD_LOOP_STATE_DIR=/tmp/ctlrace/state
mkdir -p $TMUX_TMPDIR /tmp/ctlrace/proj/.bmad-loop && : > /tmp/ctlrace/.tmux.conf
printf '[mux]\nbackend = "tmux"\n' > /tmp/ctlrace/proj/.bmad-loop/policy.toml
cd /tmp/ctlrace/proj
for i in 1 2 3; do python ctl_race.py /tmp/ctlrace/proj 2; tmux kill-server; done   # two launchers, three trials
python ctl_race.py /tmp/ctlrace/proj 1; tmux kill-server                              # control: one launcher
for i in 1 2 3; do python ctl_race.py /tmp/ctlrace/proj 2 fixed; tmux kill-server; done  # candidate fix

Measured output of one two-launcher trial (10 of 10 trials looked like this, in two separate series of 10):

backend TmuxMultiplexer ctl bmad-loop-ctl exists before: False
  worker: OK bmad-loop-ctl
  worker: LaunchError multiplexer ctl-session setup failed: tmux new-session -d failed: duplicate session: bmad-loop-ctl
exists after: True launch_errors: 1

The one-launcher control printed launch_errors: 0; the fixed variant printed launch_errors: 0 in 5 of 5 trials. In a separate run, the same two launchers released 1.5 s apart instead of at one instant also gave 0 errors, so the failure needs overlapping creates.

psmux 3.4.0 (PowerShell 7, SHELL removed). Same script, a throwaway project with [mux] backend = "psmux", its own state dir, and kill-server scoped to that project's registry after each trial:

Remove-Item Env:SHELL -ErrorAction SilentlyContinue
$Py = 'C:\path\to\bmad-loop\.venv\Scripts\python.exe'   # the checkout's interpreter
$env:PSMUX_NO_WARM = '1'; $env:PYTHONUTF8 = '1'
$root = Join-Path $env:TEMP "ctlrace-$PID"; $proj = Join-Path $root 'proj'
$env:BMAD_LOOP_STATE_DIR = Join-Path $root 'state'
New-Item -ItemType Directory -Force (Join-Path $proj '.bmad-loop') | Out-Null
Set-Content (Join-Path $proj '.bmad-loop\policy.toml') "[mux]`nbackend = `"psmux`"`n"
$reg = & $Py -c "import sys; from pathlib import Path; from bmad_loop import runs; print(runs.export_psmux_registry_root(Path(sys.argv[1])))" $proj
foreach ($i in 1..10) {
    & $Py ctl_race.py $proj 2
    $env:PSMUX_DATA_DIR = $reg; psmux kill-server; Remove-Item Env:PSMUX_DATA_DIR   # this project's registry only
}

Measured (rates vary between runs): with two launchers, 1 to 3 of 10 trials had one failure; with five launchers, 2 of 6 to 8 of 13 trials had at least one, each as

  worker: LaunchError multiplexer ctl-session setup failed: psmux new-session failed: psmux: failed to create session 'bmad-loop-ctl'

followed by exists after: True. Not every simultaneous pair has a loser on psmux: in trials without a failure both creates returned success. One launcher: 0 errors. Five launchers released 1.5 s apart: 0 errors. The fixed variant with five launchers: 0 errors in 5 of 5 trials. In these samples psmux failed less often than tmux. The sample does not establish why the rates differ. Independently, psmux v3.4.0 source contains a duplicate-server mutex; this is source-derived, not a measured explanation of the rate.

Through the TUI launch path. Five processes calling tui.launch.start_run_detached(project, <own run id>) at the same instant on a fresh project (psmux 3.4.0, one run): four runs started in their own control windows, the fifth returned LaunchError: multiplexer ctl-session setup failed: psmux new-session failed: psmux: failed to create session 'bmad-loop-ctl' and its run was never started (no run directory).

Expected vs actual

actual expected
two launches both observe no control session; one create loses the race one can be refused even though the control session now exists, with multiplexer ctl-session setup failed: ... duplicate session (tmux) / failed to create session (psmux) both start in the one control session
control session after the race exists (has_session True) exists

Source

src/bmad_loop/tui/launch.py at 88c1879c, _ensure_ctl_session (line 1048), the only creator of the control session, called by start_detached (defined at line 1252), call site line 1329:

    try:
        if not mux.has_session(name):
            mux.new_session(name, project)
    except MultiplexerError as e:
        raise LaunchError(f"multiplexer ctl-session setup failed: {e}") from e

A failed new_session is not re-checked: a create that failed because the session was created in between reads the same as one that failed for any other reason. On tmux, new_session (adapters/tmux_base.py:366) runs new-session -d -s bmad-loop-ctl, which tmux refuses with duplicate session once the name exists.

Impact on bmad-loop

Every TUI launch (run, sweep, resume, resolve) goes through start_detached -> _ensure_ctl_session (source-derived). Source-derived: this exception occurs before this invocation creates its control window or records that window. A new launch therefore does not start its child command through this path; existing resume/resolve run state is a separate concern. The operator sees an error for a launch that would have worked, and has to start it again. The window is only open while the control session does not exist yet (the first launch after a server start, or after cleanup removed it). On tmux that one session is shared by every project on the default socket, so the race should be between any two bmad-loop processes on the machine, not only within one project (source-derived, not measured).

Suggested fix

Treat a failed create as "maybe someone else created it": after new_session raises, re-check has_session(name) and continue when it is now there; raise only when it is still absent. Measured with that change applied in the probe process only (not in the checkout): 0 failures on both backends in the trials above. Not validated beyond those trials.

Ngôn ngữ chính
Python
Star
165
Fork
69
Merge trung bình
1 ngày 3 giờ
Pull request đã merge (30 ngày)
75

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của bmad-code-org/bmad-loop

Tất cả issue của bmad-code-org/bmad-loop

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.