Budget the editor-handoff pause sleep, and note why main looked green without it - #6243
Conversation
#6239 (#6165) added a bounded `thread::sleep` to `tui/ui/terminal_input.rs::pause_for_child_terminal`, which the blocking-calls ratchet has no entry for. The gate is advisory on pull requests and blocking on pushes to main, so the PR was green and main is only green because the budget steps SKIPPED - they are gated on `needs.changes.outputs.heavy == 'true'`, and that merge did not trip it. The violation is real and fires on the next heavy push. Budgeted rather than rewritten, because the site is the case the script's own message names. Its author already wrote the justification at the call site: a bounded retry capped by `TERMINAL_INPUT_CHILD_PAUSE_TIMEOUT`, in a synchronous API whose caller is about to block that very thread on a foreground editor for as long as the user keeps it open. `tokio::time` is not reachable from there and would not change what the thread does. The sibling sleep at :192 is already inside a `thread::Builder::spawn`, so the scanner never counted it. One entry added; the rest of the file is unchanged. check-blocking-calls-budget.py 625 sites across 181 files, within budget Worth a follow-up someone should own: a budget step that skips on a not-heavy-enough diff means a green main is not evidence the ratchet passed. That is how this reached main in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AJENKJ2smviQW4FVGzUTk9
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Claude finished @Hmbown's task in 1m 52s —— View job Review complete
SummaryThis is a correctly-scoped, data-only ratchet update: one JSON entry, no code changes. I traced the scanner logic by hand against the file and the numbers check out. Verified correct:
One nit (description only, not the diff): the PR body attributes the budgeted sleep to |
There was a problem hiding this comment.
Codewhale review
Complete review coverage: 1/1 passes, 1 file patches, sha256:e3aac133bcff3abde64e4ff3d74310f4763537031748db482abc0ecc2b2d3fd8.
Pass 1: The PR adds a single budget entry to scripts/check-blocking-calls-budget.json for crates/tui/src/tui/ui/terminal_input.rs with thread_sleep = 1. The hunk is syntactically valid JSON, correctly indented (2 spaces for the key, 4 for the inner count), and inserted in the file's existing alphabetical position between provider_setup.rs and tests.rs. No other content changes. The change is minimal and matches the scanner's reported violation.
Findings
- [INFO] Verify the key is not already present elsewhere in the budget file (
scripts/check-blocking-calls-budget.json:454)
The diff shows only the insertion context around line 451, so a pre-existing entry for "crates/tui/src/tui/ui/terminal_input.rs" elsewhere in the JSON cannot be ruled out from the patch alone. JSON parsers silently keep the last duplicate key while the generator (--update) may emit only one, so a duplicate would produce confusing future diffs and a budget that does not match what the author intended. Since the file is sorted alphabetically, a duplicate should be adjacent (or would break the ordering assumption), which makes this cheap to confirm with a quick grep before merge. - [INFO] This file is not validated by CI on main, so the entry cannot be machine-verified here (
scripts/check-blocking-calls-budget.json)
As the PR description states, the four budget steps are gated onneeds.changes.outputs.heavy == 'true'and wereskippedon main's run for e8a2265; combined withcontinue-on-erroron pull requests, a malformed or wrong-count entry can land without ever being exercised by check-blocking-calls-budget.py. The author explicitly declares this gating change out of scope, which is reasonable, but it means the only verification for this diff is manual: the key must byte-match the path printed by the scanner and the count must equal the site count the scanner reports for that file. Worth confirming locally with the script rather than relying on the green main signal. - [INFO] Rationale for the budgeted exception lives only outside the repository (
scripts/check-blocking-calls-budget.json:455)
The justification quoted in the PR (bounded retry capped by TERMINAL_INPUT_CHILD_PAUSE_TIMEOUT, synchronous API whose caller blocks the thread on a foreground editor) is the reason the budget is raised instead of the call being rewritten, but it will not be discoverable from the budget file, which is plain JSON with no comment support. A reader investigating later why this file carries a non-zero thread_sleep budget will have to dig through git history to find it. Nothing to fix in this diff, but noting it so the tradeoff is at least acknowledged.
Assessment
Pass 1: Correct and minimal. The added object is valid JSON, uses the file's existing 2/4-space indentation, and is placed in the correct alphabetical slot (provider_setup.rs < terminal_input.rs < tests.rs), so it will not churn under a future --update. The budgeted value of 1 matches the single site the scanner reports, and the sibling sleep at terminal_input.rs:192 is inside a thread::Builder::spawn so it is legitimately uncounted. The main risk is not in the patch but in its verification path: because the budget steps are gated behind the heavy output and are advisory-only on PRs, this entry is not actually exercised by CI, so the path/key match and absence of a duplicate key should be confirmed manually. Approve once that is checked.
Advisory review by Codewhale (codewhale review --pr 6243 --post, head fbf715889e0763ac7f5ad126e5c20e66ea6f71f2). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| "crates/tui/src/tui/ui/provider_setup.rs": { | ||
| "std_fs": 2 | ||
| }, | ||
| "crates/tui/src/tui/ui/terminal_input.rs": { |
There was a problem hiding this comment.
[INFO] Verify the key is not already present elsewhere in the budget file
The diff shows only the insertion context around line 451, so a pre-existing entry for "crates/tui/src/tui/ui/terminal_input.rs" elsewhere in the JSON cannot be ruled out from the patch alone. JSON parsers silently keep the last duplicate key while the generator (--update) may emit only one, so a duplicate would produce confusing future diffs and a budget that does not match what the author intended. Since the file is sorted alphabetically, a duplicate should be adjacent (or would break the ordering assumption), which makes this cheap to confirm with a quick grep before merge.
| "std_fs": 2 | ||
| }, | ||
| "crates/tui/src/tui/ui/terminal_input.rs": { | ||
| "thread_sleep": 1 |
There was a problem hiding this comment.
[INFO] Rationale for the budgeted exception lives only outside the repository
The justification quoted in the PR (bounded retry capped by TERMINAL_INPUT_CHILD_PAUSE_TIMEOUT, synchronous API whose caller blocks the thread on a foreground editor) is the reason the budget is raised instead of the call being rewritten, but it will not be discoverable from the budget file, which is plain JSON with no comment support. A reader investigating later why this file carries a non-zero thread_sleep budget will have to dig through git history to find it. Nothing to fix in this diff, but noting it so the tradeoff is at least acknowledged.
A second, larger blind spot in this same gate — partly confirmed, partly not(Reposted: the first version of this comment lost several filenames to shell backtick expansion.) A multi-agent review of the guard surface flagged Confirmed: the pattern anchors on the literal
|
…mbown#6247) `build_approval_grouping_key` is the scope of an "approve for the session" decision: two patches share that grant exactly when they share the key. For apply_patch the key was built by a second, weaker parser than the one the executor uses - it read paths only from `+++ b/` headers and the `replace`/`changes` array, never from the tool's own top-level `path` argument, and discarded the normalizer's error with `Err(_) => {}`. When it found nothing it returned the literal constant "no_files". Four supported shapes hit that constant: the documented `apply_patch{path, patch}` override (a bare hunk has no `+++` line, and apply_patch.rs itself tells the model "Ensure the patch includes ---/+++ headers or provide `path`"), the same shape through `File{action:"patch"}`, any `--no-prefix` diff (the executor's normalize_diff_path accepts `+++ src/x.rs` while the fingerprint demanded the literal `b/`), and any delete-only diff. So approving a card that read "patch .env.example" also approved every later path-less patch - to `.env`, to `.codewhale/settings.json` which decides what runs on the machine, to any absolute path outside the workspace, and in a non-git workspace to every file in the tree, since the Hmbown#5185 carve-out is disabled there. No second card, no notice, no receipt. is_session_approved_for_tool is a bare set lookup on this key, and its own comment names this exact class: "approving one shell command used to auto-approve the entire shell tool for the session. The contains(tool_name) clause was the escalation (ops R2)." That escalation was closed for shell. It was open for patches, through a constant instead of a tool name. The fix is reuse, not new parsing: hash `preflight_apply_patch(input)`'s touched_files - already `pub`, already what core/engine.rs and auto_review.rs call for the permission path - which folds the path override, prefix-less headers and tab timestamps. Both empty arms now fail closed to a digest of the input rather than to a shared constant, so an unparseable patch is its own family and matches nothing but a byte-identical repeat. Three regression tests, each confirmed failing without the fix by reverting the resolver and keeping the tests: grouping_key_scopes_a_path_override_to_its_own_file FAILED -> ok grouping_key_reads_prefix_less_diff_headers FAILED -> ok grouping_key_fails_closed_on_an_unresolvable_patch FAILED -> ok tools::approval_cache + tools::apply_patch + approval 323 passed; 0 failed (--test-threads=1) cargo fmt --all -- --check clean cargo clippy --workspace --all-targets --all-features --locked (CI's allow list) clean One note for the reviewer: at --test-threads=2 this set reported task_manager::tests::pending_approval_suspends_idle_and_timeout_denial_settles_failed as failing. It passes isolated and passes at --test-threads=1; it is the same load-sensitive timeout class as the compatibility_stream tests, not this change. check-blocking-calls-budget.py still fails on terminal_input.rs - that is main's existing red, fixed in Hmbown#6243, not introduced here. Closes Hmbown#6247 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AJENKJ2smviQW4FVGzUTk9
One line of JSON, plus something about the gate that is worth knowing.
No-Issue: ratchet bookkeeping for a site #6165 landed deliberately; the underlying convention is #6149.
What
#6239 (which closed #6165) added a bounded
thread::sleeptotui/ui/terminal_input.rs::pause_for_child_terminal. The blocking-calls ratchethas no entry for that file, so it now reports:
This adds the one entry. Nothing else in the file changes.
Why budgeted and not rewritten
This is the case
check-blocking-calls-budget.py's own message names — "raisethe budget with
--updateif the site can only run on synchronous code" — andits author had already written the justification at the call site:
That reads correctly to me. The caller is about to hand the terminal to
$EDITORand block until the user quits it; a few polled milliseconds beforethat is not what parks the thread. The sibling sleep at
:192is already insidea
thread::Builder::spawn, which is why the scanner never counted it.The part worth reading
Main is currently green on this, and that is not evidence.
The budget steps are gated on
if: needs.changes.outputs.heavy == 'true'. Onmain's run for
e8a226552all four budget steps reportskipped:Combined with
continue-on-erroron pull requests, a violation can cross a PRadvisory-only and then land on a main push that never runs the check. That is
exactly the path this took: green PR, green main, real violation, fires on
whatever heavy push comes next — at which point it looks like that unrelated
push's fault.
I am not changing the gating here; narrowing
heavyor making these stepsunconditional is a CI-cost tradeoff someone should make deliberately. But the
current shape means a green main does not tell you the ratchets passed, and
that seemed worth stating where someone will read it.
Evidence
Diff is one added object:
🤖 Generated with Claude Code