FAILURE MAP
← Case archive

FA-76146 / Chat ordering and read receipts / Open access

Render who is typing from start and stop events: plural form · case 01

With three typers the indicator silently drops the third name.

Verified by executionVariant 1 · 7 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The plural form decision evaluates `if len(who) >= 2: return who[0] + ' and ' + who[1] + ' are typing'` where the contract requires `if len(who) == 2: return who[0] + ' and ' + who[1] + ' are typing'`.

VERIFIED REPAIR

Use `if len(who) == 2: return who[0] + ' and ' + who[1] + ' are typing'` for the plural form decision and keep every other rule of the model unchanged.

Unsuccessful approach: Joining every name produces an unbounded list instead of the collective phrase. The attempted `if len(who) >= 2: return ' and '.join(who) + ' are typing'` still disagrees with a fixture.

Case contract

events [ts_ms, user, kind] with kind start, stop or message are applied in (ts, user, kind) order. A user is typing when their latest start has no later stop or message and now - start < 6000 ms (a start stamped in the future by a skewed clock stays active). The viewer is excluded. Typers are listed by start time: one -> "A is typing", two -> "A and B are typing", more -> "Several people are typing", none -> "". names maps ids to display names.

Why this case matters

Typing indicators are ephemeral presence; stale or misordered indicators mislead users about who is replying.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(events, me, now, names):
    last = {}
    for ts, user, kind in sorted(events):
        if kind == 'start':
            last[user] = ts
        else:
            last.pop(user, None)
    active = sorted((ts, user) for user, ts in last.items() if user != me and now - ts < 6000)
    who = [names.get(u, u) for _, u in active]
    if not who:
        return ''
    if len(who) == 1:
        return who[0] + ' is typing'
    if len(who) >= 2:
        return who[0] + ' and ' + who[1] + ' are typing'
    return 'Several people are typing'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
_CASES = {1: [('exact expiry', ([[1000, 'bo', 'start']], 'me', 7000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50001, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [301, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [701, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 11, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2501, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 21, {}), '')], 2: [('exact expiry', ([[2000, 'bo', 'start']], 'me', 8000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50002, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [302, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [702, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 12, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2502, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 22, {}), '')], 3: [('exact expiry', ([[3000, 'bo', 'start']], 'me', 9000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50003, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [303, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [703, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 13, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2503, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 23, {}), '')], 4: [('exact expiry', ([[4000, 'bo', 'start']], 'me', 10000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50004, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [304, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [704, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 14, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2504, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 24, {}), '')], 5: [('exact expiry', ([[5000, 'bo', 'start']], 'me', 11000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50005, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [305, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [705, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 15, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2505, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 25, {}), '')]}
for _label, _args, _expected in _CASES[N]:
    check(_label, solve(*_args), _expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
exact expiryPassed
future stamped startbo is typingbo is typingPassed
message clears typingPassed
start orderZed and Amy are typingZed and Amy are typingPassed
three typersA and B are typingSeveral people are typingFailed
stop overtaken in transitCy is typingCy is typingPassed
self typing hiddenPassed

SHA-256 / 5ba4ebd5eba989f515d4d954d7f8b1b761aa24c8fadc589a20f76a18eb7d237d

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(events, me, now, names):
    last = {}
    for ts, user, kind in sorted(events):
        if kind == 'start':
            last[user] = ts
        else:
            last.pop(user, None)
    active = sorted((ts, user) for user, ts in last.items() if user != me and now - ts < 6000)
    who = [names.get(u, u) for _, u in active]
    if not who:
        return ''
    if len(who) == 1:
        return who[0] + ' is typing'
    if len(who) >= 2:
        return ' and '.join(who) + ' are typing'
    return 'Several people are typing'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
_CASES = {1: [('exact expiry', ([[1000, 'bo', 'start']], 'me', 7000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50001, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [301, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [701, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 11, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2501, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 21, {}), '')], 2: [('exact expiry', ([[2000, 'bo', 'start']], 'me', 8000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50002, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [302, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [702, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 12, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2502, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 22, {}), '')], 3: [('exact expiry', ([[3000, 'bo', 'start']], 'me', 9000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50003, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [303, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [703, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 13, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2503, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 23, {}), '')], 4: [('exact expiry', ([[4000, 'bo', 'start']], 'me', 10000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50004, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [304, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [704, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 14, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2504, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 24, {}), '')], 5: [('exact expiry', ([[5000, 'bo', 'start']], 'me', 11000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50005, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [305, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [705, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 15, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2505, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 25, {}), '')]}
for _label, _args, _expected in _CASES[N]:
    check(_label, solve(*_args), _expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
exact expiryPassed
future stamped startbo is typingbo is typingPassed
message clears typingPassed
start orderZed and Amy are typingZed and Amy are typingPassed
three typersA and B and C are typingSeveral people are typingFailed
stop overtaken in transitCy is typingCy is typingPassed
self typing hiddenPassed

SHA-256 / 6c48e44d5841bb3423ed41dffe609d1943dfec725571e7bd777be7395df72da8

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(events, me, now, names):
    last = {}
    for ts, user, kind in sorted(events):
        if kind == 'start':
            last[user] = ts
        else:
            last.pop(user, None)
    active = sorted((ts, user) for user, ts in last.items() if user != me and now - ts < 6000)
    who = [names.get(u, u) for _, u in active]
    if not who:
        return ''
    if len(who) == 1:
        return who[0] + ' is typing'
    if len(who) == 2:
        return who[0] + ' and ' + who[1] + ' are typing'
    return 'Several people are typing'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
_CASES = {1: [('exact expiry', ([[1000, 'bo', 'start']], 'me', 7000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50001, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [301, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [701, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 11, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2501, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 21, {}), '')], 2: [('exact expiry', ([[2000, 'bo', 'start']], 'me', 8000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50002, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [302, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [702, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 12, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2502, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 22, {}), '')], 3: [('exact expiry', ([[3000, 'bo', 'start']], 'me', 9000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50003, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [303, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [703, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 13, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2503, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 23, {}), '')], 4: [('exact expiry', ([[4000, 'bo', 'start']], 'me', 10000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50004, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [304, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [704, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 14, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2504, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 24, {}), '')], 5: [('exact expiry', ([[5000, 'bo', 'start']], 'me', 11000, {'bo': 'Bo'}), ''), ('future stamped start', ([[50005, 'bo', 'start']], 'me', 40000, {}), 'bo is typing'), ('message clears typing', ([[100, 'bo', 'start'], [305, 'bo', 'message']], 'me', 1000, {'bo': 'Bo'}), ''), ('start order', ([[500, 'zed', 'start'], [705, 'amy', 'start']], 'me', 2000, {'zed': 'Zed', 'amy': 'Amy'}), 'Zed and Amy are typing'), ('three typers', ([[1, 'a', 'start'], [2, 'b', 'start'], [3, 'c', 'start']], 'me', 15, {'a': 'A', 'b': 'B', 'c': 'C'}), 'Several people are typing'), ('stop overtaken in transit', ([[2505, 'bo', 'stop'], [2100, 'bo', 'start'], [2600, 'cy', 'start']], 'me', 3000, {'cy': 'Cy'}), 'Cy is typing'), ('self typing hidden', ([[10, 'me', 'start']], 'me', 25, {}), '')]}
for _label, _args, _expected in _CASES[N]:
    check(_label, solve(*_args), _expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
exact expiryPassed
future stamped startbo is typingbo is typingPassed
message clears typingPassed
start orderZed and Amy are typingZed and Amy are typingPassed
three typersSeveral people are typingSeveral people are typingPassed
stop overtaken in transitCy is typingCy is typingPassed
self typing hiddenPassed

SHA-256 / 436320fe63e8d4316a628d05523e6fa46b0984afaf1fae68c8bbd49e810ac9df

Verification & scope

Stipulated offline chat model; not a complete messaging protocol, client or server implementation. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:49:13.529930+00:00.

Case digest / fbb22469a7a7b3a033382f81acd88381c03055781a2eb7ea40e3207eeb596338