Skip to content

Untagged sessions still have no ownership proof that outlives the run dir (#419 direction 2, now unhomed) #538

Description

@pbean

Status 2026-09-20 (v0.12.0). Narrowed, not closed: the cross-project kill now reaches only tmux; on psmux every arm degrades to a skip, which leaves the stranding this issue names. #537 (65183872, PR #728, in 0.12.0) gave psmux a per-project registry root: prune_sessions (src/bmad_loop/runs.py:1577) passes require_tag=not _registry_proves_ownership(project) (src/bmad_loop/runs.py:1612; helper at src/bmad_loop/runs.py:1521), and the legacy pass always runs with require_tag=True (src/bmad_loop/runs.py:1618). Consequences:


#419 listed three candidate directions. Two shipped and it was closed:

Filing it so the direction has a home. This is not a new defect report: the exposure is already
pinned in-tree as characterization. What is new is that the code and the test both point a future
reader at a closed issue for the remedy.

The exposure, as pinned today

prunable_sessions (src/bmad_loop/runs.py:365-411) proves ownership of an untagged session by
run-id collision on the filesystem, not by identity: the run id is parsed out of the session name,
and is_run(run_dir_for(project, run_id)) decides. Two projects that share a run id therefore
disagree about one live session — pinned by
test_prunable_sessions_claims_an_untagged_session_on_a_run_id_collision (tests/test_runs.py),
which asserts both views: ours classifies it prunable while theirs is still running it.
prune_sessions (runs.py:414-426) kills every id in that list. The collision needs no luck —
--run-id is caller-supplied and validated for shape only.

Why #526's backstop does not close it

_refuse_live_session guards the opposite sequence (removing the run dir out from under a live
session) and its own docstring declines to call the remedy sound (runs.py:618-625): it points the
operator at bmad-loop cleanup, which is the prune above, "so on the untagged case it can prune
another project's session on a shared run id". It cannot do better — nothing available at that point
proves the session ours, and minting a proof that outlives the run dir is this direction.

The cause #419 did not enumerate: a degraded read, not a failed write

prunable_sessions' own docstring already records it (runs.py:382-384) — untagged is reachable
"when the option read degrades (session_options reads unset as 'no answer', never as proof nothing
was written)". BaseTmuxBackend.session_options (src/bmad_loop/adapters/tmux_base.py:173-191)
returns {} for a missing binary, an exception, or any nonzero exit. So a single failed read
makes every live session read untagged at once, not one — the fallback stops being an edge case
and becomes the whole ownership signal, which is exactly the end-state #489 Q2 predicted for a reap,
reachable with no reap involved.

Worth stating precisely, because the three causes are not equally live after #523:

Cause Agent sessions Notes
Tag write failed not silent on tmuxset_session_option goes through the strict _tmux (tmux_base.py:101-110, :144-147), which raises unlike ctl windows, where set_window_option swallows (tmux_base.py:401-405) — that half is #531
psmux refuses the value cleared for ordinary paths by #523's digest; the gate remains as the general contract for every @ session option (psmux_backend.py:505-524)
Legacy / pre-digest sessions still reachable accepted_tags keeps the legacy path tag readable (runs.py:331-350)
Option read degrades reachable on both backends, affects every session at once tmux_base.py:173-191

Scope

Not a change to prunable_sessions. A durable record keyed by session name that a prune can consult
when the tag is unreadable — larger than the tag, and it touches cleanup semantics, which is why
#419 ranked it below (1).

When it lands, flip test_prunable_sessions_claims_an_untagged_session_on_a_run_id_collision
deliberately (its docstring says so) and update the two in-tree pointers that currently name
#419, now closed: runs.py:625 and that test's docstring.

Refs: #419 (closed), #523, #526, #531 (the control-window consumer of the same weak ownership), #489

Activity

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

    P3Robustness, enhancement, tests, or docs worth schedulingarea:adaptersCoding-CLI adapters and profilesarea:engineOrchestrator engine and run lifecyclebugSomething isn't workingneeds-designAwaiting a maintainer design decision before code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions