Documentation
¶
Overview ¶
Package usage holds the account-level usage aggregator: a neutral Sample shape plus one in-memory Aggregator, the kb:adr/usage-no-source-interface seam for a second usage source down the line. Nothing here is Claude-Code-format vocabulary — internal/claudecode reads the status-line payload keys into its own neutral StatusAccount, and internal/server maps that into a Sample at the seam, so this package and the adapter stay mutually dependency-free.
Index ¶
Constants ¶
const DefaultModelScopedSource = defaultModelScopedSource
DefaultModelScopedSource exposes defaultModelScopedSource for internal/server's emptyUsageInfo placeholder — the one caller outside this package that needs the same default, so it reads this instead of keeping its own copy of the literal.
const DefaultSource = defaultSource
DefaultSource exposes defaultSource for internal/server's emptyUsageInfo placeholder — the one caller outside this package that needs the same default, so it reads this instead of keeping its own copy of the literal.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Aggregator ¶
type Aggregator struct {
// contains filtered or unexported fields
}
Aggregator holds the daemon's single, account-global usage reading in memory. It starts unknown at construction — no hydration from persisted usage_sample rows across a daemon restart (kb:adr/usage-no-hydration-across-restart). Record is its single writer (only the ingest worker goroutine calls it, so no second Record can interleave); mu guards current for Current()'s readers on other goroutines against that one writer.
func NewAggregator ¶
func NewAggregator(cfg Config) *Aggregator
NewAggregator builds an Aggregator. Nothing here does I/O until Record is called.
func (*Aggregator) Current ¶
func (a *Aggregator) Current() Snapshot
Current returns the aggregator's present snapshot — GET /api/state and the WS `hello` handshake's snapshot both read this.
func (*Aggregator) Record ¶
func (a *Aggregator) Record(ctx context.Context, s Sample) error
Record applies one new sample: updates in-memory state, persists a usage_sample row, and invokes OnChange — but only when the bucket values or model changed since the last recorded sample (kb:adr/usage-sample-dedup-by-value). sampledAt advancing alone is never a change, which is why it's stamped here rather than accepted from the caller: the ~435 ms pair posts (kb:fact/status-posts-arrive-in-pairs) carry identical bucket/model values and must collapse to one broadcast and one row, not two.
type Bucket ¶
Bucket is one usage-limit readout (five-hour or seven-day). UsedPct and ResetsAt always travel together — a partial bucket is never modeled.
type ModelScoped ¶
type ModelScoped struct {
// contains filtered or unexported fields
}
ModelScoped holds the daemon's per-model weekly usage windows in memory — the second, independent usage holder kb:adr/usage-no-source-interface calls for, so Aggregator's single-writer assumption (aggregator.go's Record) stays true: each holder keeps exactly one writer (the status-line ingest path for Aggregator, musterd's own poller for this one).
func NewModelScoped ¶
func NewModelScoped(cfg ModelScopedConfig) *ModelScoped
NewModelScoped builds a ModelScoped. Nothing here does I/O until Record or SetError is called.
func (*ModelScoped) Current ¶
func (m *ModelScoped) Current() ModelSnapshot
Current returns ModelScoped's present snapshot — GET /api/state and the WS `hello` handshake's snapshot both read this.
func (*ModelScoped) Record ¶
func (m *ModelScoped) Record(ctx context.Context, windows []ModelWindow) error
Record applies one successful fetch: dedups against the current list sorted by DisplayName — callers pass a non-nil, possibly empty, slice to distinguish "fetched, no windows" from "never fetched" — persists one usage_model_sample row per window before committing to memory when the list changed, and always clears a standing error — a success after a failure must broadcast even when the list itself is unchanged, since modelScopedError changed.
func (*ModelScoped) SetError ¶
func (m *ModelScoped) SetError(kind string)
SetError records a failed poll (kb:adr/usage-model-window-polled-from-oauth-api): the last-good list and its timestamp are kept untouched. Logged at Warn the first time this error kind is seen in a row, Debug on repeats — a broadcast fires only on the Warn transition, since a repeat carries no new information the UI doesn't already have (modelScopedError is already that value).
type ModelScopedConfig ¶
type ModelScopedConfig struct {
Store *store.Store
Logger zerolog.Logger
OnChange func(ModelSnapshot)
}
ModelScopedConfig wires a ModelScoped. OnChange broadcasts the merged `usage` WS message; nil in tests.
type ModelSnapshot ¶
type ModelSnapshot struct {
Windows []ModelWindow
At *time.Time
Error *string
Source string
}
ModelSnapshot is ModelScoped's exposed state. Windows is nil until the first successful fetch; an empty-but-non-nil slice is a valid, distinct successful result (an account with no scoped windows) — At mirrors that nullability exactly. Error is nil except right after a failed poll — a success clears it even when the list itself didn't change. Source is never empty.
type ModelWindow ¶
ModelWindow is one neutral per-model weekly usage window (kb:adr/usage-model-window-polled-from-oauth-api) — internal/claudecode's own UsageWindow is this seam's other half; internal/server maps between them at the seam, mirroring StatusAccount → Sample.
type Sample ¶
type Sample struct {
FiveHour Bucket
SevenDay Bucket
Model Model
SampledAt time.Time
Source string // "subscription" in v1
}
Sample is one neutral usage reading. SampledAt is stamped by Aggregator.Record, not by the caller (mirrors internal/store's own received_at/at stamping) — a caller-supplied value here would be ignored.
type Snapshot ¶
type Snapshot struct {
FiveHour *Bucket
SevenDay *Bucket
Model *Model
SampledAt *time.Time
Source string
}
Snapshot is the aggregator's exposed state. Every field is nil until the first post-boot sample lands (kb:adr/usage-no-hydration-across-restart: no hydration from persisted usage_sample rows across a daemon restart) — Source is the one field that is never null, matching the wire shape.