Skip to content

[BUG] Review session timeout always RETRY until max_review_cycles; no salvage/converge exit #271

Description

@Haven2026

Description

decide_review_session treats any non-completed review session status the same way: if review_cycle < max_review_cycles, return RETRY; else DEFER/PAUSE.

There is no branch for:

  • timeout / timeout_fired vs crash vs user-stop,
  • “prior review cycle already left the story status: done + verify-green — converge and refile residual follow-up instead of burning another full review,”
  • “do not spend max_followup_reviews after a pure timeout with no new review artifact.”

Result under unattended multi-story runs: a 90m session timeout on review-1 (or a heavy codex second-opinion) becomes review-retry → review-2 → review-3… until the outer cycle budget dies, even when the product tree from dev is already good enough to commit/salvage.

Steps to reproduce

  1. Policy: review.enabled = true, review.trigger = "recommended" (or always), limits.session_timeout_min = 90, limits.max_review_cycles = 3, limits.max_followup_reviews = 1.
  2. Adapter review on a slower/heavier model (field: codex second opinion) so a single review pass can hit session timeout.
  3. Dev pass finishes with a green-enough tree and often already followup_review_recommended from inline review.
  4. Watch journal: review session non-completed → review-retry → next cycle → repeat until cycles exhausted or human stops the run.

Expected behavior

At least one of (policy-selectable is fine):

  1. Timeout exit class: after N timeouts (default 1) on review, if the last known good state is spec status: done (and optional verify commands green), converge: commit / refile lingering follow-up to deferred-work, do not open another full review pass solely because the session timed out.
  2. No follow-up grant after timeout: a timeout that produced no successful review result_json should not consume or re-arm followup_review_recommended damping in a way that schedules another independent pass with empty product delta.
  3. Journal a distinct event e.g. review-timeout-salvage vs bare review-retry so external supervisors can act without guessing.

Actual behavior

From installed 0.8.1 escalation.decide_review_session:

if result.status != "completed":
    reason = f"review session {result.status}"
    if budget_left:
        return Decision(Action.RETRY, reason)
    return Decision(_exhausted_action(task), _exhaust_reason(task, reason))

Engine then journals review-retry and continues the review loop. Field story (health-matrix closeout style epic endgame): review-1 timeout → review-2 completes with follow-up flag → review-3 still scheduled while wall-clock and tokens burn — human/AI salvage had to stop the run and ship product via external PR because the engine would not exit the review stack.

Which area is this for?

Orchestrator / control loop

bmad-loop Version

0.8.1

Which coding CLI are you using?

Codex (codex)

(also observed with mixed claude dev + codex review)

Operating System

macOS

Relevant log output

# pattern (sanitized):
# review-session status=timeout (or non-completed)
# journal: review-retry reason="review session timeout"
# review_cycle increments
# eventually max_review_cycles → DEFER, or human bmad-loop stop
# worktree still holds good product code; origin may never see it without salvage

No diagnose dump attached (contains project paths). Happy to produce a sanitized bmad-loop diagnose excerpt if a maintainer wants a specific run shape.

Confirm

  • I've searched for existing issues
  • If I attached a diagnose dump, I reviewed it and confirmed it contains no PII, secrets, or project-specific information
  • I'm using the latest version

Related

Notes on philosophy fit

Still deterministic Python — classify session end reason + prior terminal status; no LLM in the control loop. Optional policy key e.g. review.on_timeout = "retry" | "salvage-if-done" | "defer" keeps default = today’s retry for safety.

Willing to PR a minimal change after design nod (CONTRIBUTING: large/behavior change → Discord first). Suggested minimal patch surface: escalation.decide_review_session + engine journal + one unit test matrix on SessionResult.status.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions