You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When the grouping LLM call fails, OCR falls back to one review subtask per file and reports it with a single stdout line (internal/agent/grouping.go:95-99):
No run through the GitHub Action can see that line. action.yml:668 always passes --audience agent, which routes [ocr] progress to io.Discard through stdout.Quiet() (shared.go:338-346, internal/stdout/stdout.go:33-37). The flag is not an action input, so no user can change it.
Nothing else states what happened:
There is no grouping.failed telemetry event to pair with the existing grouping.skipped (grouping.go:158-165).
The --json result has no field naming the strategy that produced groups.
The groups array narrows it and cannot name it. A grouping failure, a GroupingPerFile plan, and a template with no GROUPING_TASK all emit one group per file, labelled with its path (grouping.go:373-382). An enforceGroupTokenBudget split is the one case the labels do separate, because it appends " (split: <path>)" (grouping.go:351).
retry_report does carry the failed grouping request. Its listing rule lists any logical request that retried, saw an error, or did not succeed, whatever the task type (internal/llm/retry_report.go:535-542). In our incident it was the only trace that named the failure. It proves the call failed, and it does not say what OCR then decided.
The fallback is expensive, and a caller pays for all of it before it can react. Our run turned a 188-file change set into 188 review conversations, and each one is entitled to --timeout minutes and MAX_REVIEW_ROUNDS rounds. The ocr review call alone took 47m40s inside a timeout-minutes: 50 job, so under 2m20s was left for every other step, and it still exited 0. The result JSON arrives too late to help, because shared.go:696-708 writes it after the agent run returns.
Proposed Solution
Add a policy flag, --on-grouping-failure=fallback|abort. With abort, review stops before it dispatches any subtask, and it exits non-zero. Keep fallback as the default so current behaviour does not change. --max-tokens-budget shows that a flag can already carry an exit-code contract in its help text (cmd/opencodereview/shared_flags.go:58), so the surface exists. Its own rule points the other way: it exits 0 on a partial run.
Report the partition in the result JSON. A grouping block would carry the strategy behind groups (llm, bundle_all, per_file) and, when the LLM call failed, the reason. GroupingStrategy.String() already produces those names. This one is for diagnosis after a run, not for prevention.
Emit a grouping.failed telemetry event next to grouping.skipped, with the same attribute set plus the error class.
Item 1 is the one we need. A CI job cannot stop a fan-out it never learns about, and a non-zero exit is all our workflow needs to fail fast.
Alternatives Considered
Read the stdout line. It does not survive --audience agent, which the action always passes, and a log string carries no stability contract.
Write the grouping-failure line to stderr. OCR already sends [ocr] diagnostics there whatever the audience (internal/telemetry/events.go:125, cmd/opencodereview/output.go:88), and the action captures stderr to a file and prints it, so a one-line change at grouping.go:97 would surface it. It still leaves a workflow gate parsing a log line, and it still pays for the whole fan-out first.
Infer the failure from retry_report. It reports the request, not the decision: a failed grouping_task entry proves the call failed, and it still leaves the resulting partition unstated. It also arrives only after the run, which is the same problem the result JSON has.
Cancel the CI run from the workflow. The workflow cannot see the fallback, so there is nothing to trigger the cancel on. With a non-zero exit there is also nothing left to cancel.
Set --max-tokens-budget low enough to cut a per-file fan-out short. That truncates coverage instead of preventing a partition we did not ask for. The undispatched files land in coverage.failed as budget, so status reads partial and our gate does catch it, but only after we pay for the part that ran. It is also out of reach from the action today, since feat(action): expose --effort and --max-tokens-budget as action inputs #1147 is still open on exposing it as an input.
Affected Area
Review Agent / LLM interaction
CLI / Commands
Telemetry / Observability
Additional Context
Read on v1.11.1, the version this run used: the workflow pins alibaba/open-code-review@v1.11.1 and passes ocr_version: "1.11.1". Commit 8d023aa is the same commit as that tag, so every line number above matches the version that produced the failure. We run OCR through the GitHub Action on a label, with a gate step that reads the result JSON and fails the job unless the review finished. That gate is why the fallback matters to us: today it can turn a degraded review into a green check.
The run this comes from
The run is on a private GitHub organisation, so there is no shareable URL. The uploaded ocr-result.json holds the whole story. OCR reviewed 188 files as 188 groups of one file each, which is the per-file fallback. It cost 21.4M tokens and 47m40s and produced 5 comments, and status reads complete, so our gate step passed a degraded review. The retry_report records exactly one failure out of 1357 requests:
That block is the only trace that names the failure. The 188 single-file groups show the shape and not the cause. It proves the grouping call failed, and it does not say that the partition became per-file. Whether the grouping call should also retry or carry its own deadline is a separate question, and we are not raising it here.
Problem Statement
When the grouping LLM call fails, OCR falls back to one review subtask per file and reports it with a single stdout line (
internal/agent/grouping.go:95-99):No run through the GitHub Action can see that line.
action.yml:668always passes--audience agent, which routes[ocr]progress toio.Discardthroughstdout.Quiet()(shared.go:338-346,internal/stdout/stdout.go:33-37). The flag is not an action input, so no user can change it.Nothing else states what happened:
grouping.failedtelemetry event to pair with the existinggrouping.skipped(grouping.go:158-165).--jsonresult has no field naming the strategy that producedgroups.groupsarray narrows it and cannot name it. A grouping failure, aGroupingPerFileplan, and a template with noGROUPING_TASKall emit one group per file, labelled with its path (grouping.go:373-382). AnenforceGroupTokenBudgetsplit is the one case the labels do separate, because it appends" (split: <path>)"(grouping.go:351).retry_reportdoes carry the failed grouping request. Its listing rule lists any logical request that retried, saw an error, or did not succeed, whatever the task type (internal/llm/retry_report.go:535-542). In our incident it was the only trace that named the failure. It proves the call failed, and it does not say what OCR then decided.The fallback is expensive, and a caller pays for all of it before it can react. Our run turned a 188-file change set into 188 review conversations, and each one is entitled to
--timeoutminutes andMAX_REVIEW_ROUNDSrounds. Theocr reviewcall alone took 47m40s inside atimeout-minutes: 50job, so under 2m20s was left for every other step, and it still exited 0. The result JSON arrives too late to help, becauseshared.go:696-708writes it after the agent run returns.Proposed Solution
--on-grouping-failure=fallback|abort. Withabort, review stops before it dispatches any subtask, and it exits non-zero. Keepfallbackas the default so current behaviour does not change.--max-tokens-budgetshows that a flag can already carry an exit-code contract in its help text (cmd/opencodereview/shared_flags.go:58), so the surface exists. Its own rule points the other way: it exits 0 on a partial run.groupingblock would carry the strategy behindgroups(llm,bundle_all,per_file) and, when the LLM call failed, the reason.GroupingStrategy.String()already produces those names. This one is for diagnosis after a run, not for prevention.grouping.failedtelemetry event next togrouping.skipped, with the same attribute set plus the error class.Item 1 is the one we need. A CI job cannot stop a fan-out it never learns about, and a non-zero exit is all our workflow needs to fail fast.
Alternatives Considered
--audience agent, which the action always passes, and a log string carries no stability contract.[ocr]diagnostics there whatever the audience (internal/telemetry/events.go:125,cmd/opencodereview/output.go:88), and the action captures stderr to a file and prints it, so a one-line change atgrouping.go:97would surface it. It still leaves a workflow gate parsing a log line, and it still pays for the whole fan-out first.retry_report. It reports the request, not the decision: a failedgrouping_taskentry proves the call failed, and it still leaves the resulting partition unstated. It also arrives only after the run, which is the same problem the result JSON has.--max-tokens-budgetlow enough to cut a per-file fan-out short. That truncates coverage instead of preventing a partition we did not ask for. The undispatched files land incoverage.failedasbudget, sostatusreadspartialand our gate does catch it, but only after we pay for the part that ran. It is also out of reach from the action today, since feat(action): expose --effort and --max-tokens-budget as action inputs #1147 is still open on exposing it as an input.Affected Area
Additional Context
Read on
v1.11.1, the version this run used: the workflow pinsalibaba/open-code-review@v1.11.1and passesocr_version: "1.11.1". Commit8d023aais the same commit as that tag, so every line number above matches the version that produced the failure. We run OCR through the GitHub Action on a label, with a gate step that reads the result JSON and fails the job unless the review finished. That gate is why the fallback matters to us: today it can turn a degraded review into a green check.The run this comes from
The run is on a private GitHub organisation, so there is no shareable URL. The uploaded
ocr-result.jsonholds the whole story. OCR reviewed 188 files as 188 groups of one file each, which is the per-file fallback. It cost 21.4M tokens and 47m40s and produced 5 comments, andstatusreadscomplete, so our gate step passed a degraded review. Theretry_reportrecords exactly one failure out of 1357 requests:{ "model": "qwen/qwen3.8-max", "file_path": "__grouping__", "task_type": "grouping_task", "request_no": 1, "outcome": "failed", "attempts": [ { "attempt": 1, "outcome": "error", "error_class": "timeout", "failure_phase": "context", "status_code": 200, "duration_to_headers_ms": 1308 } ] }That block is the only trace that names the failure. The 188 single-file groups show the shape and not the cause. It proves the grouping call failed, and it does not say that the partition became per-file. Whether the grouping call should also retry or carry its own deadline is a separate question, and we are not raising it here.