Did Botie Create the Inspection, or Did the Reply Get Lost?
A request times out after Botie submits an inspection. Work out what the service may already have done before sending another request.
16 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
A Lost Reply Was a Problem Before Models Called Tools
BOTIE
The practical idea is to give the intended inspection a stable identity. A repeated delivery of that operation can then recover its earlier result within the receiving service’s documented contract. Our receipt ledger is a teaching model. It does not implement a distributed transaction or promise that every request eventually succeeds.
One Approved Inspection Has Become Two Jobs
The technician approves an inspection of P7. Botie submits the request, but the connection closes before the reply arrives. He tries again. The service now shows two inspection jobs for the same request.
From Botie’s side, the first attempt failed to produce a reply. From the service’s side, it may have succeeded. We need to keep those facts separate. This lesson builds a recovery decision that can reuse a recorded result, wait for a pending operation, or stop when another attempt would be unsafe.
No work-order service is called. Receipt records and clocks are supplied fixtures.
Step 1 of 2
A timeout describes the caller’s observation
The effect and the reply happen at different points. A broken connection can separate them.
- 1
Botie
Submit operation R7-inspect-1 - 2
Work-order service
Create WO-41 and store the operation receipt - 3
Network
Reply is lost - 4
Botie
Sees a timeout; cannot infer that no work occurred
Step 2 of 2
Look at what the receiver knows
Use the same operation key and compare the complete action payload.
| Receipt state | Meaning | Recovery action |
|---|---|---|
| Completed, same payload | The intended effect has a recorded result | Return that result |
| Pending, same payload | Processing is already in progress | Query status within a bounded policy |
| Same key, different payload | The caller changed the intended action | Reject the conflict |
| No receipt | This snapshot contains no result | Apply retry safety and deadline checks |
Completed, same payload
- Meaning
- The intended effect has a recorded result
- Recovery action
- Return that result
Pending, same payload
- Meaning
- Processing is already in progress
- Recovery action
- Query status within a bounded policy
Same key, different payload
- Meaning
- The caller changed the intended action
- Recovery action
- Reject the conflict
No receipt
- Meaning
- This snapshot contains no result
- Recovery action
- Apply retry safety and deadline checks
Recover an operation without inventing another one
The receiver’s stored outcome can resolve the problem without another effect. Safety and timing must both permit a retry when no receipt resolves it.
What should happen after the missing reply?
Keep the receipt lookup, retry safety and run deadline visible on separate branches. Then combine them into one recovery action.
BOTIE
Recovery decision
{
"route": "retry same operation key",
"delay_ms": 100,
"result": null
}Follow the operations
1. Intended operation and receiver contract
{ "key": "R7-inspect-1", "payload": { "asset": "P7", "operation": "inspect" }, "effect": "write", "idempotent": true }2. Authoritative service receipt snapshot
[]
3. Look up the operation receipt
{ "status": "absent", "result": null }4. Observed result of the last attempt
unknown
5. Classify whether another attempt is safe
retry permitted
6. Backoff and per-attempt limits
{ "base_ms": 200, "cap_ms": 1000, "jitter_fraction": 0.5, "timeout_ms": 500, "max_attempts": 3 }7. Attempts already started and run clock
{ "attempts": 1, "now_ms": 1000, "deadline_ms": 2500 }8. Fit the next attempt inside the deadline
{ "delay_ms": 100, "allowed": true }9. Choose the recovery action
{ "route": "retry same operation key", "delay_ms": 100, "result": null }10. Result
{ "route": "retry same operation key", "delay_ms": 100, "result": null }
The ledger is a supplied snapshot, not a distributed database. No service is called and no exactly-once execution guarantee is claimed.
1. Classify the failure before choosing another attempt
A permission denial or invalid asset identifier will not improve because the same request is sent again. Route those permanent failures to repair or review. A temporary overload or interrupted connection may justify a retry, but the tool’s contract must establish whether repeating the operation is safe.
Keep unknown outcomes distinct from known rejections. For a write, a timeout can mean the service completed the effect without delivering a reply. For a read, another request usually avoids duplicate business effects, though it still consumes resources and can return changed data. Our fixture labels outcomes success, transient, permanent or unknown; real adapters map their documented error codes into those categories.
The model does not get to reinterpret a permission denial as an invitation to try different arguments until something works.
2. Give the intended operation an identity
Create an operation key when the application accepts the approved intent. Persist it before sending the write. Reuse it when delivering that same operation again. A genuinely new inspection needs a new key, even if its asset and other fields match an earlier inspection.
The receiver binds the key to the caller’s scope and the relevant payload. The same key with a different pump or schedule must be a conflict. Avoid relying on the model to invent or remember this identity. It belongs to application state alongside the approved action revision.
A key in a request header is only useful if the receiving service enforces the contract.
3. Recover the receiver’s answer before scheduling more work
A completed receipt can return the earlier work-order ID. A pending receipt tells us the operation is already under way. A status query or a bounded wait is appropriate; launching a second operation is not.
This requires an authoritative ledger. A stale cache or an unavailable status endpoint cannot prove that the operation never happened. When the receiving service offers neither an idempotency contract nor a reliable reconciliation path, retain the ambiguous outcome for review. Do not turn it into an automatic new write. The workshop’s absent receipt assumes a trustworthy snapshot within the stated service contract.
Approval answers whether we may perform the action. A receipt answers what happened when we tried. Neither replaces the other.
Check yourself
Questions on Parts 1 to 3
Question 1 of 2
What should the application preserve?
Use the information below.
Botie submits a write. The caller times out. The service does not promise idempotency and its status cannot currently be verified.4. What the idempotency boundary has to store
The service needs a unique operation identity, a fingerprint or canonical representation of its payload, a processing state, and a stored result. Concurrent deliveries of the same operation must compete at an atomic boundary so both cannot decide they are the first.
The exact transaction depends on where the effect happens. A database write and its receipt may share a transaction. An external email or another service cannot automatically join that transaction; outbox delivery and downstream idempotency may be needed. Receipt retention also matters. Once a receiver forgets a key, a late replay may be treated as new work. Read the actual API contract and keep retries inside its scope and retention window.
Our Python dictionary lookup demonstrates the decision only. It is not a concurrency-safe store.
Recover a known operation result
A dictionary indexed by operation key makes a completed receipt easy to find. This example performs no write.
receipts = {
"R4-inspect-1": {"asset":"P4", "status":"completed", "result":"WO-18"}
}
key = "R4-inspect-1"
receipt = receipts.get(key)
if receipt is not None and receipt["status"] == "completed":
print(receipt["result"])
else:
print("No completed receipt in this snapshot")WO-18The result belongs to the earlier operation. The full challenge will also reject a reused key whose payload changed.
5. Give every attempt a timeout and the whole run a deadline
A per-attempt timeout bounds how long the caller waits for one operation. A run deadline bounds the whole recovery process, including delays and repeated attempts. Starting a fresh timeout after each failure can otherwise keep the run alive far longer than intended.
Count attempts that have actually started, including the first. Before scheduling another, check the remaining attempt allowance and whether its delay plus timeout fits the remaining deadline. Use a monotonic clock for elapsed-time accounting in a real process. Pass the remaining deadline to downstream calls where supported. Timing out locally does not roll back an effect already accepted elsewhere.
The supplied fixture clock is an integer millisecond counter so every branch can be tested without sleeping.
6. Spread retries without letting them continue forever
Repeated immediate requests can worsen an overloaded service. Exponential backoff increases the upper delay after successive attempts, and a cap prevents unbounded waiting. Jitter chooses a delay within a range so many clients do not all retry at the same instant.
Our full-jitter demonstration supplies a fraction between zero and one rather than drawing randomness during a test. We multiply that fraction by the capped upper delay and round down to whole milliseconds. In production, draw the random value per attempt and honour the service’s documented Retry-After guidance. Keep a separate total deadline; a capped delay alone allows infinitely many retries.
upper_delay = min(cap_ms, base_ms * 2 ** max(attempts_started - 1, 0))
delay_ms = floor(upper_delay * jitter_fraction)
next_attempt_fits = now_ms + delay_ms + timeout_ms <= deadline_msEquality is allowed in our scheduling fixture. Actual transport scheduling and cancellation are not precise guarantees that a remote operation finishes at that instant.
7. Choose one place to own automatic recovery
A provider SDK may already retry a request. A framework may retry a model response that fails validation. A tool adapter may retry its service call. If each surrounding layer also repeats the whole operation, the actual number of requests can grow much faster than the visible loop count.
Inspect the layers and give each failure one deliberate recovery owner. Count transport attempts separately from model turns and tool proposals. Record the operation key, attempt number, error category and final outcome without leaking sensitive request bodies. A validation retry asks for corrected content; a transport retry resends delivery. They have different purposes and may consume different budgets.
An agent’s friendly message that it is trying again should correspond to a recorded recovery decision in the application.
8. Let a cancellation leave an honest outcome
If the user cancels after submission, stop scheduling new work and request cancellation of any supported in-flight operation. The receiver may already have committed the effect. Preserve its operation identity and reconcile the final state before reporting that nothing happened.
For a workflow with several external effects, recovery may involve a compensating action such as cancelling a created appointment. That compensation is a separate authorised operation with its own possible failure. It does not erase the fact that the original effect occurred. Make partial outcomes visible to the person responsible for the request.
The next lesson will keep budget reservations for unresolved work rather than pretending a cancelled caller immediately receives its resources back.
9. Trip the retry loop with a completed write
Begin with the missing-reply case whose service supports a stable key. The recovery rule can permit another delivery within the deadline. Then load the completed receipt. The same construction should return its stored work-order ID immediately.
Change the payload under that key and check that the result becomes a conflict. Finally, remove the idempotency guarantee and leave the result unknown. Time remaining is no longer enough to justify another write. These cases test different reasons for stopping, so a single retry-until-success loop cannot satisfy them.
The result of this lesson is a recovery decision. Executing that decision still requires the live service contract and current authorisation checks.
Check yourself
Questions on Parts 1 to 9
Question 1 of 2
What does the scheduling check require?
Use the information below.
One attempt has started. max_attempts is 3. Current time is 1000 ms, chosen delay is 200 ms, timeout is 500 ms, and deadline is 1600 ms.Try it with Botie
Build with Botie
Write and run Python