An HTTP timeout does not tell you whether a shipment succeeded
A carrier can commit a shipment and then lose its reply. The calling system is left with neither a confirmed success nor any evidence that a retry is safe. Replan is a public engineering case study built around that boundary. It approves spare-part transfers for factory repairs, executes them, and recovers when the evidence changes. This post covers how it avoids a second dispatch, and why it will not release stock it cannot account for.
A timeout is an unresolved decision
A retry loop assumes that a failed request did nothing. Across a network boundary, that is not true. The request may have reached the carrier, the carrier may have stored the shipment, and only the response may have gone missing. A rollback in your own database cannot undo a commitment that now lives in someone else's.
There are two easy ways to get this wrong:
- Retry under a new identity. If the shipment did commit, you have now booked two.
- Treat the timeout as a failure and release the stock. If the shipment did commit, the parts on that truck are now promised to another order.
Replan frames the operator's question differently: what can I still safely commit to, given what has already happened?
Scope first. The repository is MIT licensed. It uses synthetic providers: the inventory and carrier are simulators with their own PostgreSQL databases, reached over real HTTP. There are no live shipments, no real carrier integrations and no customer data. There has been no customer pilot. What it does have is real persistence, real HTTP failures and real process restarts in its tests.
One scenario: three repairs and a lost reply
The seeded demo scenario is called "The missing bearing". Three factory repairs need bearing kits from a small network of warehouses. All factories, quantities and dollar amounts are synthetic. The recorded demo walks through these steps:
| Step | What changes | What the operator can safely do |
|---|---|---|
| Approve | A $440 proposal covers three repair orders. | Authorise those allocations and their recorded evidence. |
| Lose the reply | The carrier commits the first transfer, worth $320. No usable receipt comes back. | Treat the outcome as unknown and keep its reservation held. |
| Lose lookup | The carrier cannot answer reconciliation requests. | Inspect the hold. A replacement plan cannot bypass it. |
| Change stock | Another consumer takes four units from Vienna. | Recover the first shipment, then stop at the stale reservation. |
| Replan | A new $295 proposal covers only the two remaining orders. | Review and approve that changed remainder. |
After the lost reply, the operational summary shows zero confirmed dispatches and zero confirmed cost. The evaluator view, which reads the simulator directly, shows one carrier dispatch. Both are correct. They are separate records on purpose: the carrier has committed a shipment, and the application does not yet have confirmation of it.
Optimisation proposes, approval authorises
Planning uses OR-Tools CP-SAT with a lexicographic objective. It first maximises the total priority of fully fulfilled orders, then minimises transport cost for that priority. Orders are indivisible. Stock, lane capacity and repair windows are hard constraints. A greedy baseline visits orders by priority, then deadline, and takes the cheapest route that is still feasible. It shares the same feasibility rules and the same execution path. In the seeded scenario the optimised proposal costs $440 and the greedy one $475, both covering all three orders.
The solver is treated as an untrusted proposal generator. The TypeScript boundary validates its output and independently checks order coverage, capacities, deadlines, totals and evidence versions.
Approval binds a concrete decision. A SHA-256 fingerprint covers the scenario ID, strategy, source snapshot and full solution, computed over canonical JSON. The operator approves the exact fingerprint on screen. Before execution, the application recomputes it against the stored payload and checks that each action matches the approved allocation. Proposals older than ten minutes cannot be approved. The repository is explicit that this is an integrity check, not a digital signature.
No LLM takes part in planning or authorisation, so the behaviour is reproducible without a model provider.
Record intent before touching the outside world
Each action moves through durable stages, and the stage is written before the external call is made:
pending → reserving → reserved → dispatching → dispatched → completed
↘ dispatch_unknownEvery action has a stable key derived from the plan ID and the action's position in the plan. A retry reuses that key and the same payload. The simulated providers treat a repeated identical request as the same request, and reject a reused key that arrives with different arguments. The carrier also refuses a second commitment for an order that is already dispatched in the same scenario.
Inventory follows a related rule: an observation is not a reservation. The planner works from a recorded snapshot. When execution reserves stock, the inventory service checks the expected version and the available quantity, subtracts it and creates a held reservation, all in one PostgreSQL transaction. If the version has changed, the plan stops, even when enough units happen to remain. That keeps execution tied to the evidence the operator approved.
If the application restarts mid-run, an interrupted plan becomes uncertain. Check & recover queries each provider using the original keys. If the carrier returns a receipt that matches the approved action, Replan records it and finishes the inventory bookkeeping without creating another shipment. A receipt that describes a different action is rejected, even if the HTTP exchange succeeded. Recovery is triggered by the operator; there is no background retry worker.
A 404 is not a cancellation
The subtle case is a lookup that finds nothing. A 404 only says that no record is visible right now. A dispatch that timed out could still commit a moment later. While the repair window is open, recovery may retry the same approved request with the same key. It must not switch to a new key or a new plan to escape the ambiguity.
Deadlines make the rule explicit:
- If the repair window expires before any dispatch attempt, the uncommitted reservation can be released and the plan invalidated.
- If dispatch was attempted and the deadline then passes, an empty lookup does not authorise release. Stock stays held and the plan stays uncertain until the carrier gives positive evidence or a terminal cancellation.
- If lookup is unavailable, the reservation stays held and replacement approval is blocked.
Cancellation is its own protocol. Replan persists the reason first, then asks the carrier to fence the action key under the same lock the carrier uses for creation. If the shipment had already committed, the receipt is kept and inventory consumption is reconciled. Only a terminal carrier cancellation allows the inventory reservation to be released. There is no manual "assume it failed" override.
Replan only what remains
Back in the scenario, four units leave Vienna outside Replan. Once normal service returns, recovery confirms the existing Linz shipment without duplicating it. It then attempts the next approved reservation, finds that Vienna's stock version has changed, and moves the plan to needs_replan with Linz retained as confirmed.
The operator refreshes inventory, which records a new observation. Planning now excludes completed orders and subtracts the lane capacity they used. The replacement covers only Graz and Brno: Brno to Graz for $255 and Vienna to Brno for $40, a total of $295. It is a new proposal with a new fingerprint, and it needs a new approval. The original approval never silently authorises a more expensive substitute.
The run ends with three distinct dispatches and $615 committed: $320 already spent on Linz plus the $295 remainder. The extra cost stays visible instead of disappearing into a silent retry. "Completed" means the transfers were dispatched and the bookkeeping finished. It does not mean the parts arrived or the repairs succeeded.
How it is tested, and where it stops
The integration tests launch a real Node process, two HTTP services and three PostgreSQL databases. They kill the application after a carrier commit, start a new process and check the independent provider state. Row-lock tests hold database locks so that both inventory and carrier requests commit after the client has timed out and a lookup has returned 404. That is the race a simple retry-on-failure demo never exercises.
The solver has its own evidence. Its tests check 64 small instances against an independent exhaustive oracle. The published benchmark has 27 authored synthetic cases, including infeasible demand. Across those cases, optimisation fulfils 70 orders against 53 for greedy, with priority 620 against 512, at a higher total cost of $1,881 against $1,582. Neither strategy violates the checked constraints. The README calls these finite synthetic results, not independently validated operational gains, and the cases have at most eight orders each.
Exported evidence can be checked offline. The verifier checks fingerprints, approval and dispatch intent, matching commitments, uniqueness and stock conservation. Exports are unsigned snapshots, so a pass shows internal consistency, not authenticity or proof of delivery.
What it does not claim
- No distributed transaction and no exactly-once delivery.
- Safety depends on provider contracts: durable idempotency keys, authoritative conditional inventory writes and consistent lookup of committed carrier effects. A provider without them needs a different recovery design.
- A real carrier may treat label purchase, handoff and delivery as separate commitments. The simulator's cancellation contract cannot simply be assumed.
- One active operation per workspace, serialised by a PostgreSQL advisory lock. It is not a general workflow platform.
The pattern carries over to any system that books, charges or sends something on your behalf. Before you let it retry automatically, check three things about each provider: whether it honours an idempotency key you control, whether it can reserve conditionally against a version, and whether you can look up a committed effect by your own key. If any answer is no, a timeout has to become a held, visible state that a person resolves.