Fix #2064: reject wait_loop on CONSENSUS_CONFIRMED for non-confirmed producers - #2077
Conversation
…producers A producer's own consensus_confirmed is part of what generates the global CONSENSUS_CONFIRMED signal — waiting on it before the producer has reached CONFIRMED state is a circular dependency that deadlocks the pipeline until the overseer's heartbeat-stall band-aid eventually intervenes. Observed in pipeline issue-1965 (PR #2061): the documenter proposed, called consensus confirmed but got status='pending_acks' (because coder/tester hadn't yet proposed and the global_zero_proposal guard fired), then entered the post-confirm STAY ALIVE wait_loop on CONSENSUS_CONFIRMED as if confirm had succeeded. The agent sat there for ~36 minutes waking briefly on each peer's confirm but never re-attempting its own. Only the agent-heartbeat-stall OVERSEER_ALERT broke it out. This change adds a server-side guard in /messages/wait that returns HTTP 400 with an actionable error when the caller is a producer in WORKING/PROPOSED state and CONSENSUS_CONFIRMED appears in for_types. The error tells the agent to call mcp__brc__confirm and, if it returns pending_acks, to wait on the prerequisite events instead (CONSENSUS_PROPOSE, CONSENSUS_ACK, CONSENSUS_RE_REVIEW) before retrying confirm. The fix is structural — matching the project's preference for infrastructure constraints over prompt-based rules — and applies generically to every producer role (coder, tester, documenter, and any future producer), not just the documenter case that surfaced it. - orchestrator/peer_consensus.py: add is_producer_pending_confirm helper - orchestrator/routes/messages.py: add guard at the wait endpoint - orchestrator/tests/test_messages.py: 7 cases covering the documenter scenario, dual-role agents, reviewer-only roles, missing tracker, missing role, and other for_types passthrough - orchestrator/tests/test_peer_consensus_integration.py: 5 cases for the helper itself across the producer state machine - docs/reference/agent-wait-patterns.md: anti-pattern 5 documenting the deadlock and the recovery idiom Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed end-to-end: the new server-side guard, the is_producer_pending_confirm helper, the route wiring, both test classes, and the docs change. I also walked the BRC state machine (peer_consensus.handle_propose / handle_ack / handle_nack / handle_confirmed) and the wait-loop client (sandbox/egg_agent_tools/handlers/message.py) to confirm the 400 → exit-code-3 contract. The fix is correct and well-targeted: it surfaces the deadlock as an actionable, immediate error instead of a 36-minute silent stall.
No blocking issues. A few non-blocking notes below.
Non-blocking
1. Misleading comment in test_peer_consensus_integration.py
orchestrator/tests/test_peer_consensus_integration.py:3071 reads:
# Reviewer must confirm before producers can confirm cleanly
result = t.handle_confirmed("reviewer_code")That isn't true. check_confirm_guard (orchestrator/action_guards.py:283) gates a producer's confirm only on global_zero_proposal + producer_not_fully_acked — there is no dependency on reviewer confirms. The test happens to confirm the reviewer first but doesn't need to. Either drop the reviewer confirm (and the comment) or rewrite the comment to "Confirming reviewer first to keep the assertion order tidy" so future readers don't take it as a state-machine rule.
2. Dual-role tester is fixtured but never asserted on
The PR description lists "dual-role" as one of the seven endpoint test cases, and implement_tracker does include tester in the graph — but no test exercises role=tester. The dual-role behaviour is well-defined by the existing implementation (both producer and reviewer phases transition together inside the same handle_confirmed call), and the helper covers it implicitly via the producer-phase check. Still, a one-line test like test_dual_role_tester_in_proposed_blocked would lock the contract in for the tester-specific case and match what the PR description promises. Not a blocker.
3. Import-shim style drift inside routes/messages.py
The existing peer_consensus import in _apply_delphi_filter (orchestrator/routes/messages.py:319-322) is a one-tier try / except → None pattern. The new helper at L387-395 introduces a two-tier from peer_consensus … / from ..peer_consensus … fallback. Both work, but the inconsistency invites future drift. Suggest matching the established one-tier shape — the package-relative fallback is unnecessary in this file (every other peer_consensus import here is single-tier and this code only runs in-process where peer_consensus resolves at the top-level). Tightening this also drops the # pragma: no cover line.
4. Guard ignores from_role filter
_check_producer_pending_confirm_guard rejects any wait that includes CONSENSUS_CONFIRMED, regardless of whether the caller scoped it to a peer (e.g. from=reviewer_code). Today, no documented producer pattern waits on a peer's per-agent CONSENSUS_CONFIRMED while still in WORKING/PROPOSED — every prompt only does so post-confirm — so the over-rejection is harmless. Worth a code comment noting the intentional broadness, in case a future use case (e.g. cross-producer sync) tries this and gets surprised.
5. Doc nit — exit-code wording
docs/reference/agent-wait-patterns.md: "the wrapper surfaces this as exit code 3 (permanent error)." Verified against sandbox/egg_agent_tools/handlers/message.py:319-323 (the wait_loop raises GatewayError on 4xx-non-408) and the rc=3 contract in §3 of the same doc. Accurate. No change needed — flagging only because it's the kind of cross-file claim that rots silently.
Verified correct
- Reviewer-only roles short-circuit on
is_producer(role) == False(peer_consensus.py:1259). - Dual-role agents (tester) transition both
_producer_phasesand_reviewer_phasesin the samehandle_confirmedcall (L600-604), so a successfully-confirmed tester returns False from the helper. - NACK regresses producer to WORKING (
peer_consensus.py:384); a previously-CONFIRMED producer correctly re-enters the guard's reject path until it re-proposes and re-confirms. - The guard fires before any expensive work: route enters
_check_producer_pending_confirm_guardbeforeget_message_store()and the long-poll bookkeeping (messages.py:466-472). - 4xx surface:
sandbox/egg_agent_tools/handlers/message.py:319-323correctly maps 400 →GatewayError→ permanent for both the CLI shim andmcp__brc__wait_loop. - Test class names and fixture imports match the existing
test_messages.pystyle;patch("peer_consensus.get_peer_consensus_tracker", …)correctly intercepts the route's localfrom peer_consensus import ….
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Drop misleading 'reviewer must confirm before producers' comment in test_producer_in_confirmed_is_not_pending and remove the unnecessary reviewer handle_confirmed call. check_confirm_guard only gates a producer's confirm on global_zero_proposal + producer_not_fully_acked, not on reviewer confirms. - Add test_dual_role_tester_in_proposed_blocked locking the helper's contract for the tester (dual-role) case the implement_tracker fixture exercises. - Simplify the peer_consensus import-shim in _check_producer_pending_confirm_guard to match the one-tier pattern already used by _apply_delphi_filter — drops the unused package-relative fallback. - Add an inline comment documenting the intentional broadness of the guard wrt the from_role query parameter.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-reviewed the delta between eeb10f4 and 31d32db. All five suggestions from the previous review were addressed substantively:
- Misleading comment —
test_producer_in_confirmed_is_not_pendingno longer claims reviewer must confirm first; the redundanthandle_confirmed("reviewer_code")call is gone and the new comment correctly citescheck_confirm_guard's actual gates. - Dual-role tester —
test_dual_role_tester_in_proposed_blockedadded atorchestrator/tests/test_messages.py:1709and asserts the 400 path with the right message fragments. - Import-shim drift —
_check_producer_pending_confirm_guardnow matches the one-tier shape used by_apply_delphi_filterin the same file (orchestrator/routes/messages.py:394-399); the# pragma: no coverand unused package-relative fallback are gone. from_rolebroadness — Inline comment added atorchestrator/routes/messages.py:387-393documenting the intentional over-rejection.- Exit-code wording — No change requested.
Verified: pytest orchestrator/tests/test_messages.py orchestrator/tests/test_peer_consensus_integration.py → 173 passed (was 172, +1 for the new dual-role test). Ruff clean.
No blocking issues. A few minor non-blocking observations on the new code:
Non-blocking
1. Dead weight in test_producer_in_confirmed_is_not_pending
Now that the reviewer's confirm is dropped, the immediately-prior t.handle_ack("reviewer_code", "documenter", …) (orchestrator/tests/test_peer_consensus_integration.py:3079) and its comment ("Advisory ACK still required for reviewer to satisfy 'must have reviewed' guard, even though it doesn't gate is_fully_acked") no longer serve any purpose:
ApprovalMatrix.is_fully_acked(orchestrator/approval_matrix.py:188) only iteratescritical_reviewers_for(producer)— documenter has zero critical reviewers in this fixture, so it's already fully ACKed without that line.- The "must have reviewed" guard the comment references only applies when the reviewer tries to confirm, which the test no longer does.
The test still passes; the leftover line and comment just refer to a guard that's no longer exercised. Either drop both, or trim the comment to "Advisory ACK is informational here — kept for symmetry with real pipelines" so future readers don't chase a guard that isn't being tested.
2. test_dual_role_tester_in_proposed_blocked doesn't exercise dual-role behavior
The implement_tracker fixture (orchestrator/tests/test_messages.py:1574-1581) does NOT include the ReviewEdge("tester", "coder", CRITICAL) that the default get_default_implement_graph ships with — so in this fixture tester is purely a producer, not dual-role. The docstring acknowledges this with "(producer + implicit reviewer surface in some graphs)", but the test name still implies dual-role-specific coverage.
Functionally the test does what the previous review asked for (locks tester's producer-phase contract), and the dual-role transition logic is exercised indirectly via the is_producer short-circuit in peer_consensus.py:1259. But if the intent was to lock the tester-as-reviewer-too case, the fixture would need the tester→coder edge. Otherwise consider renaming to test_tester_producer_in_proposed_blocked to match what's actually tested.
3. Comment placement on the from_role broadness
The new comment at orchestrator/routes/messages.py:387-393 sits between the if not blocking: return None early-return and the import shim, which visually ties it to the import rather than to the guard's filtering policy. Moving it up to the function docstring (or to where role is read at L382) would put it closer to where future readers will look when they wonder why from=… is ignored. Minor.
Verified correct
- The simplified import-shim mirrors
_apply_delphi_filter(orchestrator/routes/messages.py:319-325) exactly — same try/except → None → if-not-truthy guard. Drift-resolved. test_dual_role_tester_in_proposed_blockedcorrectly drives the tester throughhandle_propose(PROPOSED phase) and assertsis_producer_pending_confirm("tester")returns True via the route's 400 path.- The
# type: ignore[assignment]annotation on the newget_peer_consensus_tracker = Nonematches the existing_apply_delphi_filterannotation. - The
not get_peer_consensus_trackertruthy check correctly handles bothNone(import failed) and any future falsy sentinel without behavior change. - Ruff still clean across all three modified files.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Three non-blocking suggestions from the re-review of commit 31d32db: 1. Drop the redundant advisory ACK and stale comment in test_producer_in_confirmed_is_not_pending — documenter has zero critical reviewers in this fixture, so it is already fully ACKed without the reviewer_code/documenter advisory ACK, and the 'must have reviewed' guard the comment cited only applies when the reviewer (not the producer) tries to confirm. 2. Add ReviewEdge(tester, coder, CRITICAL) to implement_tracker so the fixture genuinely makes tester dual-role (producer of its own artifacts + reviewer of coder, matching get_default_implement_graph). test_dual_role_tester_in_proposed_blocked now locks the dual-role contract instead of just tester-as-producer, and the docstring is updated to reflect that. All other tests using the fixture continue to pass — the new edge only activates if coder confirms (it doesn't in any of these tests). 3. Move the from_role-broadness comment from the body of _check_producer_pending_confirm_guard up into its docstring so future readers see the policy when they look at the function contract, not when they wonder why the import shim is shaped the way it is.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-reviewed the delta between 31d32db and c78a4be (one commit, three localized changes). All three non-blocking suggestions from the previous re-review were addressed substantively.
Suggestions addressed
1. Dead weight in test_producer_in_confirmed_is_not_pending ✓
orchestrator/tests/test_peer_consensus_integration.py:3076 — the redundant t.handle_ack("reviewer_code", "documenter", …) call and its misleading "must have reviewed" comment are gone. The remaining setup (one critical ACK for coder, no ACK for documenter) still satisfies ApprovalMatrix.is_fully_acked("documenter") (zero critical reviewers in the fixture), so handle_confirmed("documenter") returns confirmed and the assertion that is_producer_pending_confirm("documenter") returns False still holds.
2. Dual-role tester fixture ✓
orchestrator/tests/test_messages.py:1580-1584 — ReviewEdge("tester", "coder", CRITICAL) added to the implement_tracker graph so tester is genuinely dual-role (producer of test artifacts + reviewer of coder), matching get_default_implement_graph. The docstring on test_dual_role_tester_in_proposed_blocked is updated to drop the "in some graphs" hedge and now claims dual-role coverage truthfully.
Worth noting (informational, not a critique): the assertion path itself (is_producer_pending_confirm("tester") → True via the producer-phase check at peer_consensus.py:1259-1261) is identical with or without the new edge. The improvement is fixture-realism — the test now operates on a graph that mirrors production, so a future regression that mishandles dual-role roles would be caught here rather than slipping past a producer-only fixture.
The new edge does not break test_producer_in_confirmed_passes because documenter's check_confirm_guard only gates on global_zero_proposal + producer_not_fully_acked for documenter itself, not for coder — coder's incomplete reviewer set under the new edge is irrelevant.
3. from_role-broadness comment moved to docstring ✓
orchestrator/routes/messages.py:379-385 — the policy note now lives in the function docstring where readers will find it when they look at the contract, rather than dangling above the import shim. The body of the function is correspondingly cleaner.
Verified correct
pytest orchestrator/tests/test_messages.py orchestrator/tests/test_peer_consensus_integration.py→ 173 passed (same as before this commit; the new edge doesn't regress any existing assertions).pytest …::TestProducerPendingConfirmGuard …::TestIsProducerPendingConfirm→ 13/13 pass.- The docstring for
_check_producer_pending_confirm_guardstill ends with theReturns ``None`` when …line, so the contract documentation flows correctly after the inserted policy paragraph. - No changes to
orchestrator/peer_consensus.pyordocs/reference/agent-wait-patterns.mdin this delta — both already in their final shape from the prior pass.
No blocking issues. No new non-blocking issues.
— Authored by egg
|
egg review completed. View run logs 8 previous review(s) hidden. |
Address reviewer feedback on PR #2091. The blocking instance is in docs/guides/concurrent-execution.md:99 — the same file the PR already edits, where the navigational summary still claimed "four anti-patterns to avoid" while the PR adds a cross-reference to anti-pattern 5. Also folded in the two non-blocking adjacent drifts the reviewer identified, so the same #2077 documentation drift is fully closed: - docs/reference/agent-wait-patterns.md:4 — intro of the doc that defines anti-pattern 5 contradicted itself within ~160 lines. - docs/reference/orchestrator-cli.md:137 — wait-loop reference paragraph. Left unchanged per reviewer guidance: - docs/reference/agent-wait-patterns.md:883 — historical reference to issue #1897, which originally observed four anti-patterns. Authored-by: egg
…on (#2086) * Fix #2079: wake stuck producers via brc_confirmation_timeout escalation The Tier-1 detector in `check_brc_progress` already tracks fully-ACKed producers and fires after 180s, but its escalation only added an entry to `_active_alerts` — `_escalation_callbacks` was never registered in production, so the only consumer was the overseer agent's discretionary poll of `/health/alerts`. On pipeline issue-1965 the overseer chose not to act, and documenter sat fully-ACKed-but-not-confirmed for ~37 min until the generic `agent-heartbeat-stall` alert finally fired. This wires the detector into a deterministic remediation: - Register an escalation callback in `_run_pipeline` after `init_health_monitor` that posts a directed OVERSEER_ALERT to the stuck producer (only message type that wakes its post-confirm wait_loop, which filters to CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT). The body spells out what state the producer is in and the recovery path: call `mcp__brc__confirm`; if it returns `pending_acks`, wait on the prerequisite events instead of CONSENSUS_CONFIRMED. - Add `alert_type` and `elapsed_seconds` to the escalation dict so callbacks can discriminate without parsing the reason string. - Add a per-iteration INFO breadcrumb in `check_brc_progress` so future post-mortems can verify the check ran and what it observed. - Log a WARNING (not silent skip) when a fully-acked producer past timeout has no agent_state — the branch is unexpected in practice (every producer has at minimum proposed, which routes through MESSAGE_SENT and registers state) and worth surfacing. - Add `HealthMonitor.get_current_phase()` so the callback can record the current phase on the message without reaching into private state. Independent of PR #2077 (server-side wait_loop guard for #2064 — a different layer of fix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review feedback on #2079 nudge wiring - Clarify docstring: OVERSEER_ALERT works for both pre- and post-confirm wait_loop filters; the wedged producer is in the pre-confirm wait. - Drop redundant try/except around message_store import — _get_message_store already verified the package is importable. - Reject elapsed_seconds None/<=0 as malformed instead of rendering 'have not confirmed in 0s'. - Document why add_message bypasses POST /messages/send (skips HealthMonitor.MESSAGE_SENT handler intentionally). - Replace 'global zero-proposal guard' jargon with a reference to the confirm response's blocking field. - Add tests for elapsed_seconds rejection and an integration test that drives check_brc_progress through the closure to verify phase is read at fire time (not registration time). * Drop nonexistent `blocking` field from nudge body Reviewer flagged that the body referenced `response['blocking']`, which does not exist. `mcp__brc__confirm` returns `message` plus the guard's specific list (`zero_proposal_producers`, `stale_acks`, `unresolved_nacks`, `stale_nacks`) — not a generic `blocking` field. Reword to point producers at `message` for the guard reason and the concrete event types to wait on, without referencing a field that doesn't exist. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
…[doc-updater] (#2091) * docs: update wait patterns to five anti-patterns, fix pending_acks note - docs/index.md: update "four anti-patterns" to "five anti-patterns" after #2077 added anti-pattern 5 to agent-wait-patterns.md - docs/guides/concurrent-execution.md: clarify the pending_acks (exit code 2) producer note to specify which prerequisite events to wait on and explicitly warn against entering the STAY ALIVE wait_loop on CONSENSUS_CONFIRMED before confirm has succeeded (the deadlock documented in #2064 and anti-pattern 5) Authored-by: egg * docs: fix four→five anti-pattern count in remaining files Address reviewer feedback on PR #2091. The blocking instance is in docs/guides/concurrent-execution.md:99 — the same file the PR already edits, where the navigational summary still claimed "four anti-patterns to avoid" while the PR adds a cross-reference to anti-pattern 5. Also folded in the two non-blocking adjacent drifts the reviewer identified, so the same #2077 documentation drift is fully closed: - docs/reference/agent-wait-patterns.md:4 — intro of the doc that defines anti-pattern 5 contradicted itself within ~160 lines. - docs/reference/orchestrator-cli.md:137 — wait-loop reference paragraph. Left unchanged per reviewer guidance: - docs/reference/agent-wait-patterns.md:883 — historical reference to issue #1897, which originally observed four anti-patterns. Authored-by: egg * fix(orchestrator): four→five anti-pattern count in stay-alive prompt Runtime-visible misstatement noted in PR #2091 re-review: the concurrent stay-alive instructions injected at phase-completion told agents to read agent-wait-patterns.md for 'four anti-patterns to avoid', but #2077 added anti-pattern 5. Same #2077 docs drift, but reaching into runtime prompts. --------- Co-authored-by: jwbron <8340608+jwbron@users.noreply.github.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
…2085) * Fix #2078: gate ready-to-confirm nudge on full check_confirm_guard The orchestrator's "ready to confirm" STATUS used to fire on ``is_fully_acked``, which only checks critical reviewers and ignores the global zero-proposal guard. An advisory-only producer like ``documenter`` (whose sole edge is ``reviewer_code → documenter, ADVISORY``) was nudged to confirm the moment that one ADVISORY ACK landed, even though peer producers had not yet proposed — so ``handle_confirmed`` rejected the resulting confirm with ``pending_acks``. See the issue for the recorded reproducer in pipeline ``issue-1965`` (PR #2061) at 18:04:22–18:04:35. Single source of truth: ``PeerConsensusTracker.is_ready_to_confirm`` delegates to ``check_confirm_guard``. ``_collect_newly_ready_producers`` sweeps after every state-changing handler (PROPOSE, RE_PROPOSE, ACK) and returns producers whose readiness transitioned false→true, deduped by ``(role, proposal_version)`` so a re-propose naturally re-arms the nudge. ``signals.py`` emits a STATUS for each newly-ready producer the tracker reports. Sweeping on PROPOSE handles the case the original ACK-only gate missed: a producer that becomes ready *because a peer finally proposed* and unblocked the global guard. Independent of the wait_loop fix in #2077. * Address #2078 review feedback: rollback memo on send failure Six non-blocking observations from the egg-reviewer pass: 1. Roll back the per-version nudge memo when add_message raises so the producer can be re-nudged on the next state change instead of being permanently silenced at that proposal version. Adds PeerConsensusTracker.release_nudge() and wires it into _emit_ready_to_confirm_nudges via an optional tracker argument. 2. handle_consensus_producer_push_signal now also calls _emit_ready_to_confirm_nudges for symmetry with the explicit propose/re-propose handlers. The omission was benign today but would silently regress if a future guard depended on peer versions. 3. Drop is_ready_to_confirm — it had no production caller and the test assertions it backed were redundant with the existing newly_ready checks. 4. Document that _nudged_versions is in-memory only by design and a restart-time duplicate nudge is harmless under check_confirm_guard. 5. Reword the nudge body to "ready to confirm — all blocking reviews are clear" so an operator reading the bus is not misled by "has been ACKed" when the producer is advisory-only (documenter). 6. Use distinct commit SHAs across versions in test_re_propose_re_arms_nudge so the test mirrors a real auto-repropose (which short-circuits on unchanged SHA). --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
A producer's own
consensus_confirmedis part of what generates the globalCONSENSUS_CONFIRMEDsignal — waiting on it before the producer has reachedCONFIRMEDstate is a circular dependency that deadlocks the pipeline until the overseer's heartbeat-stall band-aid eventually intervenes.This adds a server-side guard in the orchestrator's
/messages/waitendpoint that returns HTTP 400 with an actionable error when the caller is a producer inWORKING/PROPOSEDstate andCONSENSUS_CONFIRMEDappears infor_types. The error tells the agent to callmcp__brc__confirmand, if it returnspending_acks, to wait on the prerequisite events instead (CONSENSUS_PROPOSE,CONSENSUS_ACK,CONSENSUS_RE_REVIEW) before retrying confirm.The fix is structural — matching the project's preference for infrastructure constraints over prompt-based rules (
docs/design/capability-removal.md) — and applies generically to every producer role (coder, tester, documenter, and any future producer), not just the documenter case that surfaced the bug.What actually happened in pipeline
issue-1965Reading
.egg-state/brc-history/1965-implement.md:reviewer_codeACKs documenter (only reviewer; ADVISORY edge); orchestrator emits a directed STATUS nudge "ready to confirm"consensus confirmed→ returnsstatus='pending_acks'becauseglobal_zero_proposalfires (coder/tester haven't proposed yet)wait_loop --for CONSENSUS_CONFIRMED,CONSENSUS_RE_REVIEW,OVERSEER_ALERT(the post-confirm STAY ALIVE pattern, invoked prematurely as if confirm had succeeded)CONSENSUS_CONFIRMEDevent but never re-attempts its own confirmagent-heartbeat-stalloverseer alert finally fires (~3.5 min after the last other-agent confirm); ~36 min of total wedged timeThe issue title's framing ("before calling their own confirm") is slightly off: documenter did call confirm — but it failed with
pending_acks, and the agent had no event-driven recovery path from the post-confirm STAY ALIVE wait.Why a server-side guard
I considered four shapes of fix:
pending_acks.check_brc_progresshealth check — it already detects "fully ACKed but not confirmed" and escalates after 180s, but didn't fire on this pipeline (presumably a state-condition mismatch worth investigating separately). Reactive, not preventative.(4) matches
docs/design/capability-removal.md's thesis that infrastructure-level constraints beat prompt-based rules. It catches every producer role, every reasonpending_acksmight fire (global zero-proposal, missing critical ACKs, stale ACKs, unresolved NACKs), and any future producer that gets added.Behavior
GET /api/v1/pipelines/<pid>/messages/wait?for=CONSENSUS_CONFIRMED&role=documenter&...Returns 400 when
peer_consensus.get_peer_consensus_tracker(pid).is_producer_pending_confirm(role)is True (producer not yet inCONFIRMED). Error body:The sandbox
message_wait_loophandler already maps 4xx (non-408) to permanent errors persandbox/egg_agent_tools/handlers/message.py:319-323, surfaced as exit code 3 by theegg-orchCLI shim (perdocs/reference/agent-wait-patterns.md§3 contract).The guard short-circuits to allow when:
rolequery param (broadcast snapshots)CONFIRMED(the post-confirm STAY ALIVE pattern is exactly what this is for)for_typesdoesn't includeCONSENSUS_CONFIRMED(other event types are unaffected)Files
orchestrator/peer_consensus.py—is_producer_pending_confirm(role)helper onPeerConsensusTrackerorchestrator/routes/messages.py— guard wired into the wait endpointorchestrator/tests/test_messages.py— 7 endpoint tests (documenter scenario, dual-role, reviewer-only, missing tracker, missing role, other for_types passthrough, confirmed producer passthrough)orchestrator/tests/test_peer_consensus_integration.py— 5 helper unit tests across the producer state machinedocs/reference/agent-wait-patterns.md— anti-pattern Phase 3: Container extraction #5 documenting the deadlock and the recovery idiomTest plan
pytest orchestrator/tests/test_messages.py::TestProducerPendingConfirmGuard— 7/7 passpytest orchestrator/tests/test_peer_consensus_integration.py::TestIsProducerPendingConfirm— 5/5 passpytest orchestrator/tests/test_messages.py orchestrator/tests/test_peer_consensus_integration.py— 172/172 pass (no existing-test regressions)ruff checkcleanFollow-ups (not in this PR)
check_brc_progress(orchestrator/health_monitor.py:731) didn't escalate at 180s — its escalation might be the right place to also emit a directed STATUS nudge to the producer (cleaner than the current heartbeat-stall band-aid).mcp__brc__confirmreturns{ok: True, status: "pending_acks", ...}which is misleading. A future change could renameoktoacceptedor surfacepending_acksmore prominently — orthogonal to this fix.🤖 Generated with Claude Code