feat(webapp): enforce watch plan limits - #4556
Conversation
Refuse a watch whose window exceeds the plan's agentWatchMaxHours, or that would push the org past its agentWatchers count, with a new watch_limit_reached result carrying an upgrade hint. Plan limits are a floor below the existing code ceilings (min(plan, WATCH_MAX_HOURS=24) and the per-chat cap of 3, which still apply independently). Fails open: an absent limit resolves to unlimited, so self-hosted is unaffected and the upgrade nudge is gated on billing presence. TRI-12863
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThe pull request adds plan-based limits for dashboard agent watches. It limits watch duration and organization-wide active watches, adds billing-aware refusal messages, and maps limit refusals to HTTP 409. It also adds sweep-attempt persistence for stale investigations. Failed settlements are retried up to five times before force abandonment as inconclusive. Integration tests cover both watch-limit enforcement and poison-investigation handling. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…63' into feat/agent-watch-limits-tri-12863
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
The card-submit route's status ladder didn't handle watch_limit_reached, so a plan-limit refusal fell through to HTTP 500. Match the MCP route and return 409.
drizzle-kit generated them unformatted, failing the oxfmt --check code-quality gate.
…at/agent-message-quota-tri-12863
…ore the plan-window check
| export async function getCachedLimitAllowingZero( | ||
| orgId: string, | ||
| limit: keyof Limits, | ||
| fallback: number | ||
| ) { | ||
| return platformCache.limits.swr(`${orgId}:${limit}:allow-zero`, async () => | ||
| limitValueAllowingZero(await getLimits(orgId), limit, fallback) | ||
| ); | ||
| } |
There was a problem hiding this comment.
🟡 Users who upgrade are still told to upgrade before creating a watch, for up to ten minutes
The org's watch allowance is read from a cache that is never cleared when the plan changes (platformCache.limits.swr at apps/webapp/app/services/platform.v3.server.ts:511), so a customer who follows the upgrade prompt keeps getting the same refusal until the cached value ages out.
Impact: After paying to upgrade, a user can be blocked from creating watches — and shown "Upgrade your plan for more" again — for up to ten minutes.
Why the plan change doesn't reach the watch limit read
resolveWatchPlanLimits (apps/webapp/app/services/dashboardAgentWatchLimits.server.ts:34-43) reads both floors through getCachedLimitAllowingZero, which stores under ${orgId}:${limit}:allow-zero in the limits namespace (fresh 5 min, stale 10 min, see apps/webapp/app/services/platform.v3.server.ts:172-176).
setPlan invalidates plan-derived caches on every successful plan change (apps/webapp/app/services/platform.v3.server.ts:587), but invalidatePlanDerivedCaches only removes entitlement and ssoEntitlement (apps/webapp/app/services/platform.v3.server.ts:226-229); nothing ever calls platformCache.limits.remove. The refusal in createDashboardAgentWatch (apps/webapp/app/services/dashboardAgentWatches.server.ts:357-374) therefore keeps using the pre-upgrade limits. This was harmless for the existing getCachedLimit callers (concurrency defaults), but this PR makes the stale value directly user-facing behind an explicit "Upgrade your plan" call to action.
Prompt for agents
The watch plan floors are read via getCachedLimitAllowingZero, which caches under the `limits` namespace with a 5 min fresh / 10 min stale window. No code path ever removes entries from that namespace: invalidatePlanDerivedCaches in apps/webapp/app/services/platform.v3.server.ts only clears `entitlement` and `ssoEntitlement`, even though it is called after every successful plan change in setPlan. As a result, an org that upgrades in response to the new 'Upgrade your plan for more' hint from dashboardAgentWatchLimits.server.ts keeps hitting the old, tighter watch limits until the cache expires. Consider extending the plan-change invalidation so the limit keys used by the watch floors (both the plain `${orgId}:${limit}` and the new `${orgId}:${limit}:allow-zero` variants) are removed when a plan changes, or otherwise ensure the watch limit read observes a plan change promptly.
Was this helpful? React with 👍 or 👎 to provide feedback.
What & why. Watches now honour a plan's watch limits. A watch whose window exceeds the plan's
agentWatchMaxHours, or that would push the org past itsagentWatcherscount, is refused with a newwatch_limit_reachedresult and an upgrade hint (a chat line on the card, HTTP 409 on the API).Key decisions.
min(plan, WATCH_MAX_HOURS=24)for the window, and the per-chat cap of 3 still applies independently. Plans only tighten, never loosen.isBillingConfigured().TRI-12863