Skip to content

feat: trigger execution queuing, fan-out, and MCP rate limiting#2772

Draft
vnv-varun wants to merge 13 commits intomainfrom
feat/trigger-execution-queuing
Draft

feat: trigger execution queuing, fan-out, and MCP rate limiting#2772
vnv-varun wants to merge 13 commits intomainfrom
feat/trigger-execution-queuing

Conversation

@vnv-varun
Copy link
Contributor

Summary

Adds three composable layers to the scheduled trigger system: execution concurrency control, trigger audience fan-out, and MCP tool rate limiting. Enables customers to configure "daily reports for all employees" without overwhelming external API rate limits.

Full PR body to follow after testing.

- Explicitly set defaults for maxConcurrentInvocations, staggerIntervalSeconds, and audienceConfig in create route
- Add tests for creating triggers with concurrency control fields
- Add tests for creating triggers with audience config
- Add tests for default values of new fields
- Add tests for updating concurrency and audience fields
- Add tests for clearing audience config
- Add tests for recipientUserId in invocation list responses
- Verify GET returns new fields in list and single trigger responses
…unner

- Add createFanOutInvocationsStep that creates N invocations for audience userIds
- Each invocation gets recipientUserId and unique idempotency key (prefix_userId)
- Runner checks audienceConfig and uses fan-out or single creation accordingly
- Non-audience triggers continue creating exactly one invocation (unchanged)
- Add 4 tests: fan-out creation, idempotency keys, duplicate prevention, regression
Add processInvocationBatchStep that processes multiple invocations
with configurable maxConcurrentInvocations and staggerIntervalSeconds.
The fan-out path in the runner now uses batch processing with parallel
execution, while the single-invocation path remains unchanged.

- Add countRunningInvocationsForTrigger data access function
- Add countRunningInvocationsStep, listAllPendingInvocationsStep,
  and processInvocationBatchStep step functions
- Restructure runner fan-out branch for batch processing
- Add 5 integration tests for concurrency control behavior
Single invocation path now uses invocation.recipientUserId as runAsUserId
override when present, falling back to trigger's runAsUserId. The batch/
fan-out path already had this logic in processInvocationBatchStep.
McpRateLimiter with token bucket per (tenantId, toolId) tuple supporting
requestsPerMinute, requestsPerHour, and concurrentRequests limits.
Acquires block with configurable timeout and throw McpRateLimitError.
Buckets are lazily created and cleaned up after inactivity.
McpClient.tools() execute callback now checks rate limiter before calling
callTool(). Rate limiter is injected via McpClientOptions when tool has
rateLimits configured. AgentMcpManager passes a global McpRateLimiter
singleton and tool's rateLimits config when creating MCP connections.
Concurrent request tokens are released in finally block.
rateLimits field already flows through create/update/read via schema and
data access layer. Added 6 integration tests: create with rateLimits,
create without (null default), reject invalid values, update rateLimits,
and clear rateLimits by setting null.
- Fix typecheck errors in audience picker (field.value possibly undefined)
- Form already has maxConcurrentInvocations, staggerIntervalSeconds inputs
- Form already has audience multi-select user picker for fan-out
- API client types updated with new fields
- Add rateLimits schema to form validation (requestsPerMinute, requestsPerHour, concurrentRequests)
- Add rate limits fieldset section to MCP server form with number inputs
- Wire rateLimits into all three create paths (user-scoped, OAuth, standard) and update path
- Map existing rateLimits into edit form initialData
- Add cleanRateLimits helper to convert null form values to API-compatible format
- Increase pending invocations fetch limit from 100 to 1000 (matching
  audience size max) to prevent silent truncation for large audiences
- Add try/catch for unique constraint violation in fan-out creation to
  handle TOCTOU race condition gracefully
- Remove unused countRunningInvocationsStep dead code and import
@vercel
Copy link

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 19, 2026 3:37am
agents-docs Ready Ready Preview, Comment Mar 19, 2026 3:37am
agents-manage-ui Ready Ready Preview, Comment Mar 19, 2026 3:37am

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Mar 19, 2026

⚠️ No Changeset found

Latest commit: 11422e5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@itoqa
Copy link

itoqa bot commented Mar 19, 2026

Ito Test Report ✅

20 test cases ran. 20 passed.

✅ All executed checks passed, including scheduled trigger fan-out/execution settings, MCP rate-limit persistence, API validation/tamper resistance, authorization boundaries, and mobile form coverage. 🔍 Verification found no code-supported defect signals in the included results.

✅ Passed (20)
Test Case Summary Timestamp Screenshot
ROUTE-1 Created a scheduled trigger with explicit execution settings and fan-out audience, then reopened edit and confirmed persisted values remained maxConcurrent=5, stagger=30, and 2 selected recipients. 21:40 ROUTE-1_21-40.png
ROUTE-2 Edit flow prefilled expected values and persisted stagger-only changes without resetting unrelated fields. 0:00 ROUTE-2_0-00.png
ROUTE-3 Created a custom MCP server and confirmed edit prefill preserved 60/1000/5 rate-limit values. 27:39 ROUTE-3_27-39.png
ROUTE-4 Created an MCP server without rate-limit values and verified the edit form persisted all limit fields as empty. 28:45 ROUTE-4_28-45.png
ROUTE-5 Cleared previously saved rate limits on an MCP server and verified the edit form persisted null/empty limits after save. 28:46 ROUTE-5_28-46.png
LOGIC-1 Submitted once with three selected Run as Users and verified exactly three triggers were created with per-user suffixes and matching run-as identities for admin, Member One, and Member Two. 22:50 LOGIC-1_22-50.png
LOGIC-2 Editing audience settings did not overwrite the configured runAs identity; runAs stayed set to admin after audience-only changes. 0:00 LOGIC-2_0-00.png
LOGIC-3 Empty PATCH body was rejected with HTTP 400 and explicit 'No fields to update' detail. 32:27 LOGIC-3_32-27.png
LOGIC-4 All Invocations showed three rows for PW2772-REMED-LOGIC4-20260319 and ordering remained stable after refresh. 52:35 LOGIC-4_52-35.png
EDGE-1 Malformed payload JSON was rejected by client validation with an explicit error, preventing submission. 0:00 EDGE-1_0-00.png
EDGE-2 Rapid schedule-type toggles completed and saved successfully without mixed-field conflict behavior. 0:00 EDGE-2_0-00.png
EDGE-3 Minimum and maximum execution-bound values saved successfully across sequential edits and persisted in the edit form. 0:00 EDGE-3_0-00.png
EDGE-4 Audience selection could be cleared completely and the trigger saved without errors, preserving an empty audience state. 0:00 EDGE-4_0-00.png
EDGE-5 On iPhone-13 viewport (390x844), execution settings and audience controls remained accessible; audience select/remove interactions worked and a valid scheduled trigger was created successfully from the mobile layout. 40:20 EDGE-5_40-20.png
ADV-1 Rapid submit via double-click created one persisted trigger record for the submitted trigger name, with no duplicate row observed. 0:00 ADV-1_0-00.png
ADV-2 Tampered create payload was rejected with HTTP 400 validation error and no successful create response. 32:27 ADV-2_32-27.png
ADV-3 Invalid rate limit values were rejected with HTTP 400 including min/max/int validation failures. 32:27 ADV-3_32-27.png
ADV-4 Authenticated as non-admin and attempted delegated runAsUserId update for another user; API correctly returned HTTP 403 Forbidden with explicit authorization message. 38:50 ADV-4_38-50.png
ADV-5 Cross-tenant tampered request was denied with HTTP 403 and no foreign data exposure in response. 32:27 ADV-5_32-27.png
ADV-6 Deep-link edit flow completed; after save, immediate refresh, and back/forward navigation, the trigger retained the intended updated description with no observed persisted-state corruption. 25:22 ADV-6_25-22.png
📋 View Recording

Screen Recording

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.

1 participant