You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(agent-actions): generalize the review-nag cooldown into a per-command 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.
# EVERY @gittensory command (help/ask/preflight/blockers/... and the maintainer queue-digest commands),
339
+
# not just review-request pings — independent of review-nag's own thread-author-only scope. "hold"
340
+
# replies with a cooldown notice and skips the command's own dispatch. Off by default.
341
+
# commandRateLimitPolicy: off # off | hold. Default: off.
342
+
# commandRateLimitMaxPerWindow: 20 # Positive integer. Per-command invocation limit for a CHEAP command (cache-only, no AI call) within the window. Default: 20.
343
+
# commandRateLimitAiMaxPerWindow: 5 # Positive integer. Tighter limit for an AI-cost-bearing command (ask/blockers/preflight/reviewability/packet/duplicate-check/next-action/repo-fit). Default: 5.
344
+
# commandRateLimitWindowHours: 24 # Positive integer. Rolling window (hours) both limits above count against. Default: 24.
* pattern (`countRecentAuditEventsForActorAndTarget`) to EVERY `@gittensory` Q&A command, not just
8838
+
* review-request pings. Keyed by `(actor, command, targetKey)` — the command name is folded into targetKey so
8839
+
* repeatedly invoking ONE command never counts against a DIFFERENT command's own limit. Independent of, and
8840
+
* complementary to, `maybeThrottleReviewNagPing` above: that one stays scoped to the thread's OWN author and
8841
+
* can close a PR; this covers ANY authorized actor invoking ANY command and only ever holds (declines with a
8842
+
* notice), never closes. Off (`commandRateLimitPolicy: "off"`, the default) is a complete no-op.
8843
+
*/
8844
+
asyncfunctionmaybeThrottleGittensoryCommand(
8845
+
env: Env,
8846
+
args: {
8847
+
deliveryId: string;
8848
+
repoFullName: string;
8849
+
issueNumber: number;
8850
+
installationId: number;
8851
+
commenter: string;
8852
+
command: GittensoryMentionCommandName;
8853
+
settings: RepositorySettings;
8854
+
mode: ReturnType<typeofresolveAgentActionMode>;
8855
+
},
8856
+
): Promise<boolean>{
8857
+
/* v8 ignore next -- resolveRepositorySettings always resolves a concrete "off"/"hold"; the undefined side is defensive against the field's optional TS type. */
/* v8 ignore next -- resolveRepositorySettings always resolves a concrete positive integer; the undefined side is defensive against the field's optional TS type. */
/* v8 ignore next -- resolveRepositorySettings always resolves a concrete positive integer; the undefined side is defensive against the field's optional TS type. */
/* v8 ignore next -- fail-safe: an audit write failure never blocks command dispatch */
8884
+
()=>undefined,
8885
+
);
8886
+
8887
+
if(invocationCount<=maxPerWindow)returnfalse;// under threshold — normal dispatch proceeds unchanged
8888
+
8889
+
if(args.mode==="live"){
8890
+
awaitcreateIssueComment(
8891
+
env,
8892
+
args.installationId,
8893
+
args.repoFullName,
8894
+
args.issueNumber,
8895
+
`@${args.commenter} the \`${args.command}\` command has reached its rate limit (${maxPerWindow} within ${windowHours}h). Please wait for the window to pass before trying again. This is an automated maintenance action.`,
8896
+
).catch(
8897
+
/* v8 ignore next -- fail-safe: a comment-post failure must not crash the throttle decision itself */
0 commit comments