Observation time and quiet entities
By default a slot's served state is a function of an entity's events: the
state changes when a new event arrives and stays frozen while the entity is
silent. A slot whose model was trained with observation time (an
observation_time dataset and model — see training) can instead serve a state
that is a function of the events and the time it is observed at. Silence
then becomes a signal: a quiet entity's served risk moves on a schedule without
any new event, and the state says how long the entity has been quiet.
Observation time removes the blind spot of event-only scoring for quiet entities. It is not a claim that your model reacts earlier than another model; compare models on your own evaluation protocol.
The as_of contract
Every state row written by an observation-time slot records as_of, its
causal observation cutoff:
- the row is computed from the entity's events with timestamp at or before
as_of, and fromas_ofitself (the elapsed silence since the last event); as_ofis never earlier than the row'sevent_cursor.timestamp;- the same events and the same
as_ofalways give the same state, whether a realtime scorer or a batch refresh wrote it.
| What wrote the row | as_of |
|---|---|
| a new event (realtime) | the event's timestamp (silence 0) |
| a scheduled re-score sweep | the tick's grid time minus the slot's maturation_lag |
| a batch refresh | the refresh cutoff |
| a repair rebuild | the rebuilt cursor's timestamp |
A row never moves to an earlier as_of at the same event cursor: after a
promotion or a rebuild-state repair, a refresh with an earlier cutoff, a
repair or an earlier tick re-scores the row at its stored as_of, so quiet
entities keep their observed silence.
Slots without observation time are unchanged in every byte: no as_of, no
quiet state, the same fences, reads, exports and webhooks as before.
Earning enablement
A bundle that enables observation time shows you the evidence that the silence token helps this model, and you decide on it (see below). The evidence is an observation ablation: the model that consumes the token, scored against its flag-off twin and the platform's internal quality safeguards at the same evaluation points.
-
Build the observation-time dataset and its twin: the same
POST /datasetsrequest withoutobservation_time, from the samesnapshot_id. -
Train both with identical settings, except that the with-token run sets
overrides.observation_time: true. -
Run the ablation on the with-token model, naming the twin:
curl -s -X POST "$TESSERA_API_BASE/models/$MODEL_ID/observation-ablations" -H "content-type: application/json" -H "authorization: Bearer $TESSERA_API_KEY" -d "{\"without_token_model_id\": \"$TWIN_ID\"}"The response is
202withstatus: running. PollGET /models/{model_id}/observation-ablations/{report_id}until it issucceededorfailed. -
A
succeededablation reportsgates_passedandrecommend_enable. Enablement is recommended only when the model passes G1-G6 and its metric is not more than 0.002 below the twin's on every gating label. -
Create the bundle with
observation_time: {"enabled": true}. The registry re-derives the recommendation from the pinned evidence and does not trust the flag.
The ablation's report becomes the model's evaluation of record
(GET /models/{model_id}/eval-report serves it). Bundles created earlier keep
their pins. One ablation of a model runs at a time. Repeating the identical
request returns the running ablation, relaunches a failed one, or re-pins a
succeeded one.
Refusals are typed (400 failed_precondition with details[0].reason):
| Reason | Meaning |
|---|---|
observation_time_model_disabled | the model does not consume the token |
observation_ablation_twin_consumes_token | the named twin consumes it |
observation_ablation_not_twin | the named model is not the twin (another build, or other settings) |
evaluation_in_progress | another ablation of the model is running |
model_files_purged | the model's or the twin's files are being or were deleted; model_id names which |
Without a succeeded, recommending ablation, POST /models/{model_id}/bundles
with observation_time.enabled: true reports a
bundle finding:
observation_time_evidence_missing or observation_time_not_recommended
(the refusal carries observation_time_ablation_missing or
observation_time_ablation_not_recommended before the finding), with the
evidence summary and its failing arms and labels. You decide: send the code
and its facts fingerprint in acknowledge_findings with a reason and the
bundle is created (in strict mode, an admin's choice, you cannot). A model that
consumes the token cannot be bundled without enablement
(observation_time_enablement_required).
Configuring a slot
curl -s -X PUT "$TESSERA_API_BASE/slots/bank-prod/observation-time" -H "content-type: application/json" \
-H "authorization: Bearer $TESSERA_ADMIN_KEY" -d '{"enabled": true, "maturation_lag": "0d",
"silence_thresholds": ["7d", "14d", "21d"], "sweep_schedule": {"every_hours": 24}}'
enabled: truerequires the slot's active bundle to pin an observation-time model; otherwise the request fails withfailed_precondition(details[0].reason = "observation_time_bundle_required", naming the bundle).silence_thresholds(up to 8, hourshor daysd) are the silence boundaries the quiet state reports and the sweeps select on.sweep_schedulere-scores quiet entities on a fixed grid. It needsenabled: true, at least one threshold, and ascoring: realtimeslot — abatch_onlyslot getsfailed_precondition(sweeps_require_realtime_slot); use a lagged refresh there instead. On a decommissioned slot, turning sweeps on fails withfailed_precondition(slot_decommissioned): restore the slot first.maturation_lag(default0d) is your declared lagged-cutoff policy, for outcomes that are only known weeks later.
GET /slots/{slot}/observation-time returns the stored configuration (a slot
that was never configured reads enabled: false). Reading needs the read
scope; changing it needs admin, and every change is audited.
In the console, the Observation time panel on a slot's page shows and edits this configuration. Turning it on changes fingerprints, takes effect only when the slot's active bundle pins an observation-time model, and makes an explicit refresh or export cutoff later than now invalid on that slot, so enabling needs the slot name typed. Every change opens a confirmation that lists the current and the new value. A realtime slot also shows the sweep schedule; a batch-only slot does not.
Scheduled re-score sweeps
On each tick of the sweep schedule Tessera computes the tick's observation
time (scheduled_for − maturation_lag) and selects exactly the entities whose
silence crossed one of your thresholds since the last successfully published
tick. Each
selected entity is re-scored once, at that observation time, through the same
realtime scorer that handles events — so alerting, history and webhooks see a
re-score exactly like any other state write. An entity is re-scored once per
threshold it crosses, not once per tick; a new event starts a new episode.
No generated event is ever added to your event history.
The sweep schedule follows the slot's lifecycle. Deactivating the slot keeps
it: each tick is then listed as failed with the reason slot_deactivated (no
scorer, nothing re-scored), and activating a bundle again resumes the sweeps.
Decommissioning the slot pauses the schedule (the Schedules page shows it
paused, with no next tick) and keeps your configuration; restoring the slot
re-arms it when the configuration still has sweeps on, and the sweeps run again
once a bundle is active.
Quiet state
Observation-time rows also carry a quiet_state:
{
"silence_seconds": 1296000,
"silence_threshold_crossed": 1209600,
"threshold_crossed_at": "2026-07-15T09:00:00Z"
}
silence_seconds is the silence at as_of; silence_threshold_crossed is
the largest configured threshold reached (in seconds) and
threshold_crossed_at the moment it was reached (last event + threshold). Both
stay identical across consecutive quiet re-scores until the next threshold is
reached, and they disappear with the entity's next event. Alert rules select
them with the quiet_state predicate select, and the packaged
entity.went_quiet rule (POST /slots/{slot}/alert-rules:install-went-quiet)
alerts on threshold crossings — see Bands, alerts and dispositions.
Reading observation time
The existing state reads (GET …/state, scores:batch) are unchanged. Read a
row's observation-time provenance with:
curl -s "$TESSERA_API_BASE/slots/bank-prod/entities/account/acc-1041/observation" \
-H "authorization: Bearer $TESSERA_API_KEY"
The response carries the row's event_cursor, computed_at, and — for rows an
observation-time slot wrote — as_of and quiet_state.
An entity in a suppression set reads exactly like one without state (404),
as on GET …/state.
Lagged cutoffs and exports
On an enabled batch_only slot with a maturation_lag, a refresh or export
that omits cutoff uses now − maturation_lag; on a realtime slot it uses
now (its rows are in event time; the lag applies to the sweep ticks). An
explicit cutoff is used as given. The export
manifest.json records that effective cutoff, and warehouse exports that
include the as_of column carry each row's stored as_of (NULL for rows
written without observation time).
Known limits
- Sweeps select a boundary when it passes, and also catch up on data that arrives after its boundaries already passed (a backfill, a historical replay or a late batch upload): at each tick, a quiet entity whose served state was observed before the latest silence threshold it has reached is re-scored at the tick's observation time, once. When nothing readable can be re-scored (its newest events are all excluded by the contract, were retained away, or were erased), the tick records the observation on the served state instead, without re-scoring or metering: the served scores stay those of the last scored events (they are not recomputed for the new observation time), and no score-history point is written. An entity that was deleted is never written back. Changing the thresholds or the maturation lag makes the next tick re-select every crossed boundary once.
- Sweeps never create a first served state. After a bulk historical replay, run a refresh: entities that were never scored have no served state to catch up, and a sweep only re-scores entities that already have one.
- An event that arrives late with a timestamp OLDER than the entity's newest served event changes the entity's history without moving its cursor; it is reflected at the entity's next event or a refresh, not by a sweep.
- Sweeps select a boundary when it passes. An entity whose newest event arrives after its boundary already passed (a backfill or a late batch upload) is not re-scored by a sweep for that boundary; it is re-scored at its next crossed threshold, its next event, or a refresh. Changing the thresholds or the maturation lag makes the next tick re-select every crossed boundary once.
- On a slot that uses behavioral markers, check that marker windows are bounded
at the observation time before relying on them: a realtime marker window may
include events that arrived after
as_of(open finding V14 F6, tracked for a fix). - An explicit refresh or export
cutofflater than now is rejected on an observation-time slot (invalid_argument): the cutoff becomes the servedas_of, which can never be in the future. - A score history point recorded while an event's timestamp was ahead of the
scoring clock (producer clock skew) stores
as_ofequal to itscomputed_at, so that history item can show anas_ofearlier than its ownevent_cursor.timestamp; the served state keeps the event's time asas_of.