# Timeout recovery: verify before retry

Canonical: https://agentexperience.tech/examples/timeout-recovery-verify-before-retry/
Markdown: https://agentexperience.tech/examples/timeout-recovery-verify-before-retry.md

A synthetic sync job shows how an agent should treat an uncertain timeout as product history, verify state, and avoid repeating a side effect blindly.

Synthetic boundary: A synthetic fixture: the sync service, its tools and data are invented for this site. It is not a benchmark or a measurement of agent or API reliability.

## Scenario

An agent starts a partner catalogue sync with an idempotency key. The first call returns no usable result before the client timeout. A status tool can check the operation by key, and the server may expose `Retry-After` when it is temporarily unavailable.

## Product problem

A timeout does not prove that nothing happened. Retrying a state-changing call without checking its state first can do the work twice and makes the recovery harder to trust.

## AX systems touched

- Orientation and discovery
- Tools and interfaces
- Workflows and state
- Delegated action and trust
- Evaluation and improvement

## State flow

1. Start call submitted
   - Evidence: The agent generated an idempotency key and called `catalog.start_partner_sync`.
   - Next: Wait for a structured result or timeout.
2. Local timeout creates uncertainty
   - Evidence: The client has no accepted/failed result after 15 seconds.
   - Next: Record uncertainty instead of retrying the side effect.
3. Status lookup resolves duplicate risk
   - Evidence: `catalog.get_sync_status` returns `accepted` for the same idempotency key.
   - Next: Observe operation `sync_7b2` rather than submitting again.
4. Handoff is verifiable
   - Evidence: Run record contains idempotency key, operation id, status, and next check.
   - Next: Report progress only with the observed state.

## Regression case

```json
{
  "input": {
    "firstAttemptTimedOut": true,
    "idempotencyKey": "sync-key-2026-09-11-a",
    "status": "accepted",
    "operationId": "sync_7b2",
    "retryAfterSeconds": 30
  },
  "output": {
    "state": "in_progress",
    "repeatStartCall": false,
    "next": "poll existing operation after 30 seconds",
    "handoff": "First submit timed out locally; status lookup found accepted operation sync_7b2. I am not resubmitting."
  }
}
```

## Deterministic fixture

User request: “Sync the approved catalogue with the partner account and tell me when it is done.”

Starting state:
- `catalog.start_partner_sync` requires `catalog_id`, `partner_account_id`, and `idempotency_key`; its result should include `operation_id` when accepted.
- `catalog.get_sync_status` accepts either `operation_id` or `idempotency_key` and returns `unknown`, `accepted`, `running`, `complete`, or `failed`.
- The first start call times out locally after 15 seconds and returns no structured result to the agent.
- A later status check for the same idempotency key returns `accepted` with `operation_id: sync_7b2`.

Agent should do:
- Record the first attempt as uncertain, not failed or complete.
- Check status using the idempotency key before repeating the start call.
- If status is accepted/running/complete, do not call start again; observe or report the existing operation instead.
- If the service returns temporary unavailability with Retry-After, wait or schedule the follow-up according to that value before polling again.
- Return a handoff with the idempotency key, observed status, operation id, and next check.

Agent should not do:
- Do not retry the side-effecting start call merely because the client timed out.
- Do not erase the timed-out attempt from the run history.
- Do not report success until a status or result proves the terminal state.

Expected decision: Verify by idempotency key first; because the status is accepted with operation `sync_7b2`, continue observing that operation instead of submitting a second start call.

## Exercise

- Classify the timeout state before recovery begins: success, failure, or uncertain.
- Choose the next tool call and name the input that prevents duplicate work.
- Write the final user-facing handoff if the status check returns `running`.

## Answer key

- The timeout state is uncertain because the client lacks proof that the server did or did not accept the operation.
- Call `catalog.get_sync_status` with the original idempotency key; do not repeat `catalog.start_partner_sync` first.
- Handoff: “The first submit timed out locally, but status lookup found operation sync_7b2 running for the same idempotency key. I am not resubmitting. Next check should poll sync_7b2 after the advised delay or until a complete/failed status appears.”

## Limits

- The fixture assumes a trusted service checked the status against the original operation. Accepting whatever status string comes back would not count as verification.
- It describes one recovery decision. Each API still needs its own retry policy.
- It assumes the product offers an idempotency key or a status lookup. Without either, handing the decision to a person may be the safer result.
- It is a teaching case and says nothing about real-world reliability.

## Public source basis

- [MCP 2026-07-28 tools specification](https://modelcontextprotocol.io/specification/2026-07-28/server/tools): Tool result handling, self-correction, state handles, timeouts, and audit guidance.
- [RFC 9110 HTTP Semantics: Retry-After](https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.3): The HTTP field that tells a user agent how long to wait before a follow-up request.
- [RFC 9110 HTTP Semantics: 503 Service Unavailable](https://www.rfc-editor.org/rfc/rfc9110.html#name-503-service-unavailable): Temporary overload or maintenance response that may include Retry-After.
