Skip to content

A failing subprocess_gate journals empty stdout/stderr: the lowering uses stdio:'inherit' and the daemon's stdio is captured nowhere #511

Description

@khaliqgant

Summary

A failing subprocess_gate is undiagnosable. The lowering runs the author's command with stdio: 'inherit', so its stdout and stderr go to the daemon's stdio — which is captured nowhere. The journal records exit_code: 1 with empty stdout_tail and empty stderr_tail, relayflowd.log stays 0 bytes, and the CLI's own output never sees it. The operator is told only that a gate failed.

Evidence

Hit while running a 58-step authored campaign (relay.migrate.native-delivery.phase-0, run 01M2Z2Y20NJAA25RDXJKNRC054, CLI 2.0.22, macOS arm64). The agent step succeeded; its subprocess_gate failed three times and exhausted retries.

The lowering, packages/sdk/src/named-gate-lowering.ts:95-99:

case 'subprocess_gate':
  body = `if(text.includes('\\0'))process.exit(1);
const result=cp.spawnSync('/bin/sh',['-c',${JSON.stringify(gate.command)}],{
env:{...process.env,INPUT:text},stdio:'inherit'});
process.exit(result.status===0?0:1);`;

What the journal recorded for all three attempts:

{"completionReason":"retries_exhausted","disposition":"step_done",
 "output":{"exit_code":1,"stderr_tail":"","stdout_tail":""},
 "trajectory_tail":{"exit_code":1,"stderr_tail":""},
 "verification":{"detail":"exit code was 1","gate":"exit_code","verdict":"fail"}}

The gate command was a script that prints GATE_PASSED <detail> to stdout or GATE_FAILED <problems> to stderr on every path. Neither string appears in:

  • the journal (stdout_tail and stderr_tail both ""),
  • .relayflowd/relayflowd.log (0 bytes),
  • the flows run CLI output.

Compare a plain deterministic step running the same script, which journals its output correctly:

{"output":{"exit_code":0,"stdout_tail":"GATE_PASSED preflight phase=0 slug=seam branch=... base=...\n"}}

So the capture works everywhere except inside a lowered named gate.

Why it matters

subprocess_gate exists so a step's completion can be decided by a real check. In practice that check is a script with several failure modes, and the one thing the author needs on a red verdict is which one fired. Today the answer is "re-run it by hand and hope the tree still looks the same" — and for a gate that follows an agent step, the tree does not still look the same.

It also interacts badly with retries_exhausted (#506): three silent attempts collapse into one bare exit=1.

What to change

Capture the subprocess's output and journal it, the way a deterministic step's output is captured. Replacing stdio: 'inherit' with a pipe and forwarding the tails into the gate step's output would be enough:

const result=cp.spawnSync('/bin/sh',['-c',CMD],{env:{...process.env,INPUT:text},encoding:'utf8'});
process.stdout.write(result.stdout ?? '');
process.stderr.write(result.stderr ?? '');
process.exit(result.status===0?0:1);

The lowered gate is already a deterministic step, so its stdout_tail / stderr_tail are journaled — the output just has to reach them.

Worth checking the other lowered gates for the same pattern; artifact_exists and regex_match decide in-process and are unaffected, but anything spawning a child has the same exposure.

Acceptance

  • A failing subprocess_gate journals the command's stdout and stderr tails, visible in flows logs <run-id> and in the failure report.
  • A passing one does too, so a gate that passes for the wrong reason is still inspectable.
  • A test asserts a gate command writing to stdout and stderr has both captured in the journal.

Out of scope

  • Whether subprocess_gate should receive the output envelope through INPUT at all.
  • Retry policy.

Activity

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

    bugSomething isn't workinggarden-readyScoped and ready for an agent to pick up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions