Recency Aggregation Operators
The 10 recency ops cover time-since semantics (how long since first / most-recent match), windowed-recency booleans (was the last match within window N?), and streak counters (consecutive matches and non-matches). All recency ops use server processing-time (now_ms() at the apply path) per project_redis_shaped_no_event_time_ever — beava intentionally has no event-time concept.
| Op | Returns | Time source | Notes |
|---|---|---|---|
bv.first_seen |
Datetime (i64 ms) |
server now_ms() at apply |
First match's server arrival timestamp |
bv.last_seen |
Datetime (i64 ms) |
server now_ms() at apply |
Most recent match's server arrival timestamp |
bv.age |
i64 ms |
computed at read time | now_ms() - first_seen |
bv.has_seen |
bool |
n/a | Cumulative ever-matched flag |
bv.time_since |
i64 ms or null |
computed at read time | now_ms() - last_seen |
bv.time_since_last_n |
i64 ms or null |
computed at read time; n required |
Generalization: ms since the kth most recent match |
bv.streak |
i64 |
event arrival order | Live consecutive-match counter |
bv.max_streak |
i64 |
event arrival order | All-time max of streak; never decreases |
bv.negative_streak |
i64 |
event arrival order | Live consecutive-non-match counter (mirror of streak) |
bv.first_seen_in_window |
bool |
now_ms() - last_ms < window at read time; window= required |
"Was the last match within the last N ms?" |
Key invariants
- Server processing-time only. Per
project_redis_shaped_no_event_time_ever(locked 2026-04-30), beava records servernow_ms()at apply forfirst_seen/last_seen, and computes elapsed-ms using servernow_ms()at read forage/time_since/time_since_last_n/first_seen_in_window. Producers cannot influence captured timestamps via payload fields. - Read-time computation.
age,time_since,time_since_last_n, andfirst_seen_in_windowchange between reads without any new events — the right-hand side of the elapsed calculation is captured at query time, not apply time. This makes them useful staleness/recency features. bv.time_since_last_nrequiresn. Per V0-MEM-GOV-02 BoundedByRequiredKwarg("n"), the deque of timestamps must have a register-time ceiling. Missingnis rejected by the JSON-prelude shim with codeunbounded_op_in_lifetime_mode.bv.first_seen_in_windowrequireswindow. The windowed-recency boolean is meaningless without a horizon length. Thewindowparameter is enforced at register time;"forever"is rejected (usebv.has_seenfor that semantic).- Cold-start behavior is per-op. Streaks return
0. Booleans (has_seen,first_seen_in_window) returnfalse. Datetime/duration ops (first_seen,last_seen,age,time_since,time_since_last_n) returnnull. - Cold-entity eviction (
@bv.event(cold_after=...)) drops the underlying state per the Redis-TTL pattern (V0-MEM-GOV-01); recency state rebuilds from the next post-eviction event. - 9 of 10 ops share
SeenState(first_seen,last_seen,age,has_seen,time_since) or relatedStreakState(streak,max_streak) /NegativeStreakState/FirstSeenInWindowState— registering several siblings on the samewhere=predicate costs roughly the same as registering one.
See also
- Operator catalog index — full 53-op catalogue
- cost-class.md — per-op CPU tier metadata (all recency ops are Tier 1)
- Point/ordinal family — value-based first/last/N counterparts to the timestamp ops here
- Per-operator memory governance: V0-MEM-GOV-02 — every lifetime aggregation operator declares a finite per-entity memory ceiling at register-time
- Pipeline DSL compilation rules — how
bv.<op>(...)calls compile to JSON wire form