<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Tessera changelog</title>
  <id>https://docs.tessera.dev/changelog.xml</id>
  <link href="https://docs.staging.tessera.zone/changelog/" rel="alternate"/>
  <link href="https://docs.staging.tessera.zone/changelog.xml" rel="self"/>
  <updated>2026-10-22T00:00:00Z</updated>
  <author><name>Tessera</name></author>
  <entry>
    <title>Webhook deliveries say whether they are a test send</title>
    <id>https://docs.tessera.dev/changelog/#webhook-delivery-kind-header</id>
    <link href="https://docs.staging.tessera.zone/changelog/#webhook-delivery-kind-header" rel="alternate"/>
    <updated>2026-10-22T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="docs"/>
    <content type="text">From this release on, every webhook delivery carries a `Tessera-Delivery-Kind` header. It is
`test` for a delivery queued by `POST /webhook-endpoints/{endpoint_id}:send-sample`
(**Send sample** in the console), including its retries and replays, and `event`
for every other delivery. Check it after verifying the signature to keep test
sends out of your business logic. Tessera sets the header from the operation
that queued the delivery; endpoint settings cannot set it. The envelope is
unchanged, and so is `Tessera-Signature`: the header is not part of the signed
message, so existing receivers keep verifying without any change. Treat a
delivery without the header as a real delivery: attempts made while the release
rolls out, and requests that pass a proxy which strips unknown headers, can
arrive without it. See
[Recognising a test send](/platform/webhooks#recognising-a-test-send).</content>
  </entry>
  <entry>
    <title>Neutral wording across the docs, website, console and API descriptions</title>
    <id>https://docs.tessera.dev/changelog/#neutral-wording-corrections</id>
    <link href="https://docs.staging.tessera.zone/changelog/#neutral-wording-corrections" rel="alternate"/>
    <updated>2026-10-22T00:00:00Z</updated>
    <category term="fixed"/><category term="docs"/><category term="console"/><category term="api"/>
    <content type="text">Wording that described sample material in terms the product does not use for
itself has been replaced in the docs, the website, the console and the API
reference descriptions. The meaning of each sentence is unchanged:

- Earlier release entries, the guides and the API reference now describe
  sample data as generated, the webhook test send as a test event, and the
  enterprise contact path as "Contact us".
- Example identifiers in the examples for the planned Python SDK use a `sdk_sample_` prefix.
- Role changes that lower a member's role are described as downgrades.
- No API field, enum value, event type or response shape changed. The few
  identifiers that are part of the closed v1 contract keep their names.</content>
  </entry>
  <entry>
    <title>Label previews cost less per file, and say when their read time ran out</title>
    <id>https://docs.tessera.dev/changelog/#label-preview-read-cost</id>
    <link href="https://docs.staging.tessera.zone/changelog/#label-preview-read-cost" rel="alternate"/>
    <updated>2026-10-22T00:00:00Z</updated>
    <category term="fixed"/><category term="api"/><category term="console"/>
    <content type="text">On a store of many small files (thousands a day), `POST /v1/labels:preview`
and `POST /v1/pattern-labels:preview` read their whole 512-file budget so
that they reach days whose outcomes are known. Each file was then parsed up
to three times and decoded whole in the history pass, so these previews took
10-19 s, close to their 12 s read time limit. When that limit stopped a
preview before it reached those days, every sampled anchor came back censored
with no explanation.

Each file is now fetched once, its Parquet footer is parsed once, and a part
is decoded whole only when it holds a sampled entity. The answers are byte for
byte the same. When the read time limit does stop a preview, its response
carries the new warning `preview_read_limit_reached`, and the console explains
that the counts cover only the days read. `warnings` is a list of strings, so
existing API clients keep working.</content>
  </entry>
  <entry>
    <title>Dossier and card renders: renderer version 3 shows only the comparisons that matter</title>
    <id>https://docs.tessera.dev/changelog/#governance-renderer-v3</id>
    <link href="https://docs.staging.tessera.zone/changelog/#governance-renderer-v3" rel="alternate"/>
    <updated>2026-10-22T00:00:00Z</updated>
    <category term="changed"/><category term="api"/><category term="models"/><category term="console"/><category term="docs"/>
    <content type="text">Dossier downloads (`GET /dossiers/{dossier_id}:download`) and model card
exports (`GET /models/{model_id}/card:export`) in `markdown` and `pdf` now come
from governance renderer version 3 (`renderer_version: "3"`).

- A verdict criterion that compares the model with one of the platform's
  internal quality safeguards is left out of the render whole, with its
  status, rule and reason. This applies to protocol verdicts and to backtest
  results alike.
- A verdict that lost criteria this way shows
  `verdict_over_comparisons_shown`: its overall over the criteria shown (`pass`
  only if every one passes, `fail` if any fails, otherwise `incomplete`). When
  no criterion is left, the render shows no overall.
- The console's proof center follows the same rule. The "Criteria met" count of
  an evaluation or a backtest covers the comparisons shown, and a line under it
  says so. A backtest shows the model's curve and the comparators' curves and
  paired comparisons.
- A comparator you uploaded reads by one name everywhere: its declared name,
  or "Comparator 1", "Comparator 2" and so on, numbered among the comparators
  a page or render shows. Dossier renders list each name beside the
  comparator's id.
- The proof center's protocol pages no longer list the internal quality
  safeguards a protocol declares, and dossier renders leave a protocol's list of
  them out; your comparators stay.
- The model page's G1 "Internal check" shows its state only (met, not met,
  not settled or could not run). When it is not met, one sentence says an
  internal reference model scored higher than this model by more than the
  declared margin; the acknowledgement dialog says the label did not meet the
  quality bar, as dossier renders do. No
  name, id or number of the internal model is shown.
- Messages no longer name the internal models: a fallback candidate that could
  not be trained reads "no internal fallback candidate completed for label(s)
  …", and the study page and the proof center state older stored messages
  neutrally. The guides no longer describe the internal models either.

The API responses, the stored reports and the `json` dossier download are
unchanged and keep every criterion and the stored `overall`. Stored dossiers
keep their bytes and fingerprints. A new render of an unchanged dossier or card
can have a different `sha256`; `renderer_version` tells the renderers apart.</content>
  </entry>
  <entry>
    <title>Dossiers now include the model's settled backtests</title>
    <id>https://docs.tessera.dev/changelog/#dossier-backtests</id>
    <link href="https://docs.staging.tessera.zone/changelog/#dossier-backtests" rel="alternate"/>
    <updated>2026-10-22T00:00:00Z</updated>
    <category term="changed"/><category term="api"/><category term="docs"/>
    <content type="text">The backtesting section of a dossier (`sr11-7.v1`, `sr11-7.v2`) now embeds
the settled backtests of the documented model, newest first, instead of
reading `evidence: missing` with `source_not_bound`.

- **What is embedded**: one entry per backtest that succeeded and whose report
  is stored with its fingerprint. The entry is the stored report as it is:
  the cutoffs, the lead-time results per label and arm, each arm's `regime`,
  and the lead-time verdict when the protocol declares lead-time metrics.
- **Verified at assembly**: the report's bytes are checked against their
  recorded fingerprint, and the report must name the same backtest, project,
  protocol, model and engine as the backtest record. Otherwise the dossier is
  refused (`failed_precondition`), as for protocol verdicts.
- **Provenance**: each entry carries the report's provenance claim
  (`fixture`, `design-partner` or `production`) as recorded. The markdown and
  PDF downloads print it beside the numbers.
- **Budget**: reports are embedded newest first up to 1 MiB of report JSON,
  so the number of backtests no longer decides whether a dossier fits. The
  newest report is always embedded, even when it alone is larger, and a
  misreported stored size can let one more verified report in. When some are left out, a last entry (source
  `listBacktests`) lists them, newest first (up to 200, with the total and
  `truncated`). Reports left out are not read.
- **No settled backtest**: running and failed backtests are never embedded. A
  model without a settled backtest reads `evidence: missing` with the new
  reason `not_backtested`, and `detail` says how many backtests were requested
  and failed. `source_not_bound` keeps its meaning: no source in this
  deployment.

Dossiers assembled before this change are unchanged. No API response gains a
field.</content>
  </entry>
  <entry>
    <title>A refresh of a realtime slot now writes the realtime scorer's exact scores</title>
    <id>https://docs.tessera.dev/changelog/#wp-v14-realtime-refresh-parity</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-v14-realtime-refresh-parity" rel="alternate"/>
    <updated>2026-10-21T00:00:00Z</updated>
    <category term="changed"/><category term="models"/>
    <content type="text">A refresh of a slot that scores in real time now writes exactly the scores
and embeddings that the slot's realtime scorer writes for the same events.
Before, the refresh ran the model in full precision while a tier S realtime
scorer runs it int8-quantized, so the two disagreed slightly on every entity.
To make the two equal on any node, realtime scorers and every refresh Job now
run the same CPU kernels (AVX2, with the vendor-neutral branch of the math
library). Tested on a real AMD AVX-512 CPU, and under emulation on Intel CPUs
with and without AVX-512 (qemu, Intel SDE) and an AMD AVX2 CPU (qemu).

- **One-time change in realtime scores**: when the realtime scorers restart
  with this release, the scores they write move once. On slots that serve the
  full-precision fallback (or semantic bundles), every entity moves in the last
  digits (up to about 1e-7). On int8 slots most entities move (44 of 48
  measured on a full tier S model), by up to about 4e-4 in scores and 5e-3 per
  embedding component, so an entity close to a band edge can change band once.
- **One-time change in refreshes**: on tier S realtime slots, the next refresh
  moves each entity's stored embedding by up to about 0.04 per component and
  its scores in the fourth decimal, to the realtime scorer's values. Refreshes
  of batch-only slots also move once in the last digits, because their Jobs
  run the same pinned CPU kernels.
- **Cost**: on an AMD AVX-512 machine at two threads (full tier S model), a
  realtime int8 forward takes about 21% to 29% longer for 64 to 256 event
  windows, 41% at 512 and 59% at 1024; a full-precision forward (the int8
  fallback and semantic bundles) about 58% to 65% longer at 64 to 256 events
  and about 86% to 88% at 512 to 1024. The scorer's startup check of the int8 model
  takes about 0.2 to 0.3 s longer. A batch-only refresh forwards about 45% slower.
- **Faster realtime-slot refreshes**: a refresh of a realtime slot now forwards
  on the scorer's two threads, which stay inside the Job's CPU limit: still
  about 2x to 9x faster per entity than before this release for windows up to
  256 events, and about the same at 1024.
- **Not covered**: tier M and L slots, whose realtime scorer runs on a GPU
  while refreshes run on CPUs, still differ slightly between the two paths.

## Rollout

- Ship scoring-service and serving-api in the same release: the realtime
  scorers get the kernel pin from the scoring controller, refresh Jobs from
  serving-api, and until both are live the two sides run different kernels.
- Before rollout, read each live realtime slot's int8 state (the
  `scoring_int8_fallback` gauge) and the CPU platform of the node its scorer
  runs on, to know which cost row applies.
- The release restarts every realtime scorer (rolling, no downtime), and each
  restart re-runs the int8 self-validation on the pinned kernels. After the
  rollout, check `scoring_int8_fallback` for every slot (a bundle near the
  validation bounds can switch between int8 and full precision),
  `scoring_cpu_kernel_pin_mismatches` (0 on every scorer) and the forward p95
  (`scoring_stage_seconds{stage="forward"}`).
- Expect a one-time wave of band transitions on live int8 slots: entities
  near a band edge can cross it once as they are re-scored after the scorers
  restart (and on the next refresh of a realtime slot). For those entities the
  wave can trigger alerts, destination triggers and segment migrations. Plan the
  release in a quiet window and tell the slot's operator in advance.
- Re-run the realtime-versus-refresh equality check only after both services
  are live and every scorer has restarted.</content>
  </entry>
  <entry>
    <title>Availability wording corrected across the docs, website and console</title>
    <id>https://docs.tessera.dev/changelog/#availability-wording-corrections</id>
    <link href="https://docs.staging.tessera.zone/changelog/#availability-wording-corrections" rel="alternate"/>
    <updated>2026-10-21T00:00:00Z</updated>
    <category term="fixed"/><category term="docs"/><category term="console"/>
    <content type="text">Wording that read as available for integrations the shared catalog still lists
as Planned has been corrected:

- Nine earlier release entries now open with a dated correction, with the
  original text kept below it. Four cover these Planned items: the
  TypeScript package, the Kafka and Kinesis bridges, and the Segment and
  RudderStack destinations. Five referred to the Python or TypeScript SDK as
  if a package could be installed.
- The solutions pages and the website home page no longer list the planned
  SDKs, CDP destinations or bus bridges among the ways to send events today.
  The HTTPS ingestion API, file upload and the warehouse and storage sources
  are available; the rest are labelled planned.
- The setup label that read "API / SDK setup" (both SDKs are planned) is now
  "API setup" in the console source catalog, the docs capability matrix and
  the website integration strip. No setup path changed status.</content>
  </entry>
  <entry>
    <title>The trajectory inspector opens prefixes with CPU-profile enrichment results</title>
    <id>https://docs.tessera.dev/changelog/#trajectory-inspector-cpu-recipe</id>
    <link href="https://docs.staging.tessera.zone/changelog/#trajectory-inspector-cpu-recipe" rel="alternate"/>
    <updated>2026-10-20T00:00:00Z</updated>
    <category term="fixed"/><category term="changed"/><category term="api"/><category term="sdks"/>
    <content type="text">**Correction (2026-10-21):** the SDK packages are planned, and the
integration catalog lists both SDKs as Planned; this note told readers to
upgrade the SDK. The API answer described below is what changed. A client built from the repository's SDK sources must be
rebuilt from the current sources to parse it; the `Backend` and `Dtype` enums
below are in those sources.


`GET /v1/slots/{slot}/entities/{entity_type}/{entity_id}/trajectories/{prefix_id}`
answered 500 for every prefix whose feature view holds an enrichment result
computed with the CPU execution profile. It now answers 200 and serves each
stored result. Prefixes without such a result are unchanged.

In the answer, `anchors[].result.recipe.backend` and `anchors[].result.recipe.dtype`
take one of exactly two pairs: `torch` with `bfloat16` (the default CUDA
profile) or `torch-cpu.v1` with `float32` (the CPU profile). No field was added.
SDKs released before this change accept only the first pair, so they cannot
parse an answer that holds a CPU-profile result; that answer was a 500 before.
Upgrade the SDK to read it. In the Python SDK, `Recipe.backend` and
`Recipe.dtype` are now the enums `Backend` and `Dtype`, no longer plain
strings: compare with `Backend.torch`, or read `.value`.

The 2026-10-19 note on large-prefix reads now carries a dated correction:
on our hosted deployment, prefixes of 610 and 2,048 events still exceed the 15 s read
budget and answer the 503 `trajectory_read_budget`.</content>
  </entry>
  <entry>
    <title>The trajectory inspector does less work for prefixes with captured original sources</title>
    <id>https://docs.tessera.dev/changelog/#trajectory-inspector-captured-sources</id>
    <link href="https://docs.staging.tessera.zone/changelog/#trajectory-inspector-captured-sources" rel="alternate"/>
    <updated>2026-10-20T00:00:00Z</updated>
    <category term="fixed"/><category term="api"/>
    <content type="text">`GET /v1/slots/{slot}/entities/{entity_type}/{entity_id}/trajectories/{prefix_id}`
now verifies each retained intake batch it reads with one hash over the
batch's stored text, instead of re-encoding every record of the batch, and
reads each captured input once instead of twice. For prefixes whose original
sources are captured, this removes most of the time spent verifying retained
batches and one storage read per captured event. The answer is unchanged,
byte for byte.

Prefixes of about 2,000 events whose original sources are all captured can
still run past the inspector's 15 s read budget and answer the 503
`trajectory_read_budget`.</content>
  </entry>
  <entry>
    <title>The customer map updates every night, and Members can update it now</title>
    <id>https://docs.tessera.dev/changelog/#customer-map</id>
    <link href="https://docs.staging.tessera.zone/changelog/#customer-map" rel="alternate"/>
    <updated>2026-10-20T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="console"/><category term="docs"/>
    <content type="text">The customer map now keeps itself up to date. Maps with customer groups update every night
at 03:00 UTC by default, and the map you see is swapped for the new one when it is ready.
A map with no standing credential to run as waits for an Admin.

- **Update now**: Members and Admins can start an update from the Customer map page or
  with `POST /customer-map:refresh`. It takes no body and uses one fixed recipe. One update
  runs at a time, and a cooldown applies after each successful one (at least 15 minutes for
  everyone but an Admin session, with at most 24 updates a day from non-Admins).
- **Map settings**: Admins choose nightly, weekly, monthly, a custom interval of at least
  1 hour, or only on request; the time and an IANA time zone; a pause; the cooldown; and the
  slot the map follows. Read and write them with `GET` and `PUT /customer-map/settings`.
- **Pin**: an Admin can keep one map as the current map until a date, up to 90 days ahead.
  Updates resume after it.
- **History**: `GET /customer-map/editions` lists the maps of the current slot, newest first,
  with what changed and whether a map was held back after an unusually large change.
- **Schedules**: the map's schedule appears in `GET /schedules` as kind `customer_map`, with a
  link to the map settings. It is changed there, not from the schedule list.

Map updates meter nothing, so they add no `export_rows` usage. They publish `export.completed`
and `export.failed` with `data.kind` set to `projection_2d`; a succeeded update's
`data.metadata` carries `layout`, `verdict` and `alignment`, which a manual projection does not.

Four new operations (`refreshCustomerMap`, `getCustomerMapSettings`, `putCustomerMapSettings` and
`listCustomerMapEditions`) are additive and change no existing response; `getCustomerMap` now
fills in the schedule, the pin and the latest change. The new permissions are
`customer_map.settings.get` and `customer_map.editions.list` for readers, `customer_map.refresh` for
Members and `customer_map.settings.put` for Admins; a custom role made earlier needs them added. See [Customer map](/serve/customer-map/).</content>
  </entry>
  <entry>
    <title>A five-step story on the segment map</title>
    <id>https://docs.tessera.dev/changelog/#atlas-map-story</id>
    <link href="https://docs.staging.tessera.zone/changelog/#atlas-map-story" rel="alternate"/>
    <updated>2026-10-20T00:00:00Z</updated>
    <category term="added"/><category term="console"/>
    <content type="text">"Present", the `P` key or the presenter bar on a map chapter now starts a five-step
story on the segment map: the whole bank, the regions that emerge on their own, a
high-churn region chosen by rule, one of its customers with their stored predictions,
and the region explained. Every number in a caption is read from the map on screen.

- The map page is read-only while the story runs: write controls show themselves
  disabled with the reason, and the console sends no write.
- `?story=1` to `?story=5` in the address opens a step directly.
- A step with nothing to show, such as a region with no stored predictions, is left
  out.</content>
  </entry>
  <entry>
    <title>The trajectory inspector reads large prefixes in seconds</title>
    <id>https://docs.tessera.dev/changelog/#trajectory-inspector-read-budget</id>
    <link href="https://docs.staging.tessera.zone/changelog/#trajectory-inspector-read-budget" rel="alternate"/>
    <updated>2026-10-19T00:00:00Z</updated>
    <category term="fixed"/><category term="api"/>
    <content type="text">`GET /v1/slots/{slot}/entities/{entity_type}/{entity_id}/trajectories/{prefix_id}`
no longer reads a prefix's stored objects one after another. A 610-event
prefix, which could run past the gateway's 25 s read and fail with 503, now
answers in a few seconds, and larger prefixes in proportion. The answer is
unchanged, byte for byte.

When a prefix still cannot be read within the inspector's read budget (15 s),
the answer is 503 `unavailable` with
`details[0] = {"type": "unavailable", "reason": "trajectory_read_budget"}`,
`retry_after_seconds` and a `Retry-After` header: 30 seconds when the budget
ran out (a very large prefix, or a replica busy with other inspections), 1
second when a storage call timed out. Retry after that delay. The response
shapes are unchanged.

**Correction (2026-10-20):** the timings above were measured outside our hosted
deployment, with a simulated storage latency. On our hosted deployment, prefixes of 610 and 2,048 events
still run past the 15 s read budget and answer the 503 `trajectory_read_budget`,
also when retried after the `Retry-After` delay. 33-event prefixes answer in
about 1 s server-side. A follow-up change for the largest prefixes is in
progress.</content>
  </entry>
  <entry>
    <title>The segment map outlines each segment as a region, with computed names and four zoom levels</title>
    <id>https://docs.tessera.dev/changelog/#atlas-map-regions</id>
    <link href="https://docs.staging.tessera.zone/changelog/#atlas-map-regions" rel="alternate"/>
    <updated>2026-10-19T00:00:00Z</updated>
    <category term="added"/><category term="console"/>
    <content type="text">The segment map now draws each segment as a region: a soft outline around where its
customers sit on the map, on a dark map surface.

- **Region names**: a segment named by a person, or by the AI (with its AI badge), keeps
  that name. A segment with neither is named by its computed descriptor, such as
  "Higher churn risk", tagged **Computed**, with "Segment #N" as a small id under it.
  Descriptors come from a fixed rule over the map's prediction scores, never from AI,
  and the region list shows the sentences behind each one.
- **Four zoom levels**: Bank (one title for the whole map), Regions, Sub-regions and
  Customers. The wheel, the zoom buttons, the level buttons and the keys `+`, `-`, `0`
  and `1`-`4` move between them; each level shows its own labels.
- **Sub-regions**: dense areas inside a region, lettered by size (for example "4a"),
  each described against the rest of its region.
- **Region list**: the regions and their sub-regions as a keyboard tree beside the map.
  Enter selects one, opens the selection panel with its customers and moves the map to it.
- **Dark or page theme**: the map is dark in both page themes; "Match page" follows the
  page instead, and the choice is remembered in the browser.
- A map built before segment ids were attached shows up to eight density regions,
  "Map region A" onward, and says that no segment build is attached.</content>
  </entry>
  <entry>
    <title>The segment map opens with its customers settling into place</title>
    <id>https://docs.tessera.dev/changelog/#atlas-map-opening</id>
    <link href="https://docs.staging.tessera.zone/changelog/#atlas-map-opening" rel="alternate"/>
    <updated>2026-10-19T00:00:00Z</updated>
    <category term="added"/><category term="console"/>
    <content type="text">The first time an export's segment map opens in a browser tab, its points start as
one cloud and settle into their positions over about two seconds. The segment names
then fade in, and screen readers hear how many customers the map drew.

- **Once per export per tab**: going back to the map, or reloading it, draws it at once.
- **Skipped** when reduced motion is on, when the map opens on one customer, on a
  story or tour step or at a camera kept from earlier in the tab, in a hidden tab, on a
  software graphics renderer, or with `?nointro=1` in the address. The map then draws
  its final positions in its first frame.
- **Any input ends it**: a click, a scroll, a touch or a key shows the map as it ends.
- **Large maps**: above 20,000 points the same even sample a pan draws moves, and the
  full map takes its place at the end.</content>
  </entry>
  <entry>
    <title>Ask the map answers verified questions about the segment map</title>
    <id>https://docs.tessera.dev/changelog/#atlas-ask-the-map</id>
    <link href="https://docs.staging.tessera.zone/changelog/#atlas-ask-the-map" rel="alternate"/>
    <updated>2026-10-19T00:00:00Z</updated>
    <category term="added"/><category term="console"/>
    <content type="text">"Ask the map" sits above the segment map. It answers a fixed set of twelve verified
questions about the customers on the map, such as who is most likely to leave or which
region holds the most of them, and highlights the answer on the map.

- **Verified questions only**: typed text runs a question only when it is one of the
  verified wordings. Any other text offers the closest verified questions to pick from,
  with the words a pick would leave out.
- **Answered in the browser**: the answer is worked out from the map's own data and
  written by code; no AI model is called. An exact audience count is fetched only when
  someone asks for it.
- **Labels**: an unedited question is marked "Verified question"; after a chip edit the
  answer says which verified question it started from.
- **Regions by one name**: with the region outlines on, the answer names each region
  the way the map does.

Free-form questions answered by AI stay off.</content>
  </entry>
  <entry>
    <title>Final CPU discovery deadlines from the measured latency</title>
    <id>https://docs.tessera.dev/changelog/#semantic-cpu-discovery-deadlines-final</id>
    <link href="https://docs.staging.tessera.zone/changelog/#semantic-cpu-discovery-deadlines-final" rel="alternate"/>
    <updated>2026-10-17T00:00:00Z</updated>
    <category term="changed"/><category term="models"/>
    <content type="text">The CPU structural-discovery deadlines on our hosted deployment are now final. The CPU semantic
image's own latency measurement took 20 uncensored cold requests under load, with
a 95th percentile of 95.9 seconds. The discovery server's request budget is now
384 seconds (at least four times that percentile) and the contract-service
waits 414 seconds, replacing the provisional 300 and 330 seconds from 2026-10-12.
The contract-service termination grace follows the wait (474 seconds).
The longer client wait rolls out first, and the CPU workers restart with the new
budget only after it is live. GPU workers and the 60-second defaults are
unchanged.</content>
  </entry>
  <entry>
    <title>Test credentials can no longer lift live self-exclusions, delete live files sooner or erase entities</title>
    <id>https://docs.tessera.dev/changelog/#test-environment-keeps-live-data</id>
    <link href="https://docs.staging.tessera.zone/changelog/#test-environment-keeps-live-data" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="changed"/><category term="security"/><category term="api"/><category term="console"/>
    <content type="text">Suppression sets and model and dataset file retention belong to the project
and apply in both its live and test environments. A request acting in the
test environment (a `tsk_test_` key, or the console switched to test) can now
only add to them:

- **Suppression sets**: removing entries (`entries:remove`), deleting a set,
  removing a purpose from a set or changing its predicate is refused with
  `permission_denied` (`environment_not_allowed`), because it would also lift
  a live self-exclusion. Creating sets, adding entries and purposes, editing
  a description and deleting an empty list set still work.
- **Model and dataset file retention**: turning retention on or shortening it
  is refused the same way, because it would make live files be deleted
  sooner. Lengthening it or turning it off still works.

Entity erasure is refused in the test environment too:
`POST /entities/{entity_type}/{entity_id}:delete` erases and tombstones the
entity in both environments, so a request acting in test gets the same
`permission_denied` (`environment_not_allowed`), no deletion is queued and no
`entity.delete_requested` audit event is written (the refusal itself is
audited).

Make these changes with a live key, or from the console in the live
environment. A registered application may still turn retention on or shorten
it, and erase entities, in the projects it owns, also when it acts in test.
In the console, these actions now show the refusal in test mode; disabling
them there is a follow-up.</content>
  </entry>
  <entry>
    <title>Retried requests no longer keep one-time secrets in plaintext</title>
    <id>https://docs.tessera.dev/changelog/#sealed-idempotent-secret-replays</id>
    <link href="https://docs.staging.tessera.zone/changelog/#sealed-idempotent-secret-replays" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="security"/><category term="fixed"/><category term="api"/>
    <content type="text">A `POST` sent with an `Idempotency-Key` stores its successful response for
24 hours, so that a retry after a lost response gets the same answer. The
console sends a key with every write. Until now that stored copy was plain,
so the one-time secrets these operations return stayed readable in the
platform's database, in its backups and, when storing the copy failed, in its
logs:

- `createApiKey`: the API key;
- `createWebhookEndpoint` and `rotateWebhookEndpointSecret`: the signing
  secret;
- `createOrgInvite` and `createOrgRoleInvite`: the invite token;
- `createUpload`: the signed `upload_url`;
- `createBillingCheckoutSession` and `createBillingPortalSession`: the hosted
  Stripe session `url`.

These responses are now stored encrypted (AES-256-GCM) under a key derived
from a server secret and your request's own `Idempotency-Key`, which is never
stored. Only a request that sends the same `Idempotency-Key` from the same
credential opens it. A request with another key or credential is a new
request and runs.

- **Retries work as before.** The same `Idempotency-Key` from the same
  credential within 24 hours returns the identical response, the same key,
  secret, token or URL, and does nothing a second time. In particular, a
  retried `rotateWebhookEndpointSecret` never rotates twice.
- **A stored response that can no longer be opened** returns 409
  `already_exists` with `reason: idempotency_key_reused` instead of running
  the request again. This happens when the server secret it was encrypted
  under has been retired. Send the request again with a new
  `Idempotency-Key`.
- **A stored response that cannot be opened right now**, because the server
  secret is briefly unavailable, returns 503 `unavailable` with a
  `Retry-After` header. Retry with the same `Idempotency-Key`.
- **Logs.** Database errors no longer include the values of the failing
  statement.

Response shapes are unchanged.

**Transition.** Stored copies made before this release are deleted when it
deploys. Retrying such a request within its 24-hour window therefore runs it
again instead of replaying it: you get a second API key, webhook endpoint or
rotation, a new upload session or a new Stripe session, and a repeated invite
returns `already_exists`. While the release rolls out, a request answered by
a server still on the previous release is stored the old way, and a retry
that reaches an updated server gets the 409 above for a few minutes, until
that copy is deleted; a retry after that runs again. If a response was lost
around the release, check your API keys and webhook endpoints and revoke or
delete the ones you did not receive. If gateway-api is rolled back, the
previous release cannot read a copy this release stored and returns an
unreadable body for that retry; retry with a new `Idempotency-Key`.</content>
  </entry>
  <entry>
    <title>A queued training run says when it is waiting for compute</title>
    <id>https://docs.tessera.dev/changelog/#queued-run-waiting-for-compute</id>
    <link href="https://docs.staging.tessera.zone/changelog/#queued-run-waiting-for-compute" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="models"/>
    <content type="text">A training run whose compute class has no capacity (its pods cannot be
scheduled, for example while a GPU pool is out of capacity and no node can be
added) no longer sits `queued` without a reason. After five minutes of waiting,
its logs (`GET /runs/{id}/logs`, and the run page's log viewer) carry a
`WARNING` entry: "waiting for compute: no gpu-l4 capacity is available right
now; the run starts automatically when capacity frees up, or re-post it on
another compute class". The entry names the class the run waits on, is
repeated periodically while the run still waits, and appears again for a new
class when the fallback ladder moves the run. It is information only: the
run's status, error and compute classes are unchanged, and the run starts on
its own as soon as capacity frees up.</content>
  </entry>
  <entry>
    <title>Rule previews leave out an entity as soon as its erasure starts</title>
    <id>https://docs.tessera.dev/changelog/#preview-erasure-fence</id>
    <link href="https://docs.staging.tessera.zone/changelog/#preview-erasure-fence" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="security"/><category term="api"/><category term="docs"/>
    <content type="text">`POST /v1/labels:preview`, `POST /v1/pattern-labels:preview` and
`POST /v1/markers:preview` no longer count or return an entity whose deletion
has written its tombstone. Before, such an entity could still appear in a
preview until the deletion finished rewriting the event store. The entity is
left out and not replaced, so while a deletion completes, a preview's
`sampled_entities` can be smaller than the number requested. Its events no
longer set the label previews' censor clock (the type's newest event), so
they never decide which other anchors are graded. If that entity's erasure
starts while a label preview runs, the preview returns 503 `unavailable`
with `Retry-After`; retry it.

A pattern-label preview retried with the same `Idempotency-Key` now runs again
instead of replaying a stored answer, as the label and marker previews already
did. So do `POST /v1/slots/{slot}/scores:compute`,
`POST /v1/slots/{slot}/bands:derive` and
`POST /v1/sources/{source_id}/raw-preset:validate`. The API reference no longer
says that such a retry is deduplicated. The response shapes are unchanged.</content>
  </entry>
  <entry>
    <title>Automatic onboarding's contract approvals are in the audit log</title>
    <id>https://docs.tessera.dev/changelog/#onboarding-contract-approvals-audited</id>
    <link href="https://docs.staging.tessera.zone/changelog/#onboarding-contract-approvals-audited" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="security"/><category term="api"/>
    <content type="text">Every contract version that automatic onboarding approves is now recorded
as a `contract.approve` audit event, in the same transaction as the
approval. Before, only approvals made through `POST /contracts/{version}:approve`
were.

- **Actor:** an automatic approval is recorded with actor kind `service`
  and id `automatic-onboarding` (display "automatic onboarding"). The
  contract's `approved_by` already says `service:automatic-onboarding`. A
  reviewer's approval of a guided proposal is recorded as that reviewer.
- **Details:** `version`, `fingerprint`, `source_id` and `onboarding_mode`.
  When the approval superseded a version, also `superseded_version` and
  `superseded_fingerprint`.
- **Audit stream and SIEM:** consumers of the audit stream will start
  seeing these events, with this actor and these `details` keys. No field of
  the event schema changed.

Automatic onboarding also stops approving a new contract version when
nothing changed. A re-proposal from more records of the same shapes, or
from a batch without an optional field, used to mint a version whose diff
was empty. It now approves nothing and records nothing. See
[Schema contracts](/concepts/schema-contracts).</content>
  </entry>
  <entry>
    <title>Marker previews of silence patterns sample their read range evenly</title>
    <id>https://docs.tessera.dev/changelog/#marker-preview-even-sample</id>
    <link href="https://docs.staging.tessera.zone/changelog/#marker-preview-even-sample" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="api"/>
    <content type="text">For a marker pattern that can match silence (a `not` or an at-most `lte`
leaf), `POST /v1/markers:preview` samples evenly by key hash from every entity
of the type in its read range, which reaches back within the preview read
budget, so entities that went quiet are included. On a project with more than
20,000 entities of the type in that range, the preview ranked only the first
20,000 it met, starting from the newest day. Entities seen only on older days,
often the quiet ones the pattern looks for, could not be sampled.

The preview now ranks every entity it reads within the preview read budget,
with bounded memory, so the sample is even however many entities the range
holds. An entity whose last event is older than the range is still not
sampled. The response shape is unchanged.</content>
  </entry>
  <entry>
    <title>Label previews no longer blame a rule they could not judge</title>
    <id>https://docs.tessera.dev/changelog/#label-preview-open-horizons</id>
    <link href="https://docs.staging.tessera.zone/changelog/#label-preview-open-horizons" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="api"/><category term="console"/>
    <content type="text">`POST /v1/labels:preview` and `POST /v1/pattern-labels:preview` returned the
warning `rule_matches_nothing_in_sample` when every sampled anchor was still
inside its horizon. In that case the preview had graded no outcome at all, so
the warning blamed a rule that had never been judged. This happened, for
example, with 30-day rules on a store of many small files, where the preview's
read budget ends before it reaches days older than the horizon.

The warning now needs at least one closed horizon in the sample
(`observations` above 0); its meaning is otherwise unchanged. When every
anchor is still open, the label wizard and the pattern-label form explain the
counts instead: the anchors are newer than the horizon, so their outcome is
not known yet. They suggest creating the label and reading its positive rate
from the dataset build's label support, or previewing a shorter horizon.
Nothing blocks creating the label.

Request and response shapes are unchanged, so existing API clients keep working.</content>
  </entry>
  <entry>
    <title>Label previews sample entities whose outcome is known</title>
    <id>https://docs.tessera.dev/changelog/#label-preview-closed-day-sample</id>
    <link href="https://docs.staging.tessera.zone/changelog/#label-preview-closed-day-sample" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="api"/>
    <content type="text">`POST /v1/labels:preview` and `POST /v1/pattern-labels:preview` took their
sample from the entities active on the window's newest day. Those entities
were still active, so a rule about going quiet, such as churn, rarely came out
positive for them. On a 30-day churn rule the preview could show about a fifth
of the real rate.

The previews now sample the entities seen on the window's days up to the one
the rule's horizon before the type's newest event (or before now, if that is
earlier), whose older anchors' outcomes are known. The sample is spread evenly
by a hash of the entity id over all of them. If the window reaches no such
day, for example on a store younger than the horizon, the preview samples the
newest entities as before.

The window, its read budget and the response shape are unchanged, so
existing API clients keep working. Rates still come from a sample; the dataset build
counts the whole history.</content>
  </entry>
  <entry>
    <title>A reused Idempotency-Key answers only a retry of the same request</title>
    <id>https://docs.tessera.dev/changelog/#handler-idempotency-binding</id>
    <link href="https://docs.staging.tessera.zone/changelog/#handler-idempotency-binding" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="security"/><category term="api"/>
    <content type="text">An `Idempotency-Key` that already answered now answers only a retry of the
request that first used it. The same key with any other request (another
path, body, query or scoping header) returns 409 `already_exists` with
`details[0].reason` `idempotency_key_reused`, for every credential and every
operation that caches its response. That request is not run, and it never gets
the first request's response. Before, an API key or a console session that
reused a key for another request got the first request's response back, and
its own request was silently not carried out: for example, a second entity's
deletion was never queued. Two first requests sent at the same moment can
still both run.

The deletion, import and webhook operations now also bind a stored response to
the caller's environment and credential, as every other operation does. The
SCIM token issuance receipt and the billing checkout attempt are bound to the
caller too.

A response stored before this release cannot be matched to its request, so a
retry of it within its 24 hours returns the same 409. Send that request again
with a new key; the console does so on your next attempt. While the server
cannot check a stored response, the request returns 503 `unavailable` with
`Retry-After`: retry with the same key. While the key-hash pepper cannot be
read, every operation whose handler claims the key itself returns the same 503
before it runs, so none of them runs twice: an entity deletion, an alert rule
or went-quiet rule, an alert disposition, a model, event-model or
entity-baseline bundle, a shared-checkpoint import, and a webhook sample or
replay. Any other request sent with a key runs meanwhile, and its response is
not stored.

`POST /v1/events` is the exception, except for an application's credentials.
A batch under a key that answered another batch, or before this release, or
whose stored response cannot be checked, runs again, and its events' ids
deduplicate them, so the planned bus bridge keeps ingesting when a rebalance during a
mapping rollout re-sends the same offsets with other events. Such a batch is
metered again on every attempt, and the source's accepted-events count
includes it again. A mapping change that changes an event's entity, timestamp
or event id ingests that batch under both mappings. The planned bus bridge
also puts its mapping config in each batch key, and re-sends a batch under a
new key if one is ever refused: at most three times, then it stops
(`bridge_rekey_total` counts them). New counters show the refused, deferred
and re-run requests by operation and reason. The response shapes are
unchanged.</content>
  </entry>
  <entry>
    <title>Card and dossier downloads: renderer version 2 and v2 dossier templates</title>
    <id>https://docs.tessera.dev/changelog/#governance-renderer-v2</id>
    <link href="https://docs.staging.tessera.zone/changelog/#governance-renderer-v2" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="added"/><category term="changed"/><category term="api"/><category term="models"/><category term="docs"/>
    <content type="text">Model card exports (`GET /models/{model_id}/card:export`) and dossier
downloads (`GET /dossiers/{dossier_id}:download`) in `markdown` and `pdf` now
come from governance renderer version 2 (`renderer_version: "2"`). These are
documents for people to read:

- G1 results say whether each label met its quality bar, and what to do
  next. Internal evaluation details that the API returns are left out.
- When a card export leaves details out, its appendix says so. The card
  fingerprint is still the SHA-256 of the complete pinned card, which the
  `json` download of a dossier on the model embeds as its model card entry.
  A card with nothing to leave out keeps its complete appendix.
- When a dossier render leaves details out, it says so and points to the
  `json` download, which is the complete dossier.
- Authored text (governance fields and acknowledgement reasons) is still
  rendered as written.

The new templates `sr11-7.v2` and `eu-ai-act-annex-iv.v2` have one section
fewer than their v1 versions; use them for new dossiers. The v1 templates
stay listed and accepted, because a shipped template never changes and the
dossiers assembled under them keep citing it. Markdown and PDF renders of
those dossiers leave that section out.

The API responses, including `GET /models/{model_id}/card`, and the `json`
dossier download are unchanged. A new render of an unchanged card or dossier
can have a different `sha256`; `renderer_version` tells the two renderers
apart.</content>
  </entry>
  <entry>
    <title>Every credential now acts only in its own environment</title>
    <id>https://docs.tessera.dev/changelog/#environment-isolation</id>
    <link href="https://docs.staging.tessera.zone/changelog/#environment-isolation" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="fixed"/><category term="security"/><category term="changed"/><category term="api"/><category term="console"/>
    <content type="text">An API key holds exactly one project and environment, and a console session
works in the environment it selects. Requests and the outputs they set up now
stay in that environment:

- **Deployment slots**: listing, creating, activating, rolling back,
  deactivating, decommissioning and restoring slots, and the slot monitors
  (drift, freshness, realized quality, monitor configuration, alert episodes
  and acknowledgements) answer only for the credential's environment. Another
  environment's slot or bundle is `not_found`, exactly like a missing one.
  `POST /slots` with a body `environment` other than the credential's is
  refused with `permission_denied` (`environment_not_allowed`). Slot names
  stay unique per project, so a name taken in the other environment answers
  `already_exists`.
- **API keys and audit events**: an API key lists only its own environment's
  keys and audit events, and an application's organization audit feed holds
  only its registered environments' events. Console sessions keep both
  environments.
- **Webhooks**: an endpoint that an API key subscribed (created it, or last
  updated its `url` or `event_types`) receives only that key's environment's
  entity-level events (such as `score.updated` and `marker.fired`), slot
  outputs, and the events whose data names an environment: `audit.recorded`,
  `gate.failed` (whose data now carries `environment`, the model's),
  `slot.activated`, `slot.rolled_back` and `refresh.*`; those that name no
  environment, such as project-level audit records, still reach every
  endpoint, except a `gate.failed` that names none (emitted by an earlier
  version), which reaches no API key's endpoint. The slot outputs now
  include `audience.synced` and `audience.failed` (a signed link to the
  member file) and `export.completed` and `export.failed` (signed links to
  the exported files): like the other slot outputs they are delivered only
  while the endpoint's subscriber may use the slot's purpose, so an endpoint
  whose subscriber lacks it stops receiving them, and subscribing to them by
  name now needs `general` and every purpose a slot of the project declares
  (403 `permission_denied`, reason `purpose_not_allowed`, on create or
  update). An endpoint saved before subscribers were recorded stops
  receiving them in an organization that uses purposes until it is saved
  again. An endpoint subscribed from the console receives both environments'
  events. Deliveries of the other environment that were already queued when
  this shipped are abandoned before they are sent. To receive an
  environment's events on an endpoint, save its `url` or `event_types`
  (unchanged is enough) with a credential of that environment or from the
  console.
- **Per-entity events configured from the other environment**: a monitor
  configuration, marker or segment build that an API key saved on the other
  environment's slot before this change no longer sends that slot's
  `score.updated`, `marker.fired` or `segment.migration` events to any
  endpoint. For example, if a test key saved a live slot's monitor
  configuration, the live slot's `score.updated` stops. Save the
  configuration again with a credential of the slot's environment to
  restore them.
- **Contract profile samples**: `profile.examples` and `profile.top_values`
  show only values seen in the reader's environment, plus authored ones. A
  value recorded as widely shared before this change names no environment,
  so it is not shown until it is observed again: an attribute that no longer
  receives data loses those examples.</content>
  </entry>
  <entry>
    <title>The console has the title bar it shares with Argus, and every page names itself in its tab</title>
    <id>https://docs.tessera.dev/changelog/#console-shared-title-bar</id>
    <link href="https://docs.staging.tessera.zone/changelog/#console-shared-title-bar" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="changed"/><category term="added"/><category term="console"/><category term="docs"/>
    <content type="text">The console's top bar is now the title bar both Mantika consoles share. It stays at
the top of the window while a page scrolls, and so does the sidebar under it.

- **Title bar**: the Mantika logo and "Tessera" with its product symbol (one link
  home), the product switcher, the organization and project chip, the environment
  chip, search, Help, the theme and your account. It is 56 px high at every width,
  white on light and Dark Navy on dark.
- **Environment chip**: Live is shown in the critical tone, always, and Test in the
  warning tone. The chip's menu lists both environments with what each one is for.
  Switching asks first when the page has unsaved changes, as before, and the Test
  notice still spans the page while you work in Test.
- **Organization and project**: one chip shows both names (long names are
  shortened); it opens the organization and project selects.
- **Product switcher**: lists only the Mantika products your organization has.
  Every organization has Tessera; Argus appears for organizations that have it and
  opens in the same tab.
- **Account menu**: your name, email and role, account and access, help, the
  theme and sign out.
- **Walkthrough presenter bar**: it now sits directly under the title bar, and
  both stay in place while the page scrolls.
- **Tab titles**: each page names itself, such as "Sources · Tessera" or "Mapping
  · Sources · Tessera". A page about one source, model, label or other object is
  named after it once it has loaded, such as "Orders feed · Sources · Tessera".
  When a move to another page changes the title, screen readers announce the new
  one, and they say the object's name when you move from one object to another.
- **Phones**: the bar keeps navigation, the product, the environment, search and
  your account. The logo, the product switcher and the organization and project
  chip are at the top of the navigation menu; help and the theme are in the account
  menu. Choosing a project there closes the menu.
- **Lighter overlay**: the overlay behind dialogs and menus is lighter, the same as
  in Argus.
- **Headings in Manrope**: page, card and dialog titles use Manrope at weight 600,
  the console's one typeface, as in Argus.
- **Tab icon**: the console, the documentation site and the website show the Tessera
  symbol on a Dark Navy tile, and the console has a web app manifest.</content>
  </entry>
  <entry>
    <title>Source reads no longer return the adapter webhook secret's encrypted form</title>
    <id>https://docs.tessera.dev/changelog/#adapter-webhook-secret-read-form</id>
    <link href="https://docs.staging.tessera.zone/changelog/#adapter-webhook-secret-read-form" rel="alternate"/>
    <updated>2026-10-16T00:00:00Z</updated>
    <category term="security"/><category term="changed"/><category term="api"/><category term="connectors"/><category term="docs"/>
    <content type="text">Reads of a Stripe, Shopify, Amplitude or Mixpanel adapter source returned the
encrypted form of its webhook signing secret in `config.webhook_secret`. This
affected `GET /sources`, `GET /sources/{source_id}` and the create and update
responses. It was never the secret itself, and only Tessera's own services can
decrypt it, but any caller with read access could see it. Every Source response
now shows `{"set": true}` in its place.

- **Updating a source works as before.** Send the config back as you read it,
  `{"set": true}` included, and the stored secret is kept. To rotate the
  secret, send the new one as a string.
- **`{"set": true}` only keeps a stored secret.** On a create, or for a source
  that has no secret yet, it is refused with 400 `invalid_argument` on
  `config/webhook_secret`.
- **Changing `provider` needs a new secret.** An update that changes the
  provider and sends `{"set": true}` (or the old encrypted form) is refused:
  a signing secret is one provider's and never carries over to another.
- **The encrypted form is accepted only as the source's own.** A config from
  an earlier read, sent back unchanged, still works. An encrypted form copied
  from another source is refused, since that source could never verify a
  delivery with it.
- **Errors no longer repeat the secret.** A 400 for an invalid
  `webhook_secret` names the field and no longer quotes the value you sent.

The response shape is unchanged: `config` is an open object, and only the
value of `webhook_secret` changes.</content>
  </entry>
  <entry>
    <title>Score one event synchronously against a sequence-model slot</title>
    <id>https://docs.tessera.dev/changelog/#wp-v17-sequence-synchronous-scoring</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-v17-sequence-synchronous-scoring" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="docs"/>
    <content type="text">`POST /slots/{slot}/scores:compute` now answers for slots whose active bundle
is a sequence model scored in realtime (WP-V17). The slot's realtime scorer
scores the entity's recent history with the event included, exactly as it
will once the event is ingested, and stores nothing.

- **Proof**: `inclusion.sequence` names the stored score the answer was
  ordered against, the newest event the answer includes, the window size and
  the activation; the digest covers them.
- **Already covered**: an ingested event the stored score already includes
  answers `already_incorporated` with that stored score, metered as one state
  read.
- **Refusals**: an event at or before the stored score's newest event is
  `event_not_after_state_cursor`; a slot not scored in realtime is
  `slot_not_realtime`; semantic-input models are `semantic_slot_unsupported`
  for now.

See [Synchronous scoring](/serve/synchronous-scoring).</content>
  </entry>
  <entry>
    <title>Score a raw source record synchronously</title>
    <id>https://docs.tessera.dev/changelog/#wp-v17-raw-source-scoring</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-v17-raw-source-scoring" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="docs"/>
    <content type="text">`POST /slots/{slot}/scores:compute` now accepts `raw_source_event`: one
record in your raw source's own shape, for a source bound to a raw-source
preset (WP-V17 with WP-C13). The event is derived with the source's bound
preset release, exactly as raw-source intake derives it, so it has the same
event id and content as the event intake writes for that record. It is then
scored like a canonical event.

- **Proof**: `inclusion.raw_source` names the source, the preset release and
  the record's own id, and the digest covers them.
- **Refusals**: an unknown source is 404 `not_found`, an unbound one is 400
  `source_not_preset_bound`, a binding that no longer verifies is 400
  `preset_binding_unverified`, and a record the preset does not map is 400
  `invalid_argument` with the preset's reason.

See [Synchronous scoring](/serve/synchronous-scoring).</content>
  </entry>
  <entry>
    <title>Realtime scores no longer depend on the events scored beside them</title>
    <id>https://docs.tessera.dev/changelog/#wp-v17-batch-invariant-realtime-scores</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-v17-batch-invariant-realtime-scores" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="changed"/><category term="models"/>
    <content type="text">The realtime scorer now runs the model on each entity's window on its own,
while it still collects scoring requests in micro-batches. Before, an
entity's score could differ slightly depending on which other entities were
scored in the same micro-batch, most visibly on int8-quantized (tier S)
slots. Each realtime score is now a pure function of the entity's window and
the slot's model on one scorer configuration (device and precision). This is
what lets synchronous scoring of sequence models, which ships in a later
release, equal the stored realtime score bit for bit.

- **One-time change**: realtime scores on int8 slots move once, within int8
  error, the first time each entity is re-scored after this release, so a
  score close to a band edge can change band once. fp32 slots change only in
  the last bits.
- **Throughput**: on a CPU tier S model, forwarding each window alone costs
  about 1.4x to 1.6x CPU per entity for 64-event windows, 1.06x to 1.09x at
  256 events, and less than before at 1024. Short-window slots may scale to
  more scorer replicas under the same load; windows under 64 events and GPU
  tiers were not measured. The realtime load test is re-run on our hosted
  deployment before this release. A refresh of a realtime slot forwards each window on its own
  too. See the capacity section of
  [Synchronous scoring](/serve/synchronous-scoring).</content>
  </entry>
  <entry>
    <title>Import a shared checkpoint with acknowledged advisory gates</title>
    <id>https://docs.tessera.dev/changelog/#wp-v15-shared-import-acknowledgement</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-v15-shared-import-acknowledgement" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="changed"/><category term="api"/><category term="sdks"/><category term="docs"/>
    <content type="text">`POST /models:import-shared` accepts an optional `acknowledge_gates` body
member (WP-V15/WP-U23), the same acknowledgement `createModelBundle` takes.
A shared checkpoint whose local evaluation fails only advisory gates can now
be imported when you acknowledge exactly those gates with a reason; before,
any advisory failure refused the import.

- **Same rules as bundles**: the gates must exactly cover the failing
  advisory set (no missing, extra or duplicate gate, no blank reason). Each
  acknowledged gate records `gate.override`, and the bundle carries its
  `promotion` block.
- **Still refused**: blocking gates (G4-G6, and G1-G3 when the project policy
  makes them blocking), malformed gate results and every binding, provenance
  and evaluation reason, acknowledged or not.
- **Preview body unchanged**: `POST /models:preview-shared-import` still answers
  `ready: false` with `acknowledgement_required` reasons for such a model and
  never takes an acknowledgement. With `expected_preview_fingerprint`,
  `stale_preview` is checked after request validation and before the
  acknowledgement's cover.
- **Repeats**: repeating an import whose bundle exists returns it and records
  nothing again.
- **API only for now**: the console cannot send `acknowledge_gates` yet.
- **No response changes**: requests without the member behave as before.

See [Acknowledge at a shared-checkpoint import](/train/promotion-gates/#acknowledge-at-a-shared-checkpoint-import).</content>
  </entry>
  <entry>
    <title>Description-channel models acknowledge their own quality gates like any model</title>
    <id>https://docs.tessera.dev/changelog/#wp-m7-description-channel-own-gates</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m7-description-channel-own-gates" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="changed"/><category term="api"/><category term="models"/>
    <content type="text">`POST /models/{model_id}/bundles` no longer refuses a project-trained
`descriptions.v1` or `semantic.v2` model because of its own quality gates
when you acknowledge them. Its G1–G6 now follow the same path as every
other model: a failing quality gate that your project policy treats as
advisory (G1–G3 under `warn`) is shown in the refusal details with
`acknowledgement_required`, and the bundle is created when the request
names it in `acknowledge_gates` with a reason. A model whose representation
carries event counts or timing is expected to fail G3, for example. G4–G6
still block, and `strict` mode or a raised `gate_overrides` entry is still
honoured. The refusal reason `desc_channel_gates_not_passing` is no longer
returned. A model with a gating label still needs passing description-channel
studies of its recipe. Request and response shapes are unchanged.</content>
  </entry>
  <entry>
    <title>Acknowledge description-channel and observation-time evidence when you bundle</title>
    <id>https://docs.tessera.dev/changelog/#wp-m7-bundle-findings</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m7-bundle-findings" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="changed"/><category term="api"/><category term="models"/>
    <content type="text">`POST /models/{model_id}/bundles` now shows you the evidence behind a
description-channel or observation-time bundle and lets you decide, instead
of refusing. Missing or non-recommending observation-time evidence
(`observation_time_evidence_missing`, `observation_time_not_recommended`) and
a missing, failing or unresolvable description-channel study
(`desc_channel_study_missing`, `desc_channel_study_failed`,
`desc_channel_study_unresolved`) are bundle findings: the refusal lists every
advisory gate and finding at once, each finding as a `bundle_finding` detail
with its facts (the observation ablation's arms and labels, each study's rows
and per-label deltas) and a `fingerprint` of those facts, after the reason it
used to carry. Send each code with its `fingerprint` in the new request member
`acknowledge_findings`, with a reason, and the bundle is created; if the facts
changed since you saw them the request is refused with `finding_facts_changed`
and the current facts. Each acknowledged finding is recorded in a
`bundle.finding_acknowledged` audit event. A model that consumes the
elapsed-silence token can now be bundled on non-recommending evidence once you
acknowledge it. A study you have not recorded no longer stops a bundle
(`desc_channel_ablation_stale` is no longer returned); every study's
fingerprint and verdict are pinned in the `bundle.create` audit event. A
blocking gate still refuses first, now listing the findings too; runs still in
progress in any study that can be resolved, integrity failures of the evidence
and the observation-time train/serve checks still refuse (a study that cannot
be resolved is reported, not read further, and a bound on the whole recipe
resolves no study); and in `strict` mode, an admin's choice, no
finding can be acknowledged and the refusal says `policy_strict`. Response
shapes are unchanged.</content>
  </entry>
  <entry>
    <title>Entity baseline models: a strong simple model to compare with and serve</title>
    <id>https://docs.tessera.dev/changelog/#wp-m28-entity-baselines</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m28-entity-baselines" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="models"/><category term="docs"/>
    <content type="text">You can now train, evaluate and serve an entity baseline model on any
sequence dataset build (WP-M28), to compare with the sequence models trained
on the same data and to serve when you choose it. It scores each entity from
aggregate features of its recent history, with one calibrated gradient-boosted
tree model per label.

- **Training**: `POST /entity-baseline-models` trains on an existing sequence
  dataset build (any tokenizer format). Any succeeded build within the
  admission bound is admitted, and
  what Tessera found on the dataset (a training study, or a sequence model
  with a recorded G1 failure: quality, competitiveness or support) is
  recorded as `admitted_by` for
  context. Each label's arm (`gbm_default`, `gbm_tuned` or
  `rfm_gbm`) is chosen on validation or named explicitly. Taking it from a
  training study (`study_id`) takes effect when training studies ship.
- **Evaluation**: the report uses the standard shape and gates. G3 does not
  apply, and G4 checks input parity between the dataset and serving. G1 and
  G2 follow the project's promotion policy, as for every model: under the
  default policy a failure needs an acknowledgement with a reason, recorded in
  the bundle and the audit log. G4, G5 and G6 block bundle creation.
- **Bundles and serving**: `POST /entity-baseline-models/{id}/bundles`
  creates the bundle, which activates on a batch slot like any bundle; a
  non-v1 build's bundle also pins its tokenizer inputs, read with
  `GET /entity-baseline-bundles/{bundle_id}/tokenizer-inputs`; adding its
  `pins` and inserting each artifact at its `position` rebuilds the stored
  manifest that `fingerprint` covers. A
  refresh writes each entity's scores, and the new
  `GET /slots/{slot}/entities/{type}/{id}/scores` reads them. Batch score
  reads, ranked reads by score and delta, history, bands, score exports,
  destinations and the freshness monitor serve a baseline slot as well.
- **Custom roles**: the family's operations, the tokenizer inputs read
  included, are new permissions in the role catalog. A growing catalog grants
  nothing implicitly (ERRATA 2026-10-12, WP-A5), so a custom role reaches them
  only once an administrator adds them; the built-in roles carry them at the
  floors of their sequence counterparts.
- **Unchanged**: the released model, dataset, bundle and state responses.
  `GET …/state` and the other embedding-based reads refuse a baseline slot
  with `model_family_not_supported`.</content>
  </entry>
  <entry>
    <title>Training studies: the default way to train a model</title>
    <id>https://docs.tessera.dev/changelog/#wp-m27-training-studies</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m27-training-studies" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="models"/><category term="console"/><category term="sdks"/><category term="docs"/>
    <content type="text">**Correction (2026-10-21):** the Python and TypeScript SDKs are planned, and
the integration catalog lists both SDKs as Planned; this note said the SDKs
have the four training-study operations. Use the HTTPS API for
`createTrainingStudy` and the list, read and cancel operations.


A training study trains a fixed grid of ordinary runs on one dataset, picks the
winner on validation data only, evaluates that winner once and reports whether
it is competitive with the strongest baselines. It is the documented default
way to train. `POST /v1/runs`
remains for a single configuration.

- `POST /v1/training-studies` (`createTrainingStudy`) starts a study.
  `GET /v1/training-studies`, `GET /v1/training-studies/{study_id}` and
  `POST /v1/training-studies/{study_id}:cancel` list, read and cancel it. The
  Python and TypeScript SDKs have the four operations.
- The default space, `tier_s_cpu_v1`, has eight children: batch 32,768 or
  131,072 tokens, binary label weight 1 or 10, and readout `mean_pool` or
  `last_event`. Each child is an ordinary run that counts toward
  `concurrent_training_runs`. When the plan is full the study waits.
- The request is refused before anything trains when the dataset was built
  before dataset builder 1.2.0 (`dataset_rebuild_required`), has no labels
  (`study_requires_labels`), or the node-hour estimate is above the
  `max_node_hours` you set (`study_budget_exceeded`, with both numbers). The
  estimate is `epochs × train_tokens / (5,000 × 3,600)` summed over the
  children. Without `max_node_hours`, any estimate up to the ceiling of 96 is
  admitted and recorded.
- Selection uses the mean primary validation metric at each run's shipped step.
  Only the winner is evaluated, so the test split is touched once per study.
  The outcome is information, never a block: `competitive`, `not_competitive`
  or `not_assessable`, from the winner's competitiveness against the strongest
  baseline per label. The winner's verdict stays in `evaluation.promotion`;
  promoting is your explicit choice. A study never creates a bundle, promotes or activates.
- An identical request returns the existing study while it is queued, running
  or evaluating with no cancel in progress, or completed. A failed or cancelled
  study, or one whose cancel is in progress, is superseded by a new one that
  reuses its completed children. A completed
  study archives the models of its losers.
- Nothing changes until you promote a model. When the winner is not
  competitive or cannot be assessed, a baseline candidate is trained beside it
  once the entity baseline family is deployed (`fallback.mode:
  baseline_candidate`), so you can choose. The study page shows the findings
  and offers both choices, and any child can be trained as an ordinary run.
- The console creates a study from Training → New study and keeps the existing
  run form under Single run (advanced). The study page shows each child, the
  winner, the evaluation verdict and the fallback.

No released response schema changes. Children are ordinary runs and models, and
the study membership is only on the study. One behaviour of `POST /v1/runs`
changes: it never returns a run a training study created. A request for the
same configuration as an in-flight study child, a clone included, starts its
own run (evaluated automatically, never archived by the study).

The final confirmation of this work on our hosted deployment needs the WP-M26
release gate and its time holdout, so the evaluation of a study winner on our
hosted project is confirmed after that gate ships.</content>
  </entry>
  <entry>
    <title>Out-of-time holdouts for dataset builds, and labels censored at the end of the data</title>
    <id>https://docs.tessera.dev/changelog/#wp-m26-time-holdout</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m26-time-holdout" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="changed"/><category term="api"/><category term="models"/><category term="console"/>
    <content type="text">Dataset builds take an optional `temporal_holdout: {start}`. Train and
validation entities then keep only their events before `start`, and a train or
validation label observed at or after `start` is masked `holdout_embargo`. Test
entities keep their full windows, but only labels whose supervision event is
at or after `start` are graded, so the test evaluation is out-of-time and
out-of-entity. The eligibility preview cannot describe a holdout build: preview
the request without the holdout and send its `build_fingerprint` as
`expected_base_fingerprint` with the holdout, so the build keeps the pin-drift
protection. The console's build form does this when you set a holdout start.

Two new reads: `GET /datasets/{id}/holdout` returns a build's holdout kind,
start, censor clock and how many models have been evaluated on its test split,
and `GET /datasets/{id}/holdout-recipe` returns a holdout build's replay
request. The released recipe reads keep their shape and refuse a holdout build
with `temporal_holdout_recipe_unsupported`; the console's clone flow reads the
holdout recipe instead.

Derived labels now censor at the end of the data. A derived-label anchor whose
horizon ends after the newest event of the build's entity type (or after the
snapshot, when that is earlier) is masked `horizon_open` instead of graded (as a
negative, or as a positive for an absence rule). The manifest records the clock
(`censor_clock`), and the eligibility preview uses the same clock; the label
previews censor at the earlier of now and the newest event of the entity type. A build has one clock, not one
per source: with a source filter it is that source's newest event while the
look-ahead reads every source, so the filter can only censor more than the
unfiltered build. A source that ends earlier than the clock is not detected,
so if a label's outcomes come from such a source, end the window at least one
horizon before that source's end. With a derived label selected, the console's
build form says where the clock sits (the data end a day or more before the
snapshot, or a newest event later than the snapshot) as information that never
blocks the build.

A build whose pinned snapshot holds no event of its entity type (and source,
when it filters by one) now fails with `no_eligible_events`. Before, such a
build without labels succeeded with zero rows, and one with labels failed with
`rule_matches_nothing` or `insufficient_label_support`. A snapshot whose matching events all fall outside
the window still builds as before.

This is dataset builder 1.3.0. The builder version is part of a dataset's
identity, so the same build request now produces a new dataset version; a
dataset built before keeps working as it did, and its evaluation checks it with
the snapshot clock it was built with.</content>
  </entry>
  <entry>
    <title>Model quality is compared with simple baselines before promotion</title>
    <id>https://docs.tessera.dev/changelog/#wp-m26-competitiveness-gate</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m26-competitiveness-gate" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="changed"/><category term="api"/><category term="models"/>
    <content type="text">G1 (now named "Label quality vs baselines") checks a second condition for every
gating label of a sequence model: the model must be within 0.01 of the
strongest completed baseline arm scored on the same test targets, and the lower
bound of the paired 95% interval of that difference must be at least -0.03. The
report records the result per label in `gates.G1.metrics`, with the typed
reasons `not_competitive` and `baseline_unavailable`. Evaluations after a
training run, during automatic preparation and behind
`POST /models/{model_id}/signals-evaluations` now run the baseline zoo so that
G1 has a basis.

G1, G2 and G3 stay advisory: a failure is shown with its evidence, and you
choose whether to improve the model or promote it with an acknowledgement that
records the reason. Every G1 and G2 diagnostic names the corrective actions and
acknowledging as the alternative; a label that is within 0.01 but whose interval
reaches below -0.03 reads "inconclusive: the test split is too small to
establish competitiveness". G3 notes in `metrics.note` that count- and
velocity-bearing representations are expected to exceed its probe. G2 also
reports an informational `log_loss_gain` per classification label. A refused
bundle creation now lists every failing gate with its diagnostic and, for G1,
each failing label's estimate, strongest arm, delta, interval and margins.

Models evaluated before the check show a G1 finding with the reason
`evaluated_before_competitiveness_check`: they were never compared with the
baseline arms. Acknowledge the finding with a reason, or let the release
re-evaluation fill in the comparison. Under the default policy nothing is
blocked; a project's own `strict` mode or a G1 override (an admin's choice)
makes the finding blocking. Wherever the platform owner has enabled the
re-evaluation job, this release re-evaluates earlier models automatically, promoted ones
included. A
re-evaluation can publish a `gate.failed` webhook for a model that has been
serving for a long time; its bundle, slot and serving are not changed, and
nothing is promoted or activated. Models whose checkpoint or dataset is gone
keep the finding. Event models and entity baseline models are not affected.

**No rollback past this release once a model file purge has run.** A purge
after this release freezes whether the model's report records competitiveness
(`competitiveness_recorded`), and readers from before this release reject that
key, so their G1 lookups and fenced-model reads would fail.

The gate catalog served by `GET /gate-definitions` changed, so a shared-import
preview made before this release has a different fingerprint: run the preview
again before confirming the import.</content>
  </entry>
  <entry>
    <title>Baseline evaluations show a runtime estimate before launch</title>
    <id>https://docs.tessera.dev/changelog/#wp-m22-baseline-runtime-budget</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m22-baseline-runtime-budget" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="changed"/><category term="api"/><category term="docs"/>
    <content type="text">Each `POST /evaluations:baseline` run now gets a runtime estimate (WP-M22).
It is based on the declared 45-minute target for the full baseline zoo plus
one comparator on 100,000 entities with up to 1,024 events and 5 labels, and
it scales up for larger work.

- **Refused only above the shared ceiling**: a run whose estimate exceeds the
  6-hour deadline of the shared evaluation pool is refused with
  `400 failed_precondition`. `details[0]` has reason
  `runtime_budget_exceeded` and shows the estimate, the ceiling and the shape
  it was computed from. Repeating a request for an evaluation that already
  exists still returns it.
- **Never stopped by the estimate**: an admitted run runs under the pool's
  usual deadline, records its report and is metered for the time it used. A
  run that takes longer than its estimate is flagged for operators only.
- **No response changes**: the evaluation row and report are unchanged; the
  refusal detail is in the open `details` array.

See [Runtime estimate](/train/baseline-evaluations/#runtime-estimate).</content>
  </entry>
  <entry>
    <title>Full-book backtests stop at the end of the data</title>
    <id>https://docs.tessera.dev/changelog/#wp-m20-full-book-censor-clock</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m20-full-book-censor-clock" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="fixed"/><category term="api"/><category term="models"/><category term="docs"/>
    <content type="text">A `full_book_out_of_time` backtest now ends its window where the dataset's
data ends, not only at its event-store snapshot. The window ends at the
earliest of the snapshot instant, the time of the dataset's last event and
the request instant, minus the protocol labels' longest maturation. Before
this fix, a dataset exported before its snapshot was taken could be graded
past its last event: churn-style labels counted positives that never
happened, and conversion-style labels counted misses.

- **Requests**: the default 6 cutoffs end at the data end minus the
  maturation. Cutoffs past it are refused with `invalid_argument`, and a
  dataset with no matured window inside its data gets `failed_precondition`
  (`full_book_window_empty`).
- **Changed snapshot files**: finding where the data ends reads the newest
  files of the dataset's snapshot and checks their bytes against what the
  snapshot recorded. On a mismatch, `POST /backtests` answers
  `failed_precondition` (`snapshot_file_mismatch`), and a backtest that
  finds it when it runs ends `failed` with reason
  `job_failed:snapshot_file_mismatch`. Take a new snapshot, rebuild the
  dataset and backtest a model trained on the rebuilt dataset.
- **Earlier reports**: a report requested before this fix stays readable
  under its id. To recompute it, request the backtest again. The new
  request resolves the bounded window, so it gets new cutoffs and a new
  backtest id.

See [Protocols and backtests](/train/protocols-and-backtests).</content>
  </entry>
  <entry>
    <title>See what a bundle needs before you create it</title>
    <id>https://docs.tessera.dev/changelog/#wp-m14-bundle-readiness</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-m14-bundle-readiness" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="models"/>
    <content type="text">`GET /models/{model_id}/bundle-readiness` is a read-only dry run of
`POST /models/{model_id}/bundles`. It returns the verdict (`ready`,
`needs_acknowledgement` or `blocked`), every failing gate with its severity,
metrics and diagnostics, every bundle finding with its facts (the same ones a
bundle refusal carries), the first hard refusal a bundle request would get
(with `retryable` for runs still in progress, never when a gate blocks), and the exact
`acknowledge_gates` and `acknowledge_findings` sets to send. The
`observation_time` and `signals` query parameters evaluate those request
members. It writes nothing and needs a viewer role or a `read`-scoped key.
The answer is resolved when you ask; the bundle request checks everything
again. `POST /models/{model_id}/bundles` runs the same read-only checks before
it writes anything: a report missing an acknowledged gate's first diagnostic
is now refused before any object is written, and a shared import whose pins
fail verification is refused with a typed reason instead of an internal error:
`artifact_fingerprint_mismatch`, `witness_erased`, its own shared-import
reason (for example `checkpoint_identity_mismatch` or `representation_mismatch`), or
`shared_import_unverifiable`. `POST /models:import-shared`
changes the same way: an erased feature-view witness is now a 400 with reason
`witness_erased` (it had no details), and a missing task catalog or
representation object is a 400 with `shared_import_unverifiable` (it was a
500). A temporary storage outage (the storage service
answering 500, 502, 503, 504 or 429, a timeout or a lost connection) is now a retryable 503
`unavailable` with `Retry-After` from both bundle operations and the
shared-checkpoint import instead of a 500. A bundle whose `bundle.created`
webhook cannot be published is still returned as created. Response shapes are
unchanged.</content>
  </entry>
  <entry>
    <title>Raw event reads require the event-feed purpose</title>
    <id>https://docs.tessera.dev/changelog/#raw-event-reads-purpose</id>
    <link href="https://docs.staging.tessera.zone/changelog/#raw-event-reads-purpose" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="changed"/><category term="security"/><category term="api"/><category term="ingest"/><category term="console"/>
    <content type="text">A key or custom role with a purpose allow-list now needs the `event-feed` purpose to read raw event data. `event-feed` is already the purpose the committed event feed uses. Without it, these reads answer `403 permission_denied`, with `details[0].reason` `purpose_not_allowed` naming `event-feed`:

- the event feed itself: `readEventFeed`, `getEventFeedStats`, `createEventExport`, `getEventExport` and `watchEventFeedHeads`;
- the entity timeline (`listEntityEvents`) and the evidence timeline (`listEntityEvidenceTimeline`), which also still needs its slot's purpose;
- dead-letter items (`listDeadletterItems`);
- the source sample (`sampleSource`) and sync rejections (`listSyncRejections`);
- mapping previews (`previewSourceMapping` and `previewCatalogMapping`) that sample the latest landed snapshot (`from_snapshot`);
- projection exports (`createSlotExport` with `kind: projection_2d`) colored by event attributes, which are checked again when the projection runs.

Some reads keep answering but leave out raw values when the caller lacks `event-feed`:

- contract reads (`getCurrentContract`, `getContract`, `createContractDraft`, `updateContractDraft`, `approveContract`) return empty `profile.examples` and `profile.top_values`;
- `getUpload` returns a null `columns` and `sample_rows` preview;
- `getSync` and `listSourceSyncs` drop the `position` of a failed page's `keyset_position` error detail;
- the trajectory inspector (`getEntityTrajectory`) returns empty `approved_original_fields` and `original_source_events`;
- `getSlotExport` returns a succeeded projection colored by event attributes without its `artifact_url`; the console says the projection needs the `event-feed` purpose.

Sync error messages no longer quote the text of a connector or parser exception, which could contain a raw source cell. A failed extraction now reads, for example, `connector extract failed (ArrowInvalid, column 3, row 48213, expected int64)`: the exception class, the row, line or column numbers it named and the type the value should have had. Messages stored earlier are served the same way, to every caller (`getSync`, `listSourceSyncs` and the source schedule's `last_error`).

Segment cards (`listSlotSegmentCards`, `getSlotSegmentCard` and the card comparison) now check the slot's purpose, as segment members already did.

**Who is affected:** only API keys and custom roles that have a purpose allow-list without `event-feed`.

**Who is not affected:**
- keys and roles without an allow-list;
- built-in roles;
- applications.

The entity lookup and presence, mapping previews of rows you send in the request, and semantic exceptions need no purpose.

**To restore access:**
1. Register `event-feed` in the project with `putPurposes` (`PUT /v1/purposes`).
2. Add it to the key with `putApiKeyPurposes`, or to the role with `putOrgRolePurposes`.

A purpose registry that cannot be read now answers `503 unavailable`, with reason `purpose_registry_unavailable`, on every service.</content>
  </entry>
  <entry>
    <title>Dataset build retention: delete idle builds' files, and the erasure bound</title>
    <id>https://docs.tessera.dev/changelog/#dataset-build-retention</id>
    <link href="https://docs.staging.tessera.zone/changelog/#dataset-build-retention" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="console"/><category term="models"/><category term="docs"/>
    <content type="text">Dataset builds used to keep their files, and the event-store snapshot they
pinned, forever. They can now be deleted whole and verifiably:

- **Retention**: `PUT /v1/dataset-file-retention` (`putDatasetFileRetention`,
  org `admin` or the `admin` scope) sets how many days after a build became
  idle (finished, every model trained on it archived, nothing reading it) its
  files are deleted, from 1 to 365, or `null` (off, the default).
  `GET /v1/dataset-file-retention` reads it; it is versioned like the model
  file setting.
- **Explicit deletion**: `POST /v1/dataset-files/{dataset_id}:purge`
  (`purgeDatasetFiles`) requests the deletion of a finished build's files,
  due 24 hours after the request at the earliest; with
  `include_archived_models: true` it also requests the deletion of its
  archived models' files. `POST /v1/dataset-files/{dataset_id}:cancel-purge`
  (`cancelDatasetFilePurge`) withdraws the build's request until the deletion
  starts.
- **State**: `GET /v1/dataset-files` and `GET /v1/dataset-files/{dataset_id}`
  show each build's file lifecycle, its typed holds, its event-store snapshot
  pin, and how many entity deletions wait for that snapshot
  (`blocking_erasures`).
- **After the deletion starts**, training, evaluating or rebuilding the same
  request on the build is refused with reason `dataset_files_purged`; the
  dataset and recipe reads keep answering from the frozen manifest. When the
  files are deleted the build's snapshot is released once no other build holds
  it, so the erasures waiting on it complete.
- **The erasure bound.** With unpinned snapshots expiring after 7 days (see
  the snapshot TTL note of 2026-10-14) and builds deleted whole, the data
  deletion policy now states how long an erased entity's data can remain:
  `R + D` days with no readable copy and `R + D + 7` days with no copy at
  all, for a replacement made `R` days after the request and both retentions
  at `D` days (41 and 48 days at 35 and 6).
- **Console**: **Settings → File retention** gains the **Dataset builds** card
  and a partner preset that sets both retentions; each dataset page gains a
  **Files** section with **Delete files now** (and its archived models) and
  **Cancel deletion** for admins.
- **Docs**: the [data deletion policy](/policies/data-deletion/#erasure-completion-bound)
  gains *Dataset build retention* and *Erasure completion bound*, and
  [Model file retention](/train/model-file-retention/#dataset-builds) documents
  the dataset operations.
- New schemas only (`DatasetFileRetention`, `DatasetFileState`, …); no
  released response changes.</content>
  </entry>
  <entry>
    <title>Deleting an entity removes its values from contract profile samples</title>
    <id>https://docs.tessera.dev/changelog/#contract-profile-sample-erasure</id>
    <link href="https://docs.staging.tessera.zone/changelog/#contract-profile-sample-erasure" rel="alternate"/>
    <updated>2026-10-15T00:00:00Z</updated>
    <category term="changed"/><category term="security"/><category term="api"/><category term="ingest"/>
    <content type="text">Contract reads decorate each attribute with a `profile` whose `examples` and
`top_values` come from attribute values sampled during ingest. Until now, those
samples were kept with no link to the entity whose events held them. As a
result, an erased entity's values (an email, say) could still appear in
contract profiles after its deletion completed. They no longer can.

- **Attributed samples**: every sampled value now records which entities'
  events held it (each event's primary entity), as one-way digests. Deleting
  an entity removes its values from every contract profile, and the
  deletion's verification checks that none remain. A value is no longer shown
  from the moment the deletion worker closes the entity, and a value from the
  entity's events that arrives after that is judged by the same rule as one
  stored before. Values are attributed to the event's primary entity only, as
  the event store erases by primary entity: a value from an event where the
  deleted entity is only a secondary entity is not removed with it.
- **Values many entities hold**: a value the deleted entity held stays only
  while 5 or more other distinct entities, all of one environment (live or
  test), still hold it (k-anonymity 5); it goes with the deletion that leaves
  fewer. An entity present in both live and test counts once. A value 64
  distinct entities of one environment hold is treated as widely shared and
  kept through every later deletion. Limits: distinct entity ids are not
  always distinct people (for example several entity types, or duplicate
  records of one person), and a direct identifier, such as a household email,
  can still identify the people who hold it. If your events use one primary
  entity per record (an order, a session, a device), each record counts as a
  holder: a customer's email on 64 of their orders is treated as widely
  shared and stays even after those 64 orders are deleted.
- **No cached copies**: a stored `Idempotency-Key` replay of
  `createContractDraft` or `approveContract` returns the contract with empty
  `examples` and `top_values`; the live response is unchanged. Stored replays
  recorded before this release that hold sample values are deleted when a
  deletion in the project completes. During the first 24 hours, a client that
  retries a `createContractDraft` with such a key creates a new draft instead
  of receiving the stored one.
- **One-time reset**: profile samples collected before this change cannot be
  attributed to an entity, so they were cleared. Attributes your contract
  already defines now show empty `examples` and `top_values`, because sampling
  only covers attributes the contract does not yet know. `null_rate` and
  `cardinality` are unchanged. A domain pack's example values come back when a
  newer version of the pack is installed; installing the same version again
  changes nothing.

**Rollout.** These guarantees hold once gateway-api, which serves contract
reads and runs the deletion worker, runs this release. Until then, and after
a rollback of gateway-api, the previous release still serves such values and
completes deletions without removing them. The first worker runs of this
release then remove them, newest deletions first. If contract-service and
the stream router are rolled back while gateway-api stays on this release,
the previous router writes values without attribution. Deletions in projects
that stream unknown attributes then wait, and report residue, until the
versions match again. While contract-service runs the previous release, its
intake also counts new occurrences of an attributed value without recording
their entities, so a later deletion of such an entity does not remove the
value on its account (the value's recorded holders still apply).

**Operators.** The entity-deletion role needs `SELECT` and
`UPDATE (samples, sample_attribution)` on `attribute_observations` before the
first run of this release's entity-deletion worker (the bring-up runbook, §3).
On our hosted deployment they are lines of the deletion-maintenance grant manifest, which
the grant step after the release applies (the default privileges that used to
cover them are gone). Elsewhere, until the grant lands, each
deletion pass and sweep that has samples to remove fails closed and retries on
the next run.</content>
  </entry>
  <entry>
    <title>Score one event synchronously against an event-model slot</title>
    <id>https://docs.tessera.dev/changelog/#wp-v17-synchronous-event-scoring</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-v17-synchronous-event-scoring" rel="alternate"/>
    <updated>2026-10-14T00:00:00Z</updated>
    <category term="added"/><category term="api"/><category term="docs"/>
    <content type="text">`POST /slots/{slot}/scores:compute` now answers for slots whose active
bundle is an event model (WP-V17). It scores one canonical event in the same
request, with per-label scores, bands and TreeSHAP explanations, and stores
nothing: the event is not ingested and no state is written.

- **Proof**: every answer carries an `inclusion` block naming the event,
  bundle and model it used, plus a `digest` over the canonical response
  bytes (an integrity binding, not a signature).
- **Same rules as reads**: erased or suppressed entities answer an identical
  404 `entity_unavailable`, the slot's declared purpose applies to both the
  suppression sets and your key's allowed purposes, and events that ingest
  would not accept are refused with a typed reason.
- **Bounded**: each replica bounds in-flight scoring (429
  `sync_scoring_saturated`) and the memory its loaded models keep (503
  `scorer_unavailable`, retryable), and answers within a 2 s deadline (503
  `scorer_timeout`, never metered).
- **Metering**: one `scores_served` (`mode: sync`) per scored answer.

Slots serving a sequence model answer 503 `scorer_unavailable` from this
endpoint for now. See [Synchronous scoring](/serve/synchronous-scoring).</content>
  </entry>
  <entry>
    <title>Unpinned event-store snapshots now expire after 7 days</title>
    <id>https://docs.tessera.dev/changelog/#wp-o7-snapshot-ttl</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-o7-snapshot-ttl" rel="alternate"/>
    <updated>2026-10-14T00:00:00Z</updated>
    <category term="changed"/><category term="security"/><category term="api"/><category term="models"/><category term="docs"/>
    <content type="text">An event-store snapshot that nothing pins now protects the files it reads
for 7 days after it is taken, instead of 90 (WP-O7). So once nothing pins
the event-store files an entity deletion superseded, they are deleted within
7 days. Snapshots taken before this change keep their 90-day expiry.

- **Dataset builds**: a build pins its snapshot, as before. A request that
  names an unpinned `snapshot_id` which expired, or expires within 15
  minutes, is refused with `failed_precondition`, `details[0].reason =
  snapshot_expired`, before anything is pinned. The eligibility preview
  still reads such a snapshot, but starting its build is refused the same
  way.
- **Automatic preparation, segment explanations and segment cards**: a run
  that comes back to its snapshot takes a new one when less than a day is
  left. A segment-card continuation then publishes evidence that records the
  new snapshot. An explanation run given `--snapshot-id` records that
  snapshot's own expiry and refuses one with a day or less left, pinned or
  not, since the run holds no pin.
- **Operator tools**: the identifier-index Job, the entity-history
  inspector and a segment-card run given `--snapshot-id` refuse a snapshot
  with a day or less left before its own expiry, pinned or not, since they
  hold no pin and a build's pin can be released while they read. The
  project-inputs CLI refuses a `--snapshot-id` with a day or less left
  instead of replacing it.

Released schemas are unchanged; the request descriptions name the new
reason.</content>
  </entry>
  <entry>
    <title>Leakage reports and new comparator checks no longer repeat identifier values</title>
    <id>https://docs.tessera.dev/changelog/#wp-o7-leakage-evidence-digests</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-o7-leakage-evidence-digests" rel="alternate"/>
    <updated>2026-10-14T00:00:00Z</updated>
    <category term="changed"/><category term="security"/><category term="api"/><category term="models"/><category term="docs"/>
    <content type="text">An aborted build's leakage report no longer carries raw identifier values,
and neither does a comparator validation run from this release on (WP-O7).
The released schemas are unchanged: the affected values sit in fields they
already type as free objects or strings.

- **Leakage evidence**: when a build aborts because known identifier values
  were found inside a text feature (`identifier_value_hit`), the evidence
  now counts the values found (`value_count`) and lists up to five of them
  only as keyed digests (`value_digests`, each `hash:` followed by 64 hex
  characters). Before, it listed the values themselves.
- **Stored reports**: reports stored before this release are rewritten into
  the same form, and each one's `report_fingerprint` is recomputed so it
  stays self-consistent. For such a report, `value_count` is the number of
  values it listed, at most five. `GET /datasets/{id}` and
  `GET /event-datasets/{id}` then serve the new values in the same shape;
  the build's `updated_at` does not change.
- **Comparator validation**: a comparator validated from this release on
  still lists each offending row and column, but the message never repeats
  the cell's value.</content>
  </entry>
  <entry>
    <title>Evaluation reports name entities and events by pseudonym</title>
    <id>https://docs.tessera.dev/changelog/#wp-o7-evaluation-pseudonyms</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-o7-evaluation-pseudonyms" rel="alternate"/>
    <updated>2026-10-14T00:00:00Z</updated>
    <category term="changed"/><category term="security"/><category term="models"/><category term="console"/><category term="docs"/>
    <content type="text">Evaluation reports written from this release on no longer hold raw entity
ids, canonical entity keys or raw event ids, and evaluations no longer store
per-entity feature matrices (WP-O7). The released report schema is
unchanged: the affected values are strings inside blocks it already passes
through.

- **Pseudonyms**: an entity is named by `hash:` followed by its keyed entity
  hash, the same per-project hash Tessera uses everywhere else. Comparator
  point-in-time violations carry it in `entity_id`. Gates G4 and G5 name an
  entity as `{entity_type}:hash:{entity_hash}` (plus `#{instant}` for an
  observation-time row). Event-model gates G4 and G5 name an event by `hash:`
  followed by a keyed hash of its id. Your files and ids never appear in a
  new report.
- **G4 divergences** say where the two paths differ (the event, the
  channel, the first differing token position) and how many token positions
  differ. They no longer print the tokens themselves, which are the entity's
  behavioural data.
- **G5 bound**: G5 lists at most 100 violations. The new
  `violations_total` metric keeps the full count, and the console shows it
  on the report, the gate guidance and the model card.
- **No stored feature matrices**: the LightGBM baseline keeps its feature
  matrices in memory and stores only the fitted booster. A model card over
  an older report still lists that report's `baseline_feature_matrix`
  references, but no longer verifies them. Every other reference is still
  verified, and the card fails closed on a mismatch.
- **Older reports** are immutable records and are not rewritten.</content>
  </entry>
  <entry>
    <title>Deleting an entity removes it from comparator files</title>
    <id>https://docs.tessera.dev/changelog/#wp-o7-comparator-erasure</id>
    <link href="https://docs.staging.tessera.zone/changelog/#wp-o7-comparator-erasure" rel="alternate"/>
    <updated>2026-10-14T00:00:00Z</updated>
    <category term="changed"/><category term="security"/><category term="api"/><category term="models"/><category term="docs"/>
    <content type="text">Deleting an entity now also removes its rows from every comparator file of
the project (WP-O7). The released schemas are unchanged.

- **Comparator files**: the rows whose mapped `entity_id` column holds the
  deleted id are removed, and the file is validated again, so the
  comparator's counts, `as_of` range and fingerprint change. In a CSV file
  only those lines are cut; every other byte stays as it was. A Parquet file
  is written again in its own layout, and only when every column and every
  other row stays exactly as it was and the new file keeps the id in no
  identifier column, category, statistic, column name or metadata; otherwise it
  is kept and reported for review. Another entity's row that names the id in a
  different column stays, and the file is reported for review with the number
  of such rows. A file several comparators share is rewritten once, for all of
  them. When every run that used the comparator graded one entity type, only a
  deletion of that type touches it; otherwise the id is removed whatever its
  type. A file is never deleted: one that no mapping can read keeps its bytes
  and its validation reason, and when it still holds the id the deletion
  reports it for review. Earlier versions of a rewritten file are deleted too.
- **Uploads after a deletion**: validation drops the rows of a deleted entity
  before it validates a comparator file, and grading never reads them.
- **Evaluations and backtests**: a run in flight that pinned the earlier file
  fails, and its `reason` is `comparator_rewritten_by_erasure`. A request made
  while the file is being rewritten is refused with `failed_precondition`,
  reason `comparator_rewritten_by_erasure`, and the `comparator_id` in
  `details[0]`. Later requests grade the rewritten file under a new report
  id. Reports recorded before the deletion keep their grades.
- **Stored validation messages**: per-row violations stored before this
  release no longer repeat a cell's value. They now carry the message a new
  validation gives.</content>
  </entry>
</feed>
