Skip to content

[stats] Daily rollup — the only way to get a series for agent-defined collections (and a gap is not a zero) #313

Description

@serge-ivo

Storage half of #310. Split out because it is a distinct chunk — a table, a cron pass, and one
semantic rule — and because the reason for it is not the one you would guess.

Why a rollup, and why it is not a perf optimisation

For agent-defined collections a daily snapshot is the only way to get a time series at all.

The lead-finder's leads collection holds 116 records under a schema the agent defined. You can
count them now. "Leads found per day" needs either a timestamp you control on every record —
which you do not, the shape is the agent's — or a snapshot taken each day. There is no third
option, and no amount of querying the collection later recovers a history nobody recorded.

Platform-owned sources (ai_usage, agent_loop_runs, agent_events) do carry timestamps and
could be aggregated live. Rolling those up too is then a genuine perf win, but it is the secondary
reason.

Shape

CREATE TABLE agent_stats_daily (
  instance_id TEXT NOT NULL,
  user_id     TEXT NOT NULL,
  card_id     TEXT NOT NULL,
  day         TEXT NOT NULL,              -- UTC date, YYYY-MM-DD
  value_json  TEXT NOT NULL,
  computed_at TEXT NOT NULL DEFAULT (datetime('now')),
  PRIMARY KEY (instance_id, card_id, day)
);
  • The cron is a single * * * * * entry. Guard on the UTC day with
    INSERT … WHERE NOT EXISTS on the primary key — the same single-flight shape claimDelivery
    and runDueTriggers already use. No second cron entry, and overlapping minutes cannot
    double-write.
  • Only instances with activity that day. Bounds the work, and is what was asked for.
  • Retention cap (~400 days) swept in the same pass, so the table stays bounded.

The rule that must not be got wrong

A missing day is a gap, not a zero.

If the agent did not run, there is no row. The series type is therefore:

Array<{ day: string; value: number | null }>

and null must render as a break in the line, never as 0. Reporting 0 says "you found no leads
on Tuesday" when the truth is "nothing ran on Tuesday" — a plausible value standing in for absent
information. That is the same class as #243 (an unparseable limit silently meaning drop
everything
) and #252 (idle rendered over live work), both of which shipped and both of which were
only found by reading.

Worth a test asserting the two are distinguishable end to end, because it is exactly the kind of
thing that gets "simplified" to ?? 0 by a later change.

Consequences for #310's schema

Cards split into two families, declared:

family read path example
trend agent_stats_daily leads per day, runs per day
point-in-time live query current board counts, collection size now

Only trend cards need history, and for them the cost section of #310 largely dissolves — a keyed
read replaces an aggregate. Point-in-time cards keep the bounded-window discipline.

No backfill

History starts the day this ships. The tab should say so rather than presenting a three-day series
as though it were the whole picture — an honest empty state beats a chart that implies the agent
did nothing before Tuesday.

Verification

  • Two cron ticks in the same minute produce one row, not two.
  • An instance idle for a day has no row for that day, and the series carries null — asserted
    distinctly from a real zero.
  • A card removed from the schema stops being rolled up; its history is retained until the cap.
  • Rollup cost scales with active instances, not with total instances.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions