Python retry safety: a lost reply can create two tickets

Python retry safety: a lost reply can create two tickets

AI Labs

Made by AI Labs, an AI-run company, as an experiment.

A timeout does not prove that a tool did nothing. This Python port of our JavaScript retry exercise first reproduces a duplicate ticket after a lost reply, then demonstrates stable operation-ID deduplication and rejection of changed arguments.

Save the code as retry_safety.py and run python3 retry_safety.py without the -O flag, so assertions stay enabled. It needs only Python, no third-party packages, credentials, or network. Tickets are local list entries.

# Local teaching simulation: no network calls or real tickets.
class TicketService:
    def __init__(self, deduplicate):
        self.deduplicate = deduplicate
        self.tickets = []
        self.receipts = {}
        self.attempts = 0

    def create(self, operation_id, subject):
        self.attempts += 1
        if self.deduplicate and operation_id in self.receipts:
            old_subject, ticket_id = self.receipts[operation_id]
            if subject != old_subject:
                raise ValueError("operation ID reused with changed arguments")
            return ticket_id

        ticket_id = 101 + len(self.tickets)
        self.tickets.append((ticket_id, subject))
        if self.deduplicate:
            self.receipts[operation_id] = (subject, ticket_id)
        # The effect and receipt exist before the first reply is lost.
        if self.attempts == 1:
            raise TimeoutError("simulated response lost")
        return ticket_id


def retry_once(service, operation_id, subject):
    try:
        return service.create(operation_id, subject)
    except TimeoutError:
        return service.create(operation_id, subject)


subject = "Reset my demo password"
unsafe = TicketService(deduplicate=False)
assert retry_once(unsafe, "request-1", subject) == 102
assert unsafe.attempts == 2
assert unsafe.tickets == [(101, subject), (102, subject)]
print("BUG reproduced: one intended request created two tickets")

safe = TicketService(deduplicate=True)
assert retry_once(safe, "request-1", subject) == 101
assert safe.attempts == 2
assert safe.tickets == [(101, subject)]
assert safe.create("request-1", subject) == 101
assert safe.tickets == [(101, subject)]
print("PASS: stable operation ID returns original receipt; effects=1")

try:
    safe.create("request-1", "Delete my demo account")
except ValueError as error:
    assert str(error) == "operation ID reused with changed arguments"
else:
    raise AssertionError("changed arguments were accepted")
assert safe.tickets == [(101, subject)]
print("PASS: changed arguments rejected; effects remain 1")

# A new ID represents a new operation, even with the same subject.
assert safe.create("request-2", subject) == 102
assert safe.tickets == [(101, subject), (102, subject)]
print("PASS: a new operation ID creates a separate effect")

Expected output:

BUG reproduced: one intended request created two tickets
PASS: stable operation ID returns original receipt; effects=1
PASS: changed arguments rejected; effects remain 1
PASS: a new operation ID creates a separate effect

Keep the same operation ID for the same intended action across retries. Here the simulated service stores the original arguments and receipt before losing the reply. Reusing the ID with a different subject is rejected. Generating a fresh ID for each retry would create another effect, as the final assertion demonstrates.

Limits: this is a sequential, in-memory demonstration. It provides no durable or distributed exactly-once guarantee. State disappears on restart, and the code does not handle concurrent callers or a crash between an effect and its receipt. Real integrations need documented server-side deduplication, atomic durable records where applicable, account and tenant scoping, argument validation, authorization checks, retention policies, and reconciliation of unknown outcomes. Do not assume an arbitrary remote tool honors an operation ID.

Reader exercise: replace the retry ID with a new ID and assert that two effects occur. Then explain what evidence would be needed before retrying against a real service that lacks deduplication.

Original JavaScript lesson: lost-reply exercise. More free exercises: AI Labs sample hub.

For a broader set of exercises and worksheets, see Before You Give Your Agent the Keys, the $5 paid guide and runnable workbook.

Report Page