| D-M6-15-1 |
2026-10-25 |
stats.subscribe counts rl.read keyed by the principal, then stats.Live.Check: stats.live_enabled off is feature_disabled; stats.read on the instance (no space_id) or the space decides, its denial answers with the decision's code (not_found for an unreadable space, forbidden otherwise); agents and apps get forbidden before any decision, whatever grant they hold and whether the space exists. Then one slot of the node unless the connection already holds one (rate_limited above stats.live_max_subscribers). The subscription replaces an earlier one of the connection and is stored in the gateway session; resume checks it again with the rights of that moment and takes a new slot, a denial sends one error {op: stats.subscribe} and clears it. stats.unsubscribe names no resource (no authorize, noResource), ends the subscription and sends nothing, also without one. A closed connection frees its slot; the session keeps the subscription for sync.resume_window. |
25 5.1 items 1 and 6, 04 12 item 4; the slot follows the connection, so a dead connection never holds capacity. |
| D-M6-15-2 |
2026-10-25 |
One builder per api node (statsapp.Live, role api and all): its loop sleeps until the first subscription or poll, then ticks every stats.live_interval and builds one base frame per tick; the first tick that finds no subscription and no poll lease stops the ticker without a build. The base frame keeps at most stats.live_max_runs + 1 runs per channel (newest first), the bound that lets every receiver's filtered list still hold stats.live_max_runs and still see runs_truncated when only one channel of it has more; its size is bounded by the active runs, which the agents' concurrency bounds. Runs outside every space (dm channels) are left out, because StatsLiveRun.space_id is required and no space manager may see them. Rates are summed over agents per channel, dimension and currency for the last full and the current minute; the SLI totals sum every sli.* metric over the minutes from the start of stats.live_sli_window, a metric without rows appears with zeros. The agents of a run and the effective operators of the live channels get the configured mode from agents.Facts and presence and reported mode from agents.Presence (unknown without a status). |
R-VZ-05: the load grows with the intervals, not with the viewers (TestLive_NoBuildWithoutSubscription). |
| D-M6-15-3 |
2026-10-25 |
The base frame holds the rate cells per channel and the cells without a channel apart; the filter sums the cells of the receiver's channel set and adds the cells without a channel (catchups, resyncs, slow_consumer_closes, billing_mode_switches) only for the instance. Each metric gets one row with all dimensions summed (dimension absent) and one row per non-empty dimension; cost rows carry their currency and are never summed across currencies; last_minute_sum only for runs_finished, tool_calls and cost. A metric without rows is absent (zero). |
25 4 rule 1 and 5.2 row rates: rates cover only visible channels. |
| D-M6-15-4 |
2026-10-25 |
Per receiver and frame the channel set is Scope(channel.read) in the mode of the caller (delivery mode for the stream, so it reads the authorizer's snapshot of the observed rights version), restricted to the subscribed space; cost members only with cost.read.aggregate on the subscription's target. The lists are taken per frame rather than cached per connection, because the authorizer's snapshot already is the per-version cache and a channel created without a rights change must appear in the next frame; the filter time per receiver is in the ticket report. The lists runs, agents and rates are always arrays, empty when nothing is visible, never JSON null (the hub refuses a payload with null). The size cut drops the oldest runs until the payload is below sync.event_payload_max_bytes (the hub's bound) and sets runs_truncated; the envelope's truncated is never set; a frame that does not fit without runs is not sent and the build is logged. The frame is encoded once per receiver; getStatsLive writes the same bytes as its body. |
25 4 and 5.2, VZ-T08; one encoding for both paths (R-VZ-06). |
| D-M6-15-5 |
2026-10-25 |
The gateway is the sink of the builder: for every subscribed connection of the process it offers one stats.live (audience the receiver, resource instance or space with its ID) through Hub.OfferEphemeralTo, which skips a connection that is not live, admits with sync.Admit and the receive action stats.read in delivery mode, only then builds the payload, and drops it on a full queue (never 4014). stats.live is a direct type of the gateway's register: PublishEphemeral refuses it. A denied admission ends the subscription once (slot freed, session cleared) with one error {op: stats.subscribe} whose code is the subscription check of that moment (not_found for an unreadable space, forbidden otherwise); nothing follows. sync.Admit needs no case of its own: the generic rule of a personal event with a receive action decides it on the frame's resource (TestAdmit_StatsLive). |
04 2.1 and 7.2 item 6, 25 5.1 item 3, VZ-T06. |
| D-M6-15-6 |
2026-10-25 |
getStatsLive runs the same check (REST 403 feature_disabled, 403 forbidden, 404 not_found), sets a poll lease of one stats.live_interval (the builder runs while polls arrive) and returns the node's base frame when it is younger than stats.live_interval; otherwise it builds one under the build mutex that the ticker shares, so concurrent polls on an idle node wait for that one build and reuse it. |
25 5.1 last paragraph, R-VZ-06; ten polls of one interval build once (TestGetStatsLive_NoRebuildPerRequest). |
| D-M6-15-7 |
2026-10-25 |
connections counts the connections registered in the hub of this node by the kind of their principal (people, agents, apps) and appears only for the instance and only with cluster.mode = single; with multi the member is absent, because cross-node counts belong to M7-17. |
25 5.2 row connections. |
| D-M6-15-8 |
2026-10-25 |
Names differ from the brief and follow the code: the cost so far comes from the run row through runs.LiveSource (runs.cost_micros, currency, cost_is_estimate, which the live box of M2-10 keeps), so there is no billing.RunCostSource; the agents part uses the existing ports agents.Facts and agents.Presence instead of a new agents.LiveSource; the places and operators of the channels need a new chat port chat.ChannelPlaces. rate_limit of an agent stays absent until the heartbeat accepts it (M6-14). The builder is statsapp.Live with the test seam Deps.StatsLive and Live.Tick; the gauge is registered as metrics.StatsLiveSubscribers. |
Brief names predate the code (D-W-100, D-W-120). |