```ts
/** One point-of-sale till session, replayed from its events. */
const OPEN = "open";
const PAYMENT = "payment";
const PAID = "paid";
const CANCELLED = "cancelled";

export function runTillSession(
  events: (string | number)[][],
  prices: Record<string, number>,
): {
  state: string;
  items: [string, number][];
  total: number;
  paid: number;
  change: number;
} {
  for (const name of Object.keys(prices)) {
    if (!Number.isInteger(prices[name]) || prices[name] <= 0) {
      throw new Error("price must be a positive integer: " + name);
    }
  }

  const cart: Record<string, number> = {};
  let state: string = OPEN;
  let total: number = 0;
  let paid: number = 0;

  for (const event of events) {
    if (!Array.isArray(event) || event.length !== 2) {
      throw new Error("event must be a list of its action and exactly its payload");
    }

    switch (state) {
      case OPEN:
        if (event[0] === "scan") {
          const itemName = String(event[1]);
          if (!prices[itemName]) {
            throw new Error("unknown item: " + itemName);
          }
          cart[itemName] = (cart[itemName] || 0) + 1;
        } else if (event[0] === "void") {
          const itemName = String(event[1]);
          if (!(itemName in cart)) {
            throw new Error("item not in cart: " + itemName);
          }
          cart[itemName]--;
          if (cart[itemName] === 0) {
            delete cart[itemName];
          }
        } else if (event[0] === "close") {
          total = Object.values(cart).reduce((acc, qty) => acc + prices[Object.keys(cart)[qty - 1]] * qty, 0);
          state = PAYMENT;
        } else if (event[0] === "cancel") {
          state = CANCELLED;
        }
        break;

      case PAYMENT:
        if (event[0] === "pay") {
          const amount = Number(event[1]);
          if (!Number.isInteger(amount) || amount <= 0) {
            throw new Error("payment must be a positive integer");
          }
          paid += amount;
          if (paid >= total) {
            state = PAID;
            change = paid - total;
          }
        } else if (event[0] === "cancel") {
          state = CANCELLED;
        }
        break;

      case PAID:
      case CANCELLED:
        throw new Error("no further events allowed in a closed or cancelled session");
    }
  }

  return {
    state,
    items: Object.entries(cart).sort((a, b) => a[0].localeCompare(b[0])),
    total,
    paid,
    change,
  };
}
```