Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de 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)

Cerrado
#952 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Ya se ha fusionado un pull request relacionado.

  • #955 de @dracic — fusionado

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
25/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Estancado
Stack tecnológico
python
Área
cli

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
Python
Estrellas
165
Forks
69
Merge medio
1 d 3 h
PR fusionados (30 d)
75

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de bmad-code-org/bmad-loop

Todos los issues de bmad-code-org/bmad-loop

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.