Skip to content

fix(bin): stop provider-table lookup from writing broken-pipe errors to stderr - #6001

Merged
kunchenguid merged 3 commits into
kunchenguid:mainfrom
tiago-peixoto:fm/fm5956-quota-sigpipe
Sep 29, 2026
Merged

kunchenguid merged 3 commits into
kunchenguid:mainfrom
tiago-peixoto:fm/fm5956-quota-sigpipe

Conversation

@tiago-peixoto

@tiago-peixoto tiago-peixoto commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Fixes #5956

Implement ready-for-pr issue #5956: tests/fm-dispatch-resolve.test.sh fails intermittently in CI because a provider-table lookup in bin/fm-quota-axi-lib.sh writes "printf: write error: Broken pipe" to stderr. fm_quota_single_provider_for_harness reads the table from fm_quota_single_provider_table through a pipe and returns on the first match while the writer may still be printing; on GitHub Actions SIGPIPE is ignored, so bash reports the write error, which becomes a second diagnostic line. Looking up a harness in the provider table must never produce stderr output, whatever the timing and whether or not SIGPIPE is ignored, with return values and output unchanged.

What Changed

  • fm_quota_single_provider_for_harness in bin/fm-quota-axi-lib.sh now reads the whole output of fm_quota_single_provider_table before it answers. Before this change it returned on the first match, which closed the pipe while the table was still being written. Where SIGPIPE is ignored (for example on GitHub Actions), the writer then printed printf: write error: Broken pipe to stderr.
  • The first matching provider is still printed, and the function still returns 1 when the harness has no match. Output and return values are the same as before.
  • Added a comment explaining why the loop must not exit early.

Fixes #5956

Risk Assessment

✅ Low: This is a small, well-bounded change. The loop now drains the whole process-substitution table before answering, so the writer cannot hit EPIPE. First-match semantics, output and return codes are preserved for every table row (all have non-empty providers) and for harnesses not in the table, and the && chain inside the loop is safe under set -e.

Testing

I sourced the real library function and ran it in a stress loop with SIGPIPE ignored, as GitHub Actions does. This reproduced the bug on the base commit: 7 broken-pipe lines sequentially and 229, then 171, under parallel load. The target commit produced no stderr output in every run, including with default SIGPIPE handling. Output and exit codes matched the base for every harness and for the unknown and edge-case inputs. The issue's failing test, tests/fm-dispatch-resolve.test.sh, passed 3 out of 3 times with SIGPIPE ignored and printed no broken-pipe lines. Everything passed. I removed the temporary files afterwards and the worktree is clean.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Harness lookup with SIGPIPE ignored writes nothing to stderr, even under heavy concurrent load ✅ pass live quota-sigpipe-stress.txt: base produced 171 broken-pipe lines; target produced 0
Harness lookup with default SIGPIPE handling writes nothing to stderr ✅ pass live Stress driver on the target lib without the trap: 0 stderr lines
Lookup output and exit codes are unchanged for known, unknown, and edge-case harnesses ✅ pass live Parity table: all 13 inputs matched between base and target (e.g. muse→meta rc=0; omp, pi, opencode, empty and bogus → rc=1; stderr 0 bytes)
tests/fm-dispatch-resolve.test.sh passes with SIGPIPE ignored, as in CI ✅ pass live 3/3 runs: '# all fm-dispatch-resolve tests passed', 0 'Broken pipe' lines in the logs
Evidence: SIGPIPE-ignored stress transcript, base vs target

Source: SIGPIPE-ignored stress transcript, base vs target

base fa48367: stderr lines=171 (base-lib.sh: line 138: printf: write error: Broken pipe) target a1608f1: stderr lines=0 fm-dispatch-resolve.test.sh with SIGPIPE ignored: passed 3/3

# SIGPIPE-ignored stress: 4 parallel x 4000 x 3 lookups (claude, codex, muse)
base fa48367: stderr lines=171
    171 base-lib.sh: line 138: printf: write error: Broken pipe
target a1608f1: stderr lines=0

# tests/fm-dispatch-resolve.test.sh with SIGPIPE ignored (target)
# all fm-dispatch-resolve tests passed
# all fm-dispatch-resolve tests passed
# all fm-dispatch-resolve tests passed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-quota-axi-lib.sh:151 - No regression test accompanies the fix. The original failure depends on timing (the writer is still printing after the reader has returned) and on SIGPIPE being ignored, which makes a deterministic test hard to build against a 7-row table. The existing tests/fm-dispatch-resolve.test.sh paths still exercise the behavior: return values and output are unchanged for every table row and for a harness that is not in the table. No action needed.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Harness lookup with SIGPIPE ignored writes nothing to stderr, even under heavy concurrent load ✅ pass live quota-sigpipe-stress.txt: base produced 171 broken-pipe lines; target produced 0
Harness lookup with default SIGPIPE handling writes nothing to stderr ✅ pass live Stress driver on the target lib without the trap: 0 stderr lines
Lookup output and exit codes are unchanged for known, unknown, and edge-case harnesses ✅ pass live Parity table: all 13 inputs matched between base and target (e.g. muse→meta rc=0; omp, pi, opencode, empty and bogus → rc=1; stderr 0 bytes)
tests/fm-dispatch-resolve.test.sh passes with SIGPIPE ignored, as in CI ✅ pass live 3/3 runs: '# all fm-dispatch-resolve tests passed', 0 'Broken pipe' lines in the logs
  • trap '' PIPE stress driver sourcing the base fa48367 lib versus the target lib: 3000×3 lookups sequentially, then 4 parallel runs of 4000×3 lookups
  • Stress driver on the target lib with default SIGPIPE handling (3000×3 lookups)
  • Parity check of base versus target output and exit code for claude, codex, grok, kimi, cursor, agy, muse, omp, pi, opencode, the empty string, bogus, and 'claude claude', with SIGPIPE ignored
  • bash -c "trap '' PIPE; exec bash tests/fm-dispatch-resolve.test.sh" run 3 times
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Fixes kunchenguid#5956

fm_quota_single_provider_for_harness returned from its while read loop
as soon as it found a match, closing the pipe while
fm_quota_single_provider_table's printf could still be writing.
Where SIGPIPE is ignored, as on GitHub Actions runners, bash then
prints "printf: write error: Broken pipe" on the resolver's stderr,
which intermittently broke the one-diagnostic-line assertions in
tests/fm-dispatch-resolve.test.sh.

Read the whole table before answering, the way
fm_control_harness_supported already does, so the writer always
finishes. Return values and output are unchanged.

Reproduced by running tests/fm-dispatch-resolve.test.sh with SIGPIPE
ignored on a single pinned core under CPU contention: 30 of 30 runs
failed before the fix, 0 of 30 after. Note: reproducing requires
setting the trap inside the tested shell because nice(1) resets an
inherited SIGPIPE ignore to SIG_DFL. tests/fm-quota-choose.test.sh
passes and bin/fm-lint.sh is clean.
@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Low risk] Fixes stderr noise in a shell utility function.

The PR appears safe to merge; no outstanding finding or new actionable issue remains.

Reviews (3) · Last reviewed commit: "no-mistakes(ci): Fixed both Greptile fin..."

Comment thread bin/fm-quota-axi-lib.sh Outdated
Comment thread bin/fm-quota-axi-lib.sh
…ss. ci-1 (bin/fm-quota-axi-lib.sh:154). Invariant: looking up a harness must always end with status 0 and print the provider, even when the caller runs under `set -e`. The loop body `[ -z "$found" ] && [ "$harness" = "$1" ] && found=$provider` now ends in `|| :`. Every iteration succeeds and the whole table is still read. Only `fm_quota_single_provider_for_harness` loops over the table this way, so this is the one place the fix was needed. One caveat: on bash 5.3 the old code did not actually exit under `set -e`, because the `while` loop is not the function's last command, so the new `set -e` test would have passed before this fix too. The change makes the loop's success explicit, as the user asked. ci-2 (regression coverage). I added three cases to the existing `tests/fm-quota-choose.test.sh`, all calling the public lookup function after sourcing the library: 1. With SIGPIPE ignored (`trap "" PIPE`), it looks up every harness 200 times and checks that nothing reaches stderr. 2. A deterministic version of the race: the table function is wrapped so it writes the first row, pauses 0.2 s, then writes the rest. With SIGPIPE ignored, it checks that looking up `claude` prints `claude` and writes nothing to stderr. The stress loop alone reproduced the bug in only about 1 of 5 local runs, which is why this case exists. 3. A direct call under `set -e` prints `claude`. Verification: - `bash tests/fm-quota-choose.test.sh`: all pass. - Same test against the pre-PR library (fa48367, via `FM_ROOT_OVERRIDE`): fails with `printf: write error: Broken pipe`. The deterministic case failed in one run and the stress loop caught it in another. - `shellcheck` on both files: clean. - `tests/fm-dispatch-resolve.test.sh`: passes
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: whole thread read (body + Greptile; linked #5956 OPEN ready-for-pr). Diff reviewed against main tip 6b0f5a07fee0ac336332ccab2caa0bd07a5cf113.

Verdict: waiting-ci · contract-class: restore · Firstmate flag: no · closes #5956: yes

Tip vs main: fm_quota_single_provider_for_harness on main still returns on first match inside while read over <(fm_quota_single_provider_table), closing the pipe mid-write. Where SIGPIPE is ignored (GHA), that yields printf: write error: Broken pipe on stderr — the flaky second diagnostic line in #5956 / tests/fm-dispatch-resolve.test.sh. Diff drains the whole table before answering (same pattern as fm_control_harness_supported), keeps first-match output/rc, adds || : for set -e, and adds three regressions in tests/fm-quota-choose.test.sh (SIGPIPE-ignored stress, slow-writer race, set -e). Files: bin/fm-quota-axi-lib.sh, tests/fm-quota-choose.test.sh only — workflow-zero yes.

Attestation: MATCH — body no-mistakes-pipeline-attestation head_sha 4852855313b45fc24b0fa9624083479a4922ce33 == PR head.

CI / NM: Require no-mistakes SUCCESS (run 36413510896). CI 36413510766: Behavior portable serial 5 FAILURE (job 108899064696 — not ok - the header-collision line was not captured, unrelated to this quota stderr fix); all other Behavior/Lint/Repo/Herdr/macOS checks SUCCESS. Greptile alone does not block.

VISION.md per-rule:

  • One captain, one interface: aligns — removes intermittent stderr noise that muddies diagnostics; honesty under load.
  • Authority explicit: aligns — no new autonomy or default grant.
  • Scripts own mechanics: aligns — exact pipe-drain fix in deterministic shell lib + regression tests.
  • Restart is a non-event: aligns — no durability/session change.
  • Delegation with a spine: aligns — no new task shape; field flake → regression coverage.
  • Fleet outlives vendor: aligns — bash/SIGPIPE mechanics, not a vendor harness coupling.
  • Scope: aligns — quota axi lib + tests; not workshop growth.
  • Align/resist: aligns — restores the already-promised silent lookup path that was broken under ignored SIGPIPE.

Next: waiting on green CI (serial 5). Do not merge while CI red. No Firstmate flag. No workflow approvals this pass (runs already executing on head).

@tiago-peixoto

Copy link
Copy Markdown
Contributor Author

Friendly nudge: head 48528553 is mergeable and all 20 checks are green now, including Behavior portable serial 5, which passed on the re-run. The earlier triage was waiting on that. Happy to adjust anything.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: whole thread re-read (body + Greptile + prior waiting-ci stamp + author nudge that serial 5 re-ran green). Diff re-reviewed vs main 1f2c9548f468.

HEAD 4852855313b45fc24b0fa9624083479a4922ce33. MERGEABLE/CLEAN. Attestation MATCH. All tip checks SUCCESS (lint, coverage, portable serial 1–9 including serial 5, Herdr, macOS, invariants, timing, Greptile, NM). No workflow files. Author tiago-peixoto not blocked. Closes #5956: yes.

contract-class: restore — fm_quota_single_provider_for_harness already promised a quiet first-match lookup; early return closed the pipe mid-write and, where SIGPIPE is ignored, printed broken-pipe to stderr. Draining the whole table restores that quiet contract with unchanged output/return values. Not a new default (FM-LEARN-4627 / FM-LEARN-CLAIMS).

VISION.md (each rule):

  • One captain, one interface: Aligns — removes flaky stderr noise from CI/diagnostics.
  • Authority is explicit and never inferred: n/a / aligns.
  • Scripts own the mechanics, agents own the judgment: Aligns — exact pipe-drain mechanics.
  • A restart is a non-event: Aligns — deterministic lookup under SIGPIPE-ignored hosts.
  • Delegation with a spine: Aligns — quota/harness resolve stays reliable.
  • The fleet outlives any vendor: Aligns — bash/SIGPIPE host variance.
  • Scope: Aligns — tiny lib + test; not workshop growth.

Auto-merging (restore + otherwise ready).

@kunchenguid
kunchenguid merged commit a774c44 into kunchenguid:main Sep 29, 2026
38 of 39 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is merged. Thank you @tiago-peixoto — really appreciate you taking the time on this.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky dispatch-resolve test: provider-table lookup writes a Broken pipe error to stderr in CI

2 participants