diff --git a/Cargo.lock b/Cargo.lock index b39aae0..fed8a17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1104,7 +1104,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "postil-cli" -version = "0.8.25" +version = "0.8.26" dependencies = [ "aho-corasick", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 89002aa..b6f98f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "postil-cli" -version = "0.8.25" +version = "0.8.26" edition = "2024" description = "Postil: a low-noise AI review gate. Silent on clean PRs, hard gate on real risk." license = "Apache-2.0" diff --git a/src/llm.rs b/src/llm.rs index f8e3b5b..6b2f3c4 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -6826,6 +6826,20 @@ mod tests { "more operations must divide the phase further, not less" ); + // Batches run concurrently, so a four-batch review is one wave and its + // slot is the one-operation slot, not a quarter of it. Running them one + // at a time is what put each batch under a slot the review model's tail + // latency does not fit. + let concurrent = build(); + concurrent + .ensure_hosted_review_schedule(&config, 4, false, 4) + .expect("four batches at concurrency four must schedule"); + assert_eq!( + concurrent.scheduled_review_model_slot(), + Some(single_slot), + "one wave of four batches must get the same slot as one batch" + ); + // The slot only matters if the operation actually runs under it, so // check the deadline the review path builds, not just the derivation. let operation = single.for_review_model_operation(); diff --git a/src/review.rs b/src/review.rs index 6598d40..a453e11 100644 --- a/src/review.rs +++ b/src/review.rs @@ -1802,11 +1802,13 @@ async fn review_diff_at( .any(|id| !candidates.mandatory_ids.contains(id))) .then_some((candidates.manifest.as_str(), remaining)) }); - let admitted_concurrency = if deterministic_large_review { - large_diff_batch_concurrency(cfg) - } else { - 1 - }; + // Batch requests carry their own prompts and are collected by + // index, so nothing orders them. `max_hosted_review_batches` + // already prices capacity at this concurrency; running them + // one at a time instead divides the phase by the batch count + // and puts each batch under a slot the review model's tail + // latency does not fit. + let admitted_concurrency = large_diff_batch_concurrency(cfg); let scorer_system = prompt::scorer_system_prompt(cfg, current_utc_date); let admission = client.preflight_review_plan_with_output_limits( cfg, @@ -1996,11 +1998,10 @@ async fn review_diff_at( )); request_index += 1; } - let concurrency = if deterministic_large_review { - large_diff_batch_concurrency(cfg) - } else { - 1 - }; + // Must match the concurrency admission priced the schedule at, + // or the phase is divided into a different number of waves than + // the plan was checked against. + let concurrency = large_diff_batch_concurrency(cfg); let scorer_batch_evidence_budget = (MAX_SCORER_EVIDENCE_CORPUS_BYTES / total_requests.max(1)) .min(MAX_SCORER_BATCH_EVIDENCE_BYTES);