-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(webapp): enforce watch plan limits #4556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
kathiekiwi
merged 34 commits into
feat/agent-message-quota-tri-12863
from
feat/agent-watch-limits-tri-12863
Aug 12, 2026
+2,357
−1
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
f9d674e
feat(webapp): enforce watch plan limits
kathiekiwi 00933ab
Merge remote-tracking branch 'origin/feat/agent-message-quota-tri-128…
kathiekiwi 063e41e
merge: propagate review fixes from feat/agent-message-quota-tri-12863
kathiekiwi 21f5461
merge: propagate wave-2 review fixes from feat/agent-message-quota-tr…
kathiekiwi 2dd410d
fix(webapp,dashboard-agent-db): stop a stuck investigation pinning th…
kathiekiwi 7d2efc9
merge: propagate org-purge best-effort from feat/agent-message-quota-…
kathiekiwi bce03cb
fix(webapp): map a watch plan-limit refusal to 409, not 500
kathiekiwi 2db7c39
style(dashboard-agent-db): oxfmt the drizzle meta files
kathiekiwi 3fbc04a
merge: watch plan-limit 409 review-comment fixes
kathiekiwi 743644b
merge: propagate review-comment fixes from feat/agent-message-quota-t…
kathiekiwi f695267
fix(webapp): hoist a type-only import so oxlint stops failing
kathiekiwi f64f238
merge: hoist type-only import for oxlint
kathiekiwi f080779
merge: propagate second-pass fixes from feat/agent-message-quota-tri-…
kathiekiwi 3fd5cf4
chore(server-changes): consolidate the watch-limits notes into one
kathiekiwi 946831b
merge: consolidate watch-limits notes 2 to 1
kathiekiwi 47139f6
merge: propagate server-changes consolidation from feat/agent-message…
kathiekiwi 49f64a6
merge: propagate changeset consolidation and note restoration from fe…
kathiekiwi 526c3fc
merge: propagate base UI relocation + drizzle attribution
kathiekiwi 26ab506
merge: propagate tsql linter test fix
kathiekiwi 0cbe4c0
merge: propagate card-test relocation
kathiekiwi e4b02df
chore: merge feat/agent-message-quota-tri-12863 (main sync)
kathiekiwi d7c7fb7
chore: merge feat/agent-message-quota-tri-12863 (review fixes)
kathiekiwi cf74d85
chore: merge feat/agent-message-quota-tri-12863 (review fixes round 2)
kathiekiwi 372a4eb
fix(webapp): honor zero watch limits and answer instant questions bef…
kathiekiwi d4bda0a
chore: merge feat/agent-message-quota-tri-12863 (review fixes round 3)
kathiekiwi 8f93f80
chore: merge feat/dashboard-agent-flows-watch (review fixes round 3)
kathiekiwi b9656c0
chore: merge feat/agent-message-quota-tri-12863 (review fixes round 3)
kathiekiwi aced730
chore: merge feat/agent-message-quota-tri-12863 (composer escape foll…
kathiekiwi 202d2cf
chore: merge feat/agent-message-quota-tri-12863 (main sync post-#4529)
kathiekiwi 333dfcc
chore: merge feat/agent-message-quota-tri-12863 (review fixes)
kathiekiwi ac25c73
chore: merge feat/agent-message-quota-tri-12863 (prompt re-send guard)
kathiekiwi d922a78
chore: merge feat/agent-message-quota-tri-12863 (main sync post-#4525)
kathiekiwi a585ab4
chore: merge feat/agent-message-quota-tri-12863 (demo line-0 guard)
kathiekiwi 500ba80
chore: merge feat/agent-message-quota-tri-12863 (busy-rejection cap fix)
kathiekiwi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: feature | ||
| --- | ||
|
|
||
| Watches now respect your plan's limits: free plans can run a limited number of watches at once and for a shorter window, with a prompt to upgrade for more. |
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
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
59 changes: 59 additions & 0 deletions
59
apps/webapp/app/services/dashboardAgentWatchLimits.server.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import type { Limits } from "@trigger.dev/platform"; | ||
| import { WATCH_MAX_HOURS } from "@internal/dashboard-agent-contracts"; | ||
| import { getCachedLimitAllowingZero, isBillingConfigured } from "./platform.v3.server"; | ||
|
|
||
| // The unlimited sentinel, matching the message quota (TRI-12863 P1). Never Infinity: it | ||
| // serializes to null in the limit cache. | ||
| export const UNLIMITED_WATCH_LIMIT = 100_000_000; | ||
|
|
||
| // Filled by cloud billing (TRI-12863 P0). Absent until then, and always on self-hosted, so | ||
| // the fallback applies and the plan floor is off. | ||
| const WATCH_MAX_HOURS_LIMIT_KEY = "agentWatchMaxHours" as keyof Limits; | ||
| const WATCH_COUNT_LIMIT_KEY = "agentWatchers" as keyof Limits; | ||
|
kathiekiwi marked this conversation as resolved.
|
||
|
|
||
| export type WatchPlanLimits = { | ||
| /** Longest window one watch may run for, in hours. */ | ||
| maxHours: number; | ||
| /** How many active watches the org may run at once. */ | ||
| watchers: number; | ||
| }; | ||
|
|
||
| async function readLimit(organizationId: string, key: keyof Limits): Promise<number> { | ||
| // A plan of 0 means zero, not absent: an org with watches switched off must not read as | ||
| // unlimited. Only a missing limit falls open. | ||
| const cached = await getCachedLimitAllowingZero(organizationId, key, UNLIMITED_WATCH_LIMIT); | ||
| // A cache error leaves `val` empty; fall open to unlimited. | ||
| return cached.val ?? UNLIMITED_WATCH_LIMIT; | ||
| } | ||
|
kathiekiwi marked this conversation as resolved.
kathiekiwi marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * The org's plan floors for watches. Fails open: an absent limit (self-hosted, or before the | ||
| * cloud side ships) resolves to the unlimited sentinel, so neither floor bites. `read` is the | ||
| * plan-limit seam: tests pass their own reader instead of the cached platform one. | ||
| */ | ||
| export async function resolveWatchPlanLimits( | ||
| organizationId: string, | ||
| read: (organizationId: string, key: keyof Limits) => Promise<number> = readLimit | ||
| ): Promise<WatchPlanLimits> { | ||
| const [maxHours, watchers] = await Promise.all([ | ||
| read(organizationId, WATCH_MAX_HOURS_LIMIT_KEY), | ||
| read(organizationId, WATCH_COUNT_LIMIT_KEY), | ||
| ]); | ||
| return { maxHours, watchers }; | ||
| } | ||
|
kathiekiwi marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * The window ceiling actually in force: the plan floor under the code ceiling. A plan that | ||
| * allows 100 hours still caps at {@link WATCH_MAX_HOURS}. | ||
| */ | ||
| export function effectiveWatchMaxHours(planMaxHours: number): number { | ||
| return Math.min(planMaxHours, WATCH_MAX_HOURS); | ||
| } | ||
|
|
||
| /** | ||
| * A watch-limit refusal, plus an upgrade nudge when billing is present. Self-hosted never | ||
| * hits this (fails open above), and the nudge is gated so a stray refusal stays quiet there. | ||
| */ | ||
| export function watchLimitHint(base: string, billingConfigured = isBillingConfigured()): string { | ||
| return billingConfigured ? `${base} Upgrade your plan for more.` : base; | ||
| } | ||
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.