Skip to content

feat(tui,adapters): warn when a reused tmux server would plant a stale state root (#731) - #854

Open
dracic wants to merge 2 commits into
bmad-code-org:mainfrom
dracic:feat/731-stale-state-root-warning
Open

dracic wants to merge 2 commits into
bmad-code-org:mainfrom
dracic:feat/731-stale-state-root-warning

Conversation

@dracic

@dracic dracic commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

The interim for #731: detect a reused tmux server that would plant a stale BMAD_LOOP_STATE_DIR in new panes, and say so. This is Stage 1 of the design in #850. Carrying the root explicitly in the parked window's argv is Stage 2, a separate change.

Problem

A tmux server is long-lived, and its panes inherit the environment it was started with. A server cold-started under BMAD_LOOP_STATE_DIR=S1, then reused by a launch under S2, hands S1 to the parked engine window and every window-0 shell. The engine writes its control plane where the launcher never looks, and the live run reads as gone.

Change

  • TerminalMultiplexer.inherited_env(session, name, *, on_fault=None): a new non-abstract query for what a new pane will inherit. It answers None (unknown, the seam default), the UNSET sentinel (known absent), or the value, "" included. It never raises; a query that was attempted and failed reports through on_fault rather than folding into "unknown".
  • tmux: implemented on TmuxMultiplexer, not BaseTmuxBackend. It runs show-environment -t =S NAME, plus a -g fallback on an exact unknown variable miss. Reply shapes were measured on tmux 3.4: NAME=v, NAME=, the -NAME removal marker, misses, and a missing session.
  • psmux: keeps "unknown". Its show-environment shows only PSMUX* / TMUX* names, so it cannot see an inherited BMAD_LOOP_STATE_DIR; the per-project registry already closes the ordinary path there.
  • runs.resolve_state_root(env, passwd_home): the state-root cascade as a pure function. runs.state_root() delegates to it and is byte-identical: a differential over 2560 env combinations on both Windows and Linux found 0 mismatches. The passwd home is looked up lazily and only used when HOME is absent, because absent and empty HOME are different inputs.
  • _ensure_ctl_session: after both the create and the reuse arm, it compares the root a new pane would resolve with the launcher's own. On a mismatch it warns once per process through the TUI, naming both roots and giving a shell-quoted remedy.
    • Unknown answers are silent.
    • Query faults are reported.
    • An underivable launcher root is reported.
    • The launch is never blocked.
  • docs/multiplexer-backends.md: a new subsection with the operator remedy. CHANGELOG Fixed.

Compatibility

A StubMux that implements only the released abstract set completes both _ensure_ctl_session arms with no warning and no error. Out-of-tree backends need no change.

Tests

11 gates were ablated, each confirmed to fail its test without the gate. These include: the fault path, placing the implementation on the base class (psmux test), passwd use on an empty HOME, eager passwd lookup, a raw-override comparison instead of resolved roots, the loose unknown variable match, and the remedy quoting.

Local results

  • Windows, single files: 1516 passed. 4 failures in test_runs, all environmental symlink WinError 1314.
  • WSL, the 6 touched test files: 1586 passed, 0 failed.

Refs #731 #850

Summary by CodeRabbit

  • Bug Fixes
    • The TUI now warns once when new tmux windows may use a different state directory. The warning identifies both directories and suggests how to correct the tmux environment; launching continues.
  • Documentation
    • Added guidance for resolving state-directory differences in tmux, including how existing shells are affected.

…e state root

A tmux server is long-lived, and its panes inherit the environment it
was started with. A server cold-started under BMAD_LOOP_STATE_DIR=S1,
then reused by a launch under S2, hands S1 to the parked engine window
and every window-0 shell. The engine then writes its control plane
where the launcher never looks, and the live run reads as gone (bmad-code-org#731).
This is the interim from the bmad-code-org#731 design: detect it and say so; carrying
the root explicitly is a later change.

- TerminalMultiplexer.inherited_env(session, name, *, on_fault=None) is
  a new non-abstract query answering what a new pane will inherit:
  None (unknown, the seam default), the UNSET sentinel (known absent),
  or the value ("" included). It never raises; a failed query reports
  through on_fault instead of folding into unknown.
- TmuxMultiplexer implements it with show-environment -t =S NAME, plus
  the -g fallback on an exact "unknown variable" miss. It sits on
  TmuxMultiplexer, not BaseTmuxBackend, so psmux keeps "unknown": its
  show-environment cannot see inherited values.
- runs.state_root is now runs.resolve_state_root(os.environ,
  passwd_home), byte-identical over 2560 env combinations on Windows
  and Linux. The passwd home is looked up lazily and used only when
  HOME is absent: absent and empty HOME are different inputs.
- _ensure_ctl_session compares the root a new pane would resolve with
  the launcher's own, after both the create and the reuse arm, and
  warns once per process through the TUI with a shell-quoted remedy.
  Unknown answers are silent, faults are reported, and the launch is
  never blocked.

Out-of-tree backends written against today's seam are unaffected: a
StubMux implementing only the released abstract set completes both
arms with no warning.

Known gaps:
- Windows test_runs has 4 environmental symlink WinError 1314 failures.
- WSL ran the 6 touched test files only (1586 passed). The rest of the
  Linux suite is unverified locally.
- trunk check could not run locally (a broken ruff plugin); ruff,
  prettier and pyright were run directly.

Refs bmad-code-org#731
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (2)
AGENTS.md — auto-discovered
docs/testing.md — configured

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e9afb093-735e-4a33-a3df-621db9306bbd

📥 Commits

Reviewing files that changed from the base of the PR and between 68645cb and eab9e5b.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • docs/multiplexer-backends.md
  • src/bmad_loop/adapters/multiplexer.py
  • src/bmad_loop/adapters/tmux_backend.py
  • src/bmad_loop/envvars.py
  • src/bmad_loop/runs.py
  • src/bmad_loop/tui/app.py
  • src/bmad_loop/tui/launch.py
  • tests/test_multiplexer.py
  • tests/test_runs.py
  • tests/test_tui_app.py
  • tests/test_tui_launch.py

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.


Walkthrough

The TUI now checks whether new tmux windows would inherit a different state root. It warns once when the roots differ or the inherited root cannot be resolved, but continues launching. The warning includes the roots and shell-quoted commands to update the tmux environment.

Changes

Tmux state-root warning

Layer / File(s) Summary
Resolve state roots from explicit environments
src/bmad_loop/envvars.py, src/bmad_loop/runs.py, tests/test_runs.py
State-root helpers now accept environment mappings and resolve roots using supplied variables and, when needed, passwd home. Tests cover POSIX and Windows resolution, fallback behavior, and invalid candidates.
Read inherited tmux environment
src/bmad_loop/adapters/multiplexer.py, src/bmad_loop/adapters/tmux_backend.py, tests/test_multiplexer.py
The multiplexer API distinguishes environment values, confirmed absence, and unknown values. The tmux backend checks session scope before global scope; tests cover parsing, failures, and backends without query support.
Report state-root mismatches
src/bmad_loop/tui/launch.py, src/bmad_loop/tui/app.py, tests/test_tui_launch.py, tests/test_tui_app.py, docs/multiplexer-backends.md, CHANGELOG.md
Control-session setup compares the current root with the root resolved from inherited tmux values. A mismatch warning is emitted once without stopping launch and is shown as a TUI warning notification. Tests cover comparison, warning delivery, and remediation quoting; the docs and changelog describe the warning.

Priority: ⬇️ Low

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

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant BmadLoopApp
  participant launch._ensure_ctl_session
  participant TmuxMultiplexer
  participant runs.resolve_state_root
  BmadLoopApp->>launch._ensure_ctl_session: Ensure control session
  launch._ensure_ctl_session->>TmuxMultiplexer: Query inherited state-root inputs
  TmuxMultiplexer->>launch._ensure_ctl_session: Return values, UNSET, or unknown
  launch._ensure_ctl_session->>runs.resolve_state_root: Resolve inherited state root
  launch._ensure_ctl_session->>BmadLoopApp: Send warning through warning sink when roots differ
  BmadLoopApp->>BmadLoopApp: Display warning notification
Loading

Suggested reviewers: pbean

Merge Risk: ⚪ Minimal · up to eab9e

The state-root warning is non-blocking, and no issue requiring a fix before merge was established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to eab9e

The change observes inherited configuration without changing privileges, state ownership, or launch destinations. It warns about an existing state-root mismatch rather than preventing it. Unknown configuration and warning-delivery failures limit the assurance provided by this diagnostic.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected exposure is local to processes using the reachable tmux server and their project control-plane roots. The control session can serve multiple projects, but the new query adds no environment-write or privilege-changing operation.

Trust Boundaries and Controls

  • observed — Session names and environment names cross into tmux as arguments rather than shell text. Exact session targeting and strict reply parsing prevent ambiguous query failures from becoming confirmed absence. Suggested repair arguments are shell-quoted, and TUI notifications disable markup.

Resilience and Maintainability Implications

  • observed — Unknown inherited configuration is not treated as a successful consistency check. Ordinary transport faults are diagnosed, while actual session or window creation failures retain LaunchError handling. The warning does not reserve, redirect, or roll back control-plane ownership.

Hardening Proposals

  • proposed — Contain warning-sink and fault-callback failures at the advisory boundary so diagnostic delivery cannot abort launch or consume the only warning without delivering it.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.18% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 68 functions across 10 files. (2 skipped:… 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 and concisely describes the main change: warning when a reused tmux server would cause a stale state root.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 66.18% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 68 functions across 10 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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

A rabbit checks the tmux trail,
Two roots appear beyond the veil.
A gentle warning marks the way,
New windows still launch on their way.
I nibble greens and hop away!

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

@dracic

dracic commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

CI note on test (py3.14): one failure, tests/test_runs.py::test_run_removal_retries_a_transient_windows_sharing_violation[archive] (assert sleeps == [] got the full backoff series 0.001 … 0.032), out of 13464 passed. This looks like a flake, not a regression from this PR:

  • The test monkeypatches time.sleep process-wide and records every sleep in the worker, so any backoff that runs concurrently in the same xdist worker lands in its list.
  • This PR adds no sleeping code, and it does not touch the run-removal or archive path.
  • The parametrized test passes 5/5 locally on Linux (WSL, py3.13) on this branch's head.

I cannot re-run the job (it needs repository admin rights). Could a maintainer re-run the failed job? If it reproduces, I will dig in.

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.

1 participant