FAILURE MAP
← Case archive

FA-20906 / Assistive announcements / Open access

Canceling another job also stops current speech · case 01

The announcement trace violates the stated interrupt-resumption contract.

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

ROOT CAUSE

Fault site: if current is not None and current[0]==e[1]: current=None is implemented as if current is not None: current=None

VERIFIED REPAIR

Restore the stated transition: if current is not None and current[0]==e[1]: current=None

Unsuccessful approach: The attempted repair substitutes if current is not None and current[0]==e[1]: current[2]=0 and still violates a regression oracle.

Case contract

Speech jobs are [id,words,cursor]. Start queues a new job when one is active. Urgent pushes the current job on a LIFO suspended stack and begins fresh. Step speaks one word and finishes only after its last word; suspended jobs resume before queued normal work. Cancel ID removes it from every location and promotes next available job. Clear discards all jobs without speech. Report emitted [id,word] and current/suspended/waiting IDs with cursors.

Why this case matters

Deterministic controlled model of assistive announcement delivery.

1 / The failure

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

N = 1
observations = []
def solve(events):
    current=None; suspended=[]; waiting=[]; spoken=[]
    for e in events:
        if e[0] in ('start','urgent'):
            job=[e[1],list(e[2]),0]
            if e[0]=='urgent':
                if current is not None: suspended.append(current)
                current=job
            elif current is None: current=job
            else: waiting.append(job)
        elif e[0]=='cancel':
            if current is not None: current=None
            suspended=[x for x in suspended if x[0]!=e[1]]
            waiting=[x for x in waiting if x[0]!=e[1]]
        elif e[0]=='clear': current=None; suspended=[]; waiting=[]
        elif e[0]=='step' and current is not None:
            if current[2]<len(current[1]):
                spoken.append([current[0],current[1][current[2]]])
                current[2]+=1
            if current[2]>=len(current[1]): current=None
        if current is None:
            if suspended: current=suspended.pop()
            elif waiting: current=waiting.pop(0)
    return [spoken,None if current is None else [current[0],current[2]],[[x[0],x[2]] for x in suspended],[[x[0],x[2]] for x in waiting]]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = ["[['start','a',['a']*(N+2)],['step'],['urgent','b',['b']],['urgent','c',['c']],['step'],['step'],['step']]", "[['start','a',['a']*N],['start','b',['b']],['start','c',['c']]]+[['step']]*(N+2)", "[['start','a',['a']*N],['urgent','b',['b']],['start','c',['c']],['cancel','a'],['step'],['step']]", "[['start','a',['a']*N],['start','b',['beta']],['start','c',['gamma']],['cancel','b']]", "[['start','a',['a']*N],['urgent','b',['b']],['start','c',['c']],['clear']]", "[['start','a',['a']*N],['urgent','b',['b']],['urgent','c',['c']],['cancel','a']]", "[['start','a',[]],['step']]", "[['start','a',['a']*N],['cancel','a']]"]
expected = {1: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 2: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 3: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 4: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 5: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]]}[N]
for i, expression in enumerate(fixtures):
    check("trace-"+str(i+1), solve(eval(expression)), expected[i])
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
trace-1[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []][[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []]Passed
trace-2[[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []][[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []]Passed
trace-3[[['c', 'c']], None, [], []][[['b', 'b'], ['c', 'c']], None, [], []]Failed
trace-4[[], ['c', 0], [], []][[], ['a', 0], [], [['c', 0]]]Failed
trace-5[[], None, [], []][[], None, [], []]Passed
trace-6[[], ['b', 0], [], []][[], ['c', 0], [['b', 0]], []]Failed
trace-7[[], None, [], []][[], None, [], []]Passed
trace-8[[], None, [], []][[], None, [], []]Passed

SHA-256 / cad07887a8a1f757920b192a249433cab6df7961352986f501a97c87fdc98ffb

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(events):
    current=None; suspended=[]; waiting=[]; spoken=[]
    for e in events:
        if e[0] in ('start','urgent'):
            job=[e[1],list(e[2]),0]
            if e[0]=='urgent':
                if current is not None: suspended.append(current)
                current=job
            elif current is None: current=job
            else: waiting.append(job)
        elif e[0]=='cancel':
            if current is not None and current[0]==e[1]: current[2]=0
            suspended=[x for x in suspended if x[0]!=e[1]]
            waiting=[x for x in waiting if x[0]!=e[1]]
        elif e[0]=='clear': current=None; suspended=[]; waiting=[]
        elif e[0]=='step' and current is not None:
            if current[2]<len(current[1]):
                spoken.append([current[0],current[1][current[2]]])
                current[2]+=1
            if current[2]>=len(current[1]): current=None
        if current is None:
            if suspended: current=suspended.pop()
            elif waiting: current=waiting.pop(0)
    return [spoken,None if current is None else [current[0],current[2]],[[x[0],x[2]] for x in suspended],[[x[0],x[2]] for x in waiting]]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = ["[['start','a',['a']*(N+2)],['step'],['urgent','b',['b']],['urgent','c',['c']],['step'],['step'],['step']]", "[['start','a',['a']*N],['start','b',['b']],['start','c',['c']]]+[['step']]*(N+2)", "[['start','a',['a']*N],['urgent','b',['b']],['start','c',['c']],['cancel','a'],['step'],['step']]", "[['start','a',['a']*N],['start','b',['beta']],['start','c',['gamma']],['cancel','b']]", "[['start','a',['a']*N],['urgent','b',['b']],['start','c',['c']],['clear']]", "[['start','a',['a']*N],['urgent','b',['b']],['urgent','c',['c']],['cancel','a']]", "[['start','a',[]],['step']]", "[['start','a',['a']*N],['cancel','a']]"]
expected = {1: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 2: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 3: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 4: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 5: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]]}[N]
for i, expression in enumerate(fixtures):
    check("trace-"+str(i+1), solve(eval(expression)), expected[i])
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
trace-1[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []][[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []]Passed
trace-2[[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []][[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []]Passed
trace-3[[['b', 'b'], ['c', 'c']], None, [], []][[['b', 'b'], ['c', 'c']], None, [], []]Passed
trace-4[[], ['a', 0], [], [['c', 0]]][[], ['a', 0], [], [['c', 0]]]Passed
trace-5[[], None, [], []][[], None, [], []]Passed
trace-6[[], ['c', 0], [['b', 0]], []][[], ['c', 0], [['b', 0]], []]Passed
trace-7[[], None, [], []][[], None, [], []]Passed
trace-8[[], ['a', 0], [], []][[], None, [], []]Failed

SHA-256 / 332b425708d440f5c1e5c5423b074be19b9d94fa5b1e295939dfb636ca183765

3 / The verified repair

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

N = 1
observations = []
def solve(events):
    current=None; suspended=[]; waiting=[]; spoken=[]
    for e in events:
        if e[0] in ('start','urgent'):
            job=[e[1],list(e[2]),0]
            if e[0]=='urgent':
                if current is not None: suspended.append(current)
                current=job
            elif current is None: current=job
            else: waiting.append(job)
        elif e[0]=='cancel':
            if current is not None and current[0]==e[1]: current=None
            suspended=[x for x in suspended if x[0]!=e[1]]
            waiting=[x for x in waiting if x[0]!=e[1]]
        elif e[0]=='clear': current=None; suspended=[]; waiting=[]
        elif e[0]=='step' and current is not None:
            if current[2]<len(current[1]):
                spoken.append([current[0],current[1][current[2]]])
                current[2]+=1
            if current[2]>=len(current[1]): current=None
        if current is None:
            if suspended: current=suspended.pop()
            elif waiting: current=waiting.pop(0)
    return [spoken,None if current is None else [current[0],current[2]],[[x[0],x[2]] for x in suspended],[[x[0],x[2]] for x in waiting]]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = ["[['start','a',['a']*(N+2)],['step'],['urgent','b',['b']],['urgent','c',['c']],['step'],['step'],['step']]", "[['start','a',['a']*N],['start','b',['b']],['start','c',['c']]]+[['step']]*(N+2)", "[['start','a',['a']*N],['urgent','b',['b']],['start','c',['c']],['cancel','a'],['step'],['step']]", "[['start','a',['a']*N],['start','b',['beta']],['start','c',['gamma']],['cancel','b']]", "[['start','a',['a']*N],['urgent','b',['b']],['start','c',['c']],['clear']]", "[['start','a',['a']*N],['urgent','b',['b']],['urgent','c',['c']],['cancel','a']]", "[['start','a',[]],['step']]", "[['start','a',['a']*N],['cancel','a']]"]
expected = {1: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 2: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 3: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 4: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]], 5: [[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []], [[['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []], [[['b', 'b'], ['c', 'c']], None, [], []], [[], ['a', 0], [], [['c', 0]]], [[], None, [], []], [[], ['c', 0], [['b', 0]], []], [[], None, [], []], [[], None, [], []]]}[N]
for i, expression in enumerate(fixtures):
    check("trace-"+str(i+1), solve(eval(expression)), expected[i])
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
trace-1[[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []][[['a', 'a'], ['c', 'c'], ['b', 'b'], ['a', 'a']], ['a', 2], [], []]Passed
trace-2[[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []][[['a', 'a'], ['b', 'b'], ['c', 'c']], None, [], []]Passed
trace-3[[['b', 'b'], ['c', 'c']], None, [], []][[['b', 'b'], ['c', 'c']], None, [], []]Passed
trace-4[[], ['a', 0], [], [['c', 0]]][[], ['a', 0], [], [['c', 0]]]Passed
trace-5[[], None, [], []][[], None, [], []]Passed
trace-6[[], ['c', 0], [['b', 0]], []][[], ['c', 0], [['b', 0]], []]Passed
trace-7[[], None, [], []][[], None, [], []]Passed
trace-8[[], None, [], []][[], None, [], []]Passed

SHA-256 / 5e832a3e83061f1ee0093431e9d549659342050e125ab0deee199556dd419aae

Verification & scope

Stipulated bounded policy, not a browser, speech engine, platform API, or standards conformance claim. 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:40:23.482138+00:00.

Case digest / 90cbd825f74943918328f9058f30faaab8875011658bd64bd07479aeb3b64b8b