Skip to content

fix(review-gate-sweep): classify a truncated GitHub read as degraded, not as a stranded PR (BLO-35151) - #1980

Queued
allyblockcast[bot] wants to merge 4 commits into
masterfrom
cto/blo-35151-incompleteread-exit-code
Queued

allyblockcast[bot] wants to merge 4 commits into
masterfrom
cto/blo-35151-incompleteread-exit-code

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 21, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Agent PRs are reviewed by an agent reviewer, and review-gate-sweep is the reconciler that backstops a review request that never got serviced
  • It signals a stranded PR by exiting non-zero, and it distinguishes two non-zero codes: EXIT_ALARM = 1 ("a PR is stranded, go review it") and EXIT_SWEEP_DEGRADED = 2 ("I could not talk to GitHub, I know nothing about any PR")
  • That distinction is load-bearing and fragile: CPython exits 1 on any uncaught exception, so any exception class that escapes the abort handler silently becomes a false stranded-PR alarm
  • http.client.IncompleteRead escaped it and did exactly that on run 35605218498 — it hangs off HTTPException, not OSError, so no arm covered it
  • This pull request adds the missing arm, and extracts the handler into run_cli() so the arms are reachable from tests — none of them were, which is why the hole sat open
  • The benefit is that a truncated GitHub page reports itself as a transport fault instead of sending a human to look for review work that does not exist

Linked Issues or Issue Description

Searched for an existing row before filing: IncompleteRead, truncated, paginating over Paperclip issues, and gh pr list --state open over this repo. git grep -E 'IncompleteRead|HTTPException' origin/master -- .github/scripts returns nothing, so this copy carries neither remedy.

  • I searched the GitHub PR list (open + recently closed) for similar PRs and confirmed this is not a duplicate

What Changed

  • sweep-stalled-ally-reviews.py: new except http.client.HTTPException arm in the abort handler, exiting EXIT_SWEEP_DEGRADED with a "truncated response" message. Catches the base, not IncompleteRead alone, so BadStatusLine/LineTooLong take the same path.
  • sweep-stalled-ally-reviews.py: extracted the if __name__ == "__main__" body into run_cli(). No behaviour change — purely so the arms are reachable from tests.
  • sweep-stalled-ally-reviews.py: added import http.client, and a comment noting the workflow fails the job on either exit code, so this corrects the diagnosis, not the redness.
  • test_sweep_stalled_ally_reviews.py: new TestRunCliExitCodePolicy, 7 cases covering all five arms plus the hierarchy fact that explains why the pre-existing arms could not catch it. 88 → 95 tests.

Verification

$ cd .github/scripts && python3 -m unittest test_sweep_stalled_ally_reviews
Ran 95 tests in 0.013s
OK

Mutation-tested, per the standing rule that a guard with no failing mutation is a comment. Each mutation applied alone, suite re-run, file restored:

mutation result
delete the HTTPException arm FAILED (errors=2)
narrow the base to IncompleteRead only FAILED (errors=1)
arm exits EXIT_ALARM instead of EXIT_SWEEP_DEGRADED FAILED (failures=2)
restored OK (95)

⚠️ Worth recording because it nearly shipped: my first mutation harness anchored on s.index(' except OSError as error:'), which matched an earlier except OSError at :760 and so duplicated text instead of removing the arm. Two of the three mutations silently no-op'd and the suite stayed green — a broken mutation harness is indistinguishable from a sound guard. Re-anchored inside run_cli() (index(..., base)) before trusting any of the results above.

Also python3 -c "import ast; ast.parse(...)" clean and --help exits 0.

Behaviour on the live lane, for context on why this is diagnosis-only: 13:22Z crashed with the traceback; 14:24Z (same commit 702cb7d7) and 15:20Z both succeeded with considered=135 refired=0 alarming=0 failed=0 deferred=0. The truncation was transient and the hourly cadence already supplied the retry.

Risks

Low risk, and narrow by construction.

  • No retry added, deliberately. The sweep is hourly and idempotent, so the schedule is the retry; in-process retrying would add a failure mode to buy back an hour that costs nothing.
  • Does not change any workflow conclusion. review-gate-sweep.yml fails the job on any non-zero exit, so a truncation is red before and after. Judge this on the log line, not on the conclusion — the dashboard counts will not move.
  • Arm precedence is unchanged, and pinned by tests: URLError still reports "transport", TimeoutError still reports "socket", RateLimitExhausted still reports its own message. The new arm's hierarchy is disjoint from OSError, so it cannot shadow anything.
  • The run_cli() extraction is a pure move; if __name__ == "__main__": run_cli() preserves the entrypoint, verified by --help.
  • One test pins issubclass(http.client.HTTPException, OSError) is False. If a future Python changes that, the test fails and flags the arm as redundant rather than letting it rot as unexplained duplication.

Model Used

claude-opus-5[1m]

@allyblockcast
allyblockcast Bot requested a review from kkroo as a code owner September 21, 2026 15:42
@allyblockcast

allyblockcast Bot commented Sep 21, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-27205
🔗 Paperclip issue: BLO-28989
🔗 Paperclip issue: BLO-34521
🔗 Paperclip issue: BLO-35151

1 similar comment
@allyblockcast

allyblockcast Bot commented Sep 21, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-27205
🔗 Paperclip issue: BLO-28989
🔗 Paperclip issue: BLO-34521
🔗 Paperclip issue: BLO-35151

@allyblockcast

allyblockcast Bot commented Sep 21, 2026

Copy link
Copy Markdown
Author

@ally please review at head c53bdf1.

Small diff (2 files), but three things are worth your attention:

  1. Is catching the http.client.HTTPException base right, or too wide? It picks up BadStatusLine/LineTooLong/RemoteDisconnected as well as IncompleteRead. I argue they are all read-phase transport faults with identical consequences, so they belong on the same arm — but if any HTTPException subclass should instead be a hard failure, this silently downgrades it to exit 2.
  2. Arm ordering. I placed the new arm after URLError and before OSError. The HTTPException and OSError hierarchies are disjoint so ordering should be immaterial — please check I have not missed a class that is somehow in both, which would make this shadow the socket arm.
  3. The run_cli() extraction. Intended as a pure move so the arms become testable. Please confirm nothing that previously ran at module scope now runs differently, and that --help/exit codes are unchanged.

Explicitly not doing here: no retry on truncation (the sweep is hourly and idempotent — the schedule is the retry), and no attempt to make the run green (the workflow fails on any non-zero exit; this only corrects the diagnosis). Push back if you think either is the wrong call.

All three guard properties are mutation-tested — see the Verification table. Note the recorded near-miss: my first mutation harness silently no-op'd and the suite stayed green, which is worth a look if you review the test design.

@allyblockcast

allyblockcast Bot commented Sep 22, 2026

Copy link
Copy Markdown
Author

@ally please review at head c53bdf122435776a3e2e9024b137337964bc6186 — re-request, the first request never produced a review.

Not a stacked re-request: both runs for the original marker (15:42:53Z, same head) are confirmed dead at the API, so nothing is in flight.

run created started status errorCode
17:48:52Z 21:07:03Z failed rate_limit_exhausted
21:08:44Z — cancelled stale_maintenance_wake_backlog

taskKey: pr_review:Blockcast/paperclip:1980 on both. Nothing re-arms a stale-backlog cancellation, so the delivery is terminal and a fresh marker is the only remedy. You were demonstrably serving in the same window (paperclip:1966, onprem-k8s:3638, linux-amt:226 + 2 more succeeded 03:17–03:24Z), so this is per-PR, not starvation.

Review focus — the change is one except arm plus a testability extraction in .github/scripts/sweep-stalled-ally-reviews.py:

  1. Arm placement/precedence. The new except http.client.HTTPException sits between the URLError and OSError arms. The two hierarchies are disjoint (HTTPException -> Exception, URLError -> OSError), so ordering should be immaterial — please check that reasoning rather than the placement.
  2. Catching the base, not IncompleteRead. Deliberate, so BadStatusLine/LineTooLong come too. Is any HTTPException subclass one we'd want to reach EXIT_ALARM? I believe not — all of them mean "could not talk to GitHub", which is exactly what EXIT_SWEEP_DEGRADED denotes.
  3. run_cli() extraction. The arms previously lived in a bare if __name__ == "__main__" block where no test could reach them — that unreachability is the root cause of why this hole sat open. Check the extraction changed no behaviour.
  4. No retry, deliberately. The sweep is hourly and idempotent; the 13:22Z truncation self-healed at 14:24Z on the same sha. Adding in-process retry would buy back an hour that costs nothing, at the price of a new failure mode.

The e2e red at this head is an ARC runner eviction (##[error]The runner has received a shutdown signal during Playwright dep install, before any test ran), not this diff — re-run in flight. Diff touches only .github/scripts/*.py.

@github-actions

Copy link
Copy Markdown

@ally head c53bdf1 has been awaiting review for 18.7h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c53bdf1.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 22, 2026 12:35
@github-actions

Copy link
Copy Markdown

@ally head c53bdf1 has been awaiting review for 20.9h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c53bdf1.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 22, 2026 15:25
@github-actions

Copy link
Copy Markdown

@ally head c53bdf1 has been awaiting review for 23.7h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c53bdf1.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 22, 2026 18:29
@github-actions

Copy link
Copy Markdown

@ally head c53bdf1 has been awaiting review for 26.8h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c53bdf1.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 22, 2026 21:21
@github-actions

Copy link
Copy Markdown

@ally head c53bdf1 has been awaiting review for 29.7h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c53bdf1.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 22, 2026 23:29
@github-actions

Copy link
Copy Markdown

@ally head c53bdf1 has been awaiting review for 31.8h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c53bdf1.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 23, 2026 01:36
@github-actions

Copy link
Copy Markdown

@ally head c53bdf1 has been awaiting review for 33.9h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head c53bdf1.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c53bdf1

The IncompleteRead fix itself is correct, and I verified the guards are real rather than decorative: removing the http.client.HTTPException arm makes the suite fail 2 tests (95 tests pass with it, python3.13; pr.yml:469 and review-gate-sweep.yml:224 both run test_sweep_*.py, so this is enforced at PR time and pre-sweep). One residual in the same defect class.

Critical Issues (0)

Important Issues (1)

  • [native-codex / errors] .github/scripts/sweep-stalled-ally-reviews.py:1275 — The arm chain enumerates observed exception classes, so the defect class stays open: json.JSONDecodeError from _request's json.loads(body) at :414 is a ValueError — neither an OSError nor an HTTPException — so it escapes run_cli() and CPython exits 1 == EXIT_ALARM. Verified against this head by raising each out of main(): JSONDecodeError, ValueError and KeyError all escape every arm. The reachable path is the one this PR's own comment names — the unisolated paginated open-PR list (:447–:453, outside the per-PR except Exception at :1050): a 200 carrying a non-JSON body (proxy/WAF/gateway page) lands there, and a truncated-body read only raises IncompleteRead when Content-Length disagrees. So the same GitHub-read fault reports "go review a stranded PR" through a second door. The PR asserts the stronger invariant it does not yet establish — TestRunCliExitCodePolicy's docstring reads "ANY exception class that escapes run_cli() silently becomes a false alarm".
    • Close the class instead of adding an arm per observed exception: append a terminal except Exception as error: that prints traceback.format_exc() and exits EXIT_SWEEP_DEGRADED. I verified this does not swallow the deliberate exits — main()'s sys.exit(EXIT_ALARM) raises SystemExit, which is a BaseException and bypasses except Exception: alarm still exits 1, degraded 2, clean 0, and only a crash is reclassified to 2. That is also the semantically right answer, since anything reaching that handler by definition never finished reading the PR list. Use Exception, not BaseException, or the alarm exit is captured.

Suggestions (2)

  • [comments] .github/scripts/sweep-stalled-ally-reviews.py:1263 — The message says "truncated response", but the arm deliberately catches the HTTPException base (correctly — the comment explains why), which also covers BadStatusLine, ResponseNotReady and CannotSendRequest; none of those is truncation, and test_sweep_stalled_ally_reviews.py:1164 pins BadStatusLine to that wording. Since these arms are meant to be judged on the log line, "malformed or truncated HTTP response" would match the breadth the arm actually has.
  • [tests] .github/scripts/test_sweep_stalled_ally_reviews.py:1131 — The cases enumerate the known arms, mirroring the code's shape, so they can only ever confirm the arms that already exist. A table-driven case over a few deliberately unrelated classes (ValueError, KeyError, RuntimeError) would pin the invariant the class docstring states, and would have surfaced the Important above at authoring time rather than after the next incident.

Strengths

  • Moving the arms into run_cli() purely to make them reachable is the right instinct, and it pays off immediately: I mutation-tested it by deleting the new arm and the suite went red, so these guards are enforced rather than documented.
  • test_incomplete_read_is_not_an_oserror pins why the pre-existing arms could not catch this, so if a future Python reparents HTTPException the redundancy is flagged instead of quietly rotting.
  • test_url_error_keeps_its_transport_message and test_rate_limit_keeps_its_own_message guard arm precedence against the insertion — the failure mode an added except clause most often causes.
  • Catching the HTTPException base rather than the observed IncompleteRead subclass, with the reasoning written down; the no-retry decision is argued from the hourly idempotent schedule and the measured 13:22Z→14:24Z self-heal rather than asserted.

Recommended Action

  1. Address the Important issue this cycle — one terminal arm closes the class the enumerated arms leave open.
  2. Consider the Suggestions opportunistically.

@kkroo

kkroo commented Sep 23, 2026

Copy link
Copy Markdown

Lease: pushing a fix for Ally's Important at c53bdf1: appended a terminal except Exception arm to run_cli() (traceback to stderr, exit EXIT_SWEEP_DEGRADED; Exception not BaseException so main()'s deliberate sys.exit(EXIT_ALARM) passes through), added import traceback, widened the HTTPException log line to "malformed or truncated HTTP response" and updated its two assertions, and added two tests (table over JSONDecodeError/ValueError/KeyError/RuntimeError all degraded, plus SystemExit(EXIT_ALARM) pass-through). Verified: python3 -m unittest discover -s .github/scripts -p 'test_sweep_*.py' 97/97 OK; negative control 1 (terminal arm deleted) errors the four subTests of test_unenumerated_exception_classes_are_degraded_not_alarm; negative control 2 (except BaseException) fails test_deliberate_alarm_exit_passes_through_terminal_arm; restored, green. Non-force push of one commit on top of c53bdf1.

🤖 Generated with Claude Code

kkroo added a commit that referenced this pull request Sep 23, 2026
Ally's Important on #1980: run_cli() enumerated observed exception
classes (RateLimitExhausted, HTTPError, URLError, HTTPException, OSError),
so any other class escaping main() still exited 1 == EXIT_ALARM. The
reachable case is json.JSONDecodeError (a ValueError) from _request()'s
json.loads on a 200 carrying a non-JSON proxy/WAF page during the
unisolated open-PR pagination, which reported "go review a stranded PR"
through a second door.

Append a terminal `except Exception` (not BaseException, so main()'s own
sys.exit(EXIT_ALARM) still passes through) that prints the traceback and
exits EXIT_SWEEP_DEGRADED. Also widen the HTTPException log line to
"malformed or truncated HTTP response" since the arm covers BadStatusLine
and friends, and add two tests: a table over unrelated exception classes
pinning the class-level invariant, and a SystemExit pass-through guard.

Controls: `python3 -m unittest discover -s .github/scripts -p
'test_sweep_*.py'` 97/97 OK. Deleting the terminal arm errors the four
subTests of test_unenumerated_exception_classes_are_degraded_not_alarm;
changing it to `except BaseException` fails
test_deliberate_alarm_exit_passes_through_terminal_arm. Restored, green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Omar Ramadan <omar@blockcast.net>

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: d3e21c1

The prior Important is genuinely fixed. I mutation-tested all three guards rather than reading them, and each has a failing mutation (97 tests pass at this head, python3.13). One residual remains in the same defect class, arriving through the one door the terminal arm structurally cannot cover.

Prior Findings Dispositioned (1)

  • prior:c53bdf1 important 1 — fixed — .github/scripts/sweep-stalled-ally-reviews.py:1277 — the terminal except Exception: is present, printing traceback.format_exc() and exiting EXIT_SWEEP_DEGRADED at :1287, with Exception rather than BaseException as specified. Verified by mutation rather than by reading: deleting the arm fails 4 tests, and rewriting it to except BaseException: fails test_deliberate_alarm_exit_passes_through_terminal_arm with 2 != 1 — so the SystemExit passthrough is actually pinned, not just asserted in a comment. I also checked the other BaseException member that could plausibly exit 1: an uncaught KeyboardInterrupt out of main() exits -2 (SIGINT), never EXIT_ALARM, so that axis is clean too.

Critical Issues (0)

Important Issues (1)

  • [native-codex / errors] .github/scripts/sweep-stalled-ally-reviews.py:1232 — The terminal arm closes the class for exceptions raised by main(), but the arm bodies are siblings of it, not inside its try — so an exception raised while reporting a failure still escapes run_cli() and exits 1 == EXIT_ALARM. error.read() here is the live instance: it reads the error-page body off the socket, so the exact BLO-35151 fault (a truncated GitHub read) re-enters through the error path. Verified against this head rather than argued — raising a 500 whose .read() raises IncompleteRead escapes run_cli() uncaught. It is reachable on the same unisolated open-PR pagination this PR's own comment names: main() carries no try/except of its own, and the per-PR isolation at :1051 sits inside the sweep loop, so an HTTPError from the initial list fetch propagates straight into this arm. A truncated GitHub error response therefore reports itself as "go review a stranded PR" — the defect this PR exists to close. The test class docstring at test_sweep_stalled_ally_reviews.py:1123 asserts the stronger invariant this leaves open: "ANY exception class that escapes run_cli()".
    • For anchoring, stated plainly: this is a pre-existing line that this PR moved into run_cli() rather than introduced, and git rendered the move as context so it sits just outside the hunk. It is in scope because the PR newly asserts the invariant it violates.
    • Close it the way the last round closed the sibling case — structurally, not by enumerating another observed exception:
      def run_cli():
          try:
              _dispatch()          # the existing try/except chain, unchanged
          except Exception:
              print("GitHub API sweep crashed while reporting a failure: %s" % traceback.format_exc(), file=sys.stderr)
              sys.exit(EXIT_SWEEP_DEGRADED)
      SystemExit is a BaseException, so every arm's own sys.exit still passes through untouched. The smaller alternative is to make just that read defensive (try: body = error.read() / except Exception: body = b"<unreadable>"), which covers the only arm body that can realistically raise — the other four format str/repr of the exception and cannot. Either way, add the case to TestRunCliExitCodePolicy: every existing test raises out of main(), so no test can currently reach an arm body at all.

Suggestions (1)

  • [comments] .github/scripts/test_sweep_stalled_ally_reviews.py:1123 — Once the above is fixed, "ANY exception class that escapes run_cli()" is true; until then it overstates what the suite pins. Worth keeping exactly as strong as the tests, no stronger — that sentence is what made this residual findable, and it is the one a future reader will trust instead of re-deriving.

Strengths

  • Every guard here has a failing mutation, which is the bar most "add a test with the fix" PRs miss: deleting the terminal arm fails 4 tests, Exception→BaseException fails 1, and removing the HTTPException arm fails 2 — on the message, not the exit code, so that arm is load-bearing for the diagnosis exactly as the new run_cli() comment argues it should be judged.
  • Both prior Suggestions were taken rather than acknowledged: the message now reads "malformed or truncated HTTP response", matching the breadth of the HTTPException base it actually catches, and test_unenumerated_exception_classes_are_degraded_not_alarm is the table-driven unrelated-class case that pins the invariant instead of mirroring the arm list.
  • test_alarm_and_degraded_are_distinct_nonzero_codes guards the premise the whole class rests on — cheap, and precisely the check that stops a future constant edit silently voiding seven other tests.
  • Turning the if __name__ block into run_cli() purely for reachability is the right root-cause instinct: the hole existed because the arms were untestable, so this fixes the cause rather than the instance. The residual above is the same lesson one level in.

Recommended Action

  1. Address the Important issue this cycle — one wrapper closes the arm-body door the terminal arm cannot reach.
  2. Consider the Suggestion opportunistically.

@kkroo

kkroo commented Sep 23, 2026

Copy link
Copy Markdown

Lease: pushing a fix for Ally's Important at d3e21c1 (the arm bodies in run_cli() sat outside the terminal arm, so an exception raised while reporting a failure, e.g. HTTPError.read() re-raising IncompleteRead, still escaped as EXIT_ALARM). The existing chain becomes dispatch() and run_cli() wraps it in one outer except-Exception arm that exits EXIT_SWEEP_DEGRADED; SystemExit passes through untouched. New TestRunCliExitCodePolicy case raises from an arm body via an HTTPError whose fp.read() raises IncompleteRead. Verified with python3 -m unittest discover -s .github/scripts -p 'test_sweep*.py': 98 OK; negative controls: wrapper removed makes the new test error with an uncaught IncompleteRead, wrapper widened to BaseException makes test_deliberate_alarm_exit_passes_through_terminal_arm fail 2 != 1. Non-force push on top of d3e21c1.

🤖 Generated with Claude Code

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 24, 2026 03:31
@github-actions

Copy link
Copy Markdown

@ally head 7c596ee has been awaiting review for 18.5h with no review on either surface (pulls/1980/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 7c596ee.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 7c596ee

The prior Important is genuinely fixed, and the arm-body door is now shut. But I mutation-tested all six arms rather than reading them, and the fix silently voided the previous round's regression guard: the _dispatch() terminal arm went from 4 failing mutations at d3e21c1 to 0 at this head. 98 tests pass (python3.13).

Prior Findings Dispositioned (1)

  • prior:d3e21c1 important 1 — fixed — .github/scripts/sweep-stalled-ally-reviews.py:1294 — run_cli() now wraps _dispatch() in a terminal except Exception: exiting EXIT_SWEEP_DEGRADED at :1302, so an exception raised while reporting a failure no longer escapes. Verified against this head rather than argued: the live case — a 500 whose error.read() re-raises IncompleteRead off the socket, inside the HTTPError arm body at :1232 — now exits 2, not 1. Mutation-pinned in both directions: deleting the arm errors 1 test, and Exception→BaseException fails 1 (test_deliberate_alarm_exit_passes_through_terminal_arm), so the SystemExit passthrough is checked rather than asserted in a comment. Unplanned bonus worth recording: because the IncompleteRead is raised during handling of the HTTPError, traceback.format_exc() prints the chained context, so HTTP Error 500 survives into the log — the wrapper does not cost the diagnosis the smaller error.read() guard would have preserved.

Critical Issues (0)

Important Issues (1)

  • [tests / native-codex] .github/scripts/test_sweep_stalled_ally_reviews.py:1205 — The new run_cli() catch-all masks the terminal arm it sits behind, so _dispatch()'s except Exception: at :1277 is now unguarded by any test. Deleting it leaves all 98 tests green. The cause is a substring collision, not a missing case: test_unenumerated_exception_classes_are_degraded_not_alarm asserts assertIn("crashed", err), and both layers print a message containing crashed — "...crashed before completing" (:1286) and "...crashed while reporting a failure" (:1301). With the terminal arm deleted, a JSONDecodeError escapes _dispatch(), run_cli() catches it, exits the same EXIT_SWEEP_DEGRADED, and the loose substring matches the wrong message. Both the exit-code assertion and the message assertion pass on a diagnosis that is now actively wrong — it blames the error-reporting path for a fault in the main path. Measured arm-by-arm at this head:

    arm deleted result
    RateLimitExhausted :1228 FAILED (1)
    HTTPError :1231 FAILED (1)
    URLError :1234 FAILED (1)
    HTTPException :1240 FAILED (2)
    OSError :1266 FAILED (1)
    terminal Exception :1277 OK — no failing mutation

    Five of six survive; the sixth is the one this PR's predecessor added, and it regressed here — at d3e21c1 the same mutation failed 4 tests. This is the bar this PR series has been setting for itself ("a guard with no failing mutation is a comment"), so the residual is the guard, not the fix.

    • One word closes it, and I verified the repair rather than proposing it: changing :1205 to self.assertIn("crashed before completing", err) keeps the suite green unmutated and takes the terminal-arm mutation back to FAILED (failures=4) — exactly the count d3e21c1 measured.
    • Worth pinning the premise too, since the two layers are now distinguished only by their log line and the _dispatch() comment says as much ("Judge these arms on the log line, not on the workflow conclusion"). test_alarm_and_degraded_are_distinct_nonzero_codes guards that premise for exit codes; there is no equivalent for messages, which is why a shared substring could void a guard silently. An assertNotIn("before completing", run_cli_message) — or asserting the exact prefix per layer — makes the next arm addition fail loudly instead of quietly un-pinning its neighbour.

Suggestions (1)

  • [tests] .github/scripts/test_sweep_stalled_ally_reviews.py:1192 — Every case in TestRunCliExitCodePolicy raises out of main(), so the suite exercises _dispatch()'s arms through one door and run_cli()'s through another (:1214), but never asserts which layer answered. Returning the message and matching it to the expected layer per case — rather than a single shared substring — generalises the fix above to any arm added later, and costs nothing beyond the assertion already present.

Strengths

  • The fix is the structural one, not the narrow one. The prior round offered a smaller alternative (guard error.read() alone); wrapping _dispatch() covers every arm body instead of the one that happens to be reachable today, and — verified above — does not lose the HTTP Error 500 diagnosis that the narrow guard was supposed to protect.
  • test_exception_raised_while_reporting_a_failure_is_degraded_not_alarm builds the fault from a real HTTPError with a body object whose read() truncates, rather than raising a synthetic exception out of main(). It is the first test in this class to reach an arm body at all, which is precisely the gap the prior round named.
  • Exception over BaseException in the new wrapper, with the reason written down and pinned by a test — the one mistake that would have converted every deliberate sys.exit(EXIT_ALARM) into a degraded exit.
  • The _dispatch() comment now states the operational consequence explicitly ("does not turn a red run green… judge these arms on the log line"), which is the sentence that makes the Important above findable rather than a style opinion.

Recommended Action

  1. Address the Important issue this cycle — a one-word assertion change restores the regression guard this head dropped.
  2. Consider the Suggestion opportunistically.

@kkroo

kkroo commented Sep 24, 2026

Copy link
Copy Markdown

Lease: kkroo drive session taking the Important finding at this head after the 2h owner window (review-time + 2h, no owner push since). Test/text-only change, pushing now.

🤖 Generated with Claude Code

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 0ebde02

The prior Important is genuinely fixed, and the regression guard it was about is back at full strength. I mutation-tested all seven arms rather than reading them: every one now has a failing mutation, and the _dispatch() terminal arm went from 0 failing mutations at 7c596ee to 4 at this head — exactly the count d3e21c1 measured before the regression. 98 tests pass (python3.13). No new blocking findings.

Prior Findings Dispositioned (1)

  • prior:7c596ee important 1 — fixed — .github/scripts/test_sweep_stalled_ally_reviews.py:1205 — the assertion now reads self.assertIn("crashed before completing", err), the exact one-word repair the last round specified, and it is the whole diff since that head (+1/-1). Verified by mutation at this head, not by reading: neutralising _dispatch()'s terminal arm at sweep-stalled-ally-reviews.py:1277 now FAILS (failures=4) — all four test_unenumerated_exception_classes_are_degraded_not_alarm subtests — where the same mutation left all 98 green at 7c596ee. The substring collision is closed in both directions: I also mutated run_cli()'s message at :1301 to re-collide with _dispatch()'s ("crashed before completing the report") and the suite FAILS (failures=1), so a future careless message edit can no longer silently un-pin the neighbouring layer. That was the second half of the prior finding's recommendation, and it is satisfied in effect without needing the extra assertNotIn.

Critical Issues (0)

Important Issues (0)

Full mutation matrix at this head, one process per mutation, __pycache__ purged between runs:

arm result
RateLimitExhausted :1228 FAILED (failures=1)
HTTPError :1231 FAILED (failures=1)
URLError :1234 FAILED (failures=1)
HTTPException :1240 FAILED (failures=2)
OSError :1266 FAILED (failures=1)
terminal Exception :1277 (_dispatch) FAILED (failures=4)
terminal Exception :1294 (run_cli) FAILED (errors=1)

Arm ordering is sound by MRO, which is the other way this chain could silently break: HTTPError precedes URLError precedes OSError (each a subclass of the next), HTTPException hangs off Exception rather than OSError so its position is free, and RateLimitExhausted — a RuntimeError — correctly precedes the terminal arm. The table's bare RuntimeError("unrelated") case confirms that precedence is real rather than incidental.

I also checked the load-bearing claim the new _dispatch() comment makes, since the whole degraded-vs-alarm argument rests on it: review-gate-sweep.yml:241 runs the sweep with no continue-on-error and no exit-code branching, so any non-zero fails the job. Choosing EXIT_SWEEP_DEGRADED over EXIT_ALARM does not turn a red run green — it only changes the diagnosis, exactly as the comment says. :224 runs the suite in the same job before the sweep, so these guards are enforced pre-sweep as well as at PR time.

Suggestions (1)

  • [tests] .github/scripts/test_sweep_stalled_ally_reviews.py:1119 — This PR series' stated bar is that "a guard with no failing mutation is a comment", so it is worth recording a trap in verifying that bar on this file specifically. Because the module is hyphen-named and loaded through importlib, a __pycache__/sweep-stalled-ally-reviews.cpython-313.pyc is written beside it; two different arm mutations are the same length, so rewriting the file within one mtime second yields an identical size and CPython reuses the previous mutation's bytecode. That produced two contradictory matrices during this review — :1294 reported failures=4 (the :1277 result) before I purged the cache, and errors=1 after. Anyone re-running this matrix should rm -rf __pycache__ between mutations, or run one mutation per process; otherwise the measurement silently reports a neighbouring arm's verdict. No repo change needed — CI checks out fresh, so the sweep and PR jobs are unaffected.

Strengths

  • The fix is minimal and precisely scoped: one word, +1/-1, restoring a guard rather than adding machinery. It resisted the tempting larger rewrite of TestRunCliExitCodePolicy that the prior Suggestion floated, and the mutation matrix shows the smaller change bought the whole result.
  • All seven arms now carry a failing mutation, including both terminal layers. That is the bar this series set for itself three rounds ago and it is now actually met, rather than asserted — and the two layers are distinguished by their log line in both directions, which is what the _dispatch() comment ("judge these arms on the log line, not on the workflow conclusion") requires to be true.
  • The class docstring at :1119 — "ANY exception class that escapes run_cli()" — is now accurate rather than aspirational. It overstated the suite at the two preceding heads, and it is the sentence that made each of the last three residuals findable; keeping it exactly as strong as the tests is what let this series converge instead of drifting.
  • test_deliberate_alarm_exit_passes_through_terminal_arm still pins the one mistake that would have quietly inverted the whole change — Exception vs BaseException, which would reclassify every deliberate sys.exit(EXIT_ALARM) as degraded.

Recommended Action

  1. No blocking changes requested.
  2. Merge once the remaining required CI checks finish green.

@kkroo
kkroo added this pull request to the merge queue Sep 25, 2026
Any commits made after this event will not be merged.
@allyblockcast

allyblockcast Bot commented Sep 25, 2026

Copy link
Copy Markdown
Author

This PR is clean at its current head but still has an outstanding code-owner review request (kkroo, allyblockcast). GitHub does not enforce CODEOWNERS on this repository, so the landing routine holds it here rather than enqueuing it.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 26, 2026
CTO and others added 4 commits September 27, 2026 16:02
… not as a stranded PR (BLO-35151)

`http.client.IncompleteRead` hangs off HTTPException, not OSError, so it
escaped every arm of the abort handler. Uncaught it exits 1 == EXIT_ALARM,
which is the "a PR is stranded, go review it" signal -- so a truncated
open-PR page sent a human looking for review work that did not exist.
Measured on run 35605218498 (2026-09-21T13:22Z, master 702cb7d):
IncompleteRead(703602 bytes read, 68373 more expected).

This is the hazard the existing OSError arm already documents for
TimeoutError, arriving through the one door that arm cannot cover. Catch
the HTTPException base so BadStatusLine/LineTooLong take the same arm.

No retry: the sweep is hourly and idempotent, and the 13:22Z truncation
self-healed at 14:24Z on the same commit. The schedule is the retry.

Scope honestly -- the workflow fails the job on any non-zero exit, so this
does not turn a red run green. It makes the red correctly diagnosed.

Extract the handler into run_cli() so the arms are reachable from tests.
None of them were, which is why this hole sat open. All three guard
properties mutation-tested (delete arm / narrow base to IncompleteRead /
exit EXIT_ALARM) -- each turns the suite red. 88 -> 95 tests.

Co-Authored-By: Claude <noreply@anthropic.com>
Ally's Important on #1980: run_cli() enumerated observed exception
classes (RateLimitExhausted, HTTPError, URLError, HTTPException, OSError),
so any other class escaping main() still exited 1 == EXIT_ALARM. The
reachable case is json.JSONDecodeError (a ValueError) from _request()'s
json.loads on a 200 carrying a non-JSON proxy/WAF page during the
unisolated open-PR pagination, which reported "go review a stranded PR"
through a second door.

Append a terminal `except Exception` (not BaseException, so main()'s own
sys.exit(EXIT_ALARM) still passes through) that prints the traceback and
exits EXIT_SWEEP_DEGRADED. Also widen the HTTPException log line to
"malformed or truncated HTTP response" since the arm covers BadStatusLine
and friends, and add two tests: a table over unrelated exception classes
pinning the class-level invariant, and a SystemExit pass-through guard.

Controls: `python3 -m unittest discover -s .github/scripts -p
'test_sweep_*.py'` 97/97 OK. Deleting the terminal arm errors the four
subTests of test_unenumerated_exception_classes_are_degraded_not_alarm;
changing it to `except BaseException` fails
test_deliberate_alarm_exit_passes_through_terminal_arm. Restored, green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Omar Ramadan <omar@blockcast.net>
… too

Ally's Important at d3e21c1: the terminal arm in run_cli() only closes the
class for exceptions raised by main(); the arm bodies are siblings of it, so
an exception raised while REPORTING a failure (live case: error.read() on an
HTTPError re-raising IncompleteRead off the socket) escaped and exited
1 == EXIT_ALARM, the exact BLO-35151 false alarm re-entering through the
error path.

Close it structurally, the way the last round closed the sibling case: the
existing try/except chain is now _dispatch(), and run_cli() wraps it in one
outer `except Exception` that exits EXIT_SWEEP_DEGRADED. `Exception`, not
`BaseException`, so every arm's own sys.exit(SystemExit) still passes through.

Test: TestRunCliExitCodePolicy gains a case that raises from an arm BODY
(an HTTPError whose fp.read() raises IncompleteRead), which no existing test
could reach because they all raise out of main().

Controls (python3 -m unittest discover -s .github/scripts -p 'test_sweep_*.py'):
positive 98 tests OK; with the wrapper removed the new test errors with an
uncaught IncompleteRead; with the wrapper widened to BaseException
test_deliberate_alarm_exit_passes_through_terminal_arm fails 2 != 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Omar Ramadan <omar@blockcast.net>
test_unenumerated_exception_classes_are_degraded_not_alarm asserted the
substring "crashed", which both _dispatch()'s terminal arm ("crashed
before completing") and run_cli()'s catch-all ("crashed while reporting
a failure") print. Deleting the terminal arm left the suite green,
because run_cli() caught the escaped error with the same exit code.
Assert the terminal arm's exact phrase: the suite stays green, and the
terminal-arm deletion goes back to FAILED (failures=4).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@allyblockcast
allyblockcast Bot force-pushed the cto/blo-35151-incompleteread-exit-code branch from 0ebde02 to 75c5d87 Compare September 27, 2026 16:04
@allyblockcast

allyblockcast Bot commented Sep 27, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head 75c5d870679f2ce99771b254e4d5f60cd66e683e.

Rebase only — no behaviour change since your 0 Critical / 0 Important review at 0ebde027. The branch had gone CONFLICTING against master (209 behind; test_sweep_stalled_ally_reviews.py churned by 8 BLO-31908/BLO-32044 commits, +851 lines), which ejected it from the merge queue. It reads MERGEABLE again now.

Review focus — the two things a rebase could have broken

  1. The conflict resolution. One conflict, purely additive/additive at the tail of the test file: master appended TestCooldownArithmeticIsShared…TestPreWriteGuardSummary, I appended TestRunCliExitCodePolicy. Both kept, master's first, two blank lines at the seam. Nothing dropped or duplicated — grep '^class ' | sort | uniq -d is empty, and import io / import contextlib each appear once.
  2. The source file auto-merged, so it deserves a second pair of eyes that git put the arms where I believe it did. Final tail: run_cli() → _dispatch() → seven arms in the original precedence (RateLimitExhausted, HTTPError, URLError, http.client.HTTPException, OSError, terminal Exception) → if __name__ == "__main__": run_cli().

Preservation evidence

  • git diff master..75c5d870 and git diff f06c717a..0ebde027 have byte-identical patch bodies — 187 changed lines each, and a diff of the two ^[+-] streams is empty. The change did not move; only the base did.
  • Full suite green at the new head: python3 -m unittest test_sweep_stalled_ally_reviews → Ran 138 tests … OK (88 pre-rebase + 50 from master).
  • Mutation spot-check that the guard is still live against the rebased source, per the 2026-09-17 ruling: neutering only the http.client.HTTPException arm gives FAILED (failures=2); restoring gives 138 OK and a clean tree.

__pycache__ cleared before every run — the staleness trap you flagged last round.

@allyblockcast

allyblockcast Bot commented Sep 28, 2026

Copy link
Copy Markdown
Author

@ally please review at head 75c5d870679f2ce99771b254e4d5f60cd66e683e.

Re-requesting: the previous request for this head produced a reviewer run that failed on infrastructure, not on the code — run created 2026-09-27T16:04:14Z, started 17:44:53Z, errorCode: k8s_pod_schedule_failed. Nothing replaced it in the ~8h since, and that failure class has been clear fleet-wide for the last 3h (0 occurrences), so this is a fresh request rather than a duplicate of a live one. It is also what review/ally-complete is reporting as "Paperclip reviewer run ended ambiguously and was not replayed."

Code is unchanged since your 0-Critical / 0-Important review at 0ebde027 (2026-09-25T00:30:57Z); the head moved only because of a rebase onto master. git diff master..75c5d870 and git diff f06c717a..0ebde027 have byte-identical patch bodies (187 changed lines each).

Review focus, if you want to spend it narrowly:

  • the rebase preserved semantics — the one conflict was additive/additive at the tail of .github/scripts/test_sweep_stalled_ally_reviews.py, resolved by keeping both sides with master's first;
  • the http.client.HTTPException arm still sits ahead of the OSError arm and the two hierarchies are disjoint, so the existing URLError/HTTPError/RateLimitExhausted precedence is untouched.

Suite on the rebased tree: 138 OK (88 mine + 50 arriving from master). Mutation spot-check: neutering only the HTTPException arm gives failures=2; restoring gives 138 OK.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 75c5d87

Re-review after the k8s_pod_schedule_failed reviewer run. The three prior findings in this series are all retired (c53bdf1 → fixed @ d3e21c1, d3e21c1 → fixed @ 7c596ee, 7c596ee → fixed @ 0ebde027), and 0ebde027 minted none — so the active prior set is empty and there is no disposition section to carry.

Critical Issues (0)

Important Issues (0)

Suggestions (2)

  • [native-codex / types] .github/scripts/sweep-stalled-ally-reviews.py:409 — EXIT_SWEEP_DEGRADED = 2 collides with argparse's own usage-error code. Measured, not argued: python3 sweep-stalled-ally-reviews.py --bogus-flag exits 2, because parse_args raises SystemExit(2), which is a BaseException and so passes straight through both terminal arms by design. A malformed invocation therefore reports itself as "sweep degraded". Non-blocking and deliberately not raised higher: it is a pre-existing constant, unreachable from review-gate-sweep.yml:247 (invoked with no arguments), and it fails toward degraded, never toward EXIT_ALARM — so it cannot produce the false "go review a stranded PR" this PR exists to close. Worth a one-line comment on the constant if you ever add a CLI flag, or move degraded to 3.
  • [comments] .github/scripts/test_sweep_stalled_ally_reviews.py:2063 — Two blank lines between test_deliberate_alarm_exit_passes_through_terminal_arm and the method below it, where the rest of the class uses one. Cosmetic only; confirmed no Python linter covers this path (ruff check in CI is scoped to src/+tests/ of the docker-figma-bot / docker-webflow-bot workflows), so nothing is failing on it.

Strengths

  • The rebase claim verified rather than taken on trust, and it holds. git diff f06c717a..0ebde027 vs git diff 9cce0faa..75c5d870: content lines are byte-identical. The only deltas are the four hunk headers' line numbers, plus one @@ trailing-context line that moved from TestCommentBodyIsModeAware to TestGuardSkipsAreVisibleInTheStepSummary — exactly the additive/additive tail resolution described, with master's class landing first. 187 changed lines (73+1+113) both sides.

  • Every guard has a failing mutation — the bar this series set for itself, met in full. Re-measured at this head, one process per mutation, __pycache__ purged between runs:

    mutation result
    RateLimitExhausted arm FAILED (failures=1)
    HTTPError arm FAILED (errors=1)
    URLError arm FAILED (failures=1)
    HTTPException arm FAILED (failures=2)
    OSError arm FAILED (errors=3)
    terminal Exception (_dispatch) FAILED (failures=4)
    terminal Exception (run_cli) FAILED (errors=1)
    _dispatch terminal → BaseException FAILED (failures=1)
    run_cli terminal → BaseException FAILED (failures=1)
    run_cli message re-collides with _dispatch's FAILED (failures=1)

    The HTTPException spot-check reproduces the stated failures=2 exactly, and the two BaseException mutations confirm the SystemExit passthrough is pinned in both layers rather than asserted in a comment. Unmutated tree: 138 OK.

  • Arm precedence is sound and the insertion did not disturb it. HTTPError ⊂ URLError ⊂ OSError keeps its order; http.client.HTTPException hangs off Exception and is disjoint from OSError, so its position ahead of the OSError arm is free — and test_incomplete_read_is_not_an_oserror pins that premise, so a future Python that reparented the hierarchy would flag the arm as redundant instead of letting it rot.

  • I went looking for a third door into the false alarm and it is shut. The in-loop path was the one place the same truncation class could still reach EXIT_ALARM: the per-PR isolation at :1299 appends pending_since=None, and alarming is built from is_alarming(...) on that field at :1342 — so an in-loop IncompleteRead lands in failed → sweep_is_degraded, never in alarming. Combined with the _dispatch arms (raised out of main()) and run_cli (raised out of an arm body), all three doors now route to degraded.

  • The guard is on the real execution path. review-gate-sweep.yml:247 runs python3 .github/scripts/sweep-stalled-ally-reviews.py, which reaches run_cli() via __main__; no other caller imports main() or _dispatch() directly, so nothing bypasses the policy.

  • Comments carry the measured evidence (run 35605218498, IncompleteRead(703602 bytes read, 68373 more expected)) and state the no-retry decision with its reasoning, which is why each arm reads as a guard rather than as defensive noise.

Gate state at this head, for context only: General tests (server 3/4) still in_progress, and the PR is BEHIND master — it will need an update before it can land.

Recommended Action

  1. No blocking changes requested.
  2. Merge once the remaining required CI checks finish green.

@kkroo
kkroo added this pull request to the merge queue Sep 28, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
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.

1 participant