coleam00/Archon · warning

loop_group_nested_pause_not_escalated

loop_group_nested_pause_not_escalated

Error message

Node '${id}': this loop_group's terminal sink ('${sink.id}') is itself an interactive loop/loop_group — a pause inside it does not escalate to stop this loop_group's own iteration (#2753). The outer loop can run further iterations while a human's answer to the inner pause is still pending. Only a gate node directly as the terminal sink correctly stops the enclosing loop_group (#2707 step 3).

What it means

This loader warning fires when a loop_group's sole terminal sink is not a gate but another interactive loop or loop_group node. A pause (human gate) inside that nested interactive sink does not escalate to stop the outer loop_group's iteration (#2753), so the outer loop can start another iteration while the human's answer to the inner pause is still pending. Only a gate node placed directly as the terminal sink correctly pauses the enclosing loop_group (#2707 step 3).

Source

Thrown at packages/workflows/src/loader.ts:583

      }
    }

    // A body-terminal sink that is itself an interactive loop/loop_group can pause
    // without stopping THIS group (#2753) — a bare gate sink is excluded here since
    // that case is already correctly handled above (and by #2707 step 3 at runtime);
    // this covers a sink whose own pause is trapped one level down instead.
    if (bodySinks.length === 1) {
      const sink = bodySinks[0];
      if (!isIncludeDirective(sink) && !isGateNode(sink) && isUnescalatableInteractiveSink(sink)) {
        const message =
          `Node '${id}': this loop_group's terminal sink ('${sink.id}') is itself an ` +
          'interactive loop/loop_group — a pause inside it does not escalate to stop ' +
          "this loop_group's own iteration (#2753). The outer loop can run further iterations " +
          "while a human's answer to the inner pause is still pending. Only a gate node " +
          'directly as the terminal sink correctly stops the enclosing loop_group ' +
          '(#2707 step 3).';
        warnings.push(message);
        getLog().warn(
          { id, sinkId: sink.id, warning: message },
          'loop_group_nested_pause_not_escalated'
        );
      }
    }
  }

  // Recurse into a loop_group body — mirrors collectUnknownNodeKeys's own body
  // recursion. Resolved and raw body arrays share index order (Zod arrays
  // preserve it), so they're zipped by position rather than by id.
  if (isLoopGroupNode(node) && raw !== null && typeof raw === 'object') {
    const rawGroup = (raw as Record<string, unknown>).loop_group;
    const rawBody =
      rawGroup !== null && typeof rawGroup === 'object'
        ? (rawGroup as Record<string, unknown>).nodes
        : undefined;
    if (Array.isArray(rawBody)) {
      // `id` is unused on the IncludeDirective early-return path above, so a

View on GitHub (pinned to 0773b97458)

Solutions

  1. Hoist a gate node out of the nested loop and place it directly as the outer loop_group's terminal sink
  2. Flatten the nesting: move the inner loop's nodes into the outer body with a gate as the final node
  3. Restructure so the inner interactive loop lives outside the outer loop_group, or accept that outer iterations continue while the inner pause is pending
  4. If the inner group must stay, add a gate after it in the outer body as the terminal sink

Example fix

# before
outer:
  loop_group:
    nodes:
      - id: step
        agent: ...
      - id: inner          # interactive loop_group as sole sink
        loop_group:
          nodes:
            - id: review
              gate: {}
# after
outer:
  loop_group:
    nodes:
      - id: step
        agent: ...
      - id: review
        gate: {}   # gate directly as the outer terminal sink
Defensive patterns

Strategy: validation

Validate before calling

function sinkIsGate(group) {
  const dependedOn = new Set(group.nodes.flatMap(n => n.depends_on ?? []));
  const sinks = group.nodes.filter(n => !dependedOn.has(n.id));
  return sinks.length === 1 && 'gate' in sinks[0];
}

Type guard

function isGateNode(n) { return typeof n === 'object' && n !== null && 'gate' in n; }

Prevention

When it happens

Trigger: Loading a workflow YAML where a loop_group body's single un-depended-on node is itself a loop or loop_group node that contains (or may contain) a pause, and isUnescalatableInteractiveSink(sink) returns true. Bare gate sinks are excluded — they are handled by the dedicated gate warnings.

Common situations: Nesting an approval loop inside each iteration of an outer retry loop and expecting the inner gate to halt the outer iteration; wrapping a gate in a sub-loop 'for structure'; refactoring a flat gate-in-body design into a nested group without realizing pause escalation stops at the gate boundary.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/f59f0d3888fc4005. Report an issue: GitHub.