Fix BRC consensus deadlocks - #1190
Conversation
This comment has been minimized.
This comment has been minimized.
Autofix tracking{"Lint/Python": 1} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns.
The orchestrator-side consensus fixes (peer_consensus.py, concurrent_executor.py, review_graph.py) are server-enforced logic — exactly where constraints belong per the "sandbox is the constraint" principle. The checker-mode.md update correctly replaces deprecated readiness signals with BRC protocol commands, which is necessary protocol documentation (informing the agent about available tools), not procedural micromanagement — the agent retains full freedom in how it runs checks and what it reports.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review: Fix BRC consensus deadlocks
Thorough review of all 5 changed files. The re-proposal guard and improved error messages are solid. Two blocking issues identified — one is a non-functional feature, one is a correctness bug.
Blocking
1. excuse_reviewer is dead code — "Continue without" HITL option is non-functional
Files: peer_consensus.py:519-532, review_graph.py:108-117, concurrent_executor.py:279
The PR adds excuse_reviewer() and ReviewGraph.remove_edge() as building blocks for the "Continue without" HITL resolution path. However, nothing calls excuse_reviewer() when a human selects "Continue without".
The decision resolution route (routes/decisions.py:289-371) stores the resolution and emits a DECISION_RESOLVED event. No subscriber acts on it to call excuse_reviewer(). I verified: no code in the orchestrator dispatches on the "Continue without" resolution string.
Impact: Selecting "Continue without" resolves the HITL decision but leaves the dead reviewer's edges in the graph. is_fully_acked() will never pass for affected producers. Consensus remains permanently stuck — the exact deadlock this PR claims to fix.
Fix: Add a DECISION_RESOLVED handler (or a callback in _handle_single_failure's return path) that checks if the resolution is "Continue without" and calls tracker.excuse_reviewer(failed_role). The crash_result dict already carries failed_role and blocking_producers — the information needed to act.
2. False positive in handle_agent_crash when producer hasn't proposed yet
File: peer_consensus.py:473-481
entry = self.matrix.get_entry(role, producer)
latest_version = self.matrix.get_proposal_version(producer)
if (
entry is None
or entry.state != ApprovalState.ACKED
or entry.version != latest_version
):
blocking_producers.append(producer)When latest_version == 0 (producer hasn't proposed), the entry is in PENDING state (initialized by ApprovalMatrix.__init__). The condition entry.state != ApprovalState.ACKED evaluates to True, so the producer is added to blocking_producers even though there's nothing to review.
Impact: Unnecessary HITL escalation with a misleading message: "Reviewer had pending reviews for: ['coder']" when the coder hasn't proposed anything. The human is asked to make a decision based on inaccurate information.
Fix: Add a guard for latest_version > 0:
if (
latest_version > 0
and (
entry is None
or entry.state != ApprovalState.ACKED
or entry.version != latest_version
)
):
blocking_producers.append(producer)Non-blocking
3. test_re_propose_allowed_after_nack doesn't exercise _skip_ack_guard
File: test_peer_consensus_integration.py:1078-1097
After NACK, the producer's phase is WORKING (not PROPOSED) and is_fully_acked() returns False. The guard in _handle_propose_inner requires both is_fully_acked() == True and phase == PROPOSED. Neither condition holds after NACK, so the guard wouldn't trigger even without _skip_ack_guard=True.
The _skip_ack_guard bypass matters in a different scenario: handle_re_propose called with changed_artifacts that don't overlap any reviewer's artifact refs, when the producer is fully ACKed and still in PROPOSED state. Consider adding a test for that path.
4. Stale ApprovalMatrix entries after excuse_reviewer
File: peer_consensus.py:526-529
excuse_reviewer removes edges from the graph via remove_edge() but doesn't clean up the corresponding entries in ApprovalMatrix._entries. Since is_fully_acked() queries self._graph.reviewers_for(producer) — which no longer includes the excused reviewer — the stale entries don't affect correctness. But they persist in serialized output (matrix.to_dict()), which could cause confusion in debugging or state inspection.
5. _confirmed.discard(role) skipped on escalation paths
File: peer_consensus.py:498-515
Both sole_reviewer_for (pre-existing) and blocking_producers (new) return early before self._confirmed.discard(role) on line 514. If a reviewer was in _confirmed at crash time, the escalation path doesn't remove it. Not harmful in practice since the HITL decision blocks further progress, but it's cleaner to always clean up confirmed state on crash regardless of escalation. Consider moving self._confirmed.discard(role) above the reviewer-specific checks.
6. get_blocking_edges vs is_fully_acked inconsistency
File: approval_matrix.py:180-189
get_blocking_edges skips reviewers where entry is None (via if entry and ...), but is_fully_acked treats entry is None as blocking. In the new handle_confirmed message (peer_consensus.py:372-373), pending_reviewers could theoretically miss a reviewer. In practice entries are always initialized from graph edges, so this won't happen — but the asymmetry is worth noting.
What's good
- The re-proposal guard (
_handle_propose_innerACK guard) correctly prevents the deadlock scenario where a fully-ACKed producer re-proposes instead of confirming. The guard condition (fully ACKed AND PROPOSED phase) is precise. handle_re_proposecorrectly passes_skip_ack_guard=Truesince it's always a legitimate path (post-NACK or with changed artifacts).- The checker-mode.md rewrite properly aligns the checker agent instructions with the BRC protocol, replacing stale readiness-based signals with ACK/NACK/confirmed.
- The improved
handle_confirmederror message listing pending reviewers is a good debugging aid. - Test coverage for the new guard, crash scenarios,
excuse_reviewer, andremove_edgeis solid.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Addressed all blocking issues and one non-blocking issue from the review:
Blocking #1 — excuse_reviewer wired up (bf3a8ea)
_handle_single_failurenow storesfailed_role:<role>in the decision'scontextfieldresolve_decisioninroutes/decisions.pychecks for "Continue without" resolution and callstracker.excuse_reviewer(failed_role)to remove the dead reviewer's edges from the graph- Added 2 tests in
test_decisions_routes.pycovering the happy path and the negative case
Blocking #2 — False positive guard for latest_version == 0 (bf3a8ea)
- Added
latest_version > 0guard inhandle_agent_crashso producers that haven't proposed yet are not counted as blocking - Added test
test_reviewer_crash_before_producer_proposes_continues
Non-blocking #5 — _confirmed.discard(role) on escalation paths (bf3a8ea)
- Moved
self._confirmed.discard(role)above the reviewer-specific checks so it always runs, even on escalation return paths - Added test
test_reviewer_crash_clears_confirmed_on_escalation
Non-blocking #3 (test coverage for _skip_ack_guard): Acknowledged — the current test doesn't exercise the specific guard path. Will consider adding a targeted test in a follow-up.
Non-blocking #4 (stale ApprovalMatrix entries): Acknowledged — the stale entries don't affect correctness since is_fully_acked queries the graph. Cleanup could help debugging but is low priority.
Non-blocking #6 (get_blocking_edges vs is_fully_acked asymmetry): Acknowledged — in practice entries are always initialized from graph edges so this can't happen. Noted for future hardening.
All 1816 orchestrator tests pass.
— Authored by egg
There was a problem hiding this comment.
No agent-mode design concerns with the delta since 168d499.
The review-feedback fixes (wiring excuse_reviewer in decisions.py, version-0 guard, _confirmed.discard reordering) are all server-side orchestrator logic — exactly where constraints belong.
The broader changes in this squashed commit are net-positive for agent-mode alignment:
- Prompt simplification (
pipelines.py): Removing tool-specific WebSearch/WebFetch instructions and "Parallel Execution with Subagents" procedural sections reduces micromanagement. Agents should discover available tools and decide on parallelization themselves. - Skill simplification (
SKILL.md): Removing consensus fallback parsing, silent agent detection, grace period logic, and stuck pipeline rescue reduces rigid procedures. The simplified version focuses on objectives over step-by-step prescriptions. - Agent spawning (
command.py,consensus_wrapper.py): Moving frompython3 -m egg_agenttoclaude --printis an implementation change that still flows through the centralizedbuild_agent_command()abstraction — no raw API calls introduced.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review: Fix BRC consensus deadlocks (bf3a8ea)
Reviewed the delta from 168d499 → bf3a8ea. All three addressed issues verified against the actual code, tests pass per author. No new blocking issues.
Previous Issues — Verification
Blocking #1: excuse_reviewer wired up — Fixed correctly
The integration is sound:
concurrent_executor.py:284stores"failed_role:{role}"indecision.contextafteradd_decision()routes/decisions.py:350-370checks for"Continue without"resolution +"failed_role:"context prefix, extracts the role, and callstracker.excuse_reviewer(failed_role)excuse_reviewerremoves all edges viagraph.remove_edge(), cleans up_confirmedand_reviewer_phases- Error handling wraps
excuse_reviewerin try/except with logging — exception doesn't block the resolution response
Two tests cover the happy path and negative case in TestContinueWithoutExcusesReviewer.
Blocking #2: False positive guard — Fixed correctly
latest_version > 0 guard at peer_consensus.py:482 correctly skips producers that haven't proposed yet. When latest_version == 0, the producer is not added to blocking_producers, preventing spurious HITL escalation.
Test test_reviewer_crash_before_producer_proposes_continues verifies the fix.
Non-blocking #5: _confirmed.discard on escalation — Fixed correctly
self._confirmed.discard(role) moved to peer_consensus.py:457, above all reviewer-specific branches. Now runs unconditionally on every crash path, including sole_reviewer_for and blocking_producers early returns.
Test test_reviewer_crash_clears_confirmed_on_escalation verifies confirmed state is cleared even when the crash escalates.
Non-blocking observations on new code
1. excuse_reviewer doesn't validate the role is a reviewer
excuse_reviewer will silently succeed for non-reviewer roles — producers_for("coder") returns [], the loop doesn't execute, and the method returns {"status": "excused", "affected_producers": []}. This matters because _handle_single_failure creates a "Continue without" option for ALL agent types. If a human picks "Continue without" for a crashed producer, excuse_reviewer does nothing meaningful but reports success.
Not a regression — "Continue without" for producer failures is a pre-existing gap. Consider adding a guard: if not self.graph.is_reviewer(role): raise ValueError(...) or at minimum log a warning.
2. sole_reviewer_for return path excludes blocking_producers from crash result
When a reviewer is sole reviewer for some producers AND has pending reviews for others, handle_agent_crash returns on the sole_reviewer_for path (line 499) without the blocking_producers key. The HITL question then omits the pending review info. Functionally harmless since excuse_reviewer removes ALL edges regardless, but the human sees incomplete information when making their decision.
3. Previous non-blocking #3 (test coverage for _skip_ack_guard) still open
test_re_propose_allowed_after_nack exercises the re-propose path but doesn't actually trigger the guard (producer phase is WORKING post-NACK, not PROPOSED). The targeted scenario — handle_re_propose called when producer is fully ACKed and still in PROPOSED state — remains untested. Acknowledged in the feedback response as a follow-up item.
What's good in the new commit
- The
decision.contextintegration betweenconcurrent_executor.pyandroutes/decisions.pyis clean — string format is simple, documented with comments on both ends, and thecontextfield'sstrtype with""default meansstartswith()is always safe (neverNone). - The
_skip_ack_guardparameter is keyword-only (*separator) with underscore prefix — correct internal API design that prevents accidental misuse. - Test coverage is comprehensive: 8 new test classes/methods covering the guard, crash scenarios, excuse_reviewer, remove_edge, and improved error messages.
- Error handling in
resolve_decisionwrapsexcuse_reviewerin try/except so a tracker error doesn't break the resolution HTTP response.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ocking_producers in sole_reviewer crash path
|
Addressed non-blocking observations #1 and #2 from the re-review (fd68545): #1 —
#2 —
#3 (test coverage for All 1819 orchestrator tests pass. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review: Fix BRC consensus deadlocks (fd68545)
Reviewed the delta from bf3a8ea → fd68545. Two changes across two files. Both previous non-blocking suggestions correctly addressed. No blocking issues.
Previous Suggestions — Verification
#1: excuse_reviewer validates the role is a reviewer — Fixed correctly
The is_reviewer(role) guard at peer_consensus.py:535 raises ValueError before any mutation occurs. is_reviewer checks _reviewer_roles, which is built from graph edges — unknown roles and pure producers both correctly evaluate to False.
The try/except in routes/decisions.py:360-367 catches the ValueError and logs a warning instead of breaking the HTTP response. The decision is still resolved (that happened on line 335 via queue.resolve_decision()), which is the correct boundary: the decision record reflects the human's choice, and the failed side-effect is visible in logs.
Tests test_excuse_non_reviewer_raises and test_excuse_unknown_role_raises cover both cases with correct match patterns.
#2: sole_reviewer_for path includes blocking_producers — Fixed correctly
peer_consensus.py:503-506 conditionally adds blocking_producers to the result dict when populated. concurrent_executor.py:271 already checks crash_result.get("blocking_producers"), so the HITL question now surfaces pending reviews for other producers when the reviewer is sole for some but not all.
The if blocking_producers: guard at line 503 is correct — if the reviewer is sole for all assigned producers, blocking_producers is empty and omitted. The test at test_peer_consensus_integration.py:1349-1378 correctly sets up the mixed scenario (sole for coder, pending for tester with checker as backup).
Non-blocking
Test could assert blocking_producers length
test_sole_reviewer_crash_includes_blocking_producers asserts "tester" in result["blocking_producers"] but doesn't assert the list length. Adding assert result["blocking_producers"] == ["tester"] would more precisely verify that coder (which is in sole_reviewer_for) is not also erroneously included. In practice the code is correct — sole_reviewer_for and blocking_producers are mutually exclusive per-producer by construction — so this is purely a test precision suggestion.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns with the delta since bf3a8ea.
The new changes (excuse_reviewer role validation, blocking_producers in sole_reviewer crash path) are server-side orchestrator logic — exactly where constraints belong.
One minor doc observation: the "right approach" example in agent-mode-design.md:228-235 shows raw ["claude", "--print", ...] args while the prose above (line 95) correctly says to use build_agent_command(). The example could show build_agent_command(...) for consistency, but this is a documentation nit, not a design concern.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Addressed remaining suggestions from the fd68545 re-review (bcc4059): Code review suggestion — test assertion precision: Tightened Agent-mode-design doc nit: Updated the "right approach" example in All 50 peer consensus integration tests pass. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns with the delta since fd68545.
Two changes reviewed:
bcc4059: Test assertion tightened andagent-mode-design.mdexample updated to usebuild_agent_command()— the doc fix actually improves agent-mode alignment by showing the correct abstraction instead of raw CLI args.9501289(merge from main): Doc conflict inagent-mode-design.mdresolved correctly, combining the PR'sbuild_agent_command()usage with main'sprompt=...parameter signature. Merged result:build_agent_command(prompt=..., max_turns=1)— accurate and consistent with the prose.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review: Fix BRC consensus deadlocks (9501289)
Reviewed the delta from fd68545 → 9501289. Direct PR changes: one commit (bcc4059) addressing previous review nits, plus a merge from origin/main resolving one doc conflict. No blocking issues.
Previous Suggestions — Verification
Test assertion precision — Fixed correctly
test_sole_reviewer_crash_includes_blocking_producers at test_peer_consensus_integration.py:1377 now asserts result["blocking_producers"] == ["tester"] instead of "tester" in result["blocking_producers"]. This verifies the list contains exactly one element and that coder (in sole_reviewer_for) is excluded.
Agent-mode-design doc example — Fixed correctly
agent-mode-design.md:231-237 now uses build_agent_command(prompt=..., max_turns=1) with the from egg_agent import build_agent_command import. The example matches the actual function signature at command.py:11-12 (positional prompt, keyword-only max_turns).
Merge from main — Verification
The merge commit (9501289) brings in 8 already-reviewed PRs (#1183, #1187, #1188, #1189, #1191, etc.). One conflict was resolved in agent-mode-design.md:
- PR branch had:
build_agent_command(max_turns=1, ...)with old comment referencing "claude --print" - Main had: Agent SDK migration updating both comment and command references
- Resolution correctly combines both: updated comment ("Agent SDK"), updated call (
build_agent_command(prompt=..., max_turns=1))
The merged code (Agent SDK migration in consensus_wrapper.py, command.py; prompt additions in pipelines.py; new skills) is orthogonal to the PR's consensus deadlock fixes in peer_consensus.py, concurrent_executor.py, routes/decisions.py, and review_graph.py. No interaction issues.
All previous blocking issues remain fixed
Verified all three previously-addressed items are intact post-merge:
excuse_reviewerwired up:concurrent_executor.py:284stores context,routes/decisions.py:350-357callstracker.excuse_reviewer()on "Continue without" resolution. Integration path intact.- Version-0 guard:
peer_consensus.py:480—if latest_version > 0guard prevents false-positive HITL escalation for unproposed producers. _confirmed.discardordering:peer_consensus.py:457— runs unconditionally before reviewer-specific branches.
No new issues found. This PR is ready to merge.
— Authored by egg
|
egg review completed. View run logs 14 previous review(s) hidden. |
Fix four interacting bugs that caused permanent consensus stalls during pipeline
issue-1165(implement phase).Context: During concurrent execution, consensus got permanently stuck because (1) the coder re-proposed after being fully ACKed, resetting all ACKs; (2) a crashed reviewer (tester) blocked consensus even though other reviewers remained; (3) the checker agent used old readiness signals instead of BRC ACK/NACK; and (4) premature-confirm errors gave no actionable information.
Changes:
_handle_propose_inner()against proposals when the producer is already fully ACKed and in PROPOSED state, while allowing legitimate re-proposals viahandle_re_propose()handle_agent_crash()to escalate to HITL when a dead non-sole reviewer has pending (un-ACKed) reviews; addexcuse_reviewer()andReviewGraph.remove_edge()for the "Continue without" HITL resolution pathchecker-mode.mdwith BRC consensus protocol (ACK/NACK/confirmed)Issue: #1185
Test plan:
python -m pytest orchestrator/tests/test_peer_consensus_integration.py -v— 45 tests pass (36 existing + 9 new), zero regressions