# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-bft-task-commitment-stall-when-the-quorum-intersects-a-restarted-agent-s-stale-view.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-bft-task-commitment-stall-when-the-quorum-intersects-a-restarted-agent-s-stale-view.warrant.json
# Markdown SHA-256:   d246e4efbbe5c2a303b944f8d7a8a427c099140ea59238a67fcb104dcad5d69b
# Code AST SHA-256:   cd37b500beceb6ff8bb620382d32f16cc4988072dc0ff839573cc5a7d28f7823
# Verified Classes:   StaleViewQuorumError
# Defensive Guards:   1 fail-closed invariants (zero external deps)

class StaleViewQuorumError(RuntimeError):
    """Raised when a leader's quorum counter includes an agent that has
    returned a non-prepare reply for the current view, making the quorum
    unachievable without a targeted view-sync."""
    pass


def assemble_quorum(view: int, f: int, replies: dict[str, str]) -> list[str]:
    """
    replies maps agent_id -> reply_type for the current view.
    reply_type in {"PREPARE", "ALREADY_VOTED", "VIEW_MISMATCH", "TIMEOUT"}.
    Returns the list of fresh prepares if a quorum (2f+1) is formed.
    Raises StaleViewQuorumError if any reachable agent has returned a
    non-prepare reply, because waiting cannot resolve the shortfall.
    """
    quorum_size = 2 * f + 1
    fresh = [a for a, r in replies.items() if r == "PREPARE"]
    stale = [a for a, r in replies.items() if r in ("ALREADY_VOTED", "VIEW_MISMATCH")]

    if len(fresh) >= quorum_size:
        return fresh

    if stale:
        # A reachable agent has already answered for a different view.
        # Waiting or escalating the view will reproduce the stall.
        raise StaleViewQuorumError(
            f"view={view} fresh={len(fresh)}/{quorum_size} "
            f"stale_voters={stale} — targeted view-sync required before "
            f"any view change; do not count stale voters toward quorum."
        )

    # Shortfall is attributable to latency only; caller may wait.
    return fresh
