feat: report agent runtime hours usage in entitlements (#27985)

Populate `FeatureAgentRuntimeHours.Actual` on every entitlements refresh
for licenses that grant the feature. A new
`GetTotalUsageHBAgentRuntimeV1` query sums `runtime_ms` over the
license's usage period, reading `usage_events` directly:
`hb_agent_runtime_v1` is exactly one row per hourly bucket
deployment-wide with `created_at` at the bucket start, enforced by the
unique partial index introduced in #27983.

The measurement reuses the shared `measureUsage` policy from #27984
through a new `AgentRuntimeMsFn` closure (usage publisher subject):
failures publish the stable
`LicenseAgentRuntimeUsageUnavailableErrorText` and log the cause. Usage
is floored to whole hours, matching the unit of the
`agent_runtime_hours_*` claims, and at most one warning is emitted per
refresh: reaching the allocation supersedes the advisory soft limit. The
dashboard renders the soft-limit advisory muted without a sales link and
treats the runtime usage-unavailable text as a diagnostic.

**Precise usage.** `Feature.ActualMs` (JSON `actual_ms`), set only for
`agent_runtime_hours`, carries the exact stored milliseconds backing the
floored `Actual` so clients can render fractional hours (e.g. `10.3`).
It has the same freshness as `Actual`; the whole-hour warning thresholds
are unchanged.

**Unlimited licenses.** A license minted with the unlimited (`-1`)
allocation decodes to an enabled feature with a nil `Limit` (#27984), so
the warning write-back now guards the allocation dereference: no
thresholds can exist for an unlimited license, so no runtime hours
warning is ever emitted, while `Actual` is still measured and published.
`Feature.Compare` is unchanged; for usage-period features the
issued-at/end dates decide first, so a metered feature outranks an
unlimited one only on an exact timestamp tie, an edge pinned by a
`TestFeatureComparison` case and documented on
`decodeAgentRuntimeHours`.

**Grandfathered premium licenses.** Premium licenses without
`agent_runtime_hours_*` claims are now granted the feature disabled with
a zero limit over the license term, identical to an explicit
`allocation: 0`: usage is measured and published for every Premium
deployment, and chatd's pooled admission (#27902) caps concurrent
agentic chats until a license with a positive allocation is added. The
default carries a fixed early `UsagePeriod.IssuedAt` (2026-08-01, the
same mechanism as the managed-agents default) so any license actually
carrying the claims outranks it in the `AddFeature` merge regardless of
the licenses' relative issue dates; the constant must stay earlier than
the earliest legitimately issued claim-bearing license. Zero allocations
(explicit or grandfathered) emit no deployment-wide warning banner:
those deployments are steered by the in-page upgrade CTA and the
concurrency cap. Enterprise licenses are unchanged.

Part 3 of a 3-PR stack splitting up #27796 (see there for review
history). Stack: #27983 → #27984 → this PR.

Closes CODAGT-852.
This commit is contained in:
Jaayden Halko
2026-08-18 12:40:33 +07:00
committed by GitHub
parent 30dc7ebd71
commit fa8ffe4eda
24 changed files with 1532 additions and 77 deletions
+24 -14
View File
@@ -380,32 +380,42 @@ func (set FeatureSet) Features() []FeatureName {
type Feature struct {
Entitlement Entitlement `json:"entitlement"`
Enabled bool `json:"enabled"`
Limit *int64 `json:"limit,omitempty"`
// Limit is the maximum value the license grants for the feature, in the
// feature's own unit. For FeatureAgentRuntimeHours, an enabled feature
// with Limit omitted means the license grants unlimited runtime hours.
Limit *int64 `json:"limit,omitempty"`
// SoftLimit is the advisory warning threshold that accompanies Limit for
// features whose license carries it. For these features, Limit carries
// the purchased allocation.
//
// Only certain features set this field:
// - FeatureAgentRuntimeHours
// the purchased allocation; an unlimited allocation has no thresholds,
// so SoftLimit is omitted alongside the omitted Limit. Only
// FeatureAgentRuntimeHours sets this field.
SoftLimit *int64 `json:"soft_limit,omitempty"`
// HardLimit is the enforcement threshold that accompanies Limit for
// features whose license carries it. See SoftLimit for the set of
// features that use these thresholds.
HardLimit *int64 `json:"hard_limit,omitempty"`
Actual *int64 `json:"actual,omitempty"`
// Actual is the usage measured against Limit, when known: a
// point-in-time count for most features, or usage accumulated over
// UsagePeriod for features that set one. Its unit matches Limit's;
// FeatureAgentRuntimeHours reports whole hours floored from the
// recorded milliseconds, with the precise value available in
// ActualMs. FeatureAgentRuntimeHours usage can trail by roughly one
// hour because the current hour is not emitted, plus the entitlement
// refresh interval.
Actual *int64 `json:"actual,omitempty"`
// ActualMs is the precise usage backing Actual, in milliseconds, for
// features measured in time. It has the same freshness as Actual.
// Only FeatureAgentRuntimeHours sets this field.
ActualMs *int64 `json:"actual_ms,omitempty"`
// Below is only for features that use usage periods.
// UsagePeriod denotes that the usage is a counter that accumulates over
// this period (and most likely resets with the issuance of the next
// license).
//
// These dates are determined from the license that this entitlement comes
// from, see enterprise/coderd/license/license.go.
//
// Only certain features set these fields:
// - FeatureManagedAgentLimit
// - FeatureAgentRuntimeHours
// license). These dates are determined from the license that this
// entitlement comes from, see enterprise/coderd/license/license.go.
// Only FeatureManagedAgentLimit and FeatureAgentRuntimeHours set this
// field.
UsagePeriod *UsagePeriod `json:"usage_period,omitempty"`
}