Repository navigation
Description
Audited on master at 5630d74ec9cd912d4e2f325349f1943cbc15bcc6. This is a scoped implementation item under #147, based on the typed control/dataflow/effect audit. Proposed syntax below is illustrative unless explicitly marked as a current reproducer.
Gap
The current AST has no explicit retry, timeout, idempotency, or cancellation contract. Implementing these through prose/template expansion would hide precisely the effects and failure behavior that the new IR needs to check.
This is a future feature request, not a report of an existing retry implementation.
Minimum deliverable
Add opt-in Retry and Timeout IR nodes around a resolved operation, with a small specified policy: eligible error identities, finite attempt budget, backoff/clock behavior, stable idempotency information, and timeout outcome/cancellation rules.
Implementation hints
- A retryable error classification and operation replay contract are both required. A failed call alone never establishes that the operation is safe to repeat.
- Begin with operations proven safe to replay (for example, pure operations with repeatable inputs) or explicitly trusted idempotent operations. For keyed operations, require the same evaluated key and payload across attempts, and document the provider's deduplication assumptions/scope.
- Model a timeout as the caller ceasing to wait, not proof that the underlying operation stopped. Require observed cancellation/quiescence or an adequate external deduplication contract before launching another potentially overlapping attempt.
- Treat ambiguous external completion as a typed outcome requiring reconciliation where necessary. Do not claim compiler checking delivers exactly-once payments.
- Preserve and expose retry effects: clocks, sleep/backoff, attempt count, and error selection. Evaluate the initial arguments/key once as specified.
- Use injected fake clock, transport, and event recorder for tests. No real payment/network operations are needed.
- Reject Retry/Timeout on unsupported backends until an adapter implements the same policy.
- Defer Race, compensation/sagas, and general Acquire/Release combinators until their cancellation/resource semantics have their own specification.
Acceptance criteria
- A non-idempotent writer without a replay contract is rejected.
- Keyed retry uses identical key/payload, respects the bounded attempt count, and retries only allowed failures.
- A non-retryable decline never triggers another attempt.
- A timed-out operation completing late cannot cause an unchecked overlapping charge.
- Zero/success/exhaustion/cancellation/late-completion cases have deterministic trace tests with a fake clock.
- The documentation states the static guarantees and the external contracts they rely upon.
Typestate can later make ambiguous post-states easier to express; this item must not assume a timeout leaves the original state unchanged.
Implementation ordering
Integration depends on #196, #197, #198. Add the behavior/diagnostic fixtures before implementation.
Osprey-informed implementation refinements
The Osprey fixture gives this issue a concrete control boundary: retry the requested operation inside its handler, then deliver at most one successful answer to the workflow. Retrying the entire remainder could repeat receipt or fulfillment effects.
- Treat attempt replay and continuation resumption as separate IR/runtime actions. Use feat(effects): add typed lexical handlers and injectable operation implementations #207's operation handler boundary and feat(control): enforce affine single-shot handler resumption #208's consumed-token checks when implementing handler-based retry.
- Evaluate the original operation arguments and stable idempotency key before the retry region. Fresh per-attempt values are permitted only for explicitly specified attempt metadata; they cannot silently replace the protected payload/key.
- All attempt effects remain in feat(effects): infer conservative operation effects with resource identities and foreign-call contracts #197's summary, including clocks, backoff, logging, and provider calls. Handling the operation does not hide those effects or its escaping failures.
- A success closes the retry decision and delivers one answer. A non-retryable decline or exhausted policy exits through feat(errors): add checked failure sets and a portable typed exception contract #198's typed failure without executing the success continuation.
- When asynchronous support is introduced, a late/duplicate callback must not resume a closed request. Suppressing that callback is not proof of remote cancellation, quiescence, or deduplication; the original timeout obligations above still apply.
Additional acceptance cases:
- A fake trace contains timeout/failure of attempt 1, permitted success of attempt 2, one answer delivery, and one receipt; the protected key and payload match across attempts.
- A non-retryable decline yields one attempt, zero success resumptions, and zero receipts.
- Exhaustion and a late success cannot introduce a second successful answer or downstream effect.
- Two independently started requests are not mistakenly treated as the same request merely because their operation names match.
Reference: Osprey's multiplicity fixture and single-shot checker, pinned to 8792de9f6d159922141f9d191e4bc5d256fbb971. The fixture demonstrates local attempt/receipt ordering; it is not evidence of a real gateway's exactly-once behavior.
Ordinary bounded Retry/Timeout work can proceed on the original dependencies. The handler-specific integration follows #207/#208; those issues do not depend on implementing this policy first.
Activity
Mnehmos commentedon Sep 12, 2026
Slice plan (planning pass, 2026-09-13)
Current state: no retry/timeout surface, IR, or checker; the reusable inputs are
IRFailureSpec/populate_failure_sets(#198) and the conservative effect analysis (#197), which are exactly the "retryable classification" and "safe to replay" signals the policy needs.PR 1 (checker + IR only; emitters refuse via an
ETARGET002capability):RETRY:/TIMEOUT:sections; add them to_should_use_regex_canonical_parse):IRRetryPolicy/IRTimeoutPolicyas optionalIROperationcontract slots (same pattern aseffects/failures); not rendered canonically, so non-adopting files keep identical hashes.TIMEOUTrequires an explicit outcome. New ESEM codes at PR time against the current maximum.retry_policies: Falsefor both targets (fatal), so nothing is silently dropped until a runtime exists;verifystill runs the full policy gate. The Python semantics marker bumps only in PR 2, when the runtime lands.PR 2 (later): Python-only attempt-loop runtime with injected fake clock,
retry_policies: Truefor python (TypeScript stays fatal, following theloop_stepsprecedent), deterministic trace tests (zero/success/exhaustion/cancellation/late completion), then the marker bump. Handler-scoped retry follows #207/#208.Merge pull request #249 from Mnehmos/feat/201-retry-policy-check
Mnehmos commentedon Sep 13, 2026
PR 1 landed (PR #249): checked, not yet emitted.
RETRY:declaresup to N attempts on <Errors>and anidempotency key: <param>;TIMEOUT:declaresafter Nms -> <outcome>. Inline headers and duplicate sections are parse errors; policies lower ontoIROperationslots and render canonically, so they participate in the semantic hash.ESEM018finite positive budget;ESEM019an identity the operation provably cannot raise (when the failure set has unknown markers the author's classification is authoritative, since only listed identities are retried and unlisted unknowns propagate);ESEM020effectful retry requires a parameter-named idempotency key;ESEM021timeout requires a positive deadline and an explicit outcome.retry_policiesis fatal for both registered targets, and the gate now coversrunandtestas well as compile/lock:update/ci; theETARGET002hint says the feature is checked but not yet emitted.PR 2 (runtime emission with injected fake clock and deterministic trace tests: zero/success/exhaustion/cancellation/late-completion) follows when a backend implements the policy — that is also when the python semantics marker would bump. Note: the external Repowise advisory health gate flagged this PR (repo-wide score 8.0 → 7.7 against a 0.3 budget) while all 14 repository CI checks passed; recorded here for transparency.