# 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/index.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: This is a newly authored synthetic fixture. It is not a production incident report, not a benchmark, and not a measurement of agent or API reliability.

## Scenario

An agent starts a partner catalog 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 is not proof that nothing happened. Retrying a state-changing operation without checking state can duplicate work or make 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 catalog 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 status was verified against the original operation by a trusted service; accepting an arbitrary status string is not verification.
- The fixture does not define a universal retry policy for every API or agent.
- The fixture assumes the product provides an idempotency key or status lookup; without one, the safer result may be a human handoff.
- The fixture is not evidence of real-world reliability or benchmark performance.

## 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.
