Skip to content

fix(tui): report the parked-window return honestly (#227)#316

Merged
pbean merged 6 commits into
bmad-code-org:mainfrom
dracic:fix/return-attached-client-honest-227
Jul 27, 2026
Merged

fix(tui): report the parked-window return honestly (#227)#316
pbean merged 6 commits into
bmad-code-org:mainfrom
dracic:fix/return-attached-client-honest-227

Conversation

@dracic

@dracic dracic commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

return_attached_client() discarded switch_client's result and answered True unconditionally, so a failed switch plus a failed -l fallback still journaled sweep-returned-after-decisions, printed "✓ decisions recorded" and sent the sweep unattended — while the human sat in the sweep window with their terminal never handed back, every later decision silently routed to the unattended path.

The change

  • The switch_client result is now the return value, and RETURN_OPTION is cleared only on a real return, so a failed one is left for the parked window's trailer.
  • TerminalMultiplexer.detach_client widens Nonebool for the same reason: the returncode was in hand and dropped, so the detach branch (the "TUI runs outside the multiplexer" attach) carried the identical false positive. Escalated into scope by review, maintainer-approved.
  • sweep.py is untouched — it already branches on the boolean correctly.

What the honesty guarantee actually covers

tmux: fully. Smoke-measured on live tmux 3.4 (isolated -L socket): detach-client with no client attached exits 1 with no current client; with one attached it exits 0 and list-clients is empty afterwards.

psmux: not at all, and the docstrings now say so. Verified in the Rust source at the version our gate admits (v3.3.7): both the detach-client and switch-client CLI arms end in send_control(cmd)?; return Ok(()), so the only nonzero exit is an unreachable session server. A detach therefore succeeds with zero clients attached — and a flag-less detach is promoted server-side to detach-all — while no form of switch-client, -t included, carries a server reply. Later builds narrow this but do not close it: a response path landed after the tag and only for -t, leaving the -l fallback exit-0 regardless. Both seam booleans can therefore be vacuously True on psmux — the same ceiling as the rc-0 no-op (#228), reached through the booleans instead of a shell fallback.

Corrected after review: an earlier revision of this description credited switch-client -t with a reply at v3.3.7. That describes psmux main, not the gated tag, and understated the ceiling.

This is moot today — psmux's per-window option channel is inert (#310), so the parked-return path reads an empty option and answers False before reaching either verb. It becomes live the moment #310 / #315 revives that channel, which is why it is recorded in the psmux backend's degradation ledger rather than left implicit.

herdr (out-of-tree): unchanged in practice, but this is a contract change. The reference third-party backend (pbean/bmad-loop-adapter-herdr) declares detach_client() -> None, and its detach is a documented no-op — herdr has no client to detach. At runtime the widening therefore improves it: None is falsy, so the parked-return path reads "nothing was detached", keeps RETURN_OPTION, and reports no return — which is the truth for a no-op, and strictly better than the unconditional True it effectively got before. Nothing degrades.

What does change is the type: a -> None override no longer satisfies the widened ABC under static checking. The fix is two lines in its own repo, so it is not filed here; both places in docs/adapter-authoring-guide.md that described the void seam now state the requirement — a backend with no real detach returns False, never None and never a vacuous True.

Evidence levels differ, deliberately: tmux is measured against a live server, psmux is verified against the Rust source at the gated tag, herdr is inferred from this repo's own documentation of it — I have no local checkout and did not verify its source. Treat the herdr paragraph as a contract statement, not a smoke result.

Worth a reviewer's eye: this is the first mux-seam change that is not additive-only. Every prior addition (current_return_target, window_pane_pids, version()) shipped as a concrete default precisely so out-of-tree backends inherited it untouched, and there is no protocol-version or capability-negotiation mechanism for this seam. That policy gap is the real follow-up; it is recorded in the deferred-work ledger rather than smuggled into this PR.

Verification

Closes #227

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes

    • Return-to-previous-window now reports true outcomes (successful, still answerable, or unreachable) via a ReturnOutcome result.
    • Saved return target is only cleared after a successful hand-back; failures preserve it for retry.
    • Detach/switch operations now accurately report whether the backend actually performed the action.
  • Documentation

    • Updated the client attach/detach contract to reflect boolean “performed” semantics.
  • Tests

    • Expanded coverage for detach/switch success, failure, fallback behavior, and retry/option preservation.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pbean, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71af04f6-1d39-4c05-a5e5-7242c8761dcf

📥 Commits

Reviewing files that changed from the base of the PR and between 04a65ea and 740986c.

📒 Files selected for processing (3)
  • docs/adapter-authoring-guide.md
  • src/bmad_loop/sweep.py
  • src/bmad_loop/tui/launch.py

Walkthrough

The parked-window return flow now reports actual detach and switch effects, distinguishes typed return outcomes, clears RETURN_OPTION only after success, and preserves it after failures. Psmux effect detection, tmux behavior, sweep handling, documentation, and tests were updated.

Changes

Parked-window return flow

Layer / File(s) Summary
Backend effect-reporting contract
src/bmad_loop/adapters/multiplexer.py, src/bmad_loop/adapters/tmux_base.py, docs/adapter-authoring-guide.md
Detach and switch operations use effect-based boolean results, with tmux and adapter contract documentation updated.
Psmux observed client effects
src/bmad_loop/adapters/psmux_backend.py, tests/test_psmux_backend.py
Psmux measures attached-client count changes around client verbs and tests successful, failed, and unverifiable effects.
Return outcome propagation
src/bmad_loop/tui/launch.py, src/bmad_loop/sweep.py, docs/tui-guide.md, CHANGELOG.md
Typed outcomes drive option clearing, prompting state, journal messages, and documented behavior.
Return-flow validation
tests/test_multiplexer.py, tests/test_tui_launch.py, tests/test_sweep.py
Tests cover fallback and detach failures, option preservation, typed outcomes, and sweep state transitions.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SweepEngine
  participant ReturnFlow
  participant Multiplexer
  participant Backend
  SweepEngine->>ReturnFlow: request return after decisions
  ReturnFlow->>Multiplexer: detach or switch client
  Multiplexer->>Backend: execute client verb
  Backend-->>Multiplexer: report observed effect
  Multiplexer-->>ReturnFlow: return outcome
  ReturnFlow-->>SweepEngine: RETURNED, ATTENDED, or UNREACHABLE
Loading

Possibly related issues

  • #317: Directly covers honest psmux detach and switch effect reporting.
  • #222: Concerns psmux switch_client behavior when exit code zero does not indicate an actual switch.
  • #228: Addresses psmux version-specific behavior related to rc-0 client-switch no-ops.

Possibly related PRs

Suggested reviewers: pbean

Poem

A rabbit watched the clients hop,
And checked if each one truly stopped.
If switches fail, the flag stays bright—
The trailer gets another bite.
Typed outcomes guide the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main fix: making parked-window return reporting honest.
Linked Issues check ✅ Passed The PR now returns the real client-return result, preserves RETURN_OPTION on failure, and propagates detach effect semantics as required by #227.
Out of Scope Changes check ✅ Passed The additional backend, sweep, docs, and test changes all support the return-reporting and detach semantics described in the PR objectives.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes return_attached_client which previously discarded switch_client's result and returned True unconditionally — causing a failed return to still journal "sweep-returned-after-decisions", print a success message, and mark the sweep unattended while the human's terminal was never actually handed back.

  • return_attached_client now returns a three-way ReturnOutcome (RETURNED / ATTENDED / UNREACHABLE): RETURN_OPTION is only cleared on a real return; a failed switch leaves ATTENDED (client still visible, keep prompting); a failed detach leaves UNREACHABLE (no verified hand-back, go unattended silently to avoid blocking --repeat on input() forever).
  • TerminalMultiplexer.detach_client is widened from -> None to -> bool, and BaseTmuxBackend reads the exit code; psmux overrides both client verbs with a before/after attached-client-count delta because psmux's CLI exits 0 regardless of whether a client moved.
  • sweep._return_after_decisions branches correctly on all three outcomes, and new tests ablation-check each path including the psmux inert-switch and unobservable-format-field degradation cases.

Confidence Score: 5/5

Safe to merge — the fix is correctly scoped, all three outcome branches are covered by new tests with ablation checks, and the psmux measurement strategy degrades to False rather than a vacuous True when the count is unobservable.

The root bug (unconditional True return) is squarely addressed. The three-way ReturnOutcome correctly models the two distinct failure modes; RETURN_OPTION is preserved on failure and cleared only on a verified return. The tmux path reads exit codes honestly; the psmux measurement approach is sound (delta-based, not absolute-count-based). Test coverage is thorough: every new branch has a dedicated test and both new negative tests were ablation-checked by the author.

Files Needing Attention: The _attached_clients helper in psmux_backend.py uses str.isdigit() before int(), which could raise ValueError for Unicode superscript digits — a minor robustness gap with no practical impact on psmux's ASCII output.

Important Files Changed

Filename Overview
src/bmad_loop/tui/launch.py Core fix: return_attached_client now returns a three-way ReturnOutcome enum; RETURN_OPTION is cleared only on RETURNED, and the two failure modes (failed switch → ATTENDED; failed detach → UNREACHABLE) are correctly distinguished.
src/bmad_loop/adapters/psmux_backend.py New _attached_clients/_client_left helpers measure verb effect via client-count delta; detach_client and switch_client overrides use these to avoid trusting psmux's exit-0-always behavior. Minor robustness nit: isdigit() should be isdecimal() before int().
src/bmad_loop/adapters/tmux_base.py detach_client widened from -> None to -> bool, returning the exit code; transport failures correctly return False.
src/bmad_loop/adapters/multiplexer.py ABC signature for detach_client widened to bool; docstring updated to document the effect-not-dispatch contract for all backends.
src/bmad_loop/sweep.py _return_after_decisions correctly branches on all three ReturnOutcome values: ATTENDED returns early (keeps prompting), RETURNED clears prompting and journals the success, UNREACHABLE clears prompting silently and journals sweep-return-no-client.
tests/test_tui_launch.py New tests cover: switch-fails-stays-ATTENDED, detach-fails-is-UNREACHABLE, fallback-switch path; existing tests updated to assert ReturnOutcome enum values instead of bare booleans.
tests/test_psmux_backend.py New psmux-specific tests exercise: observed drop (True), zero clients before detach (False), unobservable format field (False), outside-pane guard (no verb issued), switch primary/fallback paths, and the inert-switch-leg case.
tests/test_sweep.py New test_interactive_decisions_unreachable_goes_unattended validates that UNREACHABLE silences prompting and journals sweep-return-no-client without the success message; helper factoring is clean.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[return_attached_client] --> B{mux_usable?\nwin present?\nRETURN_OPTION set?}
    B -- "No" --> C[ATTENDED\nkeep prompting]
    B -- "Yes, option=detach" --> D[mux.detach_client]
    B -- "Yes, option=pane target" --> E[mux.switch_client\nlast_fallback=True]
    D -- "True\nclient detached" --> F[RETURNED\nclear RETURN_OPTION]
    D -- "False\nno client / unobservable" --> G[UNREACHABLE\noption preserved]
    E -- "True\nclient switched" --> F
    E -- "False\nclient still here" --> C
    F --> H[sweep: prompting=False\njournal sweep-returned-after-decisions\nprint success]
    G --> I[sweep: prompting=False\njournal sweep-return-no-client\nprint nothing]
    C --> J[sweep: return early\nprompting stays True]
Loading

Reviews (4): Last reviewed commit: "docs(adapters): scope the client-verb ef..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adapter-authoring-guide.md`:
- Around line 98-101: Update the authoring guide’s detach_client/switch_client
boolean contract to allow either backend-reported command success or observed
client movement, rather than requiring actual movement and forbidding vacuous
True values. Explicitly document the psmux limitation that a successful command
may return True even when zero clients are attached, keeping the guidance
consistent with multiplexer.py and psmux_backend.py.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 622ef2ba-591b-4eaa-932b-91838edd6d7f

📥 Commits

Reviewing files that changed from the base of the PR and between ec476c1 and 9a8fec6.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/adapter-authoring-guide.md
  • src/bmad_loop/adapters/multiplexer.py
  • src/bmad_loop/adapters/psmux_backend.py
  • src/bmad_loop/adapters/tmux_base.py
  • src/bmad_loop/tui/launch.py
  • tests/test_multiplexer.py
  • tests/test_tui_launch.py

Comment thread docs/adapter-authoring-guide.md Outdated
dracic and others added 5 commits July 26, 2026 17:01
return_attached_client() discarded switch_client's result and answered
True unconditionally, so a failed switch plus a failed -l fallback still
journaled the return and sent the sweep unattended while the human sat in
the sweep window with their terminal never handed back.

Thread the result out and clear RETURN_OPTION only on a real return, so a
failed one is left for the parked window's trailer. Widen
TerminalMultiplexer.detach_client from None to bool for the same reason:
the returncode was in hand and dropped, so the detach branch carried the
identical false positive.

Closes bmad-code-org#227
…e-org#227 review)

The psmux ledger entry said only `switch-client -t` carries a server reply.
That describes psmux main, not the v3.3.7 the version gate admits: there the
whole CLI arm is `send_control(cmd)?; return Ok(())`, so no form carries a
reply and `-t` is exit-0-regardless too. The response path landed after the
tag and only for `-t`, leaving `-l` unconditional either way. Understated the
ceiling in the direction that matters, so state it at both revisions and name
it as the rc-0 no-op family (bmad-code-org#228).

Also resolve the contradiction the same round introduced: the authoring guide
promised the seam never answers a vacuous True while the ABC and the psmux
ledger both documented psmux doing exactly that. Keep the requirement —
report effect, not dispatch — and name psmux as the worked deviation, rather
than relaxing the contract to "command succeeded", which is the failure bmad-code-org#227
exists to remove.
…d-code-org#227 review)

Threading the boolean through fixes the switch branch and breaks the detach
one. `sweep._return_after_decisions` consumes it to answer "can a human still
answer here?", and the two failures answer that in opposite directions: a
failed switch leaves the client in this window with someone in front of it,
while a failed detach means there was no client to detach at all. Reporting
both as a plain False keeps a --repeat sweep prompting into a window nobody is
viewing, on input() that never returns - the livelock the function's own
docstring exists to prevent, and the documented flow walks straight into it
(tui-guide step 3 tells tmux users to detach right after answering; on herdr,
whose detach is a no-op the widened seam now requires to answer False, it is
not a race but the only outcome).

So return_attached_client answers a ReturnOutcome instead: RETURNED,
ATTENDED (keep talking to the terminal - nothing was recorded to return to,
the backend is unusable, or the switch left the client here), UNREACHABLE
(a detach found nothing attached, or the backend has no detach verb).
RETURN_OPTION still clears only on RETURNED. The sweep goes unattended on
anything but ATTENDED, and announces only a real return - UNREACHABLE journals
sweep-return-no-client and prints nothing, because there is no one to read it.
…ode (bmad-code-org#317)

The parked-return path reads an empty option on psmux only while the per-window
option channel is inert. bmad-code-org#310 revived it, so the path now reaches
switch_client/detach_client on Windows - and psmux answers both with an exit
code that means nothing. At v3.3.7 every arm ends in `send_control(cmd)?;
return Ok(())`: the only nonzero exit is an unreachable session server, so a
detach succeeds with zero clients attached, and no form of switch-client - `-t`
included - carries a server reply. Later builds narrow that only for `-t`.
Taking those codes as the seam's booleans is the rc-0 no-op (bmad-code-org#228) reached
through Python, and it hands `return_attached_client` a True that clears the
return option and reports a hand-back that never happened: bmad-code-org#227's exact
symptom, on the platform the psmux backend exists for.

So discard the exit code and measure: count the clients attached to this
session before and after the verb, answer on the drop. Never on an absolute
count - a `-t` read against a session whose server is gone answers from
whichever server the fallback picks, so "zero attached" alone proves nothing
(bmad-code-org#315); a drop needs two successful reads of the same session, the first
nonzero. Unobservable answers False, never a vacuous True: the probe is
self-detecting, since a build without #{session_attached} cannot echo a
plausible integer. The switch leg reads as no effect today - it is inert at
3.3.7 (psmux/psmux#483) - and starts reporting True on its own when upstream
lands the fix, which is why this measures rather than hardcoding.

Also state the rule at the seam it belongs to: detach_client/switch_client owe
effect, not dispatch, and the authoring guide now shows both ways to pay for it.
The case scripted no attached-client counts, so deleting the guard failed the
test on the fixture running dry rather than on the behavior. Script counts a
probe could read: the ablation now lands on `assert ... is False`.
@pbean
pbean force-pushed the fix/return-attached-client-honest-227 branch from c262708 to 04a65ea Compare July 27, 2026 00:15
@pbean

pbean commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Reviewed, rebased onto f5402e4, and pushed three commits on top (04a65ea). The core fix is right and I took it as-is: threading the result through and clearing RETURN_OPTION only on a real return is exactly what #227 asks for, the trailer-retry reasoning checks out (new_parked_window parks on the blocking read before the trailer), and the two new negative tests are ablation-shaped. Three things needed changing.

1. The detach branch inverted the safe degradation

sweep._return_after_decisions consumes the boolean to answer a different question than this makes it answer — "can a human still answer here?" — and the two failures answer that in opposite directions:

branch False means what the sweep should do
switch failed the client is still in this window keep prompting ✅ (this is #227)
detach failed there was no client to detach go unattended — the PR kept prompting ❌

With sweep.repeat = true the next cycle reaches input() and blocks forever in a window nobody is viewing — the hazard _return_after_decisions's own docstring cites. Not hypothetical off tmux: the new normative rule ("a backend with no real detach returns False") makes it deterministic on herdr, and docs/tui-guide.md:482 tells tmux users to press ctrl-b d right after answering, which is precisely the input that makes the next detach-client exit 1.

So return_attached_client answers a ReturnOutcome instead of a bool — RETURNED / ATTENDED / UNREACHABLE. The option still clears only on RETURNED; the sweep goes unattended on anything but ATTENDED, and announces only a real return (UNREACHABLE journals sweep-return-no-client and prints nothing, since there is nobody to read it). Note ATTENDED also covers "no return target recorded" — a plain foreground sweep must keep prompting, so the early returns could not fold into UNREACHABLE.

2. "Moot today" stopped being true four hours after this opened

#315 merged at 23:16Z and revived psmux's per-window option channel, so both the description and the ledger paragraph were stale on arrival — the condition #317 predicted verbatim ("the moment #310 / #315 revives that channel, this becomes reachable") had been met. On psmux set_return_pane / show_window_option now work, so the path reaches switch_client, whose leg is documented-inert (psmux/psmux#483) yet exits 0: vacuous True, option cleared, hand-back reported that never happened. #227's exact symptom, live on the platform the backend exists for, in a PR that closes #227.

Rather than defer it, psmux now measures: count the session's attached clients before and after the verb, answer on the drop. Never on an absolute count — a -t read against a session whose server is gone answers from another server, so "zero attached" alone proves nothing (the #315 lesson); a drop needs two successful reads of the same session, the first nonzero. Unobservable degrades to False, never a vacuous True, and the probe is self-detecting: a build without #{session_attached} cannot echo a plausible integer. The switch leg reads as no effect today and starts reporting True on its own when upstream lands #483 — which is why it measures rather than hardcoding.

Your v3.3.7 source reading is preserved verbatim in the new client verbs: observed effect (#317) block; I only dropped the "moot" framing, and the rebase conflict was resolved by re-authoring rather than merging (#315 had rewritten the paragraph this appended to).

3. Doc surfaces

docs/tui-guide.md:482 and the authoring guide's "psmux cannot observe effect" framing both followed from the above. The normative rule you defended against the suggested relaxation stays exactly as you wrote it — report effect, not dispatch — with both ways to pay for it now shown (exit code where it means something, measurement where it doesn't).

Verification

  • Suite failure set identical to the f5402e4 baseline: 3180 passed / 2 pre-existing local skill-sync drifts (measured on main first, not taken from the description).
  • pyright@1.1.411 and full trunk check clean.
  • Seven ablations, each confirming the test fails when the gate is deleted: the psmux delta gate, the unobservable→False degrade, the out-of-pane guard, the detach→UNREACHABLE label, the switch→ATTENDED label, the sweep's unattended trigger, and your original conditional option-clear. (The out-of-pane case initially failed on the fixture running dry rather than its assertion — fixed in 04a65ea so the ablation lands where it should.)
  • tmux 3.4, isolated -L socket, re-measuring the claim the whole fix rests on rather than accepting it: detach-client with nothing attached → rc 1, no current client, #{session_attached} = 0; with a client attached → rc 0, list-clients empty afterwards, #{session_attached} 1 → 0.

Still open before merge: #{session_attached} is verified on tmux but not on psmux. It fails safe if unsupported (→ False, which is also the truth today given the inert switch leg), but the Windows VM should confirm it before #317 is closed rather than re-scoped. Runbook in the next comment.

One housekeeping note: the description says the detach_client widening was "escalated into scope by review, maintainer-approved" — I can't find that approval on this PR or on #227. Consider it approved now, retroactively; worth having the record match.

@pbean

pbean commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Windows VM runbook — confirm #{session_attached} on psmux

The one fact PsmuxMultiplexer._attached_clients rests on. It is self-detecting (a non-integer answer degrades to False, never a vacuous True), so a negative result is safe — it just means #317 stays open for the residue instead of being closed here.

Step 0 — branch identity. Do this first, every round. Four probe rounds were lost to a stale checkout during #315.

git rev-parse HEAD          # must be 04a65ea636679c9533f17294a41a4a42b9713df2
git status --short          # must be clean

Step 1 — does psmux carry the field at all?

psmux -L probe316 kill-server 2>$null
psmux -L probe316 new-session -d -s probe
psmux -L probe316 display-message -p -t probe '#{session_attached}'

Expected 0. Anything non-numeric (an echoed #{session_attached}, empty, an error) ⇒ the field is absent, the probe degrades to False, and #317 stays open. Stop there and report what it printed.

Step 2 — does it move? Attach a client from a second terminal (psmux -L probe316 attach -t probe), then from the first:

psmux -L probe316 display-message -p -t probe '#{session_attached}'   # expect 1
psmux -L probe316 detach-client; $LASTEXITCODE                        # expect 0 (says nothing)
psmux -L probe316 display-message -p -t probe '#{session_attached}'   # expect 0 -> the drop we measure

The exit code is expected to be 0 in both the effective and the no-op case — that is the whole reason for the count. What matters is only that the number moves 1 → 0.

Step 3 — the rc-0 no-op, directly. With nothing attached:

psmux -L probe316 detach-client; $LASTEXITCODE                        # expect 0 <- #317 in one line
psmux -L probe316 display-message -p -t probe '#{session_attached}'   # expect 0, unchanged

Step 4 — end to end. Launch a sweep with a pending decision from the TUI, a to attach, answer it, and confirm the terminal comes back, the return option is gone, and the journal shows sweep-returned-after-decisions. Then repeat, detaching before the answer completes, and confirm the sweep goes unattended (sweep-return-no-client) rather than blocking.

psmux -L probe316 kill-server 2>$null                                 # cleanup

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adapter-authoring-guide.md`:
- Around line 111-117: Revise the guidance around the failed detach case to say
that False indicates no verified return effect, not proof that no client exists.
Clarify that UNREACHABLE is the caller’s conservative policy for this uncertain
outcome, while preserving the distinction from a failed switch_client and the
existing ATTENDED behavior.
- Around line 103-106: Clarify the psmux example in the adapter-authoring
guidance: do not use a session-wide attached-client count to validate
switch-client. Document the window/current-target observation that verifies
detach-client and switch-client, or specify a separate probe for the switch
operation, while preserving the existing measurement guidance for cases where
exit statuses are unreliable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a19429c-61cc-46cc-8a47-af87f1be9d2c

📥 Commits

Reviewing files that changed from the base of the PR and between 9a8fec6 and 04a65ea.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • docs/adapter-authoring-guide.md
  • docs/tui-guide.md
  • src/bmad_loop/adapters/multiplexer.py
  • src/bmad_loop/adapters/psmux_backend.py
  • src/bmad_loop/adapters/tmux_base.py
  • src/bmad_loop/sweep.py
  • src/bmad_loop/tui/launch.py
  • tests/test_multiplexer.py
  • tests/test_psmux_backend.py
  • tests/test_sweep.py
  • tests/test_tui_launch.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/bmad_loop/adapters/tmux_base.py
  • tests/test_multiplexer.py
  • CHANGELOG.md
  • src/bmad_loop/adapters/multiplexer.py
  • tests/test_tui_launch.py

Comment thread docs/adapter-authoring-guide.md Outdated
Comment thread docs/adapter-authoring-guide.md Outdated
@pbean

pbean commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Windows probe result: #{session_attached} is supported on psmux

Run by @pbean on the Windows VM. The field the measurement rests on exists and tracks reality:

step command result
1 display-message -p -t probe '#{session_attached}', nothing attached 0
2 same, one client attached 1
2 detach-client; $LASTEXITCODE 0
2 count after the detach 0 — the drop we measure
3 detach-client with nothing attached; $LASTEXITCODE 0
3 count after 0, unchanged

Step 3 is #317 in two lines. psmux exits 0 for a detach that did nothing, with the count sitting still — the vacuous True the seam used to hand return_attached_client. Step 2 exits 0 for a detach that worked. Identical exit codes, opposite effects: the count is the only thing that separates them, which is what _attached_clients now reads.

So the psmux half of this PR is verified at the transport, not just in the mocked unit tests. #317 can close with the merge.

Two things this did not prove, stated so the record is accurate:

  • The end-to-end sweep (step 4) was not run — no pending decision available on that host. The seam is measured; the full attach → answer → hand-back path on Windows is still only covered by the mocked tests.
  • The probes ran from the bmad-loop-315 checkout, not this branch. Harmless here, since steps 1–3 are pure psmux CLI calls that never read the repo — but it is the same wrong-checkout trap that cost four probe rounds during fix(adapters): give psmux a per-window option channel #315, and it would have invalidated step 4. Flagging rather than quietly counting it.

Residue unchanged and documented in the backend's degradation ledger: a switch whose target pane lives in the same session moves a client between windows without changing the session's attached count, so it reads as no effect and the caller keeps prompting — the safe direction.

…review)

Two places where the prose claimed more than the seam delivers — the same
defect class already fixed once in c262708.

The psmux worked example described the attached-client count as if it
proved any switch. It proves a detach, and the switch the return path
normally performs (the recorded target is a pane in the session the client
came from, so a real switch leaves this session) — but not a switch within
this session, which moves the client between windows without moving the
count. psmux_backend.py already carried that under `Residue:`; the guide
did not. Failing that way is conformant, so the rule is unchanged: the
caller reads no effect and keeps prompting.

"A failed detach_client means there was no client there at all" is true on
tmux and nowhere else. The same section already mandates False for an
unverifiable effect, and the guide's own herdr paragraph describes a no-op
detach whose client only a manual chord releases. False means "no verified
hand-back"; UNREACHABLE is the caller's policy for that uncertainty,
justified by the asymmetric cost — prompting into an unwatched window
blocks a --repeat cycle on input() forever, while going unattended in front
of a human only defers decisions to `bmad-loop decisions`.

launch.py and sweep.py repeated the same overstatement, so they move with
the guide rather than leaving the inconsistency relocated.

No behavior change; the no-drop-reads-False shape is already covered by
tests/test_psmux_backend.py.
@pbean
pbean merged commit 3cee37e into bmad-code-org:main Jul 27, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

return_attached_client reports success even when switch_client fails

2 participants