```python

"""One point-of-sale till session, replayed from its events."""

OPEN = "open"
PAYMENT = "payment"
PAID = "paid"
CANCELLED = "cancelled"


def run_till_session(events: list, prices: dict) -> dict:
    # Check if the price list contains only positive integers
    for name, price in prices.items():
        if isinstance(price, bool) or not isinstance(price, int) or price <= 0:
            raise ValueError("price must be a positive integer: " + str(name))

    cart = {}
    total = 0
    paid = 0
    change = 0
    state = OPEN

    for event in events:
        if len(event) != 2:
            raise ValueError("event must be a list with exactly two elements")

        action, payload = event

        if action == "scan":
            item_name = payload
            item_price = prices.get(item_name)
            if item_price is None:
                raise ValueError(f"unknown item: {item_name}")
            cart[item_name] = cart.get(item_name, 0) + 1
            total += item_price

        elif action == "void":
            item_name = payload
            if item_name not in cart:
                raise ValueError(f"cannot void {item_name}, it's not in the cart")
            cart[item_name] -= 1
            if cart[item_name] < 0:
                del cart[item_name]
            total -= prices.get(item_name)

        elif action == "close":
            if state != OPEN and state != PAYMENT:
                raise ValueError("cannot close an already closed or paid session")
            if not cart:
                raise ValueError("session cannot be closed with an empty cart")

            # Move to payment state
            state = PAYMENT

        elif action == "pay":
            pay_amount = payload
            if not isinstance(pay_amount, (int, bool)) or pay_amount <= 0:
                raise ValueError("payment amount must be a positive integer: " + str(pay_amount))
            paid += pay_amount
            change = max(0, total - paid)

        elif action == "cancel":
            if state != OPEN and state != PAYMENT:
                raise ValueError("cannot cancel an already closed or paid session")
            state = CANCELLED

        else:
            raise ValueError(f"unknown action: {action}")

        # Check if the session is in a final state
        if state == PAID or state == CANCELLED:
            for event in events[len(cart) + 1:]:
                raise ValueError("no further events allowed after payment or cancellation")

    return {
        'items': sorted([(item, quantity) for item, quantity in cart.items()]),
        'total': total,
        'paid': paid,
        'change': change
    }
```