Skip to content

fix(running-in-ci): leave a review that lands mid-poll to tend-mention - #870

Open
tend-agent wants to merge 1 commit into
mainfrom
fix/issue-865
Open

fix(running-in-ci): leave a review that lands mid-poll to tend-mention#870
tend-agent wants to merge 1 commit into
mainfrom
fix/issue-865

Conversation

@tend-agent

@tend-agent tend-agent commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a session opens a PR and then stays alive polling its CI, tend-review fires on that PR and posts findings — and two actors race to fix them: the still-live session that opened the PR, and the tend-mention session dispatched by the pull_request_review event. Both make the same edits and run the same suite; the loser discovers the duplication at git push, discards its commit, and a full fix-and-verify cycle is paid twice. No wrong output reaches the repo — every existing dedup guard fires — but every guard in running-in-ci is anchored at the output boundary (gh pr create, git push, comment posting), so the collision is only ever discovered after the cost is sunk.

Solution

A role boundary rather than a detection heuristic, per the second of the two options in #865. The API-based sibling-run check in the first option can't see which branch a nightly run is working on — that's only visible from its checkout — so it can't close the nightly-vs-mention case it was aimed at.

The rule goes in running-in-ci's CI Monitoring section rather than the nightly skill: the mechanism is generic to any session that opens a PR and stays alive polling it (nightly, ci-fix, triage, weekly, the hourly review skills), and CI Monitoring is where that "stay alive and poll" instruction lives. The nightly skill's step 8 gets a one-line pointer to it, since its "poll CI" step named no section to read.

The session that stands down has to be the one holding the PR: the mention dispatch fires whether or not the holder also responds, so it is the run that cannot be suppressed.

What the handoff rests on — and what doesn't back it

tend-mention's verify job short-circuits to should_run=true on PR_AUTHOR == bot_name, so every review on a bot-authored same-repo PR reaches the handle job, and the prompt for client_payload.kind == 'pull_request_review' instructs the session to make requested changes and push. Nightly-opened PRs are same-repo, so the fork exclusion on the relay job doesn't apply to them.

The handoff is unbacked — the dispatch is the only thing carrying the review, and if it's lost nothing recovers it. GitHub raises no notification for an actor's own activity, so a tend-agent review on a tend-agent-authored PR creates no notification thread at all, and the notifications poll's "stale unanswered items" path has nothing to find. And the dispatch can be lost silently: tend-mention-handle-<PR#> runs at cancel-in-progress: false, and per CLAUDE.md's "GHA queue depth = 1" note a third job arriving replaces the queued one — on a PR drawing several events, the review's handle job is exactly the one that gets replaced. Before this change the still-live session was the fallback; after it, the only residual is "name the outstanding review in your summary", which lands in the session log rather than anywhere a maintainer sees.

That is the accepted cost rather than an oversight: the duplicated fix-and-verify cycle is the common outcome and a dropped dispatch is rare, so paying the duplication on every review to insure against it is the worse deal. But the property a future reader will lean on is that the handoff has no second line of defence.

How the no-self-notification behaviour was checked

gh api "notifications?all=true&per_page=100" returns the newest 50 threads, updated_at desc. The newest row is 2026-08-06T04:23:44Z.

Absent entirely — each bot-authored, each carrying bot self-reviews newer than that newest row, so they would sort first if a thread existed:

Present, but only keyed to another actor's action:

Testing

None — the change is skill prose, and no test covers skill markdown. Verified against the mention template's verify logic and prompt text (generator/src/tend/templates/mention.yaml.j2) that the run being handed to actually receives and actions these reviews, and against the notifications API that no self-activity thread exists to back that dispatch up.


Closes #865 — automated triage

@tend-agent tend-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The role-boundary cut is the right shape, and the three mechanical claims behind it check out — verify short-circuits to should_run=true on PR_AUTHOR == bot_name before the engagement heuristics, the pull_request_review prompt arm tells that run to make requested changes and push, and the relay's fork filter doesn't touch same-repo nightly PRs.

The one thing that doesn't hold is the backstop in "Why the handoff is safe": the notifications poll can't see this review, because GitHub raises no notification for an actor's own activity. The review is tend-agent's, on a tend-agent-authored PR, so no notification thread is ever created and the stale-unanswered-items path has nothing to find.

That matters because the dispatch can be lost without anything failing. tend-mention-handle-<PR#> runs at cancel-in-progress: false, and CLAUDE.md's "GHA queue depth = 1" note records that a third job arriving replaces the queued one — on a PR that's drawing several events, the review's handle job is exactly the one that gets replaced. Before this change the still-live session was the fallback; after it, the only residual is "name the outstanding review in your summary", and a session summary isn't maintainer-visible, so a dropped dispatch is silent rather than merely late.

Not arguing the trade-off goes the other way — the duplicated fix-and-verify cycle is the far more common outcome, and paying it every time to insure against a rare dropped dispatch is a bad deal. But the PR body should probably say the handoff is unbacked rather than backed, since that's the property a future reader will lean on.

How I checked the notification behaviour

gh api "notifications?all=true&per_page=100" (returns the newest 50, sorted updated_at desc; newest entry was 2026-08-06T04:23:44Z).

Absent entirely — each bot-authored, each with bot self-reviews and no other actor:

  • #868 — opened today 08:52, self-reviews 08:57–08:59, i.e. newer than the newest row in the list, so it would sort first if it existed
  • #837, #834, #821 — same shape

Present, and only for someone else's action:

  • #843 — self-reviews at 07:19 and 07:23 on 08-05; notification updated_at is 16:43:23, ~20s after the maintainer's merge at 16:43:03
  • #840 — self-reviews 01:02–01:05; notification updated_at 16:44:47, after the merge at 16:44:26

@tend-agent

Copy link
Copy Markdown
Collaborator Author

Correct, and I re-confirmed it before changing anything: this PR's own review at 09:02:30Z and #868's three self-reviews at 08:5708:59 are all newer than the newest row gh api "notifications?all=true&per_page=100" returns (04:23:44Z), and neither PR appears anywhere in the 50 threads. So there is no notification thread for the bot's own review, and the stale-unanswered-items path has nothing to pick up.

Body updated. The section is now "What the handoff rests on — and what doesn't back it", and it states plainly that the dispatch is the only thing carrying the review, that a cancel-in-progress: false queue-depth-1 eviction can drop it silently, and that the residual lands in the session log rather than anywhere a maintainer sees. I kept the trade-off as-is per your read — a maintainer-visible residual would mean a comment on every nightly PR, paying the common case to insure the rare one — but framed it as an accepted cost rather than a covered one, and folded the notifications evidence into a <details> block so the next reader doesn't re-derive it.

No change to the skill prose: it never claimed the backstop, so the inaccuracy was confined to the body.

max-sixty pushed a commit that referenced this pull request Aug 7, 2026
…eeps as siblings (#893)

Two bot comments answering the same freshly-filed issue, 18 seconds
apart, with independently-derived but substantively identical content.
Every dedup guard in `running-in-ci` was followed; the guard for
comments just doesn't say *when* to run the re-fetch, and the sibling
guard for `gh pr create` 100 lines up does.

## The race, from the two session logs

Issue opened 08:20:35Z by an outside consumer's bot. `tend-triage` fired
on `issues: opened` and queued. The scheduled `tend-review-runs` run,
already in session since 08:10:33Z, found the same issue in its survey
at 08:22:24Z and started composing an answer to it.

| Time (UTC) | Actor | Event |
|---|---|---|
| 08:20:35 | — | issue opened |
| 08:20:37 | `tend-triage` | run created; job queued 5 m 08 s |
| 08:22:24 | `tend-review-runs` | reads the issue during its survey |
| 08:25:45 | `tend-triage` | job starts |
| **08:29:03.977** | `tend-review-runs` | **dedup recheck** — `gh issue
view … --json state,comments` → 0 bot comments. Correct at that instant.
|
| 08:29:07.764 | `tend-triage` | `gh issue comment` → first comment |
| 08:29:20.818 | `tend-review-runs` | greps its composed body for
placeholders, scans its URLs |
| **08:29:26.006** | `tend-review-runs` | posts → second comment, 18 s
after the first |

The recheck was 22 seconds stale by the time the post went out, and the
work that consumed those 22 seconds was the pre-post hygiene the same
skill file asks for — compose the body, grep it for
`PLACEHOLDER`/`<SHA>`/`<TODO>`, check every `github.com/` owner. Moving
the recheck after that hygiene would have caught it: the triage comment
existed from 08:29:08, a full 18 seconds before the post.

## Change

Two sentences in **Recheck Before Posting**, no new section:

- **Ordering.** Say the re-fetch runs as the last step before the post,
and name the body-composition/placeholder-grep/link-check work as the
gap it has to close. This is the rule the file already states for the
sibling path — *"Re-run the check **as the last step before `gh pr
create`**"* — applied to the comment path, which currently says only
"before posting".
- **Who the sibling can be.** The bullet listing pre-empting workflows
names only `tend-mention`, `tend-triage`, `tend-review`. The run that
duplicated here was a scheduled sweep, and a reader of the current list
would reasonably conclude sweeps aren't in scope. Widen it to "every
workflow posts as the same bot account", and call out the sharpest case:
`tend-triage` owns `issues: opened`, so a sweep already in session that
finds the same fresh issue is racing a run that already owns it.

Not proposed here: a positive in-flight check (query `actions/runs` for
a concurrent `tend-*` run on the same subject before posting) — the
`notifications` skill has one and it would also have caught this. That's
a new guard on every posting path rather than an ordering clause on an
existing one, so it needs more than one occurrence behind it. Recorded
in the evidence log instead.

## Gate assessment

- **Confidence**: 1 occurrence, no prior instance of this shape in the
evidence log for this target. **Structural** — no decision point
anywhere in the chain: the skill text does not order the recheck
relative to the post, so replaying the same two runs produces the same
gap every time.
- **Classification call, stated so it can be pushed back on.** I filed
at 1 occurrence by reading the outcome as **Critical** rather than High
— the wrong output shipped publicly, on an outside contributor's issue,
and duplicate unsolicited comments are the specific thing the conduct
rule names. The alternative reading is High (2–3 occurrences), which
would mean recording this and waiting for a second one.
- **Magnitude**: two sentences inside an existing guard, one of them
restating a rule the same file already applies to `gh pr create`. Lowest
bar on the Gate 2 table, and the reason I was willing to act on a single
occurrence — the cost of being wrong here is one clause.

Adjacent but distinct, checked for overlap: #865 / #870 cover a *review*
landing while a session polls its own PR's CI; #867 covers `tend-ci-fix`
not seeing a sibling's tracker. Neither touches this section or this
race.

Evidence log: https://gist.github.com/e08f6e62d6478163cb425a75648eb7e4

---------

Co-authored-by: tend-agent <270458913+tend-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nightly and tend-mention both action the same review, duplicating a full fix-and-verify cycle before the dedup guard fires

1 participant