fix(review): read the AI-USAGE opt-in at render time, not at review start
The previous two commits put the label re-read in the webhook, at review start. That is too early to help: the review claims on the AI-REVIEW event and the re-read runs milliseconds later, while the reviewer's second click (AI-USAGE) is still a second or two away. It would have kept 404ing quietly if the path fix hadn't landed, and even fixed it caught nothing. Move the check to where the decision is actually used — just before the usage block is rendered, after the model has run. That is a minute or more after the trigger, by which time the label is there. Attribution is computed in the same branch, so a late opt-in still gets its per-comment token lines. `pr_has_label` goes through the existing gitea_get helper, which owns the /api/v1 prefix, so the path can't drift again. Any failure returns False and the payload's verdict stands: a review is never lost over a usage section. Reverts the webhook-side re-read from4ef62f2andaedea97. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B11e8TZZxJyzHW7jj7KWUN
This commit is contained in:
@@ -66,6 +66,8 @@ SHA_MARKER = "<!-- pragent:sha={sha} -->"
|
||||
_SHA_MARKER_RE = re.compile(r"<!-- pragent:sha=([0-9a-f]{7,40}) -->")
|
||||
|
||||
AI_REVIEW_LABEL = "AI-REVIEW"
|
||||
# Opt-in label for the token-usage block. Read at render time — see pr_has_label.
|
||||
AI_USAGE_LABEL = "AI-USAGE"
|
||||
SEVERITIES = ("critical", "high", "medium", "low")
|
||||
# Severity rank — higher = more severe. Used by `apply_repo_config` to drop
|
||||
# findings below `severity_threshold`. Critical=3, high=2, medium=1, low=0.
|
||||
@@ -156,6 +158,33 @@ def parse_text_blocks(content: list) -> str:
|
||||
return "\n".join(out).strip()
|
||||
|
||||
|
||||
def pr_has_label(api: str, repo: str, index: str, token: str, label: str) -> bool:
|
||||
"""True if the PR currently carries `label`. False on any failure.
|
||||
|
||||
Read at RENDER time, not at review start. A reviewer labels AI-REVIEW and
|
||||
AI-USAGE seconds apart; the review claims on the first event and the
|
||||
second is dropped by the in-flight dedupe, so the trigger payload never
|
||||
sees the opt-in. Re-reading when the review begins is no better — that is
|
||||
still milliseconds after the first click. Only a read taken once the
|
||||
review has finished (a minute or more later) reliably sees the label.
|
||||
"""
|
||||
try:
|
||||
code, raw = gitea_get(api, repo, f"issues/{index}/labels", token)
|
||||
if code >= 300:
|
||||
return False
|
||||
data = json.loads(raw.decode() or "[]")
|
||||
except Exception as e:
|
||||
print(f"pragent: could not re-read labels for {repo}#{index}: {e}",
|
||||
file=sys.stderr, flush=True)
|
||||
return False
|
||||
if not isinstance(data, list):
|
||||
return False
|
||||
return any(
|
||||
(isinstance(x, dict) and x.get("name") == label) or x == label
|
||||
for x in data
|
||||
)
|
||||
|
||||
|
||||
def _int_env(name: str, default: int) -> int:
|
||||
"""Read an int from the environment, falling back on anything unparseable.
|
||||
|
||||
@@ -1896,6 +1925,13 @@ def review_pr(
|
||||
file=sys.stderr, flush=True,
|
||||
)
|
||||
salvaged = salvage_summary(stdout)
|
||||
# The AI-USAGE opt-in is re-checked HERE, at render time: the label
|
||||
# is usually applied moments after AI-REVIEW, long after this
|
||||
# review was claimed and its trigger payload frozen.
|
||||
if not report_usage:
|
||||
report_usage = pr_has_label(api, repo, index, token, AI_USAGE_LABEL)
|
||||
if report_usage and usage and usage.get('output'):
|
||||
compute_attribution(findings, usage['output'])
|
||||
usage_section = _render_collapsible_usage(usage, model, config=config) if report_usage else ""
|
||||
post_review(api, repo, index, token, format_review_body(
|
||||
salvaged or "AI review produced no parseable output.",
|
||||
@@ -1936,6 +1972,13 @@ def review_pr(
|
||||
# for it.
|
||||
if report_usage and usage and usage.get("output"):
|
||||
compute_attribution(findings, usage["output"])
|
||||
# The AI-USAGE opt-in is re-checked HERE, at render time: the label
|
||||
# is usually applied moments after AI-REVIEW, long after this
|
||||
# review was claimed and its trigger payload frozen.
|
||||
if not report_usage:
|
||||
report_usage = pr_has_label(api, repo, index, token, AI_USAGE_LABEL)
|
||||
if report_usage and usage and usage.get('output'):
|
||||
compute_attribution(findings, usage['output'])
|
||||
usage_section = _render_collapsible_usage(usage, model, config=config) if report_usage else ""
|
||||
|
||||
# Anchor against the RAW diff, never the compressed one. Compression
|
||||
|
||||
@@ -39,7 +39,6 @@ import hmac
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from ai_review import review_pr
|
||||
@@ -177,48 +176,10 @@ def _release(key: tuple[str, str, str]) -> None:
|
||||
_inflight.discard(key)
|
||||
|
||||
|
||||
def _fetch_current_labels(repo: str, index: str) -> list:
|
||||
"""Re-read the PR's labels from the API. [] on any failure.
|
||||
|
||||
The webhook payload is a snapshot from the instant the event fired. When a
|
||||
PR is labelled AI-REVIEW first and AI-USAGE a moment later, the review is
|
||||
already claimed and running, so the second event is deduped and the usage
|
||||
opt-in is lost — the review posts without its usage block. Reading the
|
||||
labels again at review start closes that window.
|
||||
"""
|
||||
# GITEA_API is the bare host (no /api/v1) — every caller appends the
|
||||
# version prefix itself; see ai_review._get / _post.
|
||||
url = f"{GITEA_API}/api/v1/repos/{repo}/issues/{index}/labels"
|
||||
req = urllib.request.Request(url, headers={
|
||||
"Authorization": f"token {BOT_TOKEN}",
|
||||
"Accept": "application/json",
|
||||
})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return json.loads(resp.read().decode() or "[]")
|
||||
except Exception as e:
|
||||
print(f"pragent-webhook: could not re-read labels for {repo}#{index}: {e}", flush=True)
|
||||
return []
|
||||
|
||||
|
||||
def _run_review(
|
||||
key: tuple[str, str, str], title: str, body: str, report_usage: bool, base_ref: str
|
||||
) -> None:
|
||||
repo, index, sha = key
|
||||
# The AI-USAGE opt-in may have been applied *after* the label event that
|
||||
# triggered this review (labelling is two events, the review claims on the
|
||||
# first). Re-read the labels now so the later opt-in still counts.
|
||||
if not report_usage:
|
||||
report_usage = _labels_have(
|
||||
_fetch_current_labels(repo, index), AI_USAGE_LABEL
|
||||
)
|
||||
if report_usage:
|
||||
print(
|
||||
f"pragent-webhook: {repo}#{index} AI-USAGE found on re-read "
|
||||
f"(added after the trigger event)",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
try:
|
||||
with _review_slots:
|
||||
ok = review_pr(
|
||||
|
||||
Reference in New Issue
Block a user