# A return retry keeps the old choice after a correction

Source: https://rasa.community/library/casebook/retail-return/
Author: Rod Rivera
Published: 2026-10-08T09:00:00Z

The customer changes their choice to exchange. The retry returns return. Nothing new was created, but the correction was not accepted:

```text
{"effects": 0, "refund": "not_decided", "replay": true, "requests": 1, "resolution": "return", "status": "pending", "step": "retry_with_exchange_argument", "submission_key": "WS-RSUB-1EDB5CC5"}
```

We called the Willow Shop submission function directly after it accepted a return and lost the acknowledgement. This is an offline fixture experiment, not a recorded conversation or evidence that a model chose this path.

## The key lookup runs before choice validation

The [pinned submission function](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-text-retail-return-claude/lib/returns.py#L568-L595) makes a key from the conversation and item. Its early return is the important boundary:

```python
key = submission_key(conversation_id, ref)
if key in service.requests:
    record = service.requests[key]
    return _receipt(service, record, replay=True)

# Eligibility and newly supplied choice validation happen after this branch.
```

:::diagram{title="A changed retry reaches the replay branch before choice validation"}

```dot
rankdir=TB;
a [label="Submit with exchange
same conversation and item"];
b [label="Existing request key?"];
c [label="Yes: replay stored return
exchange not validated"];
d [label="No: validate current choice
then submit"];
a -> b;
b -> c [label="key exists"];
b -> d [label="new key"];
```

:::

The sample's pre-submission correction test, `test_changed_choice_blocks_the_old_request`, exercises a different boundary: it updates the choice before a request exists. A passing correction test there does not establish amendment after acceptance. The selected `SubmitTests`, `ChoiceTests`, and `FindTests` receipt has 17 passing checks without skips.

## Replay the uncertain submission, then inspect it

Use the [pinned returns example](https://github.com/RasaHQ/rasa-community-resources/tree/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-text-retail-return-claude). The canvas bag creates one request with an unavailable first acknowledgement. We change the memory and argument to exchange, preserving the original key, then look up that key:

Save this as `/tmp/retail-return-check.py`. From the example's `tests` directory, run `PYTHONPATH=. python3 -B /tmp/retail-return-check.py`:

```python
"""Run from this example's tests directory with PYTHONPATH=. Stdlib only."""
import json
import test_guard as t
svc = t.fresh()
said = ["Return the canvas weekender bag"]
memory = t.chosen(svc, t.BAG, "return", said)
first = t.wr.submit_return_request(svc, t.ME, memory, t.BAG, "return", said, "catchup")
corrected = {**memory, "selected_resolution": "exchange"}
again = t.wr.submit_return_request(svc, t.ME, corrected, t.BAG, "exchange", said + ["Actually exchange it instead"], "catchup")
looked = t.wr.check_return_status(svc, t.ME, first["submission_key"])
for name, result in (("initial_return", first), ("retry_with_exchange_argument", again), ("status_lookup", looked)):
    print(json.dumps({"step": name, "status": result["status"], "resolution": result["resolution"], "effects": result.get("effects"), "replay": result.get("replay", False), "requests": len(svc.requests), "submission_key": result.get("submission_key"), "refund": result["stages"]["refund"]}, sort_keys=True))
```

Actual offline output:

```text
{"effects": 1, "refund": "not_decided", "replay": false, "requests": 1, "resolution": "return", "status": "pending", "step": "initial_return", "submission_key": "WS-RSUB-1EDB5CC5"}
{"effects": 0, "refund": "not_decided", "replay": true, "requests": 1, "resolution": "return", "status": "pending", "step": "retry_with_exchange_argument", "submission_key": "WS-RSUB-1EDB5CC5"}
{"effects": 0, "refund": "not_decided", "replay": true, "requests": 1, "resolution": "return", "status": "authorized", "step": "status_lookup", "submission_key": "WS-RSUB-1EDB5CC5"}
```

The lookup later reports authorized, but the stored resolution remains return. Normal selection tools can reject an already-authorized item. This direct call bypasses those tools, the skill, and the confirmation gate to isolate the submission contract.

## Decide what a correction means after acceptance

| Customer intent                  | Operation to expose             | Evidence to report                                 |
| -------------------------------- | ------------------------------- | -------------------------------------------------- |
| Try the same return again        | Retry the original key          | Original stored resolution and stages              |
| Find out whether it went through | Status lookup                   | Accepted request, even if acknowledgement was lost |
| Change return to exchange        | A supported amendment operation | Acknowledged change to the original request        |

The sample has no amendment operation. Its returns-desk route is a supported escalation, not proof that an exchange happened. Authorization also leaves the refund undecided; it does not prove inspection or reimbursement.

For a production retry endpoint, compare the supplied intent with the accepted record. A different payload should produce an explicit conflict or an equally explicit old-intent replay. This is a proposed contract, not a repair demonstrated here.

If the service supports amendment, give that operation a new amendment ID linked to the original request. Do not create a second return submission to imitate a change. The service must define whether the accepted stage is still amendable and acknowledge the outcome. Preserving accepted intent during a lost acknowledgement can require another lookup and leave the customer's correction waiting; that cost is preferable to silently claiming a different operation succeeded.

:::checkpoint{id="return-retry-choice" question="The retry has zero new effects and resolution return after the caller asks for exchange. What is established?" options="The exchange was approved|The original return was replayed and the exchange remains unaccepted|The return was cancelled" answer="1"}
:::solution{title="A replay is not an amendment"}
The original record remains a return. Zero new effects proves the retry created nothing; it does not prove the corrected choice was accepted.
:::
:::

:::cta{href="/library/tutorials/guarding-irreversible-actions/" label="Separate retries from new side effects"}
Use the tutorial's idempotency lesson to decide which operation a retry is allowed to repeat.
:::

The ledger is in memory. This experiment does not establish cross-process safety, real warehouse behaviour, or customer comprehension.