Route review threads by who spoke last instead of one model call each - #193
Conversation
…he author's reply
Pull request dashboard statusMerged · refreshed 2026-07-30 13:08 UTC Status above doesn't look right?
|
There was a problem hiding this comment.
Pull request overview
Reworks unresolved review-thread routing to reduce model calls through deterministic last-speaker ownership.
Changes:
- Routes reviewer-last threads directly to authors.
- Batches author-reply and short-praise classification.
- Adds regression tests for routing and praise handling.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
classification.py |
Implements deterministic routing and binary classifiers. |
dashboard.py |
Removes the obsolete classifier constant import. |
test_top_level_actions.py |
Tests last-speaker and praise routing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
.github/scripts/pull-request-dashboard/classification.py:1037
- This assumes
comments[-1]is who spoke last, but review-thread comments are sorted byupdatedAtfirst (dashboard.py:644,652). Editing an older comment therefore moves it to the end: for example, editing the reviewer's original request after an author reply routes the thread back to the author, even though the author actually spoke last. Preserve creation order (or carry an explicit last-created-comment role) before applying this deterministic rule.
elif comments and comments[-1].get("actor_role") == "author":
.github/scripts/pull-request-dashboard/classification.py:1031
- Only one trailing praise comment is removed. A thread such as
reviewer: "Nice"followed byreviewer: "LGTM"is entirely praise, but after this singlepop()the earlier reviewer comment remains and the thread is routed to the author, contradicting the stated “thread that is nothing but praise needs nobody” behavior. Strip/classify trailing praise repeatedly (with coverage for multiple praise comments) before choosing the fallback speaker.
This issue also appears on line 1037 of the same file.
dropped = discussion["discussion_id"] in ignored
if dropped:
comments.pop()
if dropped and not comments:
|
Both low-confidence findings are mechanically right; neither fires on the 441 pull request corpus. 2b57543. Only one trailing praise comment is removed. Correct, and my wording was the thing at fault. Comments are ordered by 320 tests pass. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
.github/scripts/pull-request-dashboard/classification.py:1028
comments[-1]is not necessarily the participant who posted last.group_review_threadsstoresupdatedAtas each comment's timestamp and sorts on it (dashboard.py:643-652), so editing an older reviewer comment after an author reply moves that reviewer comment to the end. This new last-speaker routing then sends the thread back to the author (and may run praise detection on the edited old comment) even though the author's reply is still the final comment in thread order. Preserve creation/thread order for this decision; keep update time separately if it is needed for activity tracking.
comments = list(discussion.get("comments") or [])
.github/scripts/pull-request-dashboard/classification.py:1299
- When the CLI exits nonzero but still returns a valid, non-duplicate verdict, this
elsealso reports that no valid verdict was returned. That makes failure diagnostics contradictory; only append this reason whenvalid_responseis false, matching the existing top-level batch parser.
else:
reasons.append(
"Copilot CLI did not return a valid verdict for this discussion_id"
)
|
Contradictory failure diagnostics. Correct, and it was a divergence from the parser this was modelled on. Comment ordering by 322 tests pass. |
… cap" This reverts commit 339be6a.
|
Reverting 339be6a in 58e1d06, and correcting what I wrote about it. I claimed the cap "turns a slow refresh into no refresh at all". That is wrong. That undermines the change. The remaining harm is a cycle or two of staleness on a pull request nobody has: the largest live-thread count in the 441 pull request corpus is 12. Against that, So the cap behaves as it did before this pull request for every path, and 325 tests pass. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
.github/scripts/pull-request-dashboard/classification.py:1046
- Dropping praise only from this local list leaves the original thread unchanged.
build_review_thread_pending_actionslater readsdiscussion["comments"][-1](dashboard.py:1024), so it records the praise comment's timestamp aswaiting_sinceeven though ownership fell back to the preceding comment. A later “LGTM” therefore resets the displayed wait age for an older pending request/reply. Carry the effective fallback timestamp into pending-action construction (and cover both reviewer→praise and author→praise cases).
dropped = discussion["discussion_id"] in ignored
if dropped:
comments.pop()
|
Correct, and it is live rather than theoretical. 5032f06. Dropping the praise comment only from the local list meant The first understates that wait by 69 days. That is the worst direction for this field, because wait age is what makes a stale request visible — a reviewer dropping an
Both cases you named are covered: 327 tests pass. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
.github/scripts/pull-request-dashboard/dashboard.py:1215
- This timestamp comparison no longer reflects thread order.
group_review_threadsnow intentionally orders comments bycreatedAt, but each comment'stimestampis stillupdatedAt; if an earlier request is edited after the deciding reply, it is skipped here, while a later praise comment with an older timestamp can be counted as an open reviewer. That defeats the new goal of excluding only the ignored final praise and can assignopen_threadto the praising reviewer or omit the actual requester. Carry an explicit “ignored final comment”/deciding-position marker through the classification and pending-action records, then filter by position rather than comparing timestamps.
if decided_at and (comment.get("timestamp") or "") > decided_at:
continue
|
Correct, and it is a regression I introduced two commits ago. a8d9314. The premise checks out: It is also worse than described, because it does not need praise to trigger. Editing is normal rather than exotic, and 2 threads already have the ordering mismatch that makes this bite. Before this pull request Fixed positionally as you suggested, and the position was already known:
328 tests pass, and corpus routing is unchanged at 262 author / 77 reviewer / 2 none. |
Review threads cost one model call each today, which is the bulk of the dashboard's model spend. This routes them by who spoke last instead: an unresolved thread whose last comment is not the author's is the author's to answer, decided without a model. Only the author's own last word goes to a model, to tell a promise of future work from a finished reply.
A short reviewer comment is checked by a praise binary. Praise is dropped rather than treated as settling the thread, and routing falls back to the comment before it: if that was another reviewer's request, the thread stays with the author; if it was the author's own reply, the deferral binary decides. Only the last comment is checked, so a thread needs nobody exactly when its single comment is praise. Of the four threads judged praise in the corpus, two are lone comments where the two readings agree, and two have earlier comments where they differ.
Fixes #152, which is the case that motivated this: four comments, none of them the author's, where the per-thread model answered
noneand the pull request dropped off the dashboard. The last-speaker rule routes it to the author with no model involved. #151 is top-level review feedback and is not touched here; it needs the top-level classifier swapped, which is separate work with its own evaluation.On a 441 pull request corpus this replaces 341 per-thread calls with about 87 batched ones, and 134 of the 144 pull requests with a live thread keep the same thread-level ownership. Of the ten that move, six now ask the author, including #152. The other four hand off to reviewers because the deferral binary reads the author's last word as complete: an offer waiting on a reviewer to answer, a pushback, a handoff to a third party, and a discussion moved to Slack.
Whole-pull-request route agreement is not measured here. The replay corpus was collected without
commits, socompute_factsnever sees pushes or CI androute_prcannot be evaluated; #192 tracks closing that gap.