Skip to content

[Bug]: Workflow child exit is masked by stdin close broken pipe #1064

Description

@dennisonbertram

Work type

Bug / regression

Observed behavior

PR #1057 hosted test-fast run 30592451360 failed
TestSourceManagerRunWorkflowFailsOnProcessExit. The compiled child exits with
status 7, but runSourceWorkflow returns write |1: broken pipe from
stdin.Close() rather than the more informative non-zero process-exit error.
The test therefore receives a failed workflow whose error does not contain
exited.

Expected behavior

When a source-workflow child exits non-zero, the workflow must report the
process exit (including bounded stderr) even if closing its stdin concurrently
also reports a broken pipe. A standalone stdin-close failure with a successful
child must remain visible.

Reproduction

Hosted artifact: PR #1057, Actions run 30592451360,
TestSourceManagerRunWorkflowFailsOnProcessExit, where the returned error was
write |1: broken pipe.

Focused command on exact pre-fix main:

go test ./internal/workflow -run '^TestSourceManagerRunWorkflowFailsOnProcessExit$' -count=100

The focused stress passed locally; the hosted failure is scheduling-dependent.
Source inspection deterministically shows the precedence bug:
internal/workflow/source.go records both closeErr and waitErr, then returns
closeErr before inspecting waitErr.

User and operational impact

Source-workflow failures can surface a transport cleanup symptom instead of the
real child exit and bounded stderr. This obscures diagnosis and makes the
accepted CI baseline intermittent, blocking otherwise valid cron/callback
promotion work.

Suspected seam and search evidence

Owning seam: SourceManager.runSourceWorkflow in
internal/workflow/source.go, after serveProtocol, stdin.Close, and
cmd.Wait.

Repository search:

  • rg -n "broken pipe|cmd.Wait|stdin|StdinPipe|process exited|exited" internal/workflow
  • closeErr is returned before waitErr.
  • Existing exit and bounded-stderr tests already prove the intended process
    failure contract, but do not deterministically force both errors at once.

Blast-radius impact map

  • Callers/data flow: source-workflow child process, protocol pipe teardown, run
    terminal error.
  • Config/env/defaults: none.
  • API/CLI/wire formats/tools: error text and diagnostic precedence only.
  • Persistence/schema/cache: none.
  • Concurrency/lifecycle: affected; simultaneous child exit and pipe close.
  • Security/auth/privacy: bounded stderr handling must remain bounded.
  • TUI/web/macOS: they display the corrected terminal workflow error.
  • Provider/model/catalog: none.
  • Deployment/observability: clearer failure evidence and stable hosted gate.
  • Compatibility: successful workflows and timeout/protocol-error precedence
    must remain unchanged.
  • Documentation: issue plan, impact map, logs, and plans index.

Regression test first

Add a deterministic test seam or fixture that causes the child to exit non-zero
before stdin close, proving both waitErr and a broken-pipe closeErr exist.
The red assertion must show the current code returns only broken pipe.

Keep false-positive controls for:

  • non-zero child exit wins and includes bounded stderr;
  • protocol error still wins over teardown errors;
  • deadline still reports timeout;
  • a successful child with only a close failure still reports that close error;
  • successful result behavior is unchanged.

Fix boundaries

In scope: deterministic regression coverage and error-precedence ordering in
runSourceWorkflow.

Out of scope: changing the workflow protocol, ignoring all pipe errors, adding
sleeps/retries, or broadly changing process supervision.

Verification plan

  • Deterministic red for simultaneous close and process-exit errors.
  • Focused normal and race stress at -count=100.
  • Complete go test ./internal/workflow/....
  • Complete go test -race ./internal/workflow/....
  • Unchanged foreground non-TTY ./scripts/test-regression.sh.
  • Hosted test-fast and test-race.

Rollout and rollback

Small runtime error-precedence fix. No migration or data repair. Revert if it
masks a protocol or timeout error, drops bounded stderr, or changes successful
workflow behavior.

Documentation and handoff

Add an issue-specific plan and impact map; update engineering log, long-term
thinking log, and plans index. Related to epic #1000 because it blocks the
assembled cron/callback proof.

Definition of done

  • Deterministic dual-error reproduction fails before the fix
  • Non-zero process exit takes precedence over stdin-close cleanup error
  • Timeout and protocol-error precedence remain unchanged
  • Bounded stderr remains present
  • Focused normal/race stress and complete regression are green
  • Hosted checks are green
  • Required plan, impact map, logs, and indexes are current

Process acknowledgement

I will preserve the failing regression, update this issue if the root cause
changes, and record any newly discovered independent bug separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions