Skip to content

workflow run/resume: distinguish FAILED, PAUSED and ABORTED in exit code (all collapse to 1) #19

Closed

Description

@OrenOren1

Summary

workflow run / workflow resume collapse every non-COMPLETED state into exit code 1:

// src/commands/workflow.mjs:174  (cmdWorkflowRun)
// src/commands/workflow.mjs:215  (cmdWorkflowResume)
return state.status === RunStatus.COMPLETED ? 0 : 1;

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

Activity

  1. OrenOren1 commented on Sep 29, 2026

    @OrenOren1
    Author

    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:

    1. Nothing in the codebase emits exit 10. I checked across src/ — every 10 is a parseInt(x, 10) radix argument, not an exit code.
    2. 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.

  2. kanfil commented on Oct 1, 2026

    @kanfil
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions