fix(adapters): session-qualify psmux TUI-side window ids#312
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (3)
Walkthroughpsmux window identifiers are now session-qualified across parked-window creation, listing, current-window lookup, and selection. Qualified targets resolve to session-local indexes, while invalid or unqualifiable identifiers degrade safely. Contracts, documentation, changelog text, and tests were updated. Changespsmux window targeting
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant TUI
participant PsmuxMultiplexer
participant PsmuxCLI
TUI->>PsmuxMultiplexer: submit session:`@N` target
PsmuxMultiplexer->>PsmuxCLI: list session windows
PsmuxCLI-->>PsmuxMultiplexer: window_id and window_index
PsmuxMultiplexer->>PsmuxCLI: select session:window_index
PsmuxCLI-->>TUI: selected window or warning fallback
Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR completes the psmux session-qualification fix started in #254 by extending it to the TUI-launcher surfaces (
Confidence Score: 5/5Safe to merge — the changes are narrowly scoped to the psmux backend's window-id composition surfaces, leave tmux untouched, and are validated by 16 new ablation-checked tests on top of the existing 118. Every new code path — single-probe current_window_id, qualified new_parked_window, selective list_windows column rewriting, and the select_window index-translation with its miss/failure degradation — has a corresponding test that asserts the exact output and spawn sequence. The degrade rules (empty session, colon-bearing session, falsy id) are tested on both the compose side and the parse side, and a cross-check test confirms the two stay in sync for the prune comparison. The _window_index lookup intentionally calls the parent's list_windows to get bare ids that match the regex-extracted bare id — a subtle but correct choice. No pre-existing interface changed; the tmux backend is explicitly confirmed bare by a new divergence test. Files Needing Attention: No files require special attention.
|
| Filename | Overview |
|---|---|
| src/bmad_loop/adapters/psmux_backend.py | Core of the fix: adds new_parked_window, list_windows, current_window_id, select_window, and _window_index overrides; all degrade cases and the single-probe contract are correctly implemented |
| tests/test_psmux_backend.py | 16 new tests cover all new code paths: session-qualified ids, colon-session degrade, empty-session degrade, falsy-id passthrough, single-probe enforcement, list_windows column selectivity, select_window index resolution, resolve miss/failure degradation, =prefix handling, name-token passthrough, and tmux divergence |
| src/bmad_loop/tui/launch.py | Documentation-only changes: docstrings for select_ctl_window_id, set_return_pane, and start_detached updated to reflect backend-specific id forms; no logic changes |
| src/bmad_loop/adapters/multiplexer.py | Abstract contract updates: list_windows and current_window_id docstrings now document the symmetry requirement; new_parked_window docstring updated to reflect psmux now also qualifies it |
| docs/adapter-authoring-guide.md | Adapter guide updated to reflect that new_parked_window, list_windows window_id columns, and current_window_id are now all session-qualified on psmux |
| CHANGELOG.md | New changelog entry accurately describes the bug (cross-server kill-window), the fix (session:@n qualification), and the select_window index-translation approach |
Sequence Diagram
sequenceDiagram
participant TUI as tui/launch.py
participant Mux as PsmuxMultiplexer
participant Base as BaseTmuxBackend
participant Psmux as psmux CLI
Note over TUI,Psmux: start_detached / new_parked_window
TUI->>Mux: new_parked_window(session, name, cwd, argv, opt)
Mux->>Base: super().new_parked_window(...)
Base->>Psmux: "new-window -P -F #{window_id}"
Psmux-->>Base: "@2"
Base-->>Mux: "@2"
Mux->>Mux: "_qualified_window_id(session, @2)"
Note right of Mux: Degrades to bare if session empty or has colon
Mux-->>TUI: "session:@2"
Note over TUI,Psmux: prune / _ctl_window_candidates
TUI->>Mux: current_window_id()
Mux->>Psmux: "display-message -p #{session_name}:#{window_id}"
Note right of Mux: Single probe — avoids split-probe gap
Psmux-->>Mux: "session:@2"
Mux->>Mux: _QUALIFIED_ID.fullmatch
Mux-->>TUI: "session:@2"
TUI->>Mux: list_windows(session, [window_id, window_name])
Mux->>Base: super().list_windows(...)
Base->>Psmux: "list-windows -F #{window_id} #{window_name}"
Psmux-->>Base: "@1 shell, @2 run-x"
Base-->>Mux: "[(@1,shell), (@2,run-x)]"
Mux->>Mux: _qualified_window_id for each window_id column
Mux-->>TUI: "[(session:@1,shell), (session:@2,run-x)]"
Note right of TUI: current==session:@2 found in rows, prune skips own window
Note over TUI,Psmux: select_ctl_window_id
TUI->>Mux: "select_window(session:@2)"
Mux->>Mux: "_QUALIFIED_ID.fullmatch → session, id=@2"
Mux->>Mux: "_window_index(session, @2)"
Mux->>Base: super().list_windows(session, [window_id, window_index])
Base->>Psmux: "list-windows -t =session -F #{window_id} #{window_index}"
Psmux-->>Base: "@2 1"
Base-->>Mux: "[(@2,1)]"
Mux-->>Mux: "index=1"
Mux->>Psmux: select-window -t session:1
Note right of Mux: Index form passes psmux CLI-side check
Reviews (2): Last reviewed commit: "docs(adapters): mark the select-window r..." | Re-trigger Greptile
and that process usually runs outside any pane — where a bare `@N`
resolves through psmux's most-recent-session fallback, not the session
that minted it. The ctl-window prune replayed such ids as kill-window
targets and could close another server's identically-numbered window
with rc 0.
new_parked_window, the window_id columns of list_windows, and
current_window_id now emit `session:@N`. The prune skips its own window
by comparing the last two, so they must agree — and they come from one
`#{session_name}:#{window_id}` expansion rather than two probes: a split
probe can resolve the id while the session fails, and list_windows
qualifies rows from the session it was passed, so a bare id could never
equal one and the prune would kill the window it runs in.
select_window resolves the id to an index before sending. psmux
validates a scoped target's window part CLI-side against window
index/name only, never window id, while its server resolves ids fine
(FocusWindowById) — so the rejection is a pre-send gate, not a model
boundary. Every psmux behavior relied on here was read from the source
at tag v3.3.7, the tag matching the installed build.
tmux is untouched: its ids are server-global.
Closes bmad-code-org#291
…ng (bmad-code-org#291 review) The only caller that reaches the qualified-id path (select_ctl_window_id, from the TUI resolve launch) runs inside the Textual app, which captures stderr for the app's whole run — so the warning is invisible exactly where it fires, and the pipe-pane precedent it cited is an engine-path warning where stderr is a real terminal. Keep the emission for the CLI-side callers tui/launch.py stays textual-free to allow, but stop claiming it makes the miss traceable.
56b37b7 to
d323fd6
Compare
|
Reviewed and validated. Approach is right, scope is clean, and the tmux path is provably untouched. Rebased onto Independently re-verified against the psmux source at
|
Closes #291
What
#254/#290 covered the engine seam but left the launcher's surfaces bare — and the TUI usually runs outside any psmux pane, where a bare
@Nreplayed as a-ttarget resolves through psmux's most-recent-session fallback. The ctl-window prune couldkill-windowanother server's identically-numbered window with rc 0, and the detached-resolve select/attach could land on the wrong server.new_parked_window, thewindow_idcolumns oflist_windows, andcurrent_window_idnow emitsession:@N(via the existing_qualified_window_id, sharing its degrade rules — including a new guard so an empty session name degrades on the compose side exactly like the parse side).current_window_idresolves session and id from one#{session_name}:#{window_id}expansion — a split probe can resolve the id while the session fails, which would break the prune's self-exclusion and kill the window it runs in.select_windowresolves a qualified id tosession:<index>just-in-time: psmux validates a scoped target's window part CLI-side against index/name only (neverwindow_id), while the server resolves ids fine (FocusWindowById). On a resolve miss/failure it still sends the original target (best-effort contract) but now warns on stderr — psmux'scan't find windowexit 1 lands in a discarded pipe, so the miss was previously untraceable.Every psmux behavior relied on here was read from the psmux source at tag
v3.3.7, the tag matching the installed build.Follow-ups (split out, not fixed here)
PROJECT_OPTIONprune guard cannot discriminate between projects and theRETURN_OPTIONparked-return mechanism is inert. Split out of psmux: TUI-side window ids are still bare — parked mint, list_windows fields, and prune kill targets replay @N cross-server #291 by scope review: this PR fixes id routing only; that needs a different state channel. The window-option verbs are deliberately untouched here — psmux drops the window slot of a-wtarget either way, so the qualified ids this PR introduces neither help nor harm those surfaces.current_window_idprobe degrades toNone, which disables the prune's self-exclusion; pre-existing on both backends and out of scope here — will be filed separately.Verification
pytest tests/test_psmux_backend.py tests/test_tui_launch.py tests/test_multiplexer.py tests/test_portability_guard.py -q→ 134 passed (16 new tests, incl. ablation-checked degrade/passthrough gates)trunk checkclean,pyright@1.1.411clean on the touched moduleSummary by CodeRabbit