You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
workflow run/resume: distinguish FAILED, PAUSED and ABORTED in exit code (all collapse to 1) #19
RunStatus has six members (src/factory/base.mjs:13): CREATED, RUNNING, PAUSED, COMPLETED, FAILED, ABORTED. Three of the five terminal-ish states therefore report the same code as a crash:
State
current exit
what it actually means
COMPLETED
0
success
FAILED
1
the task genuinely failed — not an infra fault
PAUSED
1
waiting for a human verdict at a gate — not a failure at all
ABORTED
1
deliberately stopped — not a failure
Why it matters
We run adlc-cli as a container entrypoint behind Argo Workflows with a retry policy that must distinguish infrastructure faults from logical failures, so it can retry the former and never the latter. A binary signal cannot express that.
The PAUSED case is the sharp one. Our supervised dispatch mode is defined as two pods split at a spec-approval gate: pod S1 runs prep/spec, the factory pauses, a human posts a verdict, S2 resumes. A completely successful supervised run therefore exits 1 on the normal path. Our orchestrator reads that as an infrastructure fault, retries three times, and re-fires a pod that is correctly waiting on a human — re-running steps the workflow contract explicitly forbids re-firing.
ABORTED collides the same way: a deliberate on_reject: abort at a gate is indistinguishable from an OOM kill.
Proposed contract
Classify rather than pass through. A suggested mapping:
Exit
Meaning
0
COMPLETED — done
10
FAILED — logical failure, do not retry
11
PAUSED — awaiting human input, not a failure
12
ABORTED — deliberately stopped
other
usage error, lease held, infra fault, unexpected — retryable
A stable, documented mapping is the main thing; the specific numbers matter less than the fact that each terminal state gets a distinct code. Suggestion:
make the classified mapping the default, since no existing caller distinguishes PAUSED from FAILED today, or
gate it behind a flag (e.g. --classified-exit / ADLC_CLASSIFIED_EXIT=1) for a deprecation cycle.
cmdAgentRun in src/commands/agent.mjs returns code ?? (signal ? 130 : 1) and has the same limitation.
Compatibility
Low risk either way. Scripts that only test for 0 vs non-zero keep working under the mapping above. The one behaviour that changes for everyone is PAUSED/ABORTED moving from 1 to their own codes — worth a minor-version note, and a reason to consider the flag.
Happy to send a PR if this is welcome — the change is localized to the two return statements plus a small helper.
Environment
adlc-cli 2.2.0 (main)
Node 22, macOS and Linux
Reproduced by reading the source and by driving workflow run to a gate pause
Blocking a downstream Argo lane rollout — requesting triage
Filed from the ADR-398 "first factory flow on Argo" lane, where this is currently on the critical path.
Why this matters to us. Our mission runner implements a bounded infra-only retry: the gate is
expression: asInt(lastRetry.exitCode) != 10
with the contract 0 = done, 10 = logical failure (never retry), anything else = infra (retry with backoff).
Two things make that contract inert today:
Nothing in the codebase emits exit 10. I checked across src/ — every 10 is a parseInt(x, 10) radix argument, not an exit code.
workflow run/resume collapses every non-COMPLETED state to 1 (commands/workflow.mjs:174 and :215).
So the expression is always true and we retry everything. A genuine agent failure — a bad issue, a real rejection — burns all four attempts on 30s/60s/120s backoff and then reports as retry-exhausted infra churn. There is no terminal-on-logical-failure path, which is the entire point of the policy.
What we specifically need: a documented way for a step to signal "logical failure, do not retry", and for run/resume to propagate it distinctly from both success and infra failure.
Related:#20 is the more urgent of the pair for us (unknown verb exits 0 → Argo reports a run that dispatched nothing as Succeeded), so if these are triaged together, #20 is the one that produces false success rather than wasted work.
Shipped in #23, released in v2.3.0: classifyWorkflowStatus (COMPLETED→0, PAUSED→3, FAILED→10, ABORTED→1) and classifyAgentExit (child non-zero→10, signal→130) — default behavior, per this issue's preferred option (no caller distinguished these states before). Two deliberate divergences from the proposal: PAUSED is 3 rather than 11 (lane expression asInt(lastRetry.exitCode) != 10 && asInt(lastRetry.exitCode) != 3 excludes both codes); ABORTED stays 1 (hard-stop class with usage/lease errors). The lane consumes paused=3 (adlc-argo-wf values track). If the lane needs 11/12 exactly, reopen and we'll remap — closing as completed meanwhile. This also clears the triage escalation above.
Activity
OrenOren1 commentedon Sep 29, 2026
Blocking a downstream Argo lane rollout — requesting triage
Filed from the ADR-398 "first factory flow on Argo" lane, where this is currently on the critical path.
Why this matters to us. Our mission runner implements a bounded infra-only retry: the gate is
with the contract
0= done,10= logical failure (never retry), anything else = infra (retry with backoff).Two things make that contract inert today:
10. I checked acrosssrc/— every10is aparseInt(x, 10)radix argument, not an exit code.workflow run/resumecollapses every non-COMPLETEDstate to1(commands/workflow.mjs:174and:215).So the expression is always true and we retry everything. A genuine agent failure — a bad issue, a real rejection — burns all four attempts on 30s/60s/120s backoff and then reports as retry-exhausted infra churn. There is no terminal-on-logical-failure path, which is the entire point of the policy.
What we specifically need: a documented way for a step to signal "logical failure, do not retry", and for
run/resumeto propagate it distinctly from both success and infra failure.Related: #20 is the more urgent of the pair for us (unknown verb exits
0→ Argo reports a run that dispatched nothing as Succeeded), so if these are triaged together, #20 is the one that produces false success rather than wasted work.kanfil commentedon Oct 1, 2026
Shipped in #23, released in v2.3.0:
classifyWorkflowStatus(COMPLETED→0, PAUSED→3, FAILED→10, ABORTED→1) andclassifyAgentExit(child non-zero→10, signal→130) — default behavior, per this issue's preferred option (no caller distinguished these states before). Two deliberate divergences from the proposal: PAUSED is 3 rather than 11 (lane expressionasInt(lastRetry.exitCode) != 10 && asInt(lastRetry.exitCode) != 3excludes both codes); ABORTED stays 1 (hard-stop class with usage/lease errors). The lane consumes paused=3 (adlc-argo-wf values track). If the lane needs 11/12 exactly, reopen and we'll remap — closing as completed meanwhile. This also clears the triage escalation above.fix: classifyWorkflowStatus case labels never matched real status values