# Retries and failure semantics

An agent loses its connection while creating an issue. Should the harness retry? That depends on what failed: the model request, the tool connection, the local worker, or only delivery of the result. A missing response does not establish that the issue was never created.

**Retry scope** is the unit of work repeated. **Idempotency** means repeated execution has the same intended effect as one execution. **Reconciliation** means checking the external system to determine what actually happened. These concepts matter more than the choice of exponential backoff.

This page compares the pinned [Codex](../../snapshots/codex/8c076942cb70/chapter.md), [pi](../../snapshots/pi/482c1381a00d/chapter.md), [Goose](../../snapshots/goose/34a222e3f2b9/chapter.md), and [Sidekick](../../snapshots/sidekick/463d653124e1/chapter.md) sources. Sidekick citations require private repository access. Findings describe inspected paths, not a universal guarantee for every provider, extension, or external service.

## Trace the ambiguous failure

Consider this illustrative execution:

1. The model requests `create_issue` with call ID `call-42`.
2. The harness records the request, then invokes the tool.
3. The remote service creates issue 123.
4. The connection or worker dies before a result is durably recorded.
5. Recovery finds a request without a result.

Repeating step two can create issue 124. Repeating the model request can also produce a new call ID for the same intended action. A tool-call ID correlates protocol messages; it only prevents duplicate effects if the destination enforces it as a stable idempotency key. Restoring conversation history cannot undo issue 123.

The recovery decision needs evidence: **not started**, **completed with stored result**, or **started with unknown outcome**. An unknown mutation should be reconciled or reported as uncertain. A retry-safe read may be rerun, but its new result can differ because the world changed.

## Compare the retry boundaries

| Harness | Model/request recovery | Broader retry boundary | Important limitation |
| --- | --- | --- | --- |
| Codex | Classified stream errors, server-directed timing, transport fallback, cancellation-aware retry wait. [Source][c-stream] | Sandbox-related tool retries re-evaluate the authority of the actual retry. [Source][c-tool] | Reconnecting or approving a second attempt does not establish external idempotency. |
| pi | Coding-session retries after retryable assistant errors, with bounded attempts and abortable backoff. [Source][p-retry] | The separate experimental durable runtime checkpoints tool intent and replay policy. [Source][p-durable] | Durable-runtime guarantees must not be attributed to every coding-session tool. |
| Goose | Provider helpers plus an outer transient retry window before the first successful stream item. [Sources][g-provider] [Stream wrapper][g-stream] | Recipe success checks can retry a whole attempted conversation suffix. [Source][g-recipe] | Resetting conversation messages does not reset the external world. |
| Sidekick | Native loop recovery sits inside retried worker attempts. [Source][s-workflow] | Durable intents distinguish redelivery, first execution, safe rerun, interruption, and cancellation. [Source][s-recovery] | A replacement worker cannot guarantee that an old external operation stopped. |

## Codex: request recovery and tool escalation are separate

Codex's sampling retry loop rebuilds subsequent request input from current session history rather than always replaying the initial input unchanged. Context-window and usage-limit failures leave this path immediately. The retry handler classifies other errors and respects server retry timing; a WebSocket-to-HTTPS fallback can reset its bounded counter. An optional feature permits unbounded retries for eligible connection failures, with additional restrictions on session source and provider. [Sampling loop][c-sampling], [retry decisions][c-stream].

The caller wraps the retry handler in cancellation and preemption handling, so the backoff wait can be interrupted. These are request-recovery mechanics, not a blanket instruction to execute every earlier tool again. Likewise, a reconnect count is not a total task budget. [Cancellation boundary][c-sampling].

Tool escalation has its own policy. Following an eligible sandbox failure, the orchestrator checks whether escalation is allowed and whether another approval is required. An automatic review for the original, narrower action does not automatically authorize the broader retry. This illustrates two independent questions: “May this attempt run?” and “Could repeating it duplicate an effect?” The inspected escalation path answers the first; it is not a generic exactly-once protocol for arbitrary commands. [Escalation and approval][c-tool].

## pi: application retries versus durable tool recovery

Pi's reusable loop ends on an errored or aborted assistant message before dispatching that message's tool calls. The coding application may then prepare another request: it checks retry settings, increments a bounded counter, emits lifecycle events, and waits with an abort signal. The failed assistant attempt remains in raw history but is omitted from the next model projection. Context overflow is routed to compaction instead of ordinary transient retry. [Core ordering][p-loop], [retry preparation][p-retry].

The separate experimental durable runtime addresses a different failure. Before execution it commits the final tool arguments and a replay policy, defaulting to unsafe. Recovery reruns only when both the saved policy and the currently resolved tool say `safe`. Otherwise it settles an interrupted result explaining that the tool may have partially run. Changing a tool's policy after a crash cannot unilaterally grant replay permission. This is evidence-based recovery, not proof of exactly-once external execution. [Intent and recovery][p-durable].

## Goose: multiple budgets and a first-item boundary

Goose's shared provider helper uses configurable bounded retries, jittered backoff, and provider-supplied rate-limit delays. Its credential refresh allowance is separate from transient-error retries. Classification also matters: the general default can retry some request failures, while permanent thinking-signature/configuration errors are excluded. [Provider policy][g-provider].

The agent's stream wrapper chooses a stricter `transient_only` policy and retries only before the first successful stream item. Once an item arrives, it forwards the stream; this wrapper does not restart generation after arbitrary partial output. Providers that manage their own context bypass this outer retry window. Provider-internal retries can still exist, so a single counter is not the complete request budget. [Stream boundary][g-stream].

A recipe retry is larger in scope. After a completed response fails configured success checks, Goose can run an `on_failure` command and reset conversation messages to the kickoff. The attempt count survives on that kickoff message. This allows another attempt at the task, but previously executed tools and the failure command can have persistent effects. Builders must design cleanup and idempotency explicitly; a transcript reset is not a transaction rollback. [Recipe checks and reset][g-recipe].

## Sidekick: replace an attempt without blindly repeating actions

Sidekick's conversation workflow declares three retries. On an ordinary native worker exception, it interrupts the attempt and returns the turn to pending; exhausted retries reach a terminal failure handler. Those workflow retries are distinct from native model-loop iterations and from tool replay. Outpost execution follows a different ownership path. [Workflow configuration][s-config], [attempt interruption and terminal handling][s-workflow].

Native intent recovery inspects what is known before another model call. A settled intent redelivers its stored payload. An unclaimed intent can execute through a conditional claim. A running intent first waits for an existing result; rerunning then requires both stored and current replay policy to permit it. Otherwise it records an interrupted outcome. Delegation receives special treatment rather than blindly spawning another child. [Recovery decisions][s-recovery].

Cancellation is also a recovery input: pending or running intents can become cancelled, while an already settled result is still redelivered. This preserves known outcomes instead of hiding completed work behind a cancellation label. It does not cancel the original external executor or reverse its effects. [Recovery contract][s-contract].

## Builder exercise: test uncertain outcomes

Use a scripted model and a fake issue service. Give the service an optional idempotency key and a lookup endpoint. Inject failures before dispatch, after issue creation, after result storage, and during retry backoff. No real model or paid service is needed.

Assert that stored results are redelivered without another mutation; uncertain mutations trigger lookup or an explicit unknown result; and retries reuse the external idempotency key. Repeat with a newly generated model call ID to expose deduplication that only works within one response. Cancel during backoff and after the service commits, verifying that cancellation prevents future attempts without claiming the issue disappeared.

Track model attempts, tool attempts, and workflow attempts separately under an overall deadline or spending allowance. Record the retry reason, scope, delay, policy, and final outcome. See [testing and evaluation](testing.md) for deterministic fault injection, [runaway agents](runaway-agents.md) for budget boundaries, and [persistence and recovery](persistence.md) for the storage contract.

[c-stream]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/responses_retry.rs#L55-L174
[c-sampling]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/session/turn.rs#L1734-L1839
[c-tool]: https://github.com/openai/codex/blob/8c076942cb70fba8e5c4732c23f1faabc4aed484/codex-rs/core/src/tools/orchestrator.rs#L428-L584
[p-loop]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/agent/src/agent-loop.ts#L237-L274
[p-retry]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/coding-agent/src/core/agent-session.ts#L3732-L3848
[p-durable]: https://github.com/earendil-works/pi/blob/482c1381a00d8d00bc81697e4a980c593db6803e/packages/durable/src/harness/tool.ts#L122-L210
[g-provider]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose-provider-types/src/retry.rs#L14-L266
[g-stream]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/reply_parts.rs#L314-L373
[g-recipe]: https://github.com/aaif-goose/goose/blob/34a222e3f2b95d8b4a43482b53c2cc957ecce26d/crates/goose/src/agents/state_machine/ops_retry.rs#L223-L279
[s-config]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/workflows/agent_conversation_workflow.py#L1120-L1145
[s-workflow]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick_worker/workflows/agent_conversation_workflow.py#L2278-L2306
[s-recovery]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/intent_recovery.py#L475-L651
[s-contract]: https://github.com/datex/sidekick/blob/463d653124e1c2cda46a3f14f01f3d6f384326b1/backend/src/sidekick/services/intent_recovery.py#L1-L44
