Skip to content

tend-mention: an inline reply's synthetic review container starts a full session — the empty-body skip only covers APPROVED #866

Description

@cargo-affected-bot

Problem

When the bot posts an inline reply on a PR, GitHub wraps it in a synthetic review object — empty body, state COMMENTED, authored by the bot. That object fires pull_request_review, which relays into a full tend-mention session. The session reads the whole PR, correctly concludes there is nothing to action, and exits. It is a guaranteed no-op: the review has no body, and its only inline comment is a reply the bot itself just wrote.

#828 named these objects and #835 taught the review skill's three review-record guards to ignore them. The tend-mention verify gate is a fourth site that still lets them through, because its empty-body skip is conditioned on approved:

if [ "$KIND" = "pull_request_review" ] \
   && [ "$REVIEW_AUTHOR" = "<<cfg.bot_name>>" ] \
   && [ "$REVIEW_STATE" = "approved" ] \
   && [ -z "$COMMENT_BODY" ]; then

generator/src/tend/templates/mention.yaml.j2#L246-L252. The comment above it explains the approved clause deliberately: "a bot CHANGES_REQUESTED/COMMENTED review keeps actionable signal". That is right for a real bot COMMENTED review — but a reply container is COMMENTED with an empty body and carries no signal at all, so it lands on the wrong side of the gate.

Occurrences

Four synthetic containers on max-sixty/cargo-affected across two windows, each one starting a relay run plus a dispatch session:

Container PR Submitted Sessions it started
4851851883 #54 2026-08-04T07:46:08Z 3088913724630889231291
4851852052 #54 2026-08-04T07:46:09Z 3088913895930889242373 (cancelled)

(The two 2026-08-04 rows are paired by submission order — both containers landed within a second of each other and both relay runs started within a second of each other, so the individual mapping is inferred rather than read off the payloads. The 2026-08-06 rows are exact: each dispatch session names its triggering review ID in the prompt.)
| 4872154023 | #77 | 2026-08-06T07:31:34Z | 3108125995531081269852, $0.50 / 11 turns |
| 4872195438 | #77 | 2026-08-06T07:36:32Z | 3108158338331081591885, $0.49 / 8 turns |

Both 2026-08-06 sessions reasoned their way to the right answer and posted nothing — 31081269852 wrote "The triggering review has an empty body and its only inline comment is a reply I authored as the PR author — not a request directed at me". That is the correct conclusion, reached at $0.99 for the pair, and it is reachable from the review record alone without starting a session. On a busy PR the multiplier grows: every inline reply the bot posts produces one.

Proposed fix

Skip when the review is a reply container rather than a submission. The review body being empty plus every attached inline comment being a bot-authored reply (in_reply_to_id != null) is a precise discriminator — it cannot match a real review, because a review that only replies to existing threads is not a review:

if [ "$KIND" = "pull_request_review" ] \
   && [ "$REVIEW_AUTHOR" = "<<cfg.bot_name>>" ] \
   && [ -z "$COMMENT_BODY" ] \
   && [ "$REVIEW_STATE" != "changes_requested" ]; then
  # A submission has at least one non-reply inline comment; a synthetic reply
  # container has none. `[] | all` is vacuously true, which also covers the
  # empty-body APPROVED case the old gate handled.
  if gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PAYLOAD_PR/reviews/$PAYLOAD_ID/comments" \
       --jq '[.[] | .in_reply_to_id != null and (.user.login == "<<cfg.bot_name>>")] | all'; then
    echo "should_run=false" >> "$GITHUB_OUTPUT"
    exit 0
  fi
fi

This subsumes the current approved branch rather than sitting beside it, and preserves #166's actionable bot self-reviews: those carry either a body or standalone inline findings, and fail the all test either way. A human reply creates a container too, and user.login keeps that firing.

One caveat I could not settle from the API alone: the /reviews/{id}/comments call adds a request to every review-triggered verify job, on the path that currently short-circuits cheapest. The verify job already makes that exact call a few lines later for the @bot mention scan (L254-L262), so ordering the new check after it and reusing the response would avoid a second round-trip — I have not written that version, since the merge depends on how you would rather structure the gate.

Filed from tend-review-runs on max-sixty/cargo-affected under the standing cross-repo exception in that repo's running-tend overlay.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions