Skip to content

stop/attach: a run id typed in another letter case reports "stopped" while the agent session keeps running (psmux 3.4.0, Windows) #904

Closed

Description

@dracic

Summary

On Windows, bmad-loop stop <run id> with the id typed in a different letter case (for example 20261013-150001-ABCD for run 20261013-150001-abcd) resolves the run directory, prints run … stopped and exits 0, but its backstop kill misses the run's agent session. When the run's engine is already gone (the case that backstop exists for), that session keeps running; a live engine attempts teardown under its persisted run ID (source-derived, not observed). bmad-loop attach with the same spelling answers nothing to attach while that session is live. The run ref is resolved through a case-insensitive filesystem and then replayed with the typed spelling as a multiplexer session name, and since psmux 3.4.0 session targets are matched case-exactly, like tmux. A stop that is reported as done and is not is the misleading-success shape: the operator walks away from a coding CLI that is still running.

Environment

psmux 3.4.0 (20aa1d7), Windows 11 Enterprise 10.0.26200, PowerShell 7.6.6 (launching shell; SHELL removed); bmad-loop 4c00c37 (identical runs.py and cli.py on main after the #900 merge).

Steps to reproduce

Launched from PowerShell 7, in a bmad-loop checkout (uv sync done). The agent session is created with raw psmux in the registry bmad-loop derives for the project, standing in for a run whose engine died before tearing its session down (the case the stop's backstop kill exists for). Zero LLM tokens.

Remove-Item Env:SHELL -ErrorAction SilentlyContinue
$env:BMAD_LOOP_MUX_BACKEND = 'psmux'; $env:PSMUX_NO_WARM = '1'; $env:PYTHONUTF8 = '1'
$env:BMAD_LOOP_STATE_DIR = "$env:TEMP\bl-case\state"
$Proj = "$env:TEMP\bl-case\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
$Reg = ((uv run bmad-loop mux --project $Proj) -match '^registry: ') -replace '^registry: (.*) \(derived.*$', '$1'
$rid = '20261013-150001-abcd'
New-Item -ItemType Directory -Force "$Proj\.bmad-loop\runs\$rid" | Out-Null
Set-Content "$Proj\.bmad-loop\runs\$rid\state.json" "{""run_id"": ""$rid"", ""project"": ""x"", ""started_at"": ""2026-10-13T14:00:00""}"
$env:PSMUX_DATA_DIR = $Reg; psmux new-session -d -s "bmad-loop-$rid"; Remove-Item Env:PSMUX_DATA_DIR   # rc 0

uv run bmad-loop attach $rid.ToUpper() --project $Proj    # rc 1: nothing to attach for run 20261013-150001-ABCD
uv run bmad-loop stop   $rid.ToUpper() --project $Proj    # rc 0: run 20261013-150001-ABCD stopped
$env:PSMUX_DATA_DIR = $Reg; psmux ls -F '#{session_name}'; Remove-Item Env:PSMUX_DATA_DIR
#   bmad-loop-20261013-150001-abcd      <- still running

# positive control: the id as minted
uv run bmad-loop stop $rid --project $Proj                # rc 0: run 20261013-150001-abcd stopped
$env:PSMUX_DATA_DIR = $Reg; psmux ls -F '#{session_name}'
#   psmux: no server running on <registry>               <- session gone
psmux kill-server 2>$null; Remove-Item Env:PSMUX_DATA_DIR

Measured 3/3 (three fresh run ids in one script), positive control 3/3. Patching runs.resolve_run_dir in-process to return the on-disk spelling flips the mis-cased stop to killing the session (2/2), so the typed spelling flowing out of the resolver is the cause.

Expected vs actual

actual expected
stop <ID IN OTHER CASE> rc 0, run …-ABCD stopped, agent session bmad-loop-…-abcd still listed the run's own session killed, as with the minted spelling; or a refusal naming the run by its real id
attach <ID IN OTHER CASE> rc 1, nothing to attach for run …-ABCD while the session is live attaches to bmad-loop-…-abcd
stop <id as minted> (control) rc 0, session gone same

Source

bmad-loop 4c00c37:

  • src/bmad_loop/runs.py:1337-1343 (resolve_run_dir): the exact branch recomposes the path from the raw ref and returns it when state.json exists. On NTFS runs\20261013-150001-ABCD\state.json exists for run …-abcd, so the returned Path carries the typed spelling.
    exact = run_dir_for(project, ref)
    ...
        if stat.S_ISREG((exact / STATE_FILE).stat().st_mode):
            return exact
  • src/bmad_loop/cli.py:6488-6500 (cmd_stop): args.run_id = run_dir.name, then runs.stop_run(run_dir); src/bmad_loop/runs.py:2996 (_stop_run_once): the backstop kill_session(run_dir.name), which sends kill-session -t =bmad-loop-20261013-150001-ABCD with check=False (adapters/tmux_base.py:391-399), so the missed target does not make stop fail.
  • cmd_attach (cli.py:6437) feeds the same run_dir.name to attach_plan, whose agent_session_exists(session_name(run_id)) and ctl-window name match (m.group(1) != run_id) are both case-exact.
  • src/bmad_loop/adapters/psmux_backend.py:351-359 (session_name_key) still folds case, and its docstring states that a mis-cased target "answers has-session … and a kill through it takes the lowercase session down" (measured on 3.3.8). That stopped being true in 3.4.0.

psmux v3.4.0 (tag commit 20aa1d7; case-target fix 624ea6df, psmux/psmux#774): an explicit session target whose stored spelling differs is "not that session", src/main.rs:1359 (general -t path) and src/main.rs:3530 (kill-session):

if !std::path::Path::new(&port_path).exists()
    || crate::session::registry_case_variant(&session_name).is_some()
{
    eprintln!("psmux: can't find session: {}", shown);

registry_case_variant does not exist at v3.3.8 (source-derived, not observed: 3.3.8 is not installed here), where the target resolved by opening <name>.port case-insensitively, so this path worked by accident before 3.4.0.

Mixed signal inside bmad-loop, observed: runs.live_session_may_be_ours(project, "…-ABCD") answers True (it lists sessions and compares through the folding key), so the removal guard behind delete would see a live session for that spelling (source-derived, delete itself not run) that stop and attach with the same spelling cannot reach.

Impact on bmad-loop

bmad-loop stop and bmad-loop attach on Windows with psmux 3.4.0, whenever the run id is typed in a different case than it was minted: an uppercase-typed hex suffix, or a caller-supplied --run-id (for example MyRun) later typed as myrun. Source-derived, not observed here: partial refs use case-sensitive name matching and a miscased partial ref fails with no such run. The TUI's listed-ID path avoids this typed-spelling mismatch. Cleanup enumerates session names in their stored spelling and can reach this orphan through its normal ownership and liveness checks. On a case-sensitive filesystem, the miscased full ref misses the exact branch and fails unless another listed run matches. Issue #879 discusses case-only duplicate creation and deliberately retains the registry-slot folding key. This defect instead passes the operator's spelling through run-ref resolution into exact session targets.

Suggested fix

Not validated as a code change: make resolve_run_dir return the run dir under its on-disk spelling (for example, take the exact branch's answer from list_run_dirs by os.path.normcase equality, or Path.resolve() the hit on Windows), so every by-name verb downstream gets the minted run id. The in-process patch above shows the direction for stop; fixing it at the resolver covers stop, attach, resume, delete and every other resolve_run_dir caller at once.

Activity

  1. added a commit that references this issue on Oct 14, 2026
    d3a238e
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions