Repository navigation
std:retry has no validation combinator: retry-until-predicate exists only at the workflow altitude #466
Description
Summary
std:retry retries on failure. There is no combinator that retries on a
validation predicate — "call this, check the result, call it again with the
failure in hand if the check does not hold, up to N times."
goal ... over ... { until reached("label") budget { max_iterations: N } } (#409,
5.0.0) supplies exactly this at the workflow altitude. Nothing supplies it at the
function altitude, so a caller who wants a bounded validated retry around a single
call has to stand up a workflow to get one.
Current state
src/nodus/stdlib/retry.nd is two functions:
fn available() { return retry_available() }
fn call(func, policy) { return retry_call(func, policy) }
retry_call re-attempts on error. A result that returns successfully but is wrong
— a malformed edit, a schema-invalid payload, a plan that fails its own check — is
not a retry trigger.
The workflow-altitude form does work, and the state carrier is real: builtin_run_goal_pursuit
resumes the same graph from the last checkpoint rather than restarting it
(src/nodus/vm/vm.py:1433-1448), so workflow state survives across passes. A step
can write the failure into state and the next pass reads it. So the semantics are
already settled — this issue is about exposing them one altitude down.
What to add
import "std:retry"
let result = retry.until(
fn() { return edit(source) },
fn(r) { return r.ok }, // predicate; retry while false
{max_attempts: 3}
)
The predicate receives the previous result so the caller can decide; the failing
result should also be reachable by the next attempt (a last field on the policy
map, or pass it as a second argument to func) — otherwise "re-send the error"
has no carrier and the retry is a blind re-roll, which is the thing the pattern
exists to avoid.
Design notes
- Do not duplicate the goal budget vocabulary. If
retry.untiltakes
{max_attempts, deadline_ms}it should mean the same thingbudget { max_iterations, deadline_ms }means, so the two altitudes read alike. @retryrequires thenodus-lang[retry]extra; the runtime falls back to the
built-inInMemoryEffectStorewhen absent. Decide whetherretry.untilneeds
the effect store at all — it probably does not, since a bounded in-process loop
has nothing to make idempotent.
Provenance
C:\codev\Aider research\AIDER_AINDY_LENS_AUDIT.md §5 "Absorb into Nodus": "the
max_reflections=3, re-send-the-error loop is a reusable in-VM control structure;
lift it into the language as a guarded retry-on-validation-failure step." Written
2026-06-24 against 4.0.5, before #409 existed. Half of it shipped in 5.0.0; this is
the other half.
Affected versions
v5.0.4 (current when filed).
Activity
Masterplanner25 commentedon Aug 18, 2026
Filed together from one cross-check of the four
C:\codev\Aider researchdocuments against 5.0.4 (AIDER_AINDY_LENS_AUDIT.md,AIDER_ON_AINDY_RUNTIME_PORTABILITY_ANALYSIS.md,ACCURACY_CHECK_vs_aindy-runtime_2.4.0.md;AIDER_ARCHITECTURAL_AUDIT.mdcontains no Nodus claims). The set:std:retryvalidation combinator (the function-altitude half of goal as a stopping condition, and the DSL thesis: how does the language stop being incidental to the orchestration? #409)fs_scope; the sharpest of the four)Ranked: #467 first, then #466, then #465, then #468.
Nothing else the documents raised is Nodus work — the remaining corrections are aindy-runtime pinning
nodus-lang==5.0.1and needing 5.0.4 (#411 annotation forgery, #185 shared memory store, #390 workflow ownership all land in 5.0.2/5.0.3, and 5.0.3 alone breaks nodus-sdk).feat: retry.until, and a documented plan-then-act handoff (#466, #465) (
release: 5.8.0 -- work already in motion can be stopped, and a bad re…
#691: run a closure against the chunk it was compiled from (#695)