Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le 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)

Chiusa
#952 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Una pull request collegata è già stata integrata.

  • #955 di @dracic — integrata

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
25/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
python
Ambito
cli

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.

Lingua principale
Python
Stelle
165
Fork
69
Merge medio
23h 42m
PR unite (30g)
87

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di bmad-code-org/bmad-loop

Tutte le issue di bmad-code-org/bmad-loop

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.