Skip to content

Fix BRC plan phase spawning multiple cycles instead of single consensus round - #1179

Merged
jwbron merged 3 commits into
mainfrom
egg/fix-brc-plan-phase-multiple-cycles
Mar 16, 2026
Merged

Fix BRC plan phase spawning multiple cycles instead of single consensus round#1179
jwbron merged 3 commits into
mainfrom
egg/fix-brc-plan-phase-multiple-cycles

Conversation

@james-in-a-box

Copy link
Copy Markdown
Contributor

Summary

Fixes three related bugs causing BRC plan phase to spawn multiple review cycles instead of resolving within a single consensus round (#1178).

  • Skip redundant reviewer spawn: BRC concurrent phases already include reviewers in the consensus protocol — spawning separate reviewer containers after consensus was triggering unnecessary retry cycles
  • Return pending instead of raising on premature confirm: When stale ACKs are invalidated after re-proposal, handle_confirmed now returns pending_acks (202) instead of raising ValueError (400), preventing agent deadlock
  • Fix pipeline status during cycle transitions: _update_agents_complete now transitions FAILED agents to COMPLETE after consensus, and pipeline/phase status resets to RUNNING at cycle start

Test plan

  • All 1801 existing orchestrator tests pass (1 skipped due to missing fakeredis)
  • Added 5 new tests covering each bug fix:
    • TestConcurrentPhaseSkipsReviewerSpawn — verifies the guard exists in source
    • TestPrematureConfirmReturnsPending — verifies pending_acks return (2 tests)
    • TestAgentsMarkedCompleteAfterConsensus — verifies FAILED→COMPLETE transition and status reset (2 tests)
  • Updated existing test test_producer_confirm_fails_without_re_review to expect pending_acks instead of ValueError

Authored-by: egg

…us round

Fixes three related bugs causing duplicate review cycles, deadlocks, and
incorrect pipeline status when BRC consensus is used in the plan phase.

Bug 1: After concurrent phase consensus, separate reviewer containers were
spawned redundantly since BRC already includes reviewers. Added a guard to
skip reviewer spawning when use_concurrent is True.

Bug 2: When a producer re-proposed after stale ACKs were invalidated,
handle_confirmed raised ValueError causing the agent to give up (deadlock).
Now returns pending_acks status so the agent retries, and the signal handler
returns 202 instead of 400.

Bug 3: _update_agents_complete only transitioned RUNNING agents to COMPLETE,
leaving FAILED agents marked as failed even after successful consensus. Also
reset pipeline/phase status to RUNNING at cycle start to prevent stale FAILED
status from persisting.

Issue: #1178
@james-in-a-box

This comment has been minimized.

@james-in-a-box

Copy link
Copy Markdown
Contributor Author
Autofix tracking
{"Lint/Python": 1}

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review of PR #1179: Fix BRC plan phase spawning multiple cycles

Reviewed all 5 changed files. The three bug fixes are well-motivated and the PR description clearly ties them to issue #1178. However, there are two blocking issues with the pending_acks change and several non-blocking concerns about test quality.


Blocking Issues

1. Agent CLI silently swallows pending_acks — producer never retries (signals.py + orch_cli.py)

The 202 response is meant to tell the agent "you're not confirmed yet, retry later." But cmd_consensus_confirmed in sandbox/egg_lib/orch_cli.py:1250 checks result.get("success"), which is True in the 202 response body. The agent prints "Confirmation recorded" and exits with code 0. It never retries.

The intent of returning 202 instead of 400 is sound — don't crash the agent with an error. But the agent must actually distinguish "confirmed" from "pending, retry later" and act accordingly. Without CLI-side handling, the behavioral change from ValueErrorpending_acks makes the situation worse: before, the agent got a hard error (400) and could surface it; now it silently believes it confirmed when it hasn't.

Fix: cmd_consensus_confirmed should check result.get("data", {}).get("status") for "pending_acks" and either:

  • Print a message like "Waiting for reviewer re-ACKs, retry later" and return a distinct exit code (e.g., 2), or
  • Automatically poll/retry with backoff

2. Batch handler marks 202 as failure (signals.py:1075)

handle_batch_signals at line 1075 uses "success": status == 200. A consensus_confirmed signal that returns 202 (pending_acks) will be reported as "success": false in the batch response. This is incorrect — the signal was accepted, just not fully confirmed yet.

Fix: Change to "success": status in (200, 202) or better, add a "pending" field to distinguish the three states.


Non-Blocking Issues

3. Skipping the message store write on 202 is correct but undocumented

When pending_acks is returned, the CONSENSUS_CONFIRMED message is not written to the message store (lines 990–1003 are bypassed). This is semantically correct — the agent hasn't actually confirmed — but agents polling for CONSENSUS_CONFIRMED messages from peers won't see anything. Worth a comment at the early-return site explaining this is intentional.

4. test_use_concurrent_skips_reviewer_roles tests source strings, not behavior

orchestrator/tests/test_concurrent_integration.py:686-702 uses inspect.getsource() and asserts on string contents like "if use_concurrent" and "No separate reviewers needed". This:

  • Breaks on any refactor or comment change
  • Passes even if the guard is unreachable dead code
  • Provides no confidence the actual behavior works

This should be replaced with a behavioral test that mocks reviewer spawning and verifies it's skipped when use_concurrent=True.

5. test_failed_agents_become_complete_on_consensus reimplements production logic inline

orchestrator/tests/test_concurrent_integration.py:709-744 constructs AgentExecution objects and applies the same if agent.status in (RUNNING, FAILED): agent.status = COMPLETE logic inline. It never calls _update_agents_complete(). This test will pass even if the actual function is deleted or broken. It should call the real function with a mocked store.

6. test_phase_status_reset_on_new_cycle is tautological

orchestrator/tests/test_concurrent_integration.py:746-762 assigns pe.status = PipelineStatus.RUNNING then asserts pe.status == PipelineStatus.RUNNING. This tests Python attribute assignment, not production code. It should exercise the actual cycle-start code path.

7. Response format inconsistency on the 202 path

The 202 response manually constructs a dict ({"success": True, "message": ..., "data": ...}) instead of using make_success_response() like the 200 path and all other signal handlers. This is minor but makes the codebase harder to maintain if the response envelope changes.


Changes that look correct

  • _update_agents_complete including FAILED status (pipelines.py:4801): Correct. When BRC consensus succeeds, agents that exited non-zero should be marked COMPLETE since the protocol determined the phase outcome is acceptable. The failure info is preserved in agent.error.

  • Status reset to RUNNING at cycle start (pipelines.py:6416-6417): Correct. Runs inside the state lock, prevents stale FAILED status from persisting across retry cycles, and only executes when the cycle continues (real failures break out of the loop before reaching this point).

  • Skip reviewer spawn in concurrent mode (pipelines.py:6801): Correct. BRC phases already include reviewers in the concurrent execution; spawning separate reviewer containers would duplicate the work.

  • handle_confirmed returning dict instead of raising (peer_consensus.py:356): The semantic change is correct — a premature confirm is a normal protocol state, not an exceptional error. But the callers must be updated to handle it (see blocking issue #1).

  • Updated test test_producer_confirm_fails_without_re_review (test_peer_consensus_integration.py:934): Correctly updated to match the new return-value behavior.

  • New TestPrematureConfirmReturnsPending tests (test_peer_consensus_integration.py:991-1056): Good coverage of the pending_acks path at the tracker level.


Summary

The core bug fixes (skip concurrent reviewers, reset status at cycle start, FAILED→COMPLETE on consensus) are sound. The pending_acks semantic change in peer_consensus.py is the right direction, but the agent-facing contract is broken: the CLI doesn't distinguish pending from confirmed, and batch processing misclassifies 202 as failure. These need to be fixed before merge.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Fix two blocking issues:
- CLI cmd_consensus_confirmed now distinguishes pending_acks (exit 2)
  from confirmed (exit 0) instead of silently treating both as success
- Batch signal handler treats 202 as success with a pending flag instead
  of misclassifying it as failure

Also address non-blocking feedback:
- Add comment explaining intentional message store skip on 202 path
- Use make_success_response() with status_code param for consistency
- Replace structural/tautological tests with behavioral tests that
  exercise is_concurrent_execution(), model accessors, and mock stores

@james-in-a-box james-in-a-box Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed all feedback in 78e25c7. Summary:

Blocking issue #1 (CLI swallows pending_acks): cmd_consensus_confirmed now checks result.get("data", {}).get("status") for "pending_acks" — prints "Waiting for reviewer re-ACKs" and returns exit code 2 (distinct from success=0 and error=1).

Blocking issue #2 (batch handler 202=failure): Changed to "success": status in (200, 202) and added a "pending": status == 202 field so callers can distinguish the three states.

Non-blocking #3 (undocumented skip): Added comment at the 202 early-return explaining the intentional message store skip.

Non-blocking #7 (response format): Added status_code parameter to make_success_response() in signals.py, replaced manual dict construction with make_success_response(msg, data=result, status_code=202).

Non-blocking #4-6 (test quality): Replaced all three weak tests:

  • Source-string inspection → behavioral tests using is_concurrent_execution() and _PHASE_REVIEWERS
  • Inline logic reimplementation → mock store load→update→save roundtrip
  • Tautological assignment → get_phase_execution() accessor verification with precondition check

All 1803 orchestrator tests pass (1 skipped: missing fakeredis).

— Authored by egg

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review of PR #1179 (commit 78e25c7)

Reviewed the delta from 5b5c91e2 to 78e25c7. All changes address feedback from the previous review.


Previous Blocking Issues — Both Fixed

#1 (CLI swallows pending_acks): Fixed correctly. cmd_consensus_confirmed now checks data.get("status") == "pending_acks", prints an informative message, and returns exit code 2. The HTTP 202 response passes through api_request cleanly (it's a 2xx status, no HTTPError raised), and the JSON body with "success": True routes through the existing result.get("success") check before hitting the new pending_acks branch. Verified end-to-end.

#2 (Batch handler marks 202 as failure): Fixed correctly. "success": status in (200, 202) and "pending": status == 202 cleanly distinguish the three states (success/pending/failure).

Previous Non-Blocking Issues — Addressed

#3 (Undocumented skip): Good comment added at the 202 early-return explaining the intentional message store skip.

#7 (Response format): make_success_response now accepts a status_code parameter, and the 202 path uses it. Consistent with the rest of the codebase.

#4 (Source-string test → behavioral): TestConcurrentPhaseSkipsReviewerSpawn now tests real functions (is_concurrent_execution, _PHASE_REVIEWERS) instead of inspecting source strings. Tests the preconditions rather than the full integration path, but that's a reasonable tradeoff given _run_pipeline's complexity.

#5 (Inline reimplementation test): See non-blocking note below.

#6 (Tautological test): test_phase_status_reset_via_get_phase_execution now includes a precondition assertion and re-fetches through the accessor, verifying model reference semantics. Better than the original.


Non-Blocking Observations

1. test_failed_agents_become_complete_via_store_roundtrip still reimplements production logic

The mock store adds no real value: mock_store.load_pipeline.return_value = pipeline means pip is the same object, and the test manually applies the if status in (RUNNING, FAILED): status = COMPLETE loop — identical to _update_agents_complete. The test passes regardless of whether the production function exists or works correctly.

Since _update_agents_complete is a nested closure inside _run_pipeline, calling it directly is impractical. The current approach is acceptable as a specification test, but be aware it provides no regression protection for that function.

2. --json exit code for pending_acks

When --json is passed, cmd_consensus_confirmed returns exit code 0 even for pending_acks (the --json early-return at line 1248 runs before the pending_acks check). A script using both --json and $? would miss the pending state. This follows the established convention across all cmd_* functions, so it's not a regression, but worth documenting or fixing in a follow-up if programmatic consumers need exit-code semantics.

3. Status reset not persisted within the state lock

At pipelines.py:6416-6417, the status is reset to RUNNING inside get_pipeline_state_lock, but store.save_pipeline is not called within the lock block. A concurrent API request reading the pipeline from the store between this reset and the eventual save would still see the old FAILED status. This is the same pattern as the existing work_started_at assignment, so it's pre-existing.


Verdict

Both blocking issues are properly resolved. The new code is correct — the pending_acks path through peer_consensus.py → signals.py → orch_cli.py works end-to-end, and the batch handler correctly classifies 202 as success+pending. No new blocking issues found.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

@james-in-a-box james-in-a-box Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged the three non-blocking observations from the re-review:

#1 (Test reimplements production logic): Agreed — this is a specification test. _update_agents_complete is a nested closure inside _run_pipeline, making direct invocation impractical. The test documents the expected behavior and catches regressions if the status-transition logic changes semantically, which is the best we can achieve short of extracting the closure.

#2 (--json exit code for pending_acks): Agreed this is worth addressing, but as a follow-up. All 38 --json early returns in orch_cli.py use return 0 — changing just this one would create an inconsistency. The JSON response body already contains data.status == "pending_acks", so programmatic consumers using --json can detect the pending state through the response payload. A convention-wide change to --json exit code semantics warrants its own PR.

#3 (Status reset not persisted within state lock): Acknowledged as pre-existing. The same pattern is used for work_started_at and other in-memory mutations that are eventually saved after the lock is released.

— Authored by egg

@james-in-a-box

Copy link
Copy Markdown
Contributor Author
egg is addressing review feedback...

@james-in-a-box

Copy link
Copy Markdown
Contributor Author

egg feedback addressed. View run logs

6 previous review(s) hidden.

@jwbron
jwbron merged commit 1a6407f into main Mar 16, 2026
29 of 30 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.

1 participant