feat(agent-actions): generalize the review-nag cooldown into a per-command rate limit#2637
Merged
Merged
Conversation
|
Important 🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪 🔍 Gittensory is reviewing…AI analysis is in progress. This comment will update when the review is complete. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟪 Reviewing |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2637 +/- ##
=======================================
Coverage 96.09% 96.10%
=======================================
Files 237 237
Lines 26482 26517 +35
Branches 9606 9617 +11
=======================================
+ Hits 25449 25484 +35
Misses 424 424
Partials 609 609
🚀 New features to boost your workflow:
|
JSONbored
force-pushed
the
feat/gittensory-command-rate-limit
branch
from
July 2, 2026 23:12
fb11590 to
9668fcc
Compare
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 0f26200 | Commit Preview URL Branch Preview URL |
Jul 03 2026, 01:18 AM |
JSONbored
force-pushed
the
feat/gittensory-command-rate-limit
branch
from
July 3, 2026 00:05
9668fcc to
fbc2e60
Compare
…mmand rate limit
The only throttle on the @gittensory command surface was maybeThrottleReviewNagPing,
deliberately narrow (thread's own author only, review-nag policy only). Every
other command in the catalog -- ask, preflight, blockers, packet, the maintainer
queue-digest commands, etc. -- had no cooldown at all, and most dispatch to
real, AI-cost-bearing orchestrator calls a careless or malicious actor could
spam dozens of times on the same thread.
Adds maybeThrottleGittensoryCommand, generalizing review-nag's audit-ledger
counting pattern (countRecentAuditEventsForActorAndTarget) to any command --
keyed by (actor, command, targetKey) so each command gets its own independent
counter. Config-driven per repo via commandRateLimitPolicy ("off"/"hold"),
off by default so existing repos see no behavior change. AI-cost-bearing
commands (ask/blockers/preflight/reviewability/packet/duplicate-check/
next-action/repo-fit) get a tighter default limit than cheap, cache-only
commands. A rate-limited invocation gets a clear cooldown reply, never silence.
Independent of and complementary to review-nag's own narrower thread-author-
only, close-capable cooldown -- this generalizes the pattern without
replacing it.
Closes #2560.
…delivered webhooks Gate review: recording github_app.command_invocation on every processed delivery without deduplicating on deliveryId means a GitHub webhook redelivery (timeout/retry) increments the counter twice for one real invocation, and can incorrectly rate-limit it. Add hasAuditEventForDelivery and check it before counting; also cover the previously-untested .gittensory.yml parsing branches for the new settings.
…f re-dispatching it Gate review: the redelivery guard's `if (alreadySeen) return false` fell through to normal dispatch, so a redelivered issue_comment webhook (GitHub timeout/retry) would run the command a second time for one real invocation -- uncounted and unheld, even a cost-bearing one at commandRateLimitAiMaxPerWindow: 1. The original delivery already ran the command and posted its own answer, so short-circuit the replay entirely (no dispatch, no comment) instead of treating it as an under-threshold pass-through. Also renames a reused "installation-token" test literal that tripped the gate's generic_secret_assignment scanner (verified against the real scanner directly).
…d 50-row slice Gate review: hasAuditEventForDelivery matched deliveryId IN MEMORY over a `.limit(50)` slice with no ORDER BY. Once an actor accumulated more than 50 matching audit rows within the (short) redelivery window -- exactly the burst/spam conditions this idempotency check exists to handle -- the row carrying the original deliveryId could be excluded from that arbitrary slice, producing a false negative. A redelivered webhook would then read as a brand-new invocation and dispatch the command a second time instead of being suppressed. Pushes the deliveryId match into the SQL predicate itself via json_extract on metadataJson (mirroring countRecentDeadLettersByType's own json_extract usage in the same file), making it an exact match against every row in the window regardless of how many other rows exist for that actor/event/target. Adds a regression test seeding 60 noise rows plus the target row and confirms it's still found; mutation-verified against the original `.limit(50)` implementation to fail without the fix.
JSONbored
force-pushed
the
feat/gittensory-command-rate-limit
branch
from
July 3, 2026 01:15
c998012 to
0f26200
Compare
This was referenced Jul 3, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@gittensorymention-command surface wasmaybeThrottleReviewNagPing, deliberately narrow: it only fires for the thread's OWN author, and only whenreviewNagPolicyishold/close. Every other command — the public commands (help,ask,preflight,blockers,duplicate-check,miner-context,next-action,reviewability,repo-fit,packet) and the maintainer-only queue-digest commands — had no cooldown at all, and most dispatch to real, AI-cost-bearing orchestrator calls a careless or malicious actor could spam dozens of times on the same thread.maybeThrottleGittensoryCommand, generalizing review-nag's audit-ledger counting pattern (countRecentAuditEventsForActorAndTarget) to any command — keyed by(actor, command, targetKey)(the command name folded intotargetKey) so each command gets its own independent counter; pinginghelprepeatedly never counts againstask's own limit.commandRateLimitPolicy:off/hold), off by default so existing repos see no behavior change until they opt in. AI-cost-bearing commands (ask/blockers/preflight/reviewability/packet/duplicate-check/next-action/repo-fit, i.e. everything that reachesplanNextWork/explainBlockersWithAgent/preflightBranchWithAgent/preparePrPacketWithAgent) get a tighter default limit (5/24h) than cheap, cache-only commands (20/24h) —help,miner-context, and the maintainer queue-digest commands never touch the AI orchestrator at all.RepositorySettingstype +db/repositories.tsresolver +.gittensory.ymlparsing + OpenAPI schema +.gittensory.yml.exampledocs), matching the existing settings-wiring pattern review-nag itself established.Closes #2560.
Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlint(vianpm run test:ci)npm run db:migrations:check— migration 0094, contiguous, no new duplicatesnpm run typechecknpm run test:coveragelocally — 6 new tests covering off-by-default (25 invocations, zero tracked), under-threshold (normal dispatch continues), a cheap command crossing its threshold (cooldown reply, short-circuited), an AI-cost-bearing command using the tighter default, independent per-command counters (invoking one command never throttles a different one), and dry-run (held but no live comment). All mutation-tested (temporarily reverted the off-check and the AI/cheap tier-selection logic, confirmed the relevant tests fail, restored).npm run test:workers(vianpm run test:ci)npm run build:mcp(vianpm run test:ci)npm run test:mcp-pack(vianpm run test:ci)npm run ui:openapi:check— regeneratedapps/gittensory-ui/public/openapi.jsonvianpm run ui:openapiand committed itnpm run ui:lint(vianpm run test:ci)npm run ui:typecheck(vianpm run test:ci)npm run ui:build(vianpm run test:ci)npm audit --audit-level=moderateSafety
RepositorySettings's new fields added to the OpenAPI schema and regenerated.UI Evidencesection below with screenshots. — N/A, no UI change..gittensory.yml.exampledocuments the new settings.Notes