Summary
When Claude is unavailable, reviews fall back to local Ollama — which is given a prompt far larger than its context window with no num_ctx set, silently truncates it, and is then held to identical blocker authority as Claude. Nothing in the verdict, the public comment, or the finding says which model actually decided. Reviewer votes are additionally attributed to the primary model even when the fallback produced the review.
1. The fallback is given a prompt it cannot fit
The review call sends up to 120,000 chars of diff plus an aggregate context budget of up to 240,000 chars (~60k+ tokens) — src/services/ai-review.ts:1483-1523, :1178, :1124.
The Ollama chat call (src/selfhost/ai.ts:374-402) sets max_tokens, temperature and response_format but no providerOptions / num_ctx. The only place num_ctx is ever set in the codebase is the vision call (src/queue/processors.ts:8808) — even though AiRunOptions.providerOptions was added (#4327/#4335) for exactly this.
Ollama silently left-truncates input beyond num_ctx (server/model default, typically 4k–8k tokens) and answers confidently over whatever survived — which is the tail of the prompt (context sections), not the diff.
2. The fallback carries full blocker authority
A fallback-produced review is held to the same schema validation and the same confidence semantics against aiReviewCloseConfidence. A llama3.1-class self-reported 0.95 is not the same instrument as Claude's, but the gate cannot tell them apart. Combined with #9476 (Claude timeouts retrying 3×), a Claude outage means every review is decided this way.
Only loopover_ai_review_model_fallback_total, the diagnostics, and parsedReviewModelIds record that a fallback happened — the verdict, the public copy ("AI review flagged a likely critical defect") and the finding itself are silent.
3. Votes are attributed to the wrong model
src/services/ai-review.ts:2935-2936, :3001:
if (a.review) reviewerVotes.push({ reviewer: primary.model, votedFail: … });
runWorkersOpinion iterates [primary, fallback] internally and ReviewerOpinionOutcome carries no model identity, so an Ollama-produced opinion is recorded as a claude-code vote. The doc comment claims "swap-proof attribution … slot↔model is unambiguous by construction" — true for the tie-break order swap, false for in-slot provider fallback.
Those votes become reviewer_vote audit events and feed recordRoutingShadow and scoreJudgmentAgreement. This directly poisons the input data of #8229 (evidence-weighted reviewer routing). parsedReviewModelIds (:569-571) already has the correct value from the diagnostics.
Deliverables
Tests (must fail against current main)
Related
Summary
When Claude is unavailable, reviews fall back to local Ollama — which is given a prompt far larger than its context window with no
num_ctxset, silently truncates it, and is then held to identical blocker authority as Claude. Nothing in the verdict, the public comment, or the finding says which model actually decided. Reviewer votes are additionally attributed to the primary model even when the fallback produced the review.1. The fallback is given a prompt it cannot fit
The review call sends up to 120,000 chars of diff plus an aggregate context budget of up to 240,000 chars (~60k+ tokens) —
src/services/ai-review.ts:1483-1523,:1178,:1124.The Ollama chat call (
src/selfhost/ai.ts:374-402) setsmax_tokens,temperatureandresponse_formatbut noproviderOptions/num_ctx. The only placenum_ctxis ever set in the codebase is the vision call (src/queue/processors.ts:8808) — even thoughAiRunOptions.providerOptionswas added (#4327/#4335) for exactly this.Ollama silently left-truncates input beyond
num_ctx(server/model default, typically 4k–8k tokens) and answers confidently over whatever survived — which is the tail of the prompt (context sections), not the diff.2. The fallback carries full blocker authority
A fallback-produced review is held to the same schema validation and the same
confidencesemantics againstaiReviewCloseConfidence. Allama3.1-class self-reported0.95is not the same instrument as Claude's, but the gate cannot tell them apart. Combined with #9476 (Claude timeouts retrying 3×), a Claude outage means every review is decided this way.Only
loopover_ai_review_model_fallback_total, the diagnostics, andparsedReviewModelIdsrecord that a fallback happened — the verdict, the public copy ("AI review flagged a likely critical defect") and the finding itself are silent.3. Votes are attributed to the wrong model
src/services/ai-review.ts:2935-2936,:3001:runWorkersOpinioniterates[primary, fallback]internally andReviewerOpinionOutcomecarries no model identity, so an Ollama-produced opinion is recorded as aclaude-codevote. The doc comment claims "swap-proof attribution … slot↔model is unambiguous by construction" — true for the tie-break order swap, false for in-slot provider fallback.Those votes become
reviewer_voteaudit events and feedrecordRoutingShadowandscoreJudgmentAgreement. This directly poisons the input data of #8229 (evidence-weighted reviewer routing).parsedReviewModelIds(:569-571) already has the correct value from the diagnostics.Deliverables
providerOptions: { num_ctx }for the Ollama review path, sized to the deployed model; fail loudly rather than silently truncate when the prompt does not fit (shrink deliberately, or refuse and hold).hold_for_reviewrather than a one-shot close. State the chosen policy explicitly in the gate documentation.runWorkersOpinion(or derive it from the laststatus: "parsed"diagnostic) and use it forreviewerVotes.reviewer_voterows are unreliable — decide whether calibration: evidence-weighted reviewer routing (flag-gated, report-first) #8229's calibration should discard votes recorded before this fix.Tests (must fail against current main)
num_ctxis set and the prompt fits, or that the call is refused.reviewerVotes[].reviewerequals the model that actually produced the opinion when the fallback ran.Related