TTemporalStore.AI GitHub

Data model

Control State: counters, caps, and distinct sets.

Control State keeps the safety and throttling primitives close to the request path: bucketed counters, frequency caps, velocity checks, and distinct sets — for fraud, rate limits, ad frequency capping, and agent safety.

What it is

One request-time home for the "how many / how often / how unique" question.

Throttling and safety decisions all reduce to a small number read at request time. Control State keeps that number next to the request path so the check is a fast read, not a round trip to a separate rate-limit service or a warehouse.

  • Counter — bucketed counts, sums, and rates over sliding or tumbling windows.
  • Frequency cap — per-entity impression, action, or spend caps over an hour, a day, or a campaign, enforced at read time.
  • Velocity check — how many events in the last N minutes, to catch bursts for fraud and abuse throttling.
  • Distinct set — exact or approximate unique devices, merchants, IPs, sessions, or tools seen in a window.
  • Selection state — chosen or blocked entities kept consistent across requests, including agent safety counters beside Context Management memory.

Why TemporalStore

Why caps and distinct sets belong on the request path, not in a side service.

Throttling and safety checks share an awkward property: they sit directly on the hot path, yet they depend on recent history. The usual answers each add a moving part. A separate rate-limiter service is another network hop, another thing to scale and page on, and it only knows about the one counter you gave it — the moment fraud needs "distinct merchants this card touched in 24h" it has nowhere to put that. A warehouse has the history but answers in seconds, far too slow for an allow-or-block gate. And cache-TTL hacks — an expiring Redis key per window — approximate a window with an eviction timer: they drop old counts by throwing the key away, so you get coarse fixed windows, thundering-herd resets at the TTL boundary, and no way to ask a question the key wasn't shaped for.

TemporalStore keeps the counter, the cap, and the distinct set next to the request path as first-class temporal objects. An increment is an append-structured write — a cheap ordered append, not a read-modify-write of a shared hot key — so a viral campaign or a fraud burst does not serialize every request behind a single contended counter. The window is a real bounded read over recent events, not a TTL, so "impressions in the last 24h" is exact and rolling rather than a bucket that resets on the hour.

The checks themselves run as model-aware executors at the shard. A frequency cap is a bounded count compared to a threshold; a velocity check is a count over the last few minutes; a distinct set is an exact-or-approximate unique count over a window — each computed where the data lives, so the gate is one fast read, not a round trip to a service that then queries something else. Because each key keeps its own sparse state, the model scales to millions of counters (per user, per card, per campaign, per agent session) without a limiter cluster sized for the sum of them.

And because the same engine already holds agent memory, safety counters live beside Context: rate limits, risky-action counts, and escalation history are read in the same call as the memory an agent acts on, and they replay from the WAL after failover so the audit trail survives an incident. One temporal object — not a limiter, a warehouse, and a pile of TTL keys — answers how many, how often, and how unique.

What you'd otherwise run
Rate-limiterextra hop, one counter
Warehouseseconds too slow
TTL keyscoarse, resets hard
↓ fold onto the request path ↓
TemporalStore control state
Counters & distinct sets at the shardappend-structured · exact rolling windows · beside Context

No contended hot key on increment; the gate is one bounded read, not a round trip.

In practice

Increment, count against a cap, and count distinct.

Increment a bucketed counter
ts.incr(
  table="impressions",
  entity="camp_5:user_42",
  ts_ms=now_ms,
  by=1,
)
Check a frequency cap at read time
n = ts.count(
  table="impressions",
  entity="camp_5:user_42",
  range="24h",
)
allow = n < DAILY_CAP
Count distinct in a window
u = ts.distinct(
  table="card_touch",
  entity="card_9",
  field="merchant",
  range="24h",
)

How to use it

An end-to-end gate, from increment to allow-or-block.

Each primitive is the same two-move shape: append an event when something happens, then read a bounded count, cap, distinct, or velocity check at the moment you have to decide. The write is cheap and uncontended; the read is the gate.

1 · Increment

Record the action

An append-structured write to the entity's counter — no contended hot key.

2 · Check a cap

Count vs threshold

A bounded rolling-window count compared to the limit, at read time.

3 · Distinct & velocity

Spot the pattern

Unique fan-out and burst rate over the last minutes for fraud and abuse.

4 · Gate

Allow or block

Combine the reads into one decision on the request path, beside agent memory.

1 · Increment a bucketed counter when the action happens
ts.incr(
  table="impressions",
  entity="camp_5:user_42",     # compound key: campaign x user
  ts_ms=now_ms,
  by=1,
)

Compose the entity key to match the cap you enforce — camp_5:user_42 caps per user per campaign, user_42 caps across all campaigns. The increment is an ordered append, so concurrent writes to a hot campaign do not serialize behind a single counter.

2 · Check a frequency cap at read time
n = ts.count(
  table="impressions",
  entity="camp_5:user_42",
  range="24h",                 # exact rolling window, not a TTL bucket
)
allow = n < DAILY_CAP

The range is a true rolling window ending now, so the cap does not snap back at an hour boundary the way an expiring key does. Read the count and compare in the caller; for a strict cap under heavy concurrency, increment-then-read so the current action is included in n.

3a · Distinct fan-out in a window — unusual card activity
merchants = ts.distinct(
  table="card_touch",
  entity="card_9",
  field="merchant",
  range="24h",
)
suspicious = merchants > 20    # many distinct merchants, fast
3b · Velocity burst check — too many events in minutes
recent = ts.count(
  table="card_touch",
  entity="card_9",
  range="5m",
)
burst = recent > BURST_LIMIT   # rate spike over the last few minutes

distinct counts unique values of a field over the window (exact for small sets, approximate at high cardinality); pairing it with a short-window count catches both unusual fan-out and raw bursts. Keep velocity windows short (minutes) so a spike stands out against the baseline instead of being diluted by a long range.

4 · Agent-safety counters read beside Context memory
def guard_agent_action(session, action):
    calls = ts.count(table="agent_actions", entity=session,
                     where={"kind": action.kind}, range="1m")
    if calls >= RATE_LIMIT[action.kind]:
        return "throttle"
    ts.incr(table="agent_actions", entity=session, ts_ms=now_ms,
            by=1, attrs={"kind": action.kind})
    return "allow"                # same store as the agent's memory

Because control state lives in the same engine as Context Management, an agent's rate limits and risky-action counters are read in the same request that assembles its memory — and they replay from the WAL after failover, so the safety audit trail survives an incident. Gotchas: shape the entity key to the cap, keep windows tight on the hot path, prefer approximate distinct at very high cardinality, and increment-then-read when a cap must be strict.

When to use it

When a "yes / no" gate depends on recent activity.

Reach for Control State whenever an allow-or-block decision hinges on how much, how often, or how many distinct things an entity has done lately — and the check sits on the hot path.

Use casePrimitiveWhy it fits
Ad frequency capsFrequency capEnforces per-user impression limits over hour/day/campaign windows.
API rate limitsCounterBucketed counts per key gate requests without a separate limiter.
Fraud velocityVelocity + distinctFlags bursts and unusual distinct-merchant fan-out in minutes.
Agent safetySelection + countersRate limits and risky-action counters beside agent memory.

Related models

Control state pairs with history and aggregates.

AggregatesAggregated FeatureFiltered rollups over the same events. SequencesLong Sequence FeatureOrdered behavior history. FlagshipContext ManagementSafety counters in agent memory.