```typescript
/** 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 total = 0;
  let paid = 0;
  let state = OPEN;

  function addItem(item: string): void {
    if (!prices[item]) {
      throw new Error("unknown item for scan: " + item);
    }
    cart[item] = (cart[item] || 0) + 1;
    total += prices[item];
  }

  function removeItem(item: string): void {
    if (cart[item]) {
      cart[item]--;
      if (cart[item] === 0) {
        delete cart[item];
      }
      total -= prices[item];
    } else {
      throw new Error("unknown item for void: " + item);
    }
  }

  function pay(cents: number): void {
    if (!Number.isInteger(cents) || cents <= 0) {
      throw new Error("pay amount must be a positive integer");
    }
    if (state !== PAYMENT) {
      throw new Error("cannot pay outside payment state");
    }
    paid += cents;
    if (paid >= total) {
      const change = paid - total;
      paid = total;
      state = PAID;
      return { state, items: Object.entries(cart).sort((a, b) => a[0].localeCompare(b[0])), total, paid, change };
    } else {
      return { state, items: Object.entries(cart).sort((a, b) => a[0].localeCompare(b[0])), total, paid, change };
    }
  }

  function cancel(): void {
    if (state !== OPEN && state !== PAYMENT) {
      throw new Error("cannot cancel outside open or payment state");
    }
    cart = {};
    total = 0;
    paid = 0;
    state = CANCELLED;

    return { state, items: [], total, paid, change };
  }

  for (const event of events) {
    if (event.length !== 2 || !Array.isArray(event)) {
      throw new Error("invalid event format");
    }
    const [action, payload] = event;
    switch (action) {
      case "scan":
        addItem(payload as string);
        break;
      case "void":
        removeItem(payload as string);
        break;
      case "close":
        if (Object.keys(cart).length === 0) {
          throw new Error("cannot close with empty cart");
        }
        state = PAYMENT;
        return pay(0); // Assuming zero payment for closure
      case "pay":
        const cents = payload as number;
        return pay(cents);
      case "cancel":
        cancel();
        break;
      default:
        throw new Error("unknown action: " + action);
    }
  }

  if (state !== OPEN && state !== PAYMENT) {
    throw new Error("session is not in a valid state");
  }

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