The tool endpoint
Retries, timeouts, idempotency
One rule explains this whole page: Tether retries a call only when it knows repeating it is safe. It knows that for reads. It cannot know it for writes.
The policy
| Read | Confirmed write | |
|---|---|---|
| Attempts | up to 3 | exactly 1 |
| Backoff | 150 ms, 300 ms | — |
| Retried on timeout | yes | no |
| Retried on connection failure | yes | no |
Retried on 408 429 502 503 504 | yes | no |
| Retried on any other non-2xx | no | no |
Carries X-Tether-Idempotency-Key | yes | yes |
Defaults come from ToolTransport: TimeoutSeconds: 20, MaxAttempts: 3. The timeout is
per attempt, so a read can consume up to roughly a minute in the worst case before it gives up.
Why writes are never retried
What you get back on the confirm call:
{
"status": "FailedRetryable",
"actionId": "9c1e7b6a…",
"commandType": "void_ticket",
"errorCode": "EXECUTION_FAILED_RETRYABLE",
"errorMessage": "Execution failed transiently; the action remains confirmable (1/3 attempts used)."
}
The action is released back to pending, so a person can decide to confirm again — after
checking. That decision is deliberately human, and it is bounded: after three total attempts
the action is consumed as failed.
The idempotency key
Every call carries X-Tether-Idempotency-Key, matching request_id in the body.
On a read it is a fresh value per attempt group — useful for correlating logs, not for deduplication that matters.
On a confirmed write it is the action id, in 32-hex form. It is stable across every retry of that same confirmation, which is what makes deduplication on your side possible:
case 'void_ticket': {
const key = req.get('X-Tether-Idempotency-Key')
const prior = await db.toolExecutions.findUnique({ where: { key } })
if (prior) return res.json(prior.result) // already did this one
const result = await voidTicket(tenant, args.ticket_id)
await db.toolExecutions.create({ data: { key, result } })
return res.json(result)
}
Record the key in the same transaction as the write. A separate write after the fact leaves exactly the window this is meant to close.
What Tether guarantees on its side
One execution per confirmation. Confirming an action claims it with a compare-and-set from
pending to executing. Two confirms racing produce one execution; the loser gets
Conflict or Replayed and never reaches your endpoint.
A second layer under that. A unique index on tenant, user and idempotency key means the database refuses a duplicate even if the claim logic were wrong.
Replay returns the original result. Send an idempotencyKey on the confirm call and a
repeat with the same key returns the stored result of the first successful execution, with
status Replayed. Nothing runs again.
So your endpoint should see one call per confirmation under normal operation. The key exists for the case Tether cannot rule out: a network failure after your handler committed.
Read retries and side effects
Reads retry, which is safe only if reads have no side effects. If a "read" tool sends a notification, meters a paid API, or claims a limited resource, three attempts means three of them.
That is not a retry bug. It means the tool was misclassified — see Reads and writes for the list of things that look like reads and are not.
Tuning the transport
{
"ToolTransport": {
"TimeoutSeconds": 20,
"MaxAttempts": 3,
"MaxResponseBytes": 262144
}
}
MaxAttempts: 1 disables read retries entirely. Raising TimeoutSeconds is usually the wrong
fix for a slow tool — the person waiting on the answer feels every second of it, and the model
may make several calls in sequence.