Skip to main content

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 from as_of itself (the elapsed silence since the last event);
  • as_of is never earlier than the row's event_cursor.timestamp;
  • the same events and the same as_of always give the same state, whether a realtime scorer or a batch refresh wrote it.
What wrote the rowas_of
a new event (realtime)the event's timestamp (silence 0)
a scheduled re-score sweepthe tick's grid time minus the slot's maturation_lag
a batch refreshthe refresh cutoff
a repair rebuildthe 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.

  1. Build the observation-time dataset and its twin: the same POST /datasets request without observation_time, from the same snapshot_id.

  2. Train both with identical settings, except that the with-token run sets overrides.observation_time: true.

  3. 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 202 with status: running. Poll GET /models/{model_id}/observation-ablations/{report_id} until it is succeeded or failed.

  4. A succeeded ablation reports gates_passed and recommend_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.

  5. 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):

ReasonMeaning
observation_time_model_disabledthe model does not consume the token
observation_ablation_twin_consumes_tokenthe named twin consumes it
observation_ablation_not_twinthe named model is not the twin (another build, or other settings)
evaluation_in_progressanother ablation of the model is running
model_files_purgedthe 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: true requires the slot's active bundle to pin an observation-time model; otherwise the request fails with failed_precondition (details[0].reason = "observation_time_bundle_required", naming the bundle).
  • silence_thresholds (up to 8, hours h or days d) are the silence boundaries the quiet state reports and the sweeps select on.
  • sweep_schedule re-scores quiet entities on a fixed grid. It needs enabled: true, at least one threshold, and a scoring: realtime slot — a batch_only slot gets failed_precondition (sweeps_require_realtime_slot); use a lagged refresh there instead. On a decommissioned slot, turning sweeps on fails with failed_precondition (slot_decommissioned): restore the slot first.
  • maturation_lag (default 0d) 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 cutoff later than now is rejected on an observation-time slot (invalid_argument): the cutoff becomes the served as_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_of equal to its computed_at, so that history item can show an as_of earlier than its own event_cursor.timestamp; the served state keeps the event's time as as_of.