<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P117-message-queue/ | updated: 2026-10-16 -->

# P117 Message queue

A broker that sends a message again when its acknowledgement is lost, four handlers over the same deliveries - add, replay, check and a window of keys - and every choice of lost acks counted, with a key reused for another request refused.

EML-P project `projects/message-queue` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/message-queue` runs it and `eml project verify projects/message-queue` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: at-least-once-delivery (https://efficientnewlanguage.org/cases/215-at-least-once-delivery/), idempotency-key-reuse (https://efficientnewlanguage.org/cases/281-idempotency-key-reuse/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
2

2
0
2
8
2
x
2
2,5
2
1 1 1 1
4

4
k 1
4
toolongkey
4
k-1
4
k9

4
k9
Bob
4
k9
averyverylongname
4
k9
dave

4
k9
dave
0
4
k9
dave
1000
4
k9
dave
+-3
4
k9
dave
x
4
k9
dave
-15
4
5

5
0
5
10
5
x
6

6
0
7
```

Screen:

```text
== Message queue ==
A broker delivers every message at least once: when an ack is lost, it sends the message again. Each message carries an idempotency key.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 0
Pick a number from 1 to 7.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> x
Pick a number from 1 to 7.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 
Cancelled.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 0
Type message numbers from 1 to 7, or none.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 8
Type message numbers from 1 to 7, or none.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> x
Type message numbers from 1 to 7, or none.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 2,5
Type message numbers from 1 to 7, or none.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 1 1 1 1
At most 3 lost acks for one message.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> 
Cancelled.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k 1
Type a key of up to 8 letters and digits.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> toolongkey
Type a key of up to 8 letters and digits.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k-1
Type a key of up to 8 letters and digits.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> 
Cancelled.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> Bob
Type an account of up to 10 lowercase letters.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> averyverylongname
Type an account of up to 10 lowercase letters.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> dave
amount (a whole number from -999 to 999, not 0)> 
Cancelled.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> dave
amount (a whole number from -999 to 999, not 0)> 0
Type a whole number from -999 to 999, not 0.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> dave
amount (a whole number from -999 to 999, not 0)> 1000
Type a whole number from -999 to 999, not 0.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> dave
amount (a whole number from -999 to 999, not 0)> +-3
Type a whole number from -999 to 999, not 0.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> dave
amount (a whole number from -999 to 999, not 0)> x
Type a whole number from -999 to 999, not 0.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k9
account (up to 10 lowercase letters)> dave
amount (a whole number from -999 to 999, not 0)> -15
Added message 8: k9 dave -15.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
8 messages is the most.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 5
sent again after (1 to 9 deliveries)> 
Cancelled.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 5
sent again after (1 to 9 deliveries)> 0
Type a number from 1 to 9.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 5
sent again after (1 to 9 deliveries)> 10
Type a number from 1 to 9.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 5
sent again after (1 to 9 deliveries)> x
Type a number from 1 to 9.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 6
window (1 to 9 keys)> 
Cancelled.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 6
window (1 to 9 keys)> 0
Type a number from 1 to 9.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 7
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
2 5
2
1
2
none
3
5
6
3
6
4
2
1 2 2
4
k1
alice
30
1
3
7
```

Screen:

```text
== Message queue ==
A broker delivers every message at least once: when an ack is lost, it sends the message again. Each message carries an idempotency key.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 1
Messages (a message whose ack is lost is sent again 3 deliveries later; the window keeps 2 keys):
   1  k1  alice +30
   2  k2  bob +20
   3  k3  alice -10
   4  k2  bob +25    the key of message 2, for another request
   5  k4  carol +50
   6  k4  carol +50  a retry of message 5
   7  k5  bob -5
Right: alice 20, bob 15, carol 50; message 4 refused.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 2 5
Deliveries:
   1  message 1  k1  alice +30
   2  message 2  k2  bob +20    ack lost
   3  message 3  k3  alice -10
   4  message 4  k2  bob +25
   5  message 2  k2  bob +20    sent again
   6  message 5  k4  carol +50  ack lost
   7  message 6  k4  carol +50
   8  message 7  k5  bob -5
   9  message 5  k4  carol +50  sent again
Right: alice 20, bob 15, carol 50; message 4 refused.
  add       alice 20, bob 60, carol 150: 3 applied again; message 4 applied under message 2's key.
  replay    alice 20, bob 15, carol 50: message 4 told done with message 2's answer.
  check     alice 20, bob 15, carol 50: right, keeping 5 keys with their requests.
  window 2  alice 20, bob 15, carol 50: right, keeping 2 keys with their requests.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 1
Deliveries:
   1  message 1  k1  alice +30  ack lost
   2  message 2  k2  bob +20
   3  message 3  k3  alice -10
   4  message 1  k1  alice +30  sent again
   5  message 4  k2  bob +25
   6  message 5  k4  carol +50
   7  message 6  k4  carol +50
   8  message 7  k5  bob -5
Right: alice 20, bob 15, carol 50; message 4 refused.
  add       alice 50, bob 40, carol 100: 2 applied again; message 4 applied under message 2's key.
  replay    alice 20, bob 15, carol 50: message 4 told done with message 2's answer.
  check     alice 20, bob 15, carol 50: right, keeping 5 keys with their requests.
  window 2  alice 50, bob 40, carol 50: 1 applied again; message 4 applied under message 2's key.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> none
Deliveries:
   1  message 1  k1  alice +30
   2  message 2  k2  bob +20
   3  message 3  k3  alice -10
   4  message 4  k2  bob +25
   5  message 5  k4  carol +50
   6  message 6  k4  carol +50
   7  message 7  k5  bob -5
Right: alice 20, bob 15, carol 50; message 4 refused.
  add       alice 20, bob 40, carol 100: 1 applied again; message 4 applied under message 2's key.
  replay    alice 20, bob 15, carol 50: message 4 told done with message 2's answer.
  check     alice 20, bob 15, carol 50: right, keeping 5 keys with their requests.
  window 2  alice 20, bob 15, carol 50: right, keeping 2 keys with their requests.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 3
Every choice of which messages lose their first ack: 128 runs, each lost one sent again 3 deliveries later, or sooner at the end.
  handler      right  applied twice  reuse mishandled  keys kept
  add              0            128               128          0
  replay           0              0               128          5
  check          128              0                 0          5
  window 1         0            124               120          1
  window 2        40             74                64          2
  window 3       128              0                 0          3
  applied twice: some request was applied more than once.
  reuse mishandled: a key reused for another request was not refused, or a message was wrongly refused.
The first window right on every run keeps 3 keys; check keeps 5. How small a window can be depends on how late a message comes back.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 5
sent again after (1 to 9 deliveries)> 6
A message whose ack is lost is now sent again 6 deliveries later.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 3
Every choice of which messages lose their first ack: 128 runs, each lost one sent again 6 deliveries later, or sooner at the end.
  handler      right  applied twice  reuse mishandled  keys kept
  add              0            128               128          0
  replay           0              0               128          5
  check          128              0                 0          5
  window 1         0            126               128          1
  window 2         8            118                32          2
  window 3        16             96                32          3
  window 4       128              0                 0          4
  applied twice: some request was applied more than once.
  reuse mishandled: a key reused for another request was not refused, or a message was wrongly refused.
The first window right on every run keeps 4 keys; check keeps 5. How small a window can be depends on how late a message comes back.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 6
window (1 to 9 keys)> 4
The window now keeps 4 keys.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 2
Acks lost: message numbers from 1 to 7 such as 2 5 (a number twice loses two), or none.
acks lost> 1 2 2
Deliveries:
   1  message 1  k1  alice +30  ack lost
   2  message 2  k2  bob +20    ack lost
   3  message 3  k3  alice -10
   4  message 4  k2  bob +25
   5  message 5  k4  carol +50
   6  message 6  k4  carol +50
   7  message 1  k1  alice +30  sent again
   8  message 2  k2  bob +20    sent again, ack lost
   9  message 7  k5  bob -5
  10  message 2  k2  bob +20    sent again
Right: alice 20, bob 15, carol 50; message 4 refused.
  add       alice 50, bob 80, carol 100: 4 applied again; message 4 applied under message 2's key.
  replay    alice 20, bob 15, carol 50: message 4 told done with message 2's answer.
  check     alice 20, bob 15, carol 50: right, keeping 5 keys with their requests.
  window 4  alice 20, bob 15, carol 50: right, keeping 4 keys with their requests.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 4
key (up to 8 letters and digits)> k1
account (up to 10 lowercase letters)> alice
amount (a whole number from -999 to 999, not 0)> 30
Added message 8: k1 alice +30, a retry of message 1.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 1
Messages (a message whose ack is lost is sent again 6 deliveries later; the window keeps 4 keys):
   1  k1  alice +30
   2  k2  bob +20
   3  k3  alice -10
   4  k2  bob +25    the key of message 2, for another request
   5  k4  carol +50
   6  k4  carol +50  a retry of message 5
   7  k5  bob -5
   8  k1  alice +30  a retry of message 1
Right: alice 20, bob 15, carol 50; message 4 refused.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 3
Every choice of which messages lose their first ack: 256 runs, each lost one sent again 6 deliveries later, or sooner at the end.
  handler      right  applied twice  reuse mishandled  keys kept
  add              0            256               256          0
  replay           0              0               256          5
  check          256              0                 0          5
  window 1         0            256               256          1
  window 2         0            256                64          2
  window 3         0            256                64          3
  window 4         0            256                64          4
  window 5       256              0                 0          5
  applied twice: some request was applied more than once.
  reuse mishandled: a key reused for another request was not refused, or a message was wrongly refused.
The first window right on every run keeps 5 keys; check keeps 5. How small a window can be depends on how late a message comes back.

1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit
choice> 7
Bye.
```

## Modules

### main.eml

```eml
# P117 message queue: a broker that sends a message again when its
# acknowledgement is lost, four handlers side by side over the same
# deliveries, and every choice of lost acknowledgements counted.
import broker

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def is_digits(s):
    if len(s) == 0:
        return False
    for c in s:
        if c < "0" or c > "9":
            return False
    return True

def numbers_text(ns):
    "" => out
    for i in [0:len(ns) - 1]:
        if i > 0 and i == len(ns) - 1:
            out + " and " => out
        elif i > 0:
            out + ", " => out
        out + str(ns[i]) => out
    return out

def balances_text(msgs, bal):
    "" => out
    for a in broker.accounts(msgs):
        if out != "":
            out + ", " => out
        out + a + " " + str(bal[a]) => out
    return out

def note(msgs, i):
    broker.first_with_key(msgs, i) => f
    if f == i:
        return ""
    if broker.same_request(msgs[f], msgs[i]):
        return "a retry of message " + str(f + 1)
    return "the key of message " + str(f + 1) + ", for another request"

def widths(msgs):
    0 => kw
    0 => bw
    for m in msgs:
        if len(m[0]) > kw:
            len(m[0]) => kw
        if len(broker.body_text(m)) > bw:
            len(broker.body_text(m)) => bw
    return [kw, bw]

def right_text(msgs):
    broker.expected(msgs) => want
    [] => refused
    for i in [0:len(msgs) - 1]:
        if want[1][i] == "refused":
            refused + [i + 1] => refused
    "Right: " + balances_text(msgs, want[0]) => out
    if len(refused) == 1:
        out + "; message " + numbers_text(refused) + " refused" => out
    elif len(refused) > 1:
        out + "; messages " + numbers_text(refused) + " refused" => out
    return out + "."

def show(msgs, delay, k):
    ("Messages (a message whose ack is lost is sent again " + str(delay) + " deliveries later; the window keeps " + str(k) + " keys):") ^0
    widths(msgs) => w
    for i in [0:len(msgs) - 1]:
        msgs[i] => m
        "  " + lpad(str(i + 1), 2) + "  " + pad(m[0], w[0]) + "  " => line
        note(msgs, i) => n
        if n == "":
            (line + broker.body_text(m)) ^0
        else:
            (line + pad(broker.body_text(m), w[1]) + "  " + n) ^0
    right_text(msgs) ^0

def handler_line(msgs, name, kind, k, seq):
    broker.handle(kind, k, msgs, seq) => r
    broker.problems(msgs, r) => p
    [] => parts
    if p[0] > 0:
        parts + [str(p[0]) + " applied again"] => parts
    for x in p[1]:
        "message " + str(x[0] + 1) => who
        if x[1] == "applied":
            parts + [who + " applied under message " + str(x[2] + 1) + "'s key"] => parts
        elif x[1] == "told":
            parts + [who + " told done with message " + str(x[2] + 1) + "'s answer"] => parts
        else:
            parts + [who + " wrongly refused"] => parts
    if len(parts) == 0 and not broker.balances_right(msgs, r):
        parts + ["balances wrong"] => parts
    "" => verdict
    if len(parts) == 0:
        "right" => verdict
        if kind == "replay":
            verdict + ", keeping " + str(r[4]) + " keys" => verdict
        elif kind != "add":
            verdict + ", keeping " + str(r[4]) + " keys with their requests" => verdict
    else:
        for q in parts:
            if verdict != "":
                verdict + "; " => verdict
            verdict + q => verdict
    ("  " + pad(name, 10) + balances_text(msgs, r[0]) + ": " + verdict + ".") ^0

def deliver(msgs, delay, k):
    ("Acks lost: message numbers from 1 to " + str(len(msgs)) + " such as 2 5 (a number twice loses two), or none.") ^0
    plain(input("acks lost> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    [] => lost
    for m in msgs:
        lost + [0] => lost
    if t != "none":
        "" => w
        for c in t + " ":
            if c == " ":
                if w != "":
                    if not is_digits(w) or len(w) > 2 or int(w) < 1 or int(w) > len(msgs):
                        ("Type message numbers from 1 to " + str(len(msgs)) + ", or none.") ^0
                        return
                    lost[int(w) - 1] + 1 => lost[int(w) - 1]
                    if lost[int(w) - 1] > 3:
                        "At most 3 lost acks for one message." ^0
                        return
                "" => w
            else:
                w + c => w
    broker.deliveries(msgs, lost, delay) => seq
    widths(msgs) => wd
    "Deliveries:" ^0
    for d in [0:len(seq) - 1]:
        seq[d] => e
        msgs[e[0]] => m
        "" => tag
        if e[2]:
            "sent again" => tag
        if e[1]:
            if tag != "":
                tag + ", " => tag
            tag + "ack lost" => tag
        "  " + lpad(str(d + 1), 2) + "  message " + str(e[0] + 1) + "  " + pad(m[0], wd[0]) + "  " => line
        if tag == "":
            (line + broker.body_text(m)) ^0
        else:
            (line + pad(broker.body_text(m), wd[1]) + "  " + tag) ^0
    right_text(msgs) ^0
    handler_line(msgs, "add", "add", k, seq)
    handler_line(msgs, "replay", "replay", k, seq)
    handler_line(msgs, "check", "check", k, seq)
    handler_line(msgs, "window " + str(k), "window", k, seq)

def every(msgs, delay):
    broker.census(msgs, delay) => c
    ("Every choice of which messages lose their first ack: " + str(c[0]) + " runs, each lost one sent again " + str(delay) + " deliveries later, or sooner at the end.") ^0
    "  handler      right  applied twice  reuse mishandled  keys kept" ^0
    0 => kept
    for row in c[1]:
        ("  " + pad(row[0], 10) + lpad(str(row[1]), 8) + lpad(str(row[2]), 15) + lpad(str(row[3]), 18) + lpad(str(row[4]), 11)) ^0
        if row[0] == "check":
            row[4] => kept
    "  applied twice: some request was applied more than once." ^0
    "  reuse mishandled: a key reused for another request was not refused, or a message was wrongly refused." ^0
    c[1][len(c[1]) - 1] => last
    ("The first window right on every run keeps " + str(last[4]) + " keys; check keeps " + str(kept) + ". How small a window can be depends on how late a message comes back.") ^0

def add_message(msgs):
    if len(msgs) >= 8:
        "8 messages is the most." ^0
        return msgs
    plain(input("key (up to 8 letters and digits)> ")) => key
    if key == "":
        "Cancelled." ^0
        return msgs
    if len(key) > 8:
        "Type a key of up to 8 letters and digits." ^0
        return msgs
    for c in key:
        if not ((c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9")):
            "Type a key of up to 8 letters and digits." ^0
            return msgs
    plain(input("account (up to 10 lowercase letters)> ")) => acct
    if acct == "":
        "Cancelled." ^0
        return msgs
    if len(acct) > 10:
        "Type an account of up to 10 lowercase letters." ^0
        return msgs
    for c in acct:
        if c < "a" or c > "z":
            "Type an account of up to 10 lowercase letters." ^0
            return msgs
    plain(input("amount (a whole number from -999 to 999, not 0)> ")) => t
    if t == "":
        "Cancelled." ^0
        return msgs
    1 => sign
    t => digits
    if t[0] == "-" or t[0] == "+":
        if t[0] == "-":
            0 - 1 => sign
        t[1:len(t)] => digits
    if not is_digits(digits) or len(digits) > 3 or int(digits) == 0:
        "Type a whole number from -999 to 999, not 0." ^0
        return msgs
    msgs + [[key, acct, sign * int(digits)]] => out
    note(out, len(out) - 1) => n
    if n == "":
        ("Added message " + str(len(out)) + ": " + key + " " + broker.body_text(out[len(out) - 1]) + ".") ^0
    else:
        ("Added message " + str(len(out)) + ": " + key + " " + broker.body_text(out[len(out) - 1]) + ", " + n + ".") ^0
    return out

def one_to_nine(prompt):
    plain(input(prompt)) => t
    if t == "":
        "Cancelled." ^0
        return 0
    if len(t) != 1 or t < "1" or t > "9":
        "Type a number from 1 to 9." ^0
        return 0
    return int(t)

broker.default_messages() => msgs
3 => delay
2 => k

"== Message queue ==" ^0
"A broker delivers every message at least once: when an ack is lost, it sends the message again. Each message carries an idempotency key." ^0
True => running
while running:
    "" ^0
    "1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show(msgs, delay, k)
    elif choice == "2":
        deliver(msgs, delay, k)
    elif choice == "3":
        every(msgs, delay)
    elif choice == "4":
        add_message(msgs) => msgs
    elif choice == "5":
        one_to_nine("sent again after (1 to 9 deliveries)> ") => n
        if n > 0:
            n => delay
            ("A message whose ack is lost is now sent again " + str(delay) + " deliveries later.") ^0
    elif choice == "6":
        one_to_nine("window (1 to 9 keys)> ") => n
        if n > 0:
            n => k
            ("The window now keeps " + str(k) + " keys.") ^0
    elif choice == "7":
        False => running
    else:
        "Pick a number from 1 to 7." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import broker

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def is_digits(s):
    if len(s) == 0:
        return False
    for c in s:
        if c < "0" or c > "9":
            return False
    return True

def numbers_text(ns):
    out = ""
    for i in range(0, len(ns)):
        if i > 0 and i == len(ns) - 1:
            out = out + " and "
        elif i > 0:
            out = out + ", "
        out = out + str(ns[i])
    return out

def balances_text(msgs, bal):
    out = ""
    for a in broker.accounts(msgs):
        if out != "":
            out = out + ", "
        out = out + a + " " + str(bal[a])
    return out

def note(msgs, i):
    f = broker.first_with_key(msgs, i)
    if f == i:
        return ""
    if broker.same_request(msgs[f], msgs[i]):
        return "a retry of message " + str(f + 1)
    return "the key of message " + str(f + 1) + ", for another request"

def widths(msgs):
    kw = 0
    bw = 0
    for m in msgs:
        if len(m[0]) > kw:
            kw = len(m[0])
        if len(broker.body_text(m)) > bw:
            bw = len(broker.body_text(m))
    return [kw, bw]

def right_text(msgs):
    want = broker.expected(msgs)
    refused = []
    for i in range(0, len(msgs)):
        if want[1][i] == "refused":
            refused = refused + [i + 1]
    out = "Right: " + balances_text(msgs, want[0])
    if len(refused) == 1:
        out = out + "; message " + numbers_text(refused) + " refused"
    elif len(refused) > 1:
        out = out + "; messages " + numbers_text(refused) + " refused"
    return out + "."

def show(msgs, delay, k):
    print("Messages (a message whose ack is lost is sent again " + str(delay) + " deliveries later; the window keeps " + str(k) + " keys):")
    w = widths(msgs)
    for i in range(0, len(msgs)):
        m = msgs[i]
        line = "  " + lpad(str(i + 1), 2) + "  " + pad(m[0], w[0]) + "  "
        n = note(msgs, i)
        if n == "":
            print(line + broker.body_text(m))
        else:
            print(line + pad(broker.body_text(m), w[1]) + "  " + n)
    print(right_text(msgs))

def handler_line(msgs, name, kind, k, seq):
    r = broker.handle(kind, k, msgs, seq)
    p = broker.problems(msgs, r)
    parts = []
    if p[0] > 0:
        parts = parts + [str(p[0]) + " applied again"]
    for x in p[1]:
        who = "message " + str(x[0] + 1)
        if x[1] == "applied":
            parts = parts + [who + " applied under message " + str(x[2] + 1) + "'s key"]
        elif x[1] == "told":
            parts = parts + [who + " told done with message " + str(x[2] + 1) + "'s answer"]
        else:
            parts = parts + [who + " wrongly refused"]
    if len(parts) == 0 and not broker.balances_right(msgs, r):
        parts = parts + ["balances wrong"]
    verdict = ""
    if len(parts) == 0:
        verdict = "right"
        if kind == "replay":
            verdict = verdict + ", keeping " + str(r[4]) + " keys"
        elif kind != "add":
            verdict = verdict + ", keeping " + str(r[4]) + " keys with their requests"
    else:
        for q in parts:
            if verdict != "":
                verdict = verdict + "; "
            verdict = verdict + q
    print("  " + pad(name, 10) + balances_text(msgs, r[0]) + ": " + verdict + ".")

def deliver(msgs, delay, k):
    print("Acks lost: message numbers from 1 to " + str(len(msgs)) + " such as 2 5 (a number twice loses two), or none.")
    t = plain(input("acks lost> "))
    if t == "":
        print("Cancelled.")
        return
    lost = []
    for m in msgs:
        lost = lost + [0]
    if t != "none":
        w = ""
        for c in t + " ":
            if c == " ":
                if w != "":
                    if not is_digits(w) or len(w) > 2 or int(w) < 1 or int(w) > len(msgs):
                        print("Type message numbers from 1 to " + str(len(msgs)) + ", or none.")
                        return
                    lost[int(w) - 1] = lost[int(w) - 1] + 1
                    if lost[int(w) - 1] > 3:
                        print("At most 3 lost acks for one message.")
                        return
                w = ""
            else:
                w = w + c
    seq = broker.deliveries(msgs, lost, delay)
    wd = widths(msgs)
    print("Deliveries:")
    for d in range(0, len(seq)):
        e = seq[d]
        m = msgs[e[0]]
        tag = ""
        if e[2]:
            tag = "sent again"
        if e[1]:
            if tag != "":
                tag = tag + ", "
            tag = tag + "ack lost"
        line = "  " + lpad(str(d + 1), 2) + "  message " + str(e[0] + 1) + "  " + pad(m[0], wd[0]) + "  "
        if tag == "":
            print(line + broker.body_text(m))
        else:
            print(line + pad(broker.body_text(m), wd[1]) + "  " + tag)
    print(right_text(msgs))
    handler_line(msgs, "add", "add", k, seq)
    handler_line(msgs, "replay", "replay", k, seq)
    handler_line(msgs, "check", "check", k, seq)
    handler_line(msgs, "window " + str(k), "window", k, seq)

def every(msgs, delay):
    c = broker.census(msgs, delay)
    print("Every choice of which messages lose their first ack: " + str(c[0]) + " runs, each lost one sent again " + str(delay) + " deliveries later, or sooner at the end.")
    print("  handler      right  applied twice  reuse mishandled  keys kept")
    kept = 0
    for row in c[1]:
        print("  " + pad(row[0], 10) + lpad(str(row[1]), 8) + lpad(str(row[2]), 15) + lpad(str(row[3]), 18) + lpad(str(row[4]), 11))
        if row[0] == "check":
            kept = row[4]
    print("  applied twice: some request was applied more than once.")
    print("  reuse mishandled: a key reused for another request was not refused, or a message was wrongly refused.")
    last = c[1][len(c[1]) - 1]
    print("The first window right on every run keeps " + str(last[4]) + " keys; check keeps " + str(kept) + ". How small a window can be depends on how late a message comes back.")

def add_message(msgs):
    if len(msgs) >= 8:
        print("8 messages is the most.")
        return msgs
    key = plain(input("key (up to 8 letters and digits)> "))
    if key == "":
        print("Cancelled.")
        return msgs
    if len(key) > 8:
        print("Type a key of up to 8 letters and digits.")
        return msgs
    for c in key:
        if not (c >= "a" and c <= "z" or c >= "A" and c <= "Z" or c >= "0" and c <= "9"):
            print("Type a key of up to 8 letters and digits.")
            return msgs
    acct = plain(input("account (up to 10 lowercase letters)> "))
    if acct == "":
        print("Cancelled.")
        return msgs
    if len(acct) > 10:
        print("Type an account of up to 10 lowercase letters.")
        return msgs
    for c in acct:
        if c < "a" or c > "z":
            print("Type an account of up to 10 lowercase letters.")
            return msgs
    t = plain(input("amount (a whole number from -999 to 999, not 0)> "))
    if t == "":
        print("Cancelled.")
        return msgs
    sign = 1
    digits = t
    if t[0] == "-" or t[0] == "+":
        if t[0] == "-":
            sign = 0 - 1
        digits = t[1:len(t)]
    if not is_digits(digits) or len(digits) > 3 or int(digits) == 0:
        print("Type a whole number from -999 to 999, not 0.")
        return msgs
    out = msgs + [[key, acct, sign * int(digits)]]
    n = note(out, len(out) - 1)
    if n == "":
        print("Added message " + str(len(out)) + ": " + key + " " + broker.body_text(out[len(out) - 1]) + ".")
    else:
        print("Added message " + str(len(out)) + ": " + key + " " + broker.body_text(out[len(out) - 1]) + ", " + n + ".")
    return out

def one_to_nine(prompt):
    t = plain(input(prompt))
    if t == "":
        print("Cancelled.")
        return 0
    if len(t) != 1 or t < "1" or t > "9":
        print("Type a number from 1 to 9.")
        return 0
    return int(t)

msgs = broker.default_messages()
delay = 3
k = 2
print("== Message queue ==")
print("A broker delivers every message at least once: when an ack is lost, it sends the message again. Each message carries an idempotency key.")
running = True
while running:
    print("")
    print("1) messages  2) deliver  3) every lost ack  4) add a message  5) redelivery  6) window  7) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show(msgs, delay, k)
    elif choice == "2":
        deliver(msgs, delay, k)
    elif choice == "3":
        every(msgs, delay)
    elif choice == "4":
        msgs = add_message(msgs)
    elif choice == "5":
        n = one_to_nine("sent again after (1 to 9 deliveries)> ")
        if n > 0:
            delay = n
            print("A message whose ack is lost is now sent again " + str(delay) + " deliveries later.")
    elif choice == "6":
        n = one_to_nine("window (1 to 9 keys)> ")
        if n > 0:
            k = n
            print("The window now keeps " + str(k) + " keys.")
    elif choice == "7":
        running = False
    else:
        print("Pick a number from 1 to 7.")
print("Bye.")
```

### broker.eml

```eml
# P117 message queue - a broker that delivers every message at least once:
# when a consumer's acknowledgement is lost, the broker sends the message
# again a few deliveries later. As in the corpus case at-least-once-delivery,
# what a duplicate does is up to the handler. Each message carries an
# idempotency key, and as in the corpus case idempotency-key-reuse, a client
# that reuses a key for another request has to be refused, which needs the
# request kept beside the key.
#
# A message is [key, account, amount]; every account starts at 0.

def default_messages():
    return [["k1", "alice", 30], ["k2", "bob", 20], ["k3", "alice", 0 - 10], ["k2", "bob", 25], ["k4", "carol", 50], ["k4", "carol", 50], ["k5", "bob", 0 - 5]]

def signed(n):
    if n < 0:
        return "-" + str(0 - n)
    return "+" + str(n)

def body_text(m):
    return m[1] + " " + signed(m[2])

def same_request(a, b):
    return a[1] == b[1] and a[2] == b[2]

def member(xs, x):
    for y in xs:
        if y == x:
            return True
    return False

def accounts(msgs):
    [] => out
    for m in msgs:
        if not member(out, m[1]):
            out + [m[1]] => out
    return out

def first_with_key(msgs, i):
    # the first message carrying message i's key
    for j in [0:i]:
        if msgs[j][0] == msgs[i][0]:
            return j
    return i

def expected(msgs):
    # each key's first message applied once; a later message with the same
    # key and the same request is a retry and is answered done; with another
    # request it is refused: [balances, answer per message]
    {} => bal
    for a in accounts(msgs):
        0 => bal[a]
    [] => answers
    for i in [0:len(msgs) - 1]:
        msgs[i] => m
        first_with_key(msgs, i) => f
        if f == i:
            bal[m[1]] + m[2] => bal[m[1]]
            answers + ["done"] => answers
        elif same_request(msgs[f], m):
            answers + ["done"] => answers
        else:
            answers + ["refused"] => answers
    return [bal, answers]

def deliveries(msgs, lost, delay):
    # the broker's deliveries, in order, as [message, ack lost, sent before];
    # lost[i] is how many of message i's acks are lost. A message whose ack
    # is lost is sent again delay deliveries later, or sooner when nothing
    # else is waiting.
    [] => pending
    [] => left
    for i in [0:len(msgs) - 1]:
        pending + [i] => pending
        left + [lost[i]] => left
    [] => again
    [] => sent
    [] => out
    0 => d
    while len(pending) > 0 or len(again) > 0:
        d + 1 => d
        0 - 1 => best
        for j in [0:len(again) - 1]:
            if best == 0 - 1 or again[j][0] < again[best][0]:
                j => best
        if best != 0 - 1 and (again[best][0] <= d or len(pending) == 0):
            again[best][1] => m
            [] => rest
            for j in [0:len(again) - 1]:
                if j != best:
                    rest + [again[j]] => rest
            rest => again
        else:
            pending[0] => m
            pending[1:len(pending)] => pending
        left[m] > 0 => lose
        if lose:
            left[m] - 1 => left[m]
            again + [[d + delay, m]] => again
        out + [[m, lose, member(sent, m)]] => out
        sent + [m] => sent
    return out

def handle(kind, k, msgs, seq):
    # one handler over the deliveries:
    #   add     applies every delivery;
    #   replay  keeps every key, and answers a key it has seen with the
    #           first answer, applying nothing;
    #   check   keeps every key with its request: the same request again is
    #           answered done, another request under it is refused;
    #   window  like check, but keeps only the last k keys it applied.
    # [balances, applied again, times applied per message, last answer per
    #  message, keys kept]
    {} => bal
    for a in accounts(msgs):
        0 => bal[a]
    {} => kept
    [] => window
    [] => applied
    0 => again
    [] => times
    [] => answers
    for m in msgs:
        times + [0] => times
        answers + [""] => answers
    for e in seq:
        e[0] => i
        msgs[i] => m
        "apply" => act
        if kind == "replay" or kind == "check":
            if m[0] in kept:
                if same_request(msgs[kept[m[0]]], m):
                    "repeat" => act
                elif kind == "replay":
                    "told" => act
                else:
                    "refused" => act
        elif kind == "window":
            for w in window:
                if w[0] == m[0]:
                    if same_request(msgs[w[1]], m):
                        "repeat" => act
                    else:
                        "refused" => act
        if act == "apply":
            if member(applied, [m[0], m[1], m[2]]):
                again + 1 => again
            applied + [[m[0], m[1], m[2]]] => applied
            bal[m[1]] + m[2] => bal[m[1]]
            times[i] + 1 => times[i]
            "done" => answers[i]
            if kind == "replay" or kind == "check":
                i => kept[m[0]]
            if kind == "window":
                window + [[m[0], i]] => window
                if len(window) > k:
                    window[1:len(window)] => window
        elif act == "repeat":
            "done" => answers[i]
        else:
            act => answers[i]
    0 => n
    if kind == "window":
        len(window) => n
    else:
        len(kept) => n
    return [bal, again, times, answers, n]

def problems(msgs, r):
    # what went wrong, as [applied again, list of [message, what, first]]
    expected(msgs) => want
    [] => out
    for i in [0:len(msgs) - 1]:
        first_with_key(msgs, i) => f
        if want[1][i] == "refused":
            if r[2][i] > 0:
                out + [[i, "applied", f]] => out
            elif r[3][i] == "told":
                out + [[i, "told", f]] => out
        elif r[3][i] == "refused":
            out + [[i, "refused", f]] => out
    return [r[1], out]

def balances_right(msgs, r):
    expected(msgs)[0] => want
    for a in accounts(msgs):
        if r[0][a] != want[a]:
            return False
    return True

def right(msgs, r):
    problems(msgs, r) => p
    return p[0] == 0 and len(p[1]) == 0 and balances_right(msgs, r)

def lost_choices(n):
    # every choice of which messages lose their first ack
    [[]] => out
    for i in [1:n]:
        [] => more
        for c in out:
            more + [c + [0]] => more
            more + [c + [1]] => more
        more => out
    return out

def census(msgs, delay):
    # over every choice of lost first acks, for add, replay, check and the
    # windows from 1 key up to the first that is right on every run:
    # [runs, rows of [name, right, applied twice, reuse mishandled, kept]]
    lost_choices(len(msgs)) => choices
    [] => runs
    for c in choices:
        runs + [deliveries(msgs, c, delay)] => runs
    [] => rows
    ["add", "replay", "check"] => kinds
    0 => k
    True => going
    while going:
        if len(kinds) > 0:
            kinds[0] => kind
            kinds[1:len(kinds)] => kinds
            kind => name
        else:
            "window" => kind
            k + 1 => k
            "window " + str(k) => name
        0 => good
        0 => twice
        0 => reuse
        0 => most
        for seq in runs:
            handle(kind, k, msgs, seq) => r
            problems(msgs, r) => p
            if p[0] == 0 and len(p[1]) == 0 and balances_right(msgs, r):
                good + 1 => good
            if p[0] > 0:
                twice + 1 => twice
            if len(p[1]) > 0:
                reuse + 1 => reuse
            if r[4] > most:
                r[4] => most
        rows + [[name, good, twice, reuse, most]] => rows
        if kind == "window" and good == len(runs):
            False => going
    return [len(runs), rows]
```

Python projection of broker.eml:

```python
def default_messages():
    return [["k1", "alice", 30], ["k2", "bob", 20], ["k3", "alice", 0 - 10], ["k2", "bob", 25], ["k4", "carol", 50], ["k4", "carol", 50], ["k5", "bob", 0 - 5]]

def signed(n):
    if n < 0:
        return "-" + str(0 - n)
    return "+" + str(n)

def body_text(m):
    return m[1] + " " + signed(m[2])

def same_request(a, b):
    return a[1] == b[1] and a[2] == b[2]

def member(xs, x):
    for y in xs:
        if y == x:
            return True
    return False

def accounts(msgs):
    out = []
    for m in msgs:
        if not member(out, m[1]):
            out = out + [m[1]]
    return out

def first_with_key(msgs, i):
    for j in range(0, i+1):
        if msgs[j][0] == msgs[i][0]:
            return j
    return i

def expected(msgs):
    bal = {}
    for a in accounts(msgs):
        bal[a] = 0
    answers = []
    for i in range(0, len(msgs)):
        m = msgs[i]
        f = first_with_key(msgs, i)
        if f == i:
            bal[m[1]] = bal[m[1]] + m[2]
            answers = answers + ["done"]
        elif same_request(msgs[f], m):
            answers = answers + ["done"]
        else:
            answers = answers + ["refused"]
    return [bal, answers]

def deliveries(msgs, lost, delay):
    pending = []
    left = []
    for i in range(0, len(msgs)):
        pending = pending + [i]
        left = left + [lost[i]]
    again = []
    sent = []
    out = []
    d = 0
    while len(pending) > 0 or len(again) > 0:
        d = d + 1
        best = 0 - 1
        for j in range(0, len(again)):
            if best == 0 - 1 or again[j][0] < again[best][0]:
                best = j
        if best != 0 - 1 and (again[best][0] <= d or len(pending) == 0):
            m = again[best][1]
            rest = []
            for j in range(0, len(again)):
                if j != best:
                    rest = rest + [again[j]]
            again = rest
        else:
            m = pending[0]
            pending = pending[1:len(pending)]
        lose = left[m] > 0
        if lose:
            left[m] = left[m] - 1
            again = again + [[d + delay, m]]
        out = out + [[m, lose, member(sent, m)]]
        sent = sent + [m]
    return out

def handle(kind, k, msgs, seq):
    bal = {}
    for a in accounts(msgs):
        bal[a] = 0
    kept = {}
    window = []
    applied = []
    again = 0
    times = []
    answers = []
    for m in msgs:
        times = times + [0]
        answers = answers + [""]
    for e in seq:
        i = e[0]
        m = msgs[i]
        act = "apply"
        if kind == "replay" or kind == "check":
            if m[0] in kept:
                if same_request(msgs[kept[m[0]]], m):
                    act = "repeat"
                elif kind == "replay":
                    act = "told"
                else:
                    act = "refused"
        elif kind == "window":
            for w in window:
                if w[0] == m[0]:
                    if same_request(msgs[w[1]], m):
                        act = "repeat"
                    else:
                        act = "refused"
        if act == "apply":
            if member(applied, [m[0], m[1], m[2]]):
                again = again + 1
            applied = applied + [[m[0], m[1], m[2]]]
            bal[m[1]] = bal[m[1]] + m[2]
            times[i] = times[i] + 1
            answers[i] = "done"
            if kind == "replay" or kind == "check":
                kept[m[0]] = i
            if kind == "window":
                window = window + [[m[0], i]]
                if len(window) > k:
                    window = window[1:len(window)]
        elif act == "repeat":
            answers[i] = "done"
        else:
            answers[i] = act
    n = 0
    if kind == "window":
        n = len(window)
    else:
        n = len(kept)
    return [bal, again, times, answers, n]

def problems(msgs, r):
    want = expected(msgs)
    out = []
    for i in range(0, len(msgs)):
        f = first_with_key(msgs, i)
        if want[1][i] == "refused":
            if r[2][i] > 0:
                out = out + [[i, "applied", f]]
            elif r[3][i] == "told":
                out = out + [[i, "told", f]]
        elif r[3][i] == "refused":
            out = out + [[i, "refused", f]]
    return [r[1], out]

def balances_right(msgs, r):
    want = expected(msgs)[0]
    for a in accounts(msgs):
        if r[0][a] != want[a]:
            return False
    return True

def right(msgs, r):
    p = problems(msgs, r)
    return p[0] == 0 and len(p[1]) == 0 and balances_right(msgs, r)

def lost_choices(n):
    out = [[]]
    for i in range(1, n+1):
        more = []
        for c in out:
            more = more + [c + [0]]
            more = more + [c + [1]]
        out = more
    return out

def census(msgs, delay):
    choices = lost_choices(len(msgs))
    runs = []
    for c in choices:
        runs = runs + [deliveries(msgs, c, delay)]
    rows = []
    kinds = ["add", "replay", "check"]
    k = 0
    going = True
    while going:
        if len(kinds) > 0:
            kind = kinds[0]
            kinds = kinds[1:len(kinds)]
            name = kind
        else:
            kind = "window"
            k = k + 1
            name = "window " + str(k)
        good = 0
        twice = 0
        reuse = 0
        most = 0
        for seq in runs:
            r = handle(kind, k, msgs, seq)
            p = problems(msgs, r)
            if p[0] == 0 and len(p[1]) == 0 and balances_right(msgs, r):
                good = good + 1
            if p[0] > 0:
                twice = twice + 1
            if len(p[1]) > 0:
                reuse = reuse + 1
            if r[4] > most:
                most = r[4]
        rows = rows + [[name, good, twice, reuse, most]]
        if kind == "window" and good == len(runs):
            going = False
    return [len(runs), rows]
```

## README

# P117 - Message queue

A broker delivers every message at least once. When a consumer's ack is
lost, the broker sends the message again, and the work is done twice
unless the handler notices. Each message carries an idempotency key.

- `main.eml` - the menu, the messages, the deliveries with four handlers
  side by side, every choice of lost acks, and the settings
- `broker.eml` - the messages, the right result, the broker's order of
  deliveries, the four handlers, and the count over every choice

How each part works:

- A message is a key, an account and an amount; every account starts at
  0. A lost ack sends the message again a set number of deliveries later,
  or sooner when nothing else is waiting. The ack can be lost again.
- The right result applies each key's first message once. A later message
  with the same key and the same request is a retry, answered done; with
  another request, it is refused.
- The four handlers, as in the corpus cases `at-least-once-delivery` and
  `idempotency-key-reuse`:
  - add applies every delivery, so a duplicate is applied again;
  - replay keeps every key and answers a key it has seen with the first
    answer: the balances are right, but a reused key is told done;
  - check keeps every key with its request and refuses another request
    under it: right on every run, and it keeps every key;
  - a window keeps only the last few keys, so it is right only while
    every duplicate comes back before its key has left. When a key has
    left and come back with another request, the window can even refuse
    a right one.
- Every choice: each message loses its first ack or not, 128 runs for the
  seven messages. With a message sent again 3 deliveries later, the first
  window right on every run keeps 3 keys; 6 deliveries later, 4. For
  every delay from 1 to 9 it is 2, 3, 3, 4, 4, 4, 5, 5 and 5 keys.

What is checked: menu choices 1 to 7; lost acks as message numbers, at
most 3 for one message, or none; a key of up to 8 letters and digits, an
account of up to 10 lowercase letters, an amount from -999 to 999 but not
0, and at most 8 messages; a delay and a window from 1 to 9. An empty
answer cancels.

Sessions: `sessions/basic.in` gives:
- the messages, with one key reused for another request and one retry;
- deliveries losing the acks of messages 2 and 5, of message 1 (the window
  of 2 is too small), and of none;
- every choice at a delay of 3, and of 6;
- a window of 4, with message 1's ack lost once and message 2's twice;
- message 8, a late retry of message 1, after which only a window of all
  5 keys is right on every run.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- lost acks: a cancel, 0, 8, x, 2,5 and 1 four times;
- new messages: a cancel, the keys k 1, toolongkey and k-1, a cancel, the
  accounts Bob and averyverylongname, a cancel, the amounts 0, 1000, +-3
  and x, then dave -15, and a ninth message;
- the delay: a cancel, 0, 10 and x; the window: a cancel and 0.

Built on the verified corpus cases `at-least-once-delivery` (duplicates
are the handler's problem, and a bounded window is an assumption about
lateness) and `idempotency-key-reuse` (a reused key with another request
is refused only when the request is kept).
