Conversation
On a repository several SchemaBot deployments serve, an unscoped control command reaches all of them and each one that does not store the named apply stays quiet so only the owner replies (AZ-5). When no owner exists — a mistyped identifier, one from another repository, or one a database engine reported rather than SchemaBot — every deployment defers to an owner that never speaks, and the operator's command produces nothing at all. Retrying looks identical, so the loop can run for a long time. The aggregate leader now closes the gap. It waits out a grace period, re-reads the command comment's acknowledgment reaction — the one signal about the command every deployment can read (UX-2) — and answers only if nothing claimed it, marking the comment as it answers so a redelivery stays quiet. The reply names where identifiers that do resolve come from, since an engine's identifier is exactly what an operator is most likely to have pasted (UX-4). The follow-up is operator visibility, never a gate: an unavailable config, a client failure, or an unreadable reaction each leave the command as silent as it is without this path, and say so in the logs. Participants stay silent throughout, since several of them replying is the duplicate noise fan-out exists to avoid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The grace sleep currently runs inside a goSafe-tracked goroutine, which can unnecessarily extend shutdown/drain time on deploys by up to the grace duration.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves operator visibility for apply-scoped PR control commands (e.g., stop) on repositories served by multiple SchemaBot deployments by ensuring that someone replies when no deployment actually owns the referenced apply. It does this by having the aggregate leader re-check the command acknowledgment reaction after a grace period and posting a single “unclaimed command” reply if the command remained unclaimed, extending AZ-5’s enforcement as documented.
Changes:
- Add aggregate-leader follow-up logic to answer apply-scoped control commands that no deployment claims (reaction-based detection after a grace period).
- Add a new rendered GitHub comment template and tests for the unclaimed-command reply UX.
- Add GitHub client support and metrics wiring for listing comment reactions; update invariants to reflect the new enforcement point.
File summaries
| File | Description |
|---|---|
| pkg/webhook/unclaimed_command.go | Implements aggregate-leader grace-period follow-up and single-reply behavior for unclaimed apply-scoped commands. |
| pkg/webhook/unclaimed_command_test.go | Adds coverage for leader/participant behavior and claimed vs unclaimed reaction cases. |
| pkg/webhook/templates/issue_comment.go | Introduces the unclaimed-command reply template rendered into PR comments. |
| pkg/webhook/templates/issue_comment_test.go | Verifies the rendered unclaimed-command reply content (UX-4 guidance included). |
| pkg/webhook/issue_comment.go | Centralizes the acknowledgment reaction string as commandAcknowledgmentReaction and documents its meaning (UX-2). |
| pkg/webhook/handler.go | Adds a test seam field to override the unclaimed-command grace duration. |
| pkg/webhook/control.go | Hooks the unclaimed-command leader follow-up into the “apply not found” silent fan-out path. |
| pkg/metrics/metrics.go | Adds a new GitHub operation constant for listing comment reactions. |
| pkg/github/rate_limit_metrics.go | Maps GET comment-reactions endpoint to the new metrics operation for rate-limit attribution. |
| pkg/github/client.go | Adds CommentHasReaction helper to cheaply detect whether a comment has a given reaction. |
| docs/invariants.md | Updates AZ-5 to include the leader’s unclaimed-command follow-up as an explicit enforcement point. |
Review details
- Files reviewed: 11/11 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… in it The leader waited out the grace inside tracked webhook work, so a shutdown drain sat through the whole period before finishing — for a reply that is operator visibility and never a gate. The wait is now a timer and only the answer is tracked work, which is the late-timer class the handler already uses for the delayed aggregate re-fold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On a repository several SchemaBot deployments serve, an unscoped control command reaches all of them, and each one that does not store the named apply stays quiet so only the owner replies (AZ-5). That is right whenever an owner exists.
When none does — a mistyped identifier, one from another repository, or one a database engine reported rather than SchemaBot — every deployment defers to an owner that never speaks. The operator's command produces nothing at all, and a retry looks exactly the same, so the loop can run for a long time before anyone concludes the identifier itself was wrong.
Before — no deployment stores the apply, so no deployment answers:
After — the leader re-reads the acknowledgment reaction and answers if nothing claimed it:
The acknowledgment reaction is what resolves it: it is the one signal about the command that every deployment can read, and per UX-2 a deployment adds it only once it has decided the command is its work. The leader marks the comment as it answers, both because answering is acting on the command and so a redelivered webhook sees the claim and stays quiet rather than posting a second copy.
Only the leader does this — several participants replying is the duplicate noise fan-out exists to remove. The reply is operator visibility, never a gate: an unavailable server config, a client failure, or an unreadable reaction each leave the command exactly as silent as it is today, and say so in the logs.
The message names where identifiers that do resolve come from, since an engine's own apply identifier is the thing an operator is most likely to have pasted (UX-4).
Invariants: extends AZ-5's enforcement — the registry entry and its
Enforced:line are updated in this PR to name the leader's follow-up alongside the existing unowned-command policy. Upholds UX-2 (the reaction still means a deployment has committed to acting; answering is the act) and UX-4. Nothing is weakened: no gate, lease, or apply state is read or written on this path.Reply to a command no deployment claimed
Apply:
apply-49ea5a453e9a4f18| Environment:productionRequested by: @Someone
No SchemaBot on this repository is driving a schema change with this identifier, so
startacted on nothing.Apply identifiers appear on the schema change comments SchemaBot posts to this pull request, and in
schemabot status -e production. Identifiers reported by a database engine are its own and are not accepted here.This pull request was written by Claude Code (Claude Opus 5).