Skip to content

Fix review bot network timeout handling - #223

Merged
terasakisatoshi merged 3 commits into
mainfrom
fix-review-bot-network-timeouts
Aug 4, 2026
Merged

Fix review bot network timeout handling#223
terasakisatoshi merged 3 commits into
mainfrom
fix-review-bot-network-timeouts

Conversation

@terasakisatoshi

Copy link
Copy Markdown
Member

Three fixes to scripts/repository-rules-review.py, all surfaced by a live run while porting the same script to tensor4all-rs (tensor4all/tensor4all-rs#568), and all inherited from the tenferro original.

1. socket.timeout escaped the exception tuple

The ported handler caught (KeyError, ValueError, urllib.error.URLError, TimeoutError). socket.timeout only became an alias of TimeoutError in Python 3.10, so on 3.9 it propagated as an unhandled traceback:

socket.timeout: The read operation timed out

This matters beyond the version gap. The workflow pipes only stdout into the report (| tee review-report.txt), so the traceback went to stderr and the PR comment would have read "Review step did not produce a report" with no diagnostic at all.

Catching OSError covers socket.timeout, TimeoutError, and urllib.error.URLError on every version, so the existing llm-review-unusable block finding reaches the PR comment as designed.

2. No retry on transient failures

One blocked PR per network blip is not an acceptable failure mode. Requests now retry once with a 5s backoff before the block finding is raised.

3. Chunk size and timeout were mismatched

A 108k single chunk timed out at the 120s default. Aggregate chunks are now capped at 60k (MAX_DIFF_CHARS) and the default timeout is 300s. Measured per-chunk latency in this repository was 26–150s depending on size, so 300s leaves real margin, and the tensor4all-rs commit that timed out now completes as two chunks in 235s.

Verification

49 script tests pass, including two new regression tests that inject socket.timeout and assert the retry succeeds and that an exhausted retry still propagates.

Refs #199

🤖 Generated with Claude Code

A live run in the tensor4all-rs port of this script surfaced three problems,
all inherited from the tenferro original.

`socket.timeout` escaped the exception tuple as an unhandled traceback. It
only aliases `TimeoutError` from Python 3.10 on, so
`(KeyError, ValueError, URLError, TimeoutError)` misses it on 3.9. This
matters beyond the version gap: the workflow tees only stdout into the
report, so the traceback went to stderr and the PR comment would have read
"Review step did not produce a report" with no diagnostic. Catching `OSError`
covers socket.timeout, TimeoutError, and URLError on every version, so the
existing `llm-review-unusable` block finding reaches the comment instead.

Transient network failures now retry once before blocking a PR. One blocked
PR per network blip is not an acceptable failure mode for a required-adjacent
check.

A 108k single chunk timed out against DeepSeek at the 120s default. Aggregate
chunks are capped at 60k and the default timeout is 300s. Per-chunk latency
measured here was 26-150s depending on size, so 300s leaves real margin.

Refs #199

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Repository rules review

Repository rules review (9c165edd761dd0190cbe9587b8459deb88e31fc6...ebe440e1f369febad618a2a64be291a05d29e3dc)
Verdict: fail
LLM review: 1 chunk(s) (26068 chars) in 0.0s; 0 finding(s) returned, 0 kept, 0 dropped by diff-anchor filtering.
Findings:
- [block] llm-review-unusable (External LLM Review) <unknown>: External LLM review did not produce usable JSON
  The repository-rules review could not parse or validate the model response: UnicodeEncodeError: 'latin-1' codec can't encode characters in position 35-39: ordinal not in range(256)

The first pass caught OSError, which covers socket.timeout, TimeoutError,
ConnectionResetError, ssl.SSLError, and urllib.error.URLError.
http.client.IncompleteRead is a sibling of OSError rather than a subclass,
so a truncated chunked response still escaped. Name both roots in one
TRANSPORT_ERRORS tuple and assert the coverage in a test.

Keeps this file identical to the tenferro-rs and tensor4all-rs copies.

Refs #199

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
terasakisatoshi added a commit to tensor4all/tensor4all-rs that referenced this pull request Aug 4, 2026
Three findings from the Codex review of this PR, plus the root cause of the
failure the bot itself hit on tensor4all/strided-rs#223.

**Typed declarations hid secrets from the pre-upload guard (P1).** For
`const API_KEY: &str = "..."`, `SECRET_ASSIGNMENT` treated the type colon as
the separator and redacted `&str`, leaving the literal intact, while
`QUOTED_SECRET_ASSIGNMENT` did not match through the annotation at all. The
credential therefore survived redaction and was uploaded to the external LLM.
Both patterns now allow a short type annotation between the name and the
value.

The detector's own fixtures were themselves secret-shaped, so this file
tripped the improved guard and would have blocked the LLM pass on every PR
touching its tests. Fixtures are now assembled at runtime; the file contains
no contiguous secret-shaped literal while the tests still exercise the real
shapes.

**Oversized hunks reported wrong line numbers (P2).** Every chunk repeated the
original hunk header even though later chunks start thousands of lines
further into the file, so `filter_findings` either dropped a real finding or
kept it against an unrelated added line that happened to collide. Each chunk
now carries a header rewritten to its own old/new offsets, counting context,
removal, and addition lines separately. An unparseable header falls back to
the previous verbatim behaviour rather than inventing offsets.

**Worktree previews skipped untracked files (P2).** `git diff <base>`
compares the working tree against a commit, so a newly created file has no
object to compare and was omitted from the documented local preview
entirely -- a brand new file was reviewed by nothing and the preview reported
a false pass. Untracked paths are now enumerated with `ls-files --others
--exclude-standard` and diffed against /dev/null with `--no-index`, which
needs no staging and does not touch the index.

**An unusable API key was reported as unparseable JSON.** HTTP header values
are latin-1, so a key carrying non-ASCII text raises UnicodeEncodeError before
any request leaves the machine. That is a ValueError subclass, so it surfaced
as "External LLM review did not produce usable JSON" and pointed the reader at
the model instead of at the secret. The key is now validated up front with a
diagnostic that names the offending offsets without echoing the value, and the
generic wording no longer claims the model responded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ported from the Codex review of tensor4all/tensor4all-rs#568, which reviewed
the same script. All three findings apply verbatim here.

Typed declarations hid secrets from the pre-upload guard. For
`const API_KEY: &str = "..."`, SECRET_ASSIGNMENT treated the type colon as the
separator and redacted the type, leaving the literal, while
QUOTED_SECRET_ASSIGNMENT did not match through the annotation at all, so the
credential was uploaded. Both patterns now allow a short type annotation.
The detector fixtures were themselves secret-shaped and tripped the improved
guard, so they are assembled at runtime now.

Oversized hunks reported wrong line numbers: every chunk repeated the original
header, so findings in later chunks came back with line numbers thousands of
lines too small. Each chunk now carries a header rewritten to its own offsets.

Worktree previews skipped untracked files entirely, so a brand new file was
reviewed by nothing and the documented preview reported a false pass.
Untracked paths are diffed against /dev/null with --no-index.

An unusable API key was reported as unparseable JSON. This is the failure the
bot hit on strided-rs#223: a non-ASCII key raises UnicodeEncodeError while
encoding the latin-1 Authorization header, and that is a ValueError subclass,
so it surfaced as "did not produce usable JSON". The key is validated up front
now, with a diagnostic that names the offending offsets without echoing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Refs #199
@terasakisatoshi
terasakisatoshi merged commit 817c1e9 into main Aug 4, 2026
8 of 10 checks passed
@terasakisatoshi
terasakisatoshi deleted the fix-review-bot-network-timeouts branch August 4, 2026 02:10
terasakisatoshi added a commit to tensor4all/tensor4all-rs that referenced this pull request Aug 4, 2026
* Add repository rules review bot

Port tenferro-rs's delta-scoped REPOSITORY_RULES review, per #566 Phase 1.
The workflow runs from the trusted base revision and treats PR contents as
data: the PR head is fetched for `git diff` only, never checked out or
executed. Findings post as a single updating PR comment; only block-severity
findings fail the check. It covers PRs to both main and develop.

#566's structural diagnosis is that #552 ported tenferro's rule text as prose
but almost none of the enforcement layer. Two rules are exactly machine
checkable, so they run deterministically, before and independently of the LLM:

- New direct `tenferro-*` dependency outside tensor4all-tensorbackend
  (Dense Layout And Linear Algebra). Cargo.toml names `tenferro-*` both as
  feature names and as dependencies, so the check tracks table context rather
  than grepping; dev-dependencies are out of scope.
- Added `ignore` / `no_run` doctest fence (Documentation Examples).

Both are delta-scoped. #566 records existing violations of each (Phase 3 for
the dependency route, Phase 1 for the two `no_run` sites), so a repo-wide gate
would fail on the backlog. These stop new violations while the backlog burns
down separately. Verified against the commits that introduced each class:
795c087 and 21c2f07 both produce the expected block.

Section routing covers all 26 rule sections. Base Branch Synchronization is
marked human-only: it is a git-workflow protocol, so nothing inside a diff can
satisfy or violate it, and showing it to a diff-scoped reviewer only invites
invented findings. A coverage test fails if a section is neither routed,
always-on, nor explicitly human-only, so the sections Phase 2 will rewrite
cannot go silently unreviewed.

The prompt carries the two-stack context from #566: the network stack and the
TCI stack differ in naming, option style, and error types by design, so a
difference between them is not by itself a finding.

Three fixes to the ported script, all found by a live run against 69a24e7:

- `socket.timeout` escaped the exception tuple as an unhandled traceback.
  It only aliases TimeoutError from Python 3.10 on, so the ported
  `(KeyError, ValueError, URLError, TimeoutError)` missed it on 3.9. Catching
  OSError covers every version, and the report reaches the PR comment instead
  of a stack trace reaching stderr.
- Transient network failures now retry once before blocking a PR.
- A 108k single chunk timed out at the 120s default. Aggregate chunks are
  capped at 60k and the default timeout is 300s; the same commit now reviews
  as two chunks in 235s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Cover ConnectionResetError, SSLError, and IncompleteRead

The first pass caught OSError, which covers socket.timeout, TimeoutError,
ConnectionResetError, ssl.SSLError, and urllib.error.URLError.
http.client.IncompleteRead is a sibling of OSError rather than a subclass,
so a truncated chunked response still escaped. Name both roots in one
TRANSPORT_ERRORS tuple and assert the coverage in a test.

Keeps this file identical to the tenferro-rs and strided-rs copies.

Refs #566

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Address Codex review: secret guard, hunk offsets, untracked previews

Three findings from the Codex review of this PR, plus the root cause of the
failure the bot itself hit on tensor4all/strided-rs#223.

**Typed declarations hid secrets from the pre-upload guard (P1).** For
`const API_KEY: &str = "..."`, `SECRET_ASSIGNMENT` treated the type colon as
the separator and redacted `&str`, leaving the literal intact, while
`QUOTED_SECRET_ASSIGNMENT` did not match through the annotation at all. The
credential therefore survived redaction and was uploaded to the external LLM.
Both patterns now allow a short type annotation between the name and the
value.

The detector's own fixtures were themselves secret-shaped, so this file
tripped the improved guard and would have blocked the LLM pass on every PR
touching its tests. Fixtures are now assembled at runtime; the file contains
no contiguous secret-shaped literal while the tests still exercise the real
shapes.

**Oversized hunks reported wrong line numbers (P2).** Every chunk repeated the
original hunk header even though later chunks start thousands of lines
further into the file, so `filter_findings` either dropped a real finding or
kept it against an unrelated added line that happened to collide. Each chunk
now carries a header rewritten to its own old/new offsets, counting context,
removal, and addition lines separately. An unparseable header falls back to
the previous verbatim behaviour rather than inventing offsets.

**Worktree previews skipped untracked files (P2).** `git diff <base>`
compares the working tree against a commit, so a newly created file has no
object to compare and was omitted from the documented local preview
entirely -- a brand new file was reviewed by nothing and the preview reported
a false pass. Untracked paths are now enumerated with `ls-files --others
--exclude-standard` and diffed against /dev/null with `--no-index`, which
needs no staging and does not touch the index.

**An unusable API key was reported as unparseable JSON.** HTTP header values
are latin-1, so a key carrying non-ASCII text raises UnicodeEncodeError before
any request leaves the machine. That is a ValueError subclass, so it surfaced
as "External LLM review did not produce usable JSON" and pointed the reader at
the model instead of at the secret. The key is now validated up front with a
diagnostic that names the offending offsets without echoing the value, and the
generic wording no longer claims the model responded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Address the second Codex review round

Cargo renaming and dependency subtables bypassed the tenferro route check;
tilde doctest fences bypassed the doctest check; quoted secrets containing
spaces were neither detected nor fully redacted; git C-quoted non-ASCII
pathnames so those files were reviewed by nothing; path-only routing never
supplied the unsafe rules for a generic filename; and cumulative retries could
outlive the job timeout and lose the report.

Adds the work log this repository's Work Logs And Design Records rule requires,
with the classification ledger for both review rounds.

Refs #566

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Address the third Codex review round

Continuation-line secrets: an assignment can stay unchanged on a context line
while only the value line is replaced, so checking added lines in isolation
sees a bare literal with no credential-shaped name. sensitive_diff_location
now walks the diff in order and tracks an assignment whose value has not
appeared yet. The redactor's separator is also line-local now; it used to
cross the newline and consume the deletion marker as the value, leaving the
new literal untouched.

Cargo manifests outside crates/ were skipped entirely because crate_of()
returned None. xtask, tools/api-dump, docs/tutorial-code, and docs/book-tests
are workspace members, so a direct tenferro dependency there passed
undetected. Scope is now every Cargo.toml, exempting the route package by its
[package] name rather than its directory.

TOML comments were treated as declarations, so a commented example or a
trailing comment produced a block finding. Comments are stripped, respecting
quoted hashes.

The new fixtures tripped the new detector, as with the earlier rounds; the
opener is assembled at runtime so the file no longer trips its own guard.

Refs #566

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Report the configured review budget, not the default

budget_exhausted_finding interpolated DEFAULT_BUDGET_SECONDS into its message
regardless of --budget-seconds, so `--budget-seconds 30` produced a diagnostic
claiming a 900s budget ran out. That misleads exactly the reader who is trying
to work out why a review came back incomplete. Pass the configured value
through and assert both halves in the test.

Refs #566

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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