Skip to content

M6-13 Stats: own measurement

No Date Ruling Reason
D-M6-13-1 2026-10-24 The port kernel/measure (standard library and kernel/ids only) holds the 18 metric constants, the closed dimension lists and the scope (channel, agent) of 25 3.2, Validate, and Value with Count, Sum, Elapsed and OK (built by One, Timed, Amount, Outcome, SLI). The SLI thresholds are not injected into the kernel (measure.SetBounds of the brief is not built): the measuring point passes the duration and the outcome, and the stats recorder decides good with stats/domain.IsGood over slibounds.gen.go. An invalid call panics in a test binary (testing.Testing()) and is dropped and counted as bothy.stats.flush.dropped{bothy.reason=invalid_key} in production; signals.md 4.3b lists the value. Modules still never import stats (02 4.4), and no process-wide mutable state is needed for the bounds; the threshold lives in one place, the recorder, which already depends on the generated file.
D-M6-13-2 2026-10-24 good counts an SLI event whose outcome is good and whose duration is at most the threshold, the bound included (R-OB-06: le buckets are inclusive); without a threshold (sli.api_availability, sli.push_acceptance) only the outcome decides. Events that slos.md 2 leaves out of "all" are not recorded: a connection that ends with client_error (sli.reconnect), a send with rejected_token (sli.push_acceptance), a run that leaves queued as cancelled (sli.run_start), and the route /api/v1/gateway (sli.api_availability). sli.api_availability counts exactly the requests http.server.request.duration records, through otel.ObservedMiddleware (a panicking request is in neither). Good and all are counted as the SLO and its histogram count them, so the traffic light and Prometheus agree.
D-M6-13-3 2026-10-24 The recorder keeps counters per minute (UTC, the clock of the process at Record) and key in maps under one mutex held for one map update. The flush swaps the maps out under the lock and writes them outside it in chunks of at most flushChunkRows rows (internal/stats/app/flush.go), one INSERT ... ON CONFLICT ... DO UPDATE with additive count, sum, good and updated_at = now() per chunk on the pool, its own transaction. A failed chunk and every later one are merged back, marked as from a failed write; more than stats.flush_queue_max keys drop keys of the oldest minute first, counted as queue_full, or as db_error for counters of a failed write. The shutdown flushes once in the stop step stats_flush of the database phase, before the pool closes; when that flush fails, every key held is dropped and counted as db_error. The periodic flush runs every stats.flush_interval with a ticker. 25 3.4: bounded, counted loss, never a wait of the domain path; chunks keep one statement and its transaction short whatever the bound; a chunk is committed or merged back as a whole, so nothing is added twice.
D-M6-13-4 2026-10-24 Domain counters record in txn.AfterCommit of the unit of work that caused them, next to the existing OpenTelemetry instrument in the same function: messages_created (with the instrument bothy.message.created; a system line, origin system, is outside the dimension list and counted only in the telemetry), threads_created (threads created through messaging.CreateThread, the API and the gateway op; a main thread of a new channel and an imported thread are no such thread), runs_started, runs_finished, sli.run_start, sli.turn_duration and the billing_mode_mismatch part of invocations_denied (all with RunTransitioned), cost (with bothy.run.cost per booked entry in runs, the caller of the booking), billing_mode_switches (a change of the mode only; a new holder within personal is no switch) and approval_cards. tool_calls records after a committed patch next to bothy.run.steps. The gateway records through gatewayapp.Measured, a decorator of its metrics port that the gateway, the hub and the WebSocket adapter share, so every gateway measuring point is the same call as its telemetry. A rolled back unit records nothing (TestMeasure_RollbackNotCounted); one call site per event keeps the two paths from diverging.
D-M6-13-5 2026-10-24 approval_cards: requested once per card message of an agent that carries at least one component with required_capability = approve_actions; approved for an interaction on such a component whose button value is once, session or always, denied for deny (the choice the allow audit records), nothing for any other interaction; expired once per card when the job runs.component_expiry expires at least one of its approval components unused. The card of an app (M2-25) counts in none of them: an app has no runs and is no agent. invocations_denied: i2_denied for every denial of agent.invoke that bothy.agent.invocation.denied counts (before a delivery, and at a token renewal), billing_mode_mismatch for a run that ends failed with that reason. Channel and agent are the card's or the run's. The dimensions follow 09 11.5 and 10 (I2, I3) and the classification of the existing audit entry and counter.
D-M6-13-6 2026-10-24 stats_minutes is partitioned per UTC day (stats_minutes_YYYYMMDD, no default partition); the migration creates none (upgrade.md M10). Partitions come from two SECURITY DEFINER functions of bothy_owner: stats_minutes_ensure_partition(day) and stats_minutes_drop_expired(cutoff, lock_timeout). The job stats.retention runs hourly and once at the start of a worker (fixed schedule, no key), creates today's and tomorrow's partition and detaches and drops every partition whose day ends at or before now - retention.stats_minutes (plain DETACH, bounded by database.lock_timeout, because DETACH ... CONCURRENTLY cannot run in a function). A flush whose rows meet a day without a partition creates the partitions of the chunk's days and writes the chunk once more. The flush never depends on the job for correctness, and the job stays a cheap hourly check; 24 runs a day keep tomorrow's partition ready long before midnight.
D-M6-13-7 2026-10-24 bothy config check (cross-check stats_retention_covers_slo_period) fails when retention.stats_minutes is shorter than the longest Period of slibounds.gen.go; the message names the key and the SLO period, never the value. 25 3.5, VZ-T14: the traffic light needs the whole SLO period.
D-M6-13-8 2026-10-24 Names differ from the brief and follow the code: the recorder is statsapp.Measurement, the flush statsapp.Flush with Flush, Run and Shutdown, the retention use case statsapp.Retention; the wiring type wire.Measure built by the start step stats_measure (after schema, before every module) with the test seam Deps.StatsMeasure; the measuring points of cost stay in runs (where bothy.run.cost is), not in billing; the gateway records through gatewayapp.Measured; httpserver.Deps.Measure and otel.ObservedMiddleware; the agents module takes the recorder through IdentityWrites.Measure. There is no internal/kernel/README.md, so the port list is not repeated there. Brief names predate the code (D-W-100, D-W-120).