Skip to content

feat(agent-actions): generalize the review-nag cooldown into a per-command rate limit#2637

Merged
JSONbored merged 4 commits into
mainfrom
feat/gittensory-command-rate-limit
Jul 3, 2026
Merged

feat(agent-actions): generalize the review-nag cooldown into a per-command rate limit#2637
JSONbored merged 4 commits into
mainfrom
feat/gittensory-command-rate-limit

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • The only throttle on the @gittensory mention-command surface was maybeThrottleReviewNagPing, deliberately narrow: it only fires for the thread's OWN author, and only when reviewNagPolicy is hold/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.
  • Adds maybeThrottleGittensoryCommand, generalizing review-nag's audit-ledger counting pattern (countRecentAuditEventsForActorAndTarget) to any command — keyed by (actor, command, targetKey) (the command name folded into targetKey) so each command gets its own independent counter; pinging help repeatedly never counts against ask's own limit.
  • Config-driven per repo (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 reaches planNextWork/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.
  • A rate-limited invocation gets a clear, public cooldown reply (reusing review-nag's own hold-notice pattern), 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.
  • Full config-as-code chain wired in one PR (migration + Drizzle schema + RepositorySettings type + db/repositories.ts resolver + .gittensory.yml parsing + OpenAPI schema + .gittensory.yml.example docs), matching the existing settings-wiring pattern review-nag itself established.

Closes #2560.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked an issue, or this is small enough that the summary explains why an issue is not needed.

Validation

  • git diff --check
  • npm run actionlint (via npm run test:ci)
  • npm run db:migrations:check — migration 0094, contiguous, no new duplicates
  • npm run typecheck
  • npm run test:coverage locally — 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 (via npm run test:ci)
  • npm run build:mcp (via npm run test:ci)
  • npm run test:mcp-pack (via npm run test:ci)
  • npm run ui:openapi:check — regenerated apps/gittensory-ui/public/openapi.json via npm run ui:openapi and committed it
  • npm run ui:lint (via npm run test:ci)
  • npm run ui:typecheck (via npm run test:ci)
  • npm run ui:build (via npm run test:ci)
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — N/A, no auth/session/CORS surface touched.
  • API/OpenAPI/MCP behavior is updated and tested where needed. — RepositorySettings's new fields added to the OpenAPI schema and regenerated.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. — N/A, no UI change.
  • Visible UI changes include a UI Evidence section below with screenshots. — N/A, no UI change.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs. — .gittensory.yml.example documents the new settings.

Notes

@dosubot dosubot Bot added the size:L label Jul 2, 2026
@loopover-orb

loopover-orb Bot commented Jul 2, 2026

Copy link
Copy Markdown

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

@loopover-orb loopover-orb Bot added gittensor gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. labels Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.10%. Comparing base (dfba160) to head (0f26200).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

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           
Files with missing lines Coverage Δ
src/db/repositories.ts 96.58% <100.00%> (+<0.01%) ⬆️
src/db/schema.ts 69.46% <ø> (ø)
src/github/commands.ts 97.67% <100.00%> (+<0.01%) ⬆️
src/openapi/schemas.ts 100.00% <ø> (ø)
src/queue/processors.ts 92.53% <100.00%> (+0.07%) ⬆️
src/signals/focus-manifest.ts 99.28% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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 added 4 commits July 2, 2026 18:12
…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
JSONbored force-pushed the feat/gittensory-command-rate-limit branch from c998012 to 0f26200 Compare July 3, 2026 01:15
@JSONbored
JSONbored merged commit 066398e into main Jul 3, 2026
12 checks passed
@JSONbored
JSONbored deleted the feat/gittensory-command-rate-limit branch July 3, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.

Development

Successfully merging this pull request may close these issues.

feat(agent-actions): generalize the review-nag cooldown into a per-command rate limit

1 participant