Skip to content

fix(adapters,engine): tell a lost mux session apart from an exited CLI - #522

Merged
pbean merged 5 commits into
bmad-code-org:mainfrom
dracic:fix/489-lost-session-diagnosis
Aug 11, 2026
Merged

fix(adapters,engine): tell a lost mux session apart from an exited CLI#522
pbean merged 5 commits into
bmad-code-org:mainfrom
dracic:fix/489-lost-session-diagnosis

Conversation

@dracic

@dracic dracic commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Addresses Q1 of #489 (the shared-ctl-session/untagged-fallback question, Q2, stays out — it is gated on #419)

Problem

Sessions complete on a hook Stop event or on window death — a hard invariant. But _window_alive is a membership test over list_window_ids(session), and that list is empty for two different worlds: the window died inside a live session (the CLI exited), and the session itself no longer exists. Both scored crashed, so a session destroyed under a run (an external reaper such as psmux/psmux#546, this tool's own prune/stop, an operator kill-session, a mux server crash, a sleeping host) presented as an ordinary CLI crash — the reason an operator reads said only dev session crashed, pointing at the agent when the host was at fault.

Approach

Once a crash verdict is already reached, _final asks has_session — the only call that separates the two worlds. The answer is a diagnostic label, never a routing input:

  • SessionResult.session_vanished, stamped only when the verdict is already crashed — never to reach a verdict, and never on a read-back upgrade to completed (a session reaped after flushing its result did produce something)
  • surfaced in the shared reason builder session_failure_reason (… session crashed: the multiplexer no longer reports the session, so the window's disappearance is not evidence the CLI exited), adopted at the dev/review deciders, the blocking-workflow defer, and the sweep migration/triage sites
  • journaled structurally on every role's session-end entry via the _session_end_extras chokepoint (beside env_fault), plus dev-decision
  • a session-vanished breadcrumb in session-lifecycle.jsonl carrying the session name and verdict
  • composed with the environment-fault pause (Dev session lost to an API/transport failure is charged as a story timeout, exhausting the attempt budget #194) so the two diagnoses cannot cancel out — a lost session whose pane-log tail also matches a transport pattern names both facts

The wording states what the evidence withdraws, not what it proves: the weak-False contract (False = "the backend did not confirm the session"; transport failure raises MultiplexerError, never returns False) is now declared on the TerminalMultiplexer.has_session seam. MultiplexerError from the probe degrades to "not vanished" — the same "unknown is not dead" rule the liveness probe follows.

Routing is deliberately untouched: _ensure_session re-creates the session, so a retry already self-heals. Adapters with no session to lose (opencode-http) are inert via a constant-False base hook.

Testing

  • Three-way probe parametrization (session present / absent / transport error), breadcrumb pinned by count and evidence fields
  • Ablation-verified pins: deleting the status == "crashed" gate or the env-fault composition fails the covering tests; the gate test reads the final status, so a regression to gating on the fallback also fails
  • Engine-level pins for session-end + dev-decision journal fields (True on a vanished crash; absent/False on a plain crash), the read-back-upgrade skip, the non-crash skip, the post-kill-reconcile pass-through of a flagged verdict, and the blocking-workflow defer reason end to end
  • Unit tests stay off the host multiplexer (_UnitMux); uv run pytest green, uv run pyright clean but for the pre-existing platform_util win32 pair, trunk check clean

Summary by CodeRabbit

  • Bug Fixes

    • Improved diagnostics for sessions that disappear from the multiplexer, distinguishing them from ordinary crashes.
    • Journal entries and lifecycle breadcrumbs now record vanished sessions with clearer failure reasons.
    • Environment-fault diagnostics include session-loss details when applicable.
    • Retry, routing, and pause behavior remain unchanged.
  • Documentation

    • Updated feature and Journal guides to describe session-vanishing diagnostics and the new lifecycle breadcrumb.

@coderabbitai

coderabbitai Bot commented Aug 10, 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: 52 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: e578be5e-a804-48a4-b089-66f3ed4196d1

📥 Commits

Reviewing files that changed from the base of the PR and between 9bc39ef and f368c9b.

📒 Files selected for processing (1)
  • tests/test_engine.py

Walkthrough

The change detects vanished multiplexer sessions during crash handling. It records session_vanished and session-vanished diagnostics in results and journals. Shared failure reasons include multiplexer loss while preserving existing retry and pause routing.

Changes

Session-loss diagnostics

Layer / File(s) Summary
Session detection and result propagation
src/bmad_loop/adapters/base.py, src/bmad_loop/adapters/generic.py, src/bmad_loop/adapters/multiplexer.py, src/bmad_loop/adapters/tmux_base.py, tests/test_generic_tmux.py
Adapters probe session existence during crash finalization. Confirmed disappearance sets SessionResult.session_vanished and emits a lifecycle event. Probe failures remain non-vanishing, and post-kill reconciliation does not alter the flag.
Failure reasons and routing integration
src/bmad_loop/escalation.py, src/bmad_loop/engine.py, src/bmad_loop/sweep.py, tests/test_escalation.py, tests/test_plugin_workflows.py, tests/test_sweep.py
A shared failure-reason helper reports session loss for development, review, migration, triage, and blocking workflow paths. Existing retry, defer, salvage, and pause routing remains unchanged.
Journal records and documentation
src/bmad_loop/engine.py, tests/test_engine.py, docs/FEATURES.md, docs/tui-guide.md, CHANGELOG.md
Decision and session-end journals record vanished-session diagnostics. Documentation and the changelog describe the new session_vanished field and session-vanished breadcrumb.

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

Sequence Diagram(s)

sequenceDiagram
  participant GenericAdapter
  participant Multiplexer
  participant Escalation
  participant Journal
  GenericAdapter->>Multiplexer: Probe session existence after a crash
  Multiplexer-->>GenericAdapter: Return confirmed presence or absence
  GenericAdapter->>Escalation: Provide SessionResult.session_vanished
  Escalation-->>Journal: Record diagnostic reason without changing routing
  GenericAdapter->>Journal: Emit session-vanished lifecycle data
Loading

Possibly related issues

  • bmad-code-org/bmad-loop#525: Adds and clarifies has_session-based multiplexer liveness probing, which matches this change’s session-disappearance detection.

Suggested reviewers: pbean

Poem

A rabbit checks the vanished trace,
And marks the session’s missing place.
Crash details bloom in journal light,
While retries keep their former flight.
session-vanished hops in sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.57% 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 and concisely describes distinguishing a lost multiplexer session from an exited CLI, which is the primary change.
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.
✨ 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.

@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

🧹 Nitpick comments (2)
src/bmad_loop/adapters/generic.py (1)

298-311: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace getattr with direct attribute access.

getattr(self, "session_name") with no default has the same failure behavior as self.session_name. Both raise AttributeError if the attribute is missing, so the "fail loud, no default" intent in the comment holds either way. Use direct attribute access; it is equally safe and more idiomatic.

🔧 Proposed fix
             self._note_lifecycle(
                 handle.task_id,
                 "session-vanished",
-                session=getattr(self, "session_name"),
+                session=self.session_name,
                 status=status,
             )
🤖 Prompt for 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.

In `@src/bmad_loop/adapters/generic.py` around lines 298 - 311, In the
vanished-session branch of the lifecycle handling, replace getattr(self,
"session_name") with direct self.session_name access when passing the session
value to _note_lifecycle. Preserve the existing fail-loud behavior and all other
arguments unchanged.

Source: Linters/SAST tools

CHANGELOG.md (1)

163-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the CHANGELOG.md entry terse and imperative.

The entry uses a long narrative and a declarative opening. Replace it with a short imperative summary that names the diagnostic fields and states that routing is unchanged.

Proposed wording
-- **A lost multiplexer session no longer reads as an agent that crashed (`#489`).** A window is
-  equally gone when the CLI exits and when something destroys the whole session under the run ...
+- **Improve crash diagnosis when the multiplexer no longer reports a session (`#489`).** Include
+  the diagnostic in crash reasons, `session-end`/`dev-decision` journal entries, and
+  `session-vanished` lifecycle breadcrumbs. Preserve environment-fault composition and retry routing.

As per coding guidelines, CHANGELOG.md entries must be under Unreleased and remain terse, scannable, and imperative.

🤖 Prompt for 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.

In `@CHANGELOG.md` around lines 163 - 172, Rewrite the CHANGELOG entry as a terse,
imperative summary under the Unreleased section. Name the affected diagnostic
fields—crash verdict, operator-facing reason, session_vanished journal entry,
and session-vanished lifecycle breadcrumb—and explicitly state that routing is
unchanged.

Source: Coding guidelines

🤖 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/tui-guide.md`:
- Around line 223-224: Update the `session-vanished` diagnostic description in
the event list to say “the mux no longer reported the session during the run”
instead of asserting that the mux lost the session, preserving the wording as an
unconfirmed negative lookup.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Around line 163-172: Rewrite the CHANGELOG entry as a terse, imperative
summary under the Unreleased section. Name the affected diagnostic fields—crash
verdict, operator-facing reason, session_vanished journal entry, and
session-vanished lifecycle breadcrumb—and explicitly state that routing is
unchanged.

In `@src/bmad_loop/adapters/generic.py`:
- Around line 298-311: In the vanished-session branch of the lifecycle handling,
replace getattr(self, "session_name") with direct self.session_name access when
passing the session value to _note_lifecycle. Preserve the existing fail-loud
behavior and all other arguments unchanged.
🪄 Autofix

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: 4fb9255d-fbe8-4b02-a0b0-0e7bc43550aa

📥 Commits

Reviewing files that changed from the base of the PR and between 99fa36e and 7296287.

📒 Files selected for processing (15)
  • CHANGELOG.md
  • docs/FEATURES.md
  • docs/tui-guide.md
  • src/bmad_loop/adapters/base.py
  • src/bmad_loop/adapters/generic.py
  • src/bmad_loop/adapters/multiplexer.py
  • src/bmad_loop/adapters/tmux_base.py
  • src/bmad_loop/engine.py
  • src/bmad_loop/escalation.py
  • src/bmad_loop/sweep.py
  • tests/test_engine.py
  • tests/test_escalation.py
  • tests/test_generic_tmux.py
  • tests/test_plugin_workflows.py
  • tests/test_sweep.py

Comment thread docs/tui-guide.md Outdated
Sessions complete on a hook Stop or on window death, and `list_window_ids`
answers [] for both "the CLI exited" and "the whole session is gone" — a
missing session exits non-zero and degrades to the same empty list. So a
session destroyed under the run (an external reaper, a concurrent prune or
stop, an operator kill-session, a server crash, the host sleeping) scored
`crashed` exactly like a clean CLI exit, and the retry/defer reason an
operator reads said only `dev session crashed`, pointing at the agent when
the host was at fault.

Ask `has_session` once a crash verdict is already reached, from the single
`_final` chokepoint all five crash sites funnel through. Safe to ask that
late: run()'s teardown kills the window, never the session. The answer rides
`SessionResult.session_vanished` into the reason text, the `dev-decision`
journal entry, and a `session-vanished` lifecycle breadcrumb.

Worded as the observation, not the conclusion — the probe cannot tell who
destroyed the session. Composed into `env_fault_pause_reason` so a lost
session whose log also matches a transport pattern keeps both diagnoses
instead of the env-fault branch swallowing one. A session reaped after
flushing its result still scores `completed` and is not diagnosed: it
produced something.

Diagnosis only; routing is unchanged and a retry re-creates the session.

Unit tests get a mux stand-in: the probe was the first unstubbed `self.mux`
access on the crash path, so `make_dev_adapter`'s adapters were reaching the
host multiplexer for real — scoring eight existing crash tests
`session_vanished` and writing breadcrumbs, against that file's "unit tests
need no tmux" contract.

Refs bmad-code-org#489
@dracic
dracic force-pushed the fix/489-lost-session-diagnosis branch from 7296287 to 264d84a Compare August 10, 2026 10:38
@dracic

dracic commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit findings in 264d84a: the tui-guide breadcrumb description no longer overclaims ("no longer reported the session during the run"), and the CHANGELOG entry is rewritten terse and imperative. The getattrself.session_name nitpick is deliberately skipped: _ResultFileMixin does not declare session_name (the opencode-http adapter has none), so direct access fails the pinned pyright — the comment at the call site now says so.

@pbean

pbean commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 264d84a655

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# its own exit rather than the window dying — the label stays truthful
# there because it reports what the mux answered, not how the window
# ended.
vanished = status == "crashed" and self._session_vanished()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict the vanished-session probe to window-death verdicts

When the crashed fallback comes from the SessionEnd arm in wait_for_completion, the CLI has explicitly announced its exit; if the mux session is also absent by the time this probe runs, this stamps session_vanished=True and produces a reason claiming that the window disappearance is not evidence the CLI exited. That misdiagnoses exactly the exited-CLI case this change is meant to distinguish. Pass the crash origin into _final, or skip this probe for the SessionEnd path, so only a crash inferred from a failed window-liveness check receives this label.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified against the code at 264d84a — declining this one. The conjunction it describes (SessionEnd received AND session absent at probe time) can only arise when a host-level actor destroyed the session, and that is precisely when the suffix must fire:

  • A normal CLI exit can never make _session_vanished() true. Window 0 is a parked shell ("Window 0 is a plain shell so the session survives task windows closing", tmux_base.py), and run()'s teardown kills only the window, never the session (comment in _session_vanished). So reaching this corner requires an independent destroyer — mux server crash, external reaper, operator/concurrent kill-session, host sleep — the exact candidates enumerated in generic.py's probe comment.
  • In every one of those cases, "the multiplexer no longer reports the session" is a true and operator-relevant fact that an announced CLI exit cannot explain. The likeliest route into the corner is causal, not coincidental: the destroyer HUPs the CLI, whose hook flushes SessionEnd on the way down. Skipping the probe on the SessionEnd arm would make exactly that case read as a plain CLI crash — reintroducing the psmux can destroy a live session out from under a run (psmux#546) — decide the exposure and whether the reconcile can tell #489 misdiagnosis this PR fixes.
  • The suffix withdraws an inference; it does not assert the CLI failed to exit. That wording is deliberate on both sides of the seam: session_failure_reason's docstring ("states what the evidence withdraws, not what it proves") and _final's comment, which names the SessionEnd arm explicitly as considered. Routing is unchanged either way, and the SessionEnd receipt remains on disk in the run's events/ directory (SignalWatcher never unlinks consumed event files), so no evidence is lost to the operator.

Threading the crash origin into _final to vary one explanation string in a double-fault corner would trade real plumbing for a marginal wording refinement; if anything, the improvement would be phrasing that acknowledges both facts, not narrowing the probe's scope.

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 264d84a655

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@dracic — heads up, I pushed a merge commit (9a0cc73) to this branch to clear a conflict that appeared while the PR sat. Main moved 79 commits since your last push (#502, #517, #518, #523, #526/#532) and the branch had gone CONFLICTING.

Both conflicts were docs-only and purely additive:

No code conflicts, and nothing of yours was rewritten — git diff main...HEAD is byte-identical to the pre-merge PR diff (15 files, +422/-18), so the merge changed nothing about what this PR contributes.

Verified locally on the merge commit: 4976 passed, 45 skipped, 5 xfailed; pyright 0 errors; trunk check clean on both resolved files.

Happy to revert if you'd rather resolve it yourself.

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@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 `@tests/test_engine.py`:
- Around line 7113-7120: Add an assertion that len(adapter.sessions) equals 2
after engine.run() in tests/test_engine.py lines 7113-7120, tests/test_sweep.py
lines 1646-1653, and tests/test_sweep.py lines 1663-1670, confirming both retry
sessions were consumed.
🪄 Autofix

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: becc8dd2-208a-4ccc-ad1b-f1163df8a112

📥 Commits

Reviewing files that changed from the base of the PR and between 7296287 and 9a0cc73.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • docs/FEATURES.md
  • docs/tui-guide.md
  • src/bmad_loop/adapters/generic.py
  • src/bmad_loop/adapters/multiplexer.py
  • src/bmad_loop/adapters/tmux_base.py
  • src/bmad_loop/engine.py
  • tests/test_engine.py
  • tests/test_sweep.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/bmad_loop/adapters/multiplexer.py
  • docs/tui-guide.md
  • CHANGELOG.md
  • src/bmad_loop/adapters/tmux_base.py
  • docs/FEATURES.md
  • src/bmad_loop/engine.py

Comment thread tests/test_engine.py
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 9a0cc73774

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The three bmad-code-org#489 diagnostic tests queue two crashed results but asserted only
over the journal entries produced, so a regression that stopped after the
first crash still passed: `all(...)` holds over a one-element list and
`[-1]` reads the only decision there is. Ablating the engine to a single
attempt kept every original assertion green.

Assert both attempts are spent, matching the sibling env-fault test in
test_sweep.py that already pins the same thing.
@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai — following up on the second nitpick from the first-round review body (the one collapsed under "🧹 Nitpick comments", src/bmad_loop/adapters/generic.py 298-311), since it has no thread of its own.

Declining "Replace getattr with direct attribute access" — the proposed fix breaks the typecheck.

The premise is right as far as it goes: getattr(self, "session_name") with no default and self.session_name do have identical runtime behavior, both raising AttributeError. But the construct is not there for runtime semantics — it is there for static analysis, which is why the author left a comment on it naming the mixin.

Applied the proposed diff verbatim and ran the repo's pinned typechecker:

src/bmad_loop/adapters/generic.py:310:30 - error: Cannot access attribute "session_name" for class "_ResultFileMixin*"
    Attribute "session_name" is unknown (reportAttributeAccessIssue)
1 error, 0 warnings, 0 informations

uv run pyright is a required CI gate here (typecheck (pyright)), so this change would turn the build red. _ResultFileMixin deliberately does not declare session_nameopencode-http has no session name at all, and only a mux-backed adapter can reach that branch because the base _session_vanished is a constant False. The getattr is what lets the mixin reach an attribute its own class cannot declare, while the missing default preserves the fail-loud intent.

Reverted; getattr stays. No action needed on the other first-round items — the tui-guide wording and the CHANGELOG entry were addressed by @dracic in 264d84a.

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@pbean Confirmed. The original nitpick does not apply.

_ResultFileMixin cannot use self.session_name because it does not declare that attribute. The reported pyright error makes direct access invalid for this repository. Retaining getattr(self, "session_name") preserves the required fail-loud runtime behavior without failing the required typecheck.

I withdraw the nitpick. The other addressed items need no further action.

(ᵔᴥᵔ)


✏️ Learnings added
Learnt from: pbean
URL: https://github.com/bmad-code-org/bmad-loop/pull/522

Timestamp: 2026-08-11T14:51:52.775Z
Learning: In `src/bmad_loop/adapters/generic.py`, `_ResultFileMixin` must use `getattr(self, "session_name")` instead of `self.session_name`. `_ResultFileMixin` does not declare `session_name`, because adapters such as `opencode-http` have no session name. Direct attribute access fails the repository’s required pyright check, while `getattr` without a default retains intentional fail-loud `AttributeError` behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 389a87bf32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +92 to +94
def session_failure_reason(role: str, result: SessionResult) -> str:
"""The reason text for a non-completed session: ``<role> session <status>``,
plus the lost-session diagnosis (#489).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate vanished-session reasons through fix retries

When the mux disappears during a plain, non-environment-fault _fix_phase session, this new reason builder is never used: _fix_phase leaves reason set to the original verification failure and ultimately returns an empty DEFER, so its callers defer with that stale verification reason instead of the vanished-session diagnosis. This affects repair sessions after a clean review or with review disabled; incorporate session_failure_reason("fix", result) into the non-completed fix path so the operator-facing defer explains why the repair did not run.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified against the code — the gap is real, but declining it for this PR: the root cause is pre-existing and _fix_phase is not part of this change.

This PR does not touch _fix_phase. git diff origin/main...389a87b -- src/bmad_loop/engine.py is exactly four hunks: the session_failure_reason import, the blocking-workflow _defer reason (~1538), session_vanished on the dev-decision entry (~1759), and _session_end_extras (~4016). _fix_phase begins at 4730 and is untouched.

The empty DEFER predates the change. return Decision(Action.DEFER) with no reason is already on origin/main at line 4811. The reason-substitution behavior the callers exhibit is therefore what the fix path did before #489 was addressed at all — reverting this PR entirely would not change it.

What the fix path already gets from this PR: the session-vanished lifecycle breadcrumb (adapter-side, role-agnostic), and session_vanished on the fix session's own session-end journal entry — _fix_phase calls _run_session(..., role="dev", ...), and every session-end funnels through _session_end_extras, whose comment names the roles deliberately: "EVERY role — dev, review, fix, migration, triage, injected workflows". So the greppable forensic record for a lost fix session is complete; what is missing is the operator-facing reason string on the exhaustion path, plus session_vanished on the fix-decision entry.

That narrower gap was already caught in this PR's pre-merge review and triaged as a follow-up (residual R1), precisely because closing it means touching _fix_phase reason plumbing that this diagnosis-only change deliberately stays out of — routing is unchanged here by design. It is getting filed as its own issue rather than widened into this PR.

Good catch on the substance; wrong PR to land it in.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reversing my earlier decline — this finding is in scope and is now fixed in 9bc39ef. My reasoning above was wrong and worth naming, since it was wrong in a way that would have shipped a half-delivered contract.

I graded the finding by provenance: _fix_phase is not in the diff, the bare Decision(Action.DEFER) already exists on main, therefore not this PR's problem. Both facts are true and neither is the right test. Applied consistently that rule says a change fixing a class of bug is finished once it has fixed the lines it happened to touch.

The right test is whether the PR delivers its stated purpose on this path, and its own FEATURES.md entry is explicit about what that purpose is: "the retry/defer reason an operator reads said only dev session crashed — pointing at the agent when the host was at fault … now … says so in the reason." Ran the scenario — two repair sessions destroyed by the mux, budget exhausted:

task.defer_reason -> "verify commands kept failing after clean review"

That is the #489 misdiagnosis one layer further out, and a worse instance of it: the original at least named a crashed session, while this blames the tree's verify commands for repairs that never ran. I also read past the disconfirming evidence already in the diff — this PR had rewritten a defer reason on another path (the blocking-workflow _defersession_failure_reason), which settles that reason-string propagation was in scope. fix was an inconsistency in the PR's own coverage, not adjacent work.

The fix: carry the last non-completed session's own failure into the exhaustion DEFER. The callers already do fix.reason or "<fallback>", so no caller changes, and the verify-centric wording is untouched when the repair actually ran and only verify failed. fix-decision also gains session_vanished, matching dev-decision.

- verify commands kept failing after clean review
+ fix session crashed: the multiplexer no longer reports the session, so the
+ window's disappearance is not evidence the CLI exited

Two tests, both ablated: the new one fails with exactly the old string when the change is reverted, and a guard pin proves the repair-actually-ran path keeps its own wording. 4978 passed, pyright 0, trunk clean.

Thanks for holding the line on this one — the P2 was right and my triage was not.

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 389a87bf32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

`_fix_phase` returned a bare DEFER on budget exhaustion, and both callers
substitute verify-centric text for an empty reason. So a repair session the
multiplexer destroyed was filed as `verify commands kept failing after clean
review` — the bmad-code-org#489 misdiagnosis one layer out, blaming the tree for repairs
that never ran, where the original at least named a crashed session.

Carry the last non-completed session's own failure into the DEFER. The
callers' `fix.reason or ...` fallback picks it up, so no caller changes and
the wording is untouched when the repair actually ran and only verify failed.
`fix-decision` gains `session_vanished` for parity with `dev-decision`.

Diagnosis only; routing is unchanged.
@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 9bc39ef6d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_engine.py (1)

7122-7128: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pin one journal record per consumed session.

Both tests use all(...) on non-empty lists. A regression that drops a later journal record can still pass.

  • tests/test_engine.py#L7122-L7128: assert len(decisions) == 2 and len(ends) == 2 before checking the diagnostic fields.
  • tests/test_engine.py#L7234-L7235: assert len(fixes) == 2 before checking session_vanished.
🤖 Prompt for 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.

In `@tests/test_engine.py` around lines 7122 - 7128, Strengthen the journal
assertions in tests/test_engine.py at lines 7122-7128 by requiring exactly two
decisions and exactly two session-end records before checking their fields; at
lines 7234-7235, require exactly two fixes before validating session_vanished.
Preserve the existing diagnostic assertions.
🤖 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.

Outside diff comments:
In `@tests/test_engine.py`:
- Around line 7122-7128: Strengthen the journal assertions in
tests/test_engine.py at lines 7122-7128 by requiring exactly two decisions and
exactly two session-end records before checking their fields; at lines
7234-7235, require exactly two fixes before validating session_vanished.
Preserve the existing diagnostic assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a64cf9ba-445f-42db-aecc-03f6864cf2f3

📥 Commits

Reviewing files that changed from the base of the PR and between 9a0cc73 and 9bc39ef.

📒 Files selected for processing (3)
  • src/bmad_loop/engine.py
  • tests/test_engine.py
  • tests/test_sweep.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_sweep.py

`all(...)` over a non-empty list is vacuous about length, so a regression
that dropped a later dev-decision, session-end or fix-decision still passed
while the session-count assertions held: those pin that the sessions RAN,
not that each was recorded. Count the records too.
@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@coderabbitai accepted and fixed in f368c9b — all three sites.

Verified the counts before asserting them (dev-decision 2, session-end 2, fix-decision 2, sessions 2). The point is exact: assert len(adapter.sessions) == 2 pins that both sessions ran, and says nothing about whether each was recorded — so a regression dropping a later journal entry slipped through the all(...), which is vacuous about length. That is the same vacuity class as your earlier finding, one layer in, and these tests exist specifically to make the diagnosis greppable in the journal, so record count is the thing they should be pinning.

-    assert decisions and all(d["session_vanished"] is True for d in decisions)
+    assert len(decisions) == 2 and all(d["session_vanished"] is True for d in decisions)
-    assert ends and all(e["session_vanished"] is True for e in ends)
+    assert len(ends) == 2 and all(e["session_vanished"] is True for e in ends)
-    assert fixes and all(f["session_vanished"] is True for f in fixes)
+    assert len(fixes) == 2 and all(f["session_vanished"] is True for f in fixes)

Left the two test_sweep.py lost-session tests alone deliberately: they read a single decision via [-1] rather than folding all(...) over a list, so the pattern does not apply there.

4978 passed, pyright 0 errors, trunk clean.

@pbean

pbean commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@pbean Confirmed. The added len(...) == 2 assertions verify that both expected journal records exist before checking session_vanished.

len(adapter.sessions) == 2 verifies execution only. It does not verify journal completeness. The unchanged test_sweep.py assertions use a single selected record, so this specific issue does not apply there.

Thanks for the precise fix and validation results.

🐇


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: f368c9b04d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pbean
pbean merged commit a165f79 into bmad-code-org:main Aug 11, 2026
10 checks passed
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Aug 11, 2026
The floor was never written down anywhere — no constant, no gate, no docs
sentence — so the only floor a reader could derive was the one implied by the
argv grammar, which is far older than anything the project tests. Reading it
that way advertises support for tmux releases nobody has verified, so say the
real number and say explicitly that the grammar is not the floor.

Nothing is enforced here: tmux is still selected on the presence of the binary
alone. psmux's version gate is a separate requirement for unrelated reasons
(recycled-PID kills up to 3.3.6) and is untouched.

Also records the psmux has-session foreign-server residual, which bmad-code-org#522 made
user-facing: one server per session means a -t read naming a dead session can
be answered by another server. A wrong True drops the lost-session diagnosis
rather than inventing one, and needs an operator-created name collision.
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.

2 participants