Skip to content

retire the per-registry control-session name once a psmux release carries the psmux#599 fix #733

Closed

Description

@dracic

#537 names the psmux control session per registry — bmad-loop-ctl-<16-hex digest of the resolved registry root> — instead of the fixed bmad-loop-ctl. That suffix exists for one reason, and it is upstream's:

psmux keys its single-server-per-name mutex on the session name, not on PSMUX_DATA_DIR. acquire_session_mutex builds Local\psmux-session-{port_file_base} (src/platform.rs:346), and port_file_base() is {socket_name}__{session_name} or the bare session name (src/types.rs) — the data root never enters it. So two per-project registries, correctly isolated for lookup and cleanup, still share one global name namespace: with a fixed control-session name, every project's TUI launch but the first fails as a duplicate cold-spawn.

Filed upstream as psmux/psmux#599, with two reproductions on installed psmux 3.3.8 (66cf613) and a source read confirming acquire_session_mutex and port_file_base() are byte-identical between v3.3.8 and master 60e59fb.

What to do when it is fixed upstream

Retire the suffix behind a version gate — do not simply delete it. While any supported psmux build lacks the fix, removing the suffix reinstates the failure for every project after the first. The shape is the one this project already used for the 3.3.7 → 3.3.8 workaround retirements in #658: the deletion lands only behind a _LAST_UNSUPPORTED bump to a release that carries the fix.

Note the retirement is operator-visible, unlike most of that family: the control session's name changes back, so psmux ls output and any operator muscle memory or scripting around bmad-loop-ctl-<digest> changes with it. That deserves a changelog entry rather than a silent revert.

Two things to check at that point, both currently load-bearing:

  • runs.ctl_session_for and runs.is_ctl_session_name — the mint-side and parse-side predicates. They answer different questions on purpose (#537 conflated them three times before separating them); a retirement must not re-merge them.
  • The reserved-shape refusal in --run-id is independent of this workaround and should survive it: an id equal to the control session's name hands a run the live control session as its own agent session, which is true of the fixed name too — it is why --run-id ctl was already dangerous on main.

Related: #537, #660 (the psmux adoption umbrella — this belongs on its watch list), psmux/psmux#599.

Activity

  1. added
    blocked:upstreamWaiting on a change in another repository
    area:psmuxpsmux terminal-multiplexer backend
    P4Parked - needs design, blocked upstream/PR, or speculative
    on Sep 20, 2026
  2. added 2 commits that reference this issue on Oct 13, 2026
    9159f00
    28a9eda
  3. dracic commented on Oct 13, 2026

    @dracic
    ContributorAuthor

    cleanup and attach never mention a pre-upgrade bmad-loop-ctl-<digest> control session after the rename (psmux 3.4.0)

    After the rename in #900, a control session started under the old bmad-loop-ctl-<16 hex> name keeps running in the project's own registry, as the CHANGELOG says. What the CHANGELOG does not say is that cleanup does not report this surviving session in the primary registry, and attach does not locate its window. With such a session holding a parked run window tagged for the project, bmad-loop cleanup --dry-run prints nothing to clean up, cleanup --json reports "legacy_leftovers": [], and bmad-loop attach <that run> answers nothing to attach (rc 1). The legacy session and its run window remain on that session's psmux server. The docs tell the operator to "end the old session once its windows have parked", but an operator who did not read the CHANGELOG is told, by these cleanup and attach commands, that there is nothing there.

    Measured on psmux 3.4.0 (20aa1d7), Windows 11 10.0.26200, bmad-loop 4c00c37, three runs plus a positive control. The block below is launched from PowerShell 7 (SHELL removed) in a bmad-loop checkout with uv sync done; zero LLM tokens. The legacy session is seeded through bmad-loop's own PsmuxMultiplexer, the way the old release left it: the old digest name, plus a parked run window tagged for the project with set_window_option(wid, runs.PROJECT_OPTION, runs.project_tag(project)). The positive control seeds the identical window under the current name.

    Remove-Item Env:SHELL, Env:PSMUX_DATA_DIR -ErrorAction SilentlyContinue
    $env:BMAD_LOOP_MUX_BACKEND = 'psmux'; $env:PSMUX_NO_WARM = '1'; $env:PYTHONUTF8 = '1'
    $env:BMAD_LOOP_STATE_DIR = "$env:TEMP\bl-legacy\state"
    $Proj = "$env:TEMP\bl-legacy\proj"
    New-Item -ItemType Directory -Force $Proj | Out-Null; git -C $Proj init -q -b main
    Set-Content "$Proj\src.txt" x; git -C $Proj -c user.email=t@t -c user.name=t add -A; git -C $Proj -c user.email=t@t -c user.name=t commit -q -m init
    $rid = '20261013-190001-abcd'
    New-Item -ItemType Directory -Force "$Proj\.bmad-loop\runs\$rid" | Out-Null; Set-Content "$Proj\.bmad-loop\runs\$rid\state.json" '{}'
    
    # Seeds a control session in the project's own registry, through bmad-loop's own adapter:
    # a parked run window tagged for the project exactly as bmad-loop tags it.
    # "legacy" = the bmad-loop-ctl-<digest> name the previous release minted; "current" = bmad-loop-ctl.
    $seed = @'
    import hashlib, sys
    from pathlib import Path
    from bmad_loop import runs
    from bmad_loop.adapters.multiplexer import get_multiplexer
    project, rid, which = Path(sys.argv[1]), sys.argv[2], sys.argv[3]
    root = runs.export_psmux_registry_root(project)  # the project's derived registry
    name = "bmad-loop-ctl-" + hashlib.sha256(root.lower().encode()).hexdigest()[:16] if which == "legacy" else "bmad-loop-ctl"
    mux = get_multiplexer()
    mux.new_session(name, project)
    wid = mux.new_parked_window(name, "run-" + rid, project, ["pwsh", "-NoProfile", "-Command", "exit 0"], "@bmad_return")
    mux.set_window_option(wid, runs.PROJECT_OPTION, runs.project_tag(project))
    print(root)
    print(name)
    '@
    
    $Reg, $Legacy = uv run python -c $seed $Proj $rid legacy; Start-Sleep 3
    uv run bmad-loop cleanup --dry-run --project $Proj    # rc 0: nothing to clean up
    uv run bmad-loop cleanup --json --project $Proj       # rc 0: a real cleanup; every list empty, "legacy_leftovers": []
    uv run bmad-loop attach $rid --project $Proj          # rc 1: nothing to attach for run 20261013-190001-abcd
    $env:PSMUX_DATA_DIR = $Reg; psmux list-windows -t "=$Legacy" -F '#{window_id} #{window_name}'
    #   @1 shell
    #   @2 run-20261013-190001-abcd                        <- still there after the real cleanup
    psmux kill-server; Remove-Item Env:PSMUX_DATA_DIR
    
    # positive control: the same window, seeded the same way, under the name bmad-loop addresses now
    $null = uv run python -c $seed $Proj $rid current; Start-Sleep 3
    uv run bmad-loop cleanup --dry-run --project $Proj    # rc 0: would close ctl window run-20261013-190001-abcd
    $env:PSMUX_DATA_DIR = $Reg; psmux kill-server; Remove-Item Env:PSMUX_DATA_DIR
    actual expected
    cleanup / cleanup --dry-run nothing to clean up, legacy_leftovers: [] the legacy control session named, as cleanup already names a surviving control session in a legacy registry (left in <registry> (not migrated): …)
    attach <run whose window is in it> rc 1, nothing to attach either attaches, or says the run's window is in the pre-upgrade session and names it
    the window and session still running after a real cleanup still running is fine (CHANGELOG); being unreported is the gap

    Observed at symbol level as well, 3/3: runs.legacy_registry_leftovers(project) returns ({}, []), launch.prunable_ctl_windows ([], []), launch.ctl_window_lookup (None, 0) for the legacy window. At symbol level, kill_ctl_window returns 0 while the legacy window remains; a TUI consequence is source-derived, not independently observed here. Under bmad-loop-ctl the same window gives ctl_window_lookup ('bmad-loop-ctl:@2', 0) and prunable_ctl_windows (['run-20261013-190001-abcd'], []), so the outcome flips on the session name alone.

    Source, bmad-loop 4c00c37. runs.legacy_registry_leftovers (src/bmad_loop/runs.py:2309) is the one reader that names a surviving control session to the operator, but it only walks _legacy_registries() (runs.py:2478: get_multiplexer().legacy_registries()), that is, psmux's default registry and a displaced root:

    # src/bmad_loop/runs.py:2426, :2441
            legacies = _legacy_registries()
    ...
                if is_ctl_session_name(legacy.session_name_key(name)):

    The digest-named session lives in the project's own derived registry, the primary one, which no leftover reader lists. cmd_cleanup (src/bmad_loop/cli.py:6706) prints nothing to clean up from those empty lists:

    # src/bmad_loop/cli.py:6802-6804
        if args.dry_run:
            if not killed and not windows:
                print("nothing to clean up")

    attach_plan calls ctl_window_lookup (src/bmad_loop/tui/launch.py:467), which lists ctl_session(project) only, and that is bmad-loop-ctl since ctl_session_for (runs.py:1805) returns CTL_SESSION (runs.py:1824):

    # src/bmad_loop/tui/launch.py:494-496
        rows = _list_ctl_windows(
            get_multiplexer(), ctl_session(project), ["window_id", "window_name", runs.PROJECT_OPTION]
        )

    is_ctl_session_name (runs.py:1827) still recognises the digest shape, so the prune spares it. Sparing it is correct; nothing reports it.

    Suggested, not validated: in legacy_registry_leftovers, or in a second reader that cmd_cleanup and the TUI's cleanup toast already print beside it, also list ctl-shaped sessions in the primary registry other than ctl_session_for(project). That puts the remainder in the same left in <registry> (not migrated) line and the same legacy_leftovers JSON field. cmd_attach could say "this run's window may be in " when it finds nothing and such a session exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P4Parked - needs design, blocked upstream/PR, or speculativearea:psmuxpsmux terminal-multiplexer backendblocked:upstreamWaiting on a change in another repository

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions