# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-detect-budget-starvation-before-subagent-fan-out-under-a-hard-token-ceiling.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-detect-budget-starvation-before-subagent-fan-out-under-a-hard-token-ceiling.warrant.json
# Markdown SHA-256:   569a859e997be1f8b8ddf860b28eba0aba7340dcd6264d6882c63d93be52a918
# Code AST SHA-256:   2158686ceb23ea0036fdd0149242033f722f39a9a799cd02d155d57ec13883b5
# Verified Classes:   BudgetStarvationError
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class BudgetStarvationError(RuntimeError):
    """Raised when a fan-out plan cannot satisfy the aggregate reservation invariant."""
    pass


def plan_fanout(
    ceiling: int,
    n_subagents: int,
    overhead_per_subagent: int,
    min_useful_content: int,
    parent_reserve: int,
    synthesis_reserve: int,
) -> dict:
    """
    Compute a feasible fan-out plan under a hard token ceiling.

    Invariant enforced:
        parent_reserve + synthesis_reserve
        + n * (overhead_per_subagent + content_budget_i) <= ceiling

    Raises BudgetStarvationError before dispatch if the plan is infeasible.
    """
    if n_subagents < 1:
        raise BudgetStarvationError(f"invalid fan-out width: {n_subagents}")

    fixed = parent_reserve + synthesis_reserve
    per_subagent_fixed = overhead_per_subagent + min_useful_content
    n_max = (ceiling - fixed) // per_subagent_fixed

    if n_max < 1:
        raise BudgetStarvationError(
            f"ceiling {ceiling} cannot fund even one subagent: "
            f"fixed={fixed}, per_subagent_fixed={per_subagent_fixed}"
        )

    if n_subagents > n_max:
        raise BudgetStarvationError(
            f"fan-out width {n_subagents} exceeds feasible N_max={n_max} "
            f"under ceiling {ceiling} (fixed={fixed}, "
            f"per_subagent_fixed={per_subagent_fixed}). "
            f"Reduce N, raise ceiling, or batch sequentially."
        )

    residual = ceiling - fixed - n_subagents * per_subagent_fixed
    extra_per_subagent = residual // n_subagents

    return {
        "n_subagents": n_subagents,
        "n_max": n_max,
        "reserved_total": fixed + n_subagents * overhead_per_subagent,
        "content_budget_per_subagent": min_useful_content + extra_per_subagent,
        "unallocated_slack": residual - extra_per_subagent * n_subagents,
    }


def redistribute(
    plan: dict,
    released_content: int,
    active_subagents: int,
) -> int:
    """
    Redistribute only *content* budget from terminated subagents.
    Reservations are never redistributed.
    """
    if active_subagents < 1:
        raise BudgetStarvationError("no active subagents to receive redistribution")
    if released_content < 0:
        raise BudgetStarvationError(
            f"negative released content: {released_content}; "
            f"reservations may have been double-released"
        )
    return released_content // active_subagents
