Changelog
All notable changes to the Tessera platform APIs, ingestion, console, SDKs, connectors, models, and docs — newest first. Also available as an Atom feed. Deprecations follow the deprecation policy.
October 2026
Webhook deliveries say whether they are a test send
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.
Neutral wording across the docs, website, console and API descriptions
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.
Label previews cost less per file, and say when their read time ran out
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.
Dossier and card renders: renderer version 3 shows only the comparisons that matter
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 (passonly if every one passes,failif any fails, otherwiseincomplete). 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.
Dossiers now include the model's settled backtests
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-partnerorproduction) 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 andtruncated). Reports left out are not read. - No settled backtest: running and failed backtests are never embedded. A
model without a settled backtest reads
evidence: missingwith the new reasonnot_backtested, anddetailsays how many backtests were requested and failed.source_not_boundkeeps its meaning: no source in this deployment.
Dossiers assembled before this change are unchanged. No API response gains a field.
A refresh of a realtime slot now writes the realtime scorer's exact scores
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_fallbackgauge) 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_fallbackfor 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.
Availability wording corrected across the docs, website and console
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.
The trajectory inspector opens prefixes with CPU-profile enrichment results
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.
The trajectory inspector does less work for prefixes with captured original sources
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.
The customer map updates every night, and Members can update it now
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
GETandPUT /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/editionslists 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 /schedulesas kindcustomer_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.
A five-step story on the segment map
"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=1to?story=5in the address opens a step directly.- A step with nothing to show, such as a region with no stored predictions, is left out.
The trajectory inspector reads large prefixes in seconds
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.
The segment map outlines each segment as a region, with computed names and four zoom levels
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
+,-,0and1-4move 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.
The segment map opens with its customers settling into place
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=1in 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.
Ask the map answers verified questions about the segment map
"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.
Final CPU discovery deadlines from the measured latency
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.
Test credentials can no longer lift live self-exclusions, delete live files sooner or erase entities
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 withpermission_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.
Retried requests no longer keep one-time secrets in plaintext
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;createWebhookEndpointandrotateWebhookEndpointSecret: the signing secret;createOrgInviteandcreateOrgRoleInvite: the invite token;createUpload: the signedupload_url;createBillingCheckoutSessionandcreateBillingPortalSession: the hosted Stripe sessionurl.
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-Keyfrom 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 retriedrotateWebhookEndpointSecretnever rotates twice. - A stored response that can no longer be opened returns 409
already_existswithreason: idempotency_key_reusedinstead of running the request again. This happens when the server secret it was encrypted under has been retired. Send the request again with a newIdempotency-Key. - A stored response that cannot be opened right now, because the server
secret is briefly unavailable, returns 503
unavailablewith aRetry-Afterheader. Retry with the sameIdempotency-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.
A queued training run says when it is waiting for compute
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.
Rule previews leave out an entity as soon as its erasure starts
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.
Automatic onboarding's contract approvals are in the audit log
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
serviceand idautomatic-onboarding(display "automatic onboarding"). The contract'sapproved_byalready saysservice:automatic-onboarding. A reviewer's approval of a guided proposal is recorded as that reviewer. - Details:
version,fingerprint,source_idandonboarding_mode. When the approval superseded a version, alsosuperseded_versionandsuperseded_fingerprint. - Audit stream and SIEM: consumers of the audit stream will start
seeing these events, with this actor and these
detailskeys. 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.
Marker previews of silence patterns sample their read range evenly
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.
Label previews no longer blame a rule they could not judge
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.
Label previews sample entities whose outcome is known
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.
A reused Idempotency-Key answers only a retry of the same request
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.
Card and dossier downloads: renderer version 2 and v2 dossier templates
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
jsondownload 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
jsondownload, 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.
Every credential now acts only in its own environment
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 /slotswith a bodyenvironmentother than the credential's is refused withpermission_denied(environment_not_allowed). Slot names stay unique per project, so a name taken in the other environment answersalready_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
urlorevent_types) receives only that key's environment's entity-level events (such asscore.updatedandmarker.fired), slot outputs, and the events whose data names an environment:audit.recorded,gate.failed(whose data now carriesenvironment, the model's),slot.activated,slot.rolled_backandrefresh.*; those that name no environment, such as project-level audit records, still reach every endpoint, except agate.failedthat names none (emitted by an earlier version), which reaches no API key's endpoint. The slot outputs now includeaudience.syncedandaudience.failed(a signed link to the member file) andexport.completedandexport.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 needsgeneraland every purpose a slot of the project declares (403permission_denied, reasonpurpose_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 itsurlorevent_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.firedorsegment.migrationevents to any endpoint. For example, if a test key saved a live slot's monitor configuration, the live slot'sscore.updatedstops. Save the configuration again with a credential of the slot's environment to restore them. - Contract profile samples:
profile.examplesandprofile.top_valuesshow 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.
The console has the title bar it shares with Argus, and every page names itself in its tab
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.
Source reads no longer return the adapter webhook secret's encrypted form
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 400invalid_argumentonconfig/webhook_secret.- Changing
providerneeds 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_secretnames 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.
Score one event synchronously against a sequence-model slot
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.sequencenames 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_incorporatedwith 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 isslot_not_realtime; semantic-input models aresemantic_slot_unsupportedfor now.
See Synchronous scoring.
Score a raw source record synchronously
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_sourcenames 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 400source_not_preset_bound, a binding that no longer verifies is 400preset_binding_unverified, and a record the preset does not map is 400invalid_argumentwith the preset's reason.
See Synchronous scoring.
Realtime scores no longer depend on the events scored beside them
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.
Import a shared checkpoint with acknowledged advisory gates
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 itspromotionblock. - 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-importstill answersready: falsewithacknowledgement_requiredreasons for such a model and never takes an acknowledgement. Withexpected_preview_fingerprint,stale_previewis 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_gatesyet. - No response changes: requests without the member behave as before.
Description-channel models acknowledge their own quality gates like any model
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.
Acknowledge description-channel and observation-time evidence when you bundle
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.
Entity baseline models: a strong simple model to compare with and serve
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-modelstrains 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 asadmitted_byfor context. Each label's arm (gbm_default,gbm_tunedorrfm_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}/bundlescreates the bundle, which activates on a batch slot like any bundle; a non-v1 build's bundle also pins its tokenizer inputs, read withGET /entity-baseline-bundles/{bundle_id}/tokenizer-inputs; adding itspinsand inserting each artifact at itspositionrebuilds the stored manifest thatfingerprintcovers. A refresh writes each entity's scores, and the newGET /slots/{slot}/entities/{type}/{id}/scoresreads 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 …/stateand the other embedding-based reads refuse a baseline slot withmodel_family_not_supported.
Training studies: the default way to train a model
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}andPOST /v1/training-studies/{study_id}:cancellist, 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 readoutmean_poolorlast_event. Each child is an ordinary run that counts towardconcurrent_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 themax_node_hoursyou set (study_budget_exceeded, with both numbers). The estimate isepochs × train_tokens / (5,000 × 3,600)summed over the children. Withoutmax_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_competitiveornot_assessable, from the winner's competitiveness against the strongest baseline per label. The winner's verdict stays inevaluation.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.
Out-of-time holdouts for dataset builds, and labels censored at the end of the data
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.
Model quality is compared with simple baselines before promotion
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.
Baseline evaluations show a runtime estimate before launch
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 reasonruntime_budget_exceededand 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
detailsarray.
See Runtime estimate.
Full-book backtests stop at the end of the data
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 getsfailed_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 /backtestsanswersfailed_precondition(snapshot_file_mismatch), and a backtest that finds it when it runs endsfailedwith reasonjob_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 what a bundle needs before you create it
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.
Raw event reads require the event-feed purpose
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,getEventExportandwatchEventFeedHeads; - 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 (
previewSourceMappingandpreviewCatalogMapping) that sample the latest landed snapshot (from_snapshot); - projection exports (
createSlotExportwithkind: 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 emptyprofile.examplesandprofile.top_values; getUploadreturns a nullcolumnsandsample_rowspreview;getSyncandlistSourceSyncsdrop thepositionof a failed page'skeyset_positionerror detail;- the trajectory inspector (
getEntityTrajectory) returns emptyapproved_original_fieldsandoriginal_source_events; getSlotExportreturns a succeeded projection colored by event attributes without itsartifact_url; the console says the projection needs theevent-feedpurpose.
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:
- Register
event-feedin the project withputPurposes(PUT /v1/purposes). - Add it to the key with
putApiKeyPurposes, or to the role withputOrgRolePurposes.
A purpose registry that cannot be read now answers 503 unavailable, with reason purpose_registry_unavailable, on every service.
Dataset build retention: delete idle builds' files, and the erasure bound
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, orgadminor theadminscope) 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, ornull(off, the default).GET /v1/dataset-file-retentionreads 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; withinclude_archived_models: trueit 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-filesandGET /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 + Ddays with no readable copy andR + D + 7days with no copy at all, for a replacement madeRdays after the request and both retentions atDdays (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 gains Dataset build retention and Erasure completion bound, and Model file retention documents the dataset operations.
- New schemas only (
DatasetFileRetention,DatasetFileState, …); no released response changes.
Deleting an entity removes its values from contract profile samples
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-Keyreplay ofcreateContractDraftorapproveContractreturns the contract with emptyexamplesandtop_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 acreateContractDraftwith 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
examplesandtop_values, because sampling only covers attributes the contract does not yet know.null_rateandcardinalityare 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.
Score one event synchronously against an event-model slot
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
inclusionblock naming the event, bundle and model it used, plus adigestover 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 (503scorer_unavailable, retryable), and answers within a 2 s deadline (503scorer_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.
Unpinned event-store snapshots now expire after 7 days
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_idwhich expired, or expires within 15 minutes, is refused withfailed_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-idrecords 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-idrefuse 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-idwith a day or less left instead of replacing it.
Released schemas are unchanged; the request descriptions name the new reason.
Leakage reports and new comparator checks no longer repeat identifier values
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, eachhash: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_fingerprintis recomputed so it stays self-consistent. For such a report,value_countis the number of values it listed, at most five.GET /datasets/{id}andGET /event-datasets/{id}then serve the new values in the same shape; the build'supdated_atdoes 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.
Evaluation reports name entities and events by pseudonym
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 inentity_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 byhash: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_totalmetric 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_matrixreferences, 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.
Deleting an entity removes it from comparator files
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_idcolumn holds the deleted id are removed, and the file is validated again, so the comparator's counts,as_ofrange 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
reasoniscomparator_rewritten_by_erasure. A request made while the file is being rewritten is refused withfailed_precondition, reasoncomparator_rewritten_by_erasure, and thecomparator_idindetails[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.
Dataset builds record each split's longest row
Dataset builder 1.2.0 records the longest row of each split, in tokens, in the
dataset manifest (token_stats.split_max_row_tokens). A training run that
omits both trainer.global_batch_tokens and trainer.max_tokens_per_microbatch
on a single process uses it to size its microbatches to the batch, so each
optimizer step really holds about one batch of tokens.
The builder version is part of a dataset's identity, so the same build request
now produces a new dataset version instead of reusing an earlier build. A
dataset built before this change keeps working as it did, with the
131,072-token microbatch cap, until it is rebuilt. A dataset recipe from
GET /datasets/{id}/recipe still names the builder version that made it.
Earn observation-time enablement with an observation ablation
POST /models/{model_id}/observation-ablations evaluates a model that
consumes the elapsed-silence token together with its flag-off twin
(without_token_model_id: the model trained on the twin dataset build
without observation_time) and returns 202 with the ablation's status;
read it with GET /models/{model_id}/observation-ablations/{report_id}
(running, succeeded with gates_passed and recommend_enable, or
failed with a typed failure_reason). The evaluation is the model's
default one (same seed, baselines and signals included) plus the report's
observation_time_ablation evidence, which grades the with-token model, the
without-token twin, a tuned GBM and BG/NBD with Gamma-Gamma at the same
evaluation points. When it succeeds it becomes the model's evaluation of
record, so POST /models/{model_id}/bundles with
observation_time.enabled: true passes when the evidence recommends
enablement. A model that does not consume the token, or a twin that does,
is refused with a typed reason (observation_time_model_disabled,
observation_ablation_twin_consumes_token). One observation ablation runs
per model at a time (failed_precondition with reason
evaluation_in_progress). Starting one needs a member role or an
admin-scoped key; reading one needs read. Existing operations and
responses are unchanged.
Field declarations: declare fields and approve them before the first event
Projects can now declare their fields before any data arrives (WP-D7).
PUT /v1/field-declarations stores one versioned, fingerprinted document per
project. Each declaration gives a field's type, privacy class (pii),
proposed role, expected missingness and description, plus an opaque
annotations map that Tessera stores and never acts on.
- Approve before the first event. A put feeds the declared fields into the
project's profiling input and creates one contract draft. Fields the
approved contract does not hold yet take the declared values instead of
inferred ones; fields it already holds take the declared role, privacy
class, missingness and description, as an edit of the draft would. Approve
the draft with
POST /v1/contracts/{version}:approve; the put never approves. When nothing in the contract would change, no draft is created. - Safe to retry. An identical document answers the current version and
writes nothing;
expected_versionrefuses a put when the declarations changed since you read them. A changed type for an approved field, adirectprivacy class on afeature, or an identifier proposed as afeatureis refused with a typed reason, and nothing is stored. - Reads:
GET /v1/field-declarations(current) andGET /v1/field-declarations/{version}, at the same access level as contract reads. Replacing the declarations needs theadminrole or anadmin-scoped key. - Changed: installing a domain pack now carries each attribute's authored description into the draft contract it creates (before, install dropped it).
No existing response changed shape: the contract, leakage-policy and domain-pack responses are byte-identical.
warehouse_export webhooks for every state-snapshot export
Every state-snapshot export now also ends with a warehouse_export.completed
or warehouse_export.failed webhook. Its data is the warehouse export record
(GET /v1/slots/{slot}/warehouse-exports/{export_id}): destination, column
groups, write mode, and the schedule tick for scheduled exports. output is
summarized as the manifest URI, file_count and table; read the full file
list from the GET.
The webhook is sent once, right after the export finishes, with a stable
envelope id per export and event type, so a repeated send is deduplicated.
If the service stops at that exact moment, the webhook can be missing:
reconcile from the GET when an export ends without one.
These are by-name-only types: an endpoint receives them only when its
event_types lists them by name. An endpoint subscribed to "*" does not
receive them.
export.completed and export.failed are unchanged: every export, Snowflake
and extended column groups included, still sends one with the same data as
before. Their envelope id is now also stable per export and event type.
A security-lake playbook that CI runs
The Playbooks section gains a security-lake playbook. It starts from an OCSF
lake in a customer bucket and installs the ocsf domain pack. It syncs the
lake on schedules, approves the contract and trains a model. Its scores carry
the events that moved them, and an alert rule opens episodes that an analyst
dispositions. Scores and evidence are written back to Snowflake. The run ends
with a detection-quality report in the published-results format, which the
last step recomputes from the report's own appendix. CI runs every step on each
docs or API change. The lake is generated, so the report is a fixture
result.
No API or response changes.
Recompute a published result yourself
A published-results document can now be checked with one command.
tessera-doctest recompute repeats the calls in its recompute appendix against
the project the numbers were judged on. It then checks that every report
re-hashes to the fingerprint the document cites and that every number comes
back exactly. tessera-doctest publish writes such a document from a protocol
and the reports judged under it. The design-partner driver uses the same
builder. The Playbooks section gains a "Recompute this yourself" page.
No API or response changes.
A PostHog-loop playbook that CI runs
The Playbooks section gains a PostHog-loop playbook. A protocol is declared
first, with the salt that fixes which users are held out. A baseline is then
written down before any model exists, and the trained model is judged against
the same bar. The model's band is written to PostHog as a person property, and
the playbook checks that no held-out user received it. The comparison is
published in the published-results format and recomputed from its own
appendix. CI runs every step on each docs or API change. The events are
the quickstart's generated sample, so the results are fixture results. The playbook then reads the holdout's
incrementality readout from the cycles the delivery recorded; its follow-up
outcomes are generated and blind to the group, so the readout finds no effect.
No API or response changes.
Model file retention: delete archived models' files
Archiving a model version used to leave its files in object storage forever. They can now be deleted whole and verifiably:
- Retention:
PUT /v1/model-file-retention(putModelFileRetention, orgadminor theadminscope) sets how many days after the archive an archived model version's files are deleted, from 1 to 365, ornull(off, the default).GET /v1/model-file-retentionreads it. The setting covers both environments and is versioned:expected_versionrefuses a stale write with reasonversion_mismatch. - Explicit deletion:
POST /v1/model-files/{model_id}:purge(purgeModelFiles) requests the deletion of an archived model version's files. It is due 24 hours after the request at the earliest, its cancellation window:POST /v1/model-files/{model_id}:cancel-purge(cancelModelFilePurge) withdraws it until the deletion starts, after which it is refused with reasonpurge_already_started. - State:
GET /v1/model-filesandGET /v1/model-files/{model_id}show each model version's file lifecycle (retained,scheduled,held,deleting,awaiting_gcs_hard_delete,deleted), the typed holds that keep a deletion waiting (a slot or source still using a bundle, work in flight, the 24-hour drain, a pending project deletion, the organization's legal hold), and what was deleted. - What is deleted: the model version's run checkpoints or event-model
artifacts, its evaluation and backtest reports, and its bundles: every live
and noncurrent generation. Dataset builds (with their fitted encoders),
shared checkpoint copies, dossiers, the audit trail and the metadata rows
stay. The storage provider removes its 7-day soft-deleted copies by
provider_retained_until. - After the deletion starts, activating, rolling back to, bundling or
evaluating the model version is refused with reason
model_files_purged, and its evaluation report and card are no longer served. - Console: Settings → Model files holds the retention setting; each model page gains a Files section with its state and holds, Delete files now and Cancel deletion for admins; the activation, rollback and bundle dialogs explain the new refusals.
- Docs: the data deletion policy gains a Model file retention section, and Model file retention documents the six operations.
- New response schemas only (
ModelFileRetention,ModelFileState,ModelFileStatePage,ModelFileRefusalDetail, …); no released response changes.
The holdout incrementality readout now returns a measurement
GET /v1/slots/{slot}/experiments/{experiment_id}/readout used to answer
failed_precondition (readout_unavailable) for every request. It now reports
the treated and held-out outcome rates of one binary label over a window, their
difference and a bootstrap 95% interval, computed from the control manifests a
delivery path recorded. The response shape is unchanged, and the readout is an
output of the experiment's purpose, like its control manifest.
The method is fixed and documented, so a number can be reproduced: only cycles
recorded in the window count; an entity's outcome is the value of its first
observation of the label that matured within one maturation period after the
entity's first cycle; an entity without such an observation is left out of its
arm rather than counted as a failure, and n counts the entities with an
outcome; the interval is the percentile 95% interval of 1,000 unpaired
entity-level resamples with a seed derived from the request. Each arm needs at
least 30 entities with an outcome and at most 100,000. A readout that cannot be
computed (a non-binary label, no label store, no manifest in the window, an
empty arm, too few matured outcomes or an arm above the cap) still answers
readout_unavailable, with a message that says which; a storage fault that may
heal answers unavailable. No delivery path records control manifests yet, so
until one does a readout reports that no manifest was recorded.
Delivery paths now record holdout control manifests, and erasure reaches them
Exports, audience materializations, destination deliveries, alert webhooks and
the per-entity webhooks (score.updated, segment.migration, marker.fired)
now record, for every delivery cycle that runs under a holdout experiment's
purpose, the entities they withheld and the entities they delivered. The
control manifest endpoint (GET /v1/slots/{slot}/experiments/{id}/control-manifest)
serves them, and the incrementality readout is computed from them, so an
experiment with a delivery now has data to read.
Each path defines its cycle in the activate documentation. The per-entity webhooks have no durable copy, so their recording is best effort: each recording window is written as one cycle holding both arms, and a window that is lost is lost for both arms, which leaves the readout unbiased.
Deleting an entity (POST /v1/entities/{type}/{id}:delete) removes it from every
recorded control manifest of the project, and a recording made after the
deletion leaves it out. A recording that cannot be written never blocks a
delivery: it is counted by reason (control_manifest_record_failures_total) and the
cycle's two arms are lost together. The readout refuses a label whose
point-in-time read would hold more than 400,000 observations or 64 MiB, with a
message that says so, and an empty readout can mean that recording was
unavailable.
Enable hazard horizons through a pre-registered ablation
Hazard horizons can now be enabled on a bundle. Enablement rests on evidence
declared before the data: a hazard-ablation protocol
(POST /protocols:hazard-ablation) names the hazard targets and horizons it
covers, the margins and the bootstrap seed; both metrics (auc, brier)
and both arms are always judged. The arms are the same model trained without hazard heads on the
identical build, and the BG/NBD timing family.
POST /evaluations:hazard-ablation grades a model with hazard heads against
its flag-off twin model on identical points: the held-out anchors whose
horizon has closed. Each target, horizon, metric and arm gets its own
criterion, and a criterion passes only when the paired bootstrap confidence
bound clears the margin. Criteria that cannot be graded are reported as not
applicable and never count as a pass. Examples are the twin model at horizons
other than its label's own, and horizons with too few closed points. The label
heads must not regress, and the model must pass its gates.
GET /evaluations/{report_id}:hazard-ablation serves the evaluation and its
verified report.
POST /models/{model_id}/bundles:hazard (createModelHazardBundle) with hazard: {"enabled": true} then reads
every succeeded hazard-ablation evaluation of the model that graded its
current eval report and calibration. When there is at least one and all of
them pass, the enablement rests on that verdict, so declaring several protocols
cannot shop for a pass. Otherwise the request reports what it found as bundle
findings the user decides on (inform, don't block): hazard_ablation_missing,
hazard_ablation_failed or hazard_ablation_incomplete. Repeating the request
with acknowledge_findings (each finding's code and fingerprint, plus a
reason) enables hazard serving and records the acknowledgement in audit and in
the bundle's hazard sidecar. A report that fails re-verification or does not
bind to its protocol, model and config is still refused, with
hazard_ablation_unverifiable. So is a report whose protocol lacks the full arm
and metric set or a declared seed, which only pre-ruling or tampered content can
carry; an acknowledgement never accepts either. Existing
responses and the standard protocol and evaluation operations do not change.
A one-command design-partner driver
tessera-milestone-partner (in tools/milestones) runs the judging half of
the design-partner runbook in one command. It evaluates a trained model against
its registered protocol, runs the lead-time backtest when the protocol declares
lead-time metrics, and archives the protocol and the reports by fingerprint. It
then writes a published-results document and the command that repeats the run.
Every threshold comes from the protocol. The provenance class is the protocol's,
capped by where the run executes. The driver writes nothing but evaluations and
backtests.
No API or response changes.
A deposit-flight playbook that CI runs
The Playbooks section gains a deposit-flight playbook. A generated deposit
book lands as a typed Parquet upload, a value-aware label marks a transfer
that moves most of a balance out, and the protocol is declared before any
build. The playbook then builds the dataset with observation time and its
twin without it, trains both, pins the observation ablation that compares
them (POST /models/{model_id}/observation-ablations), and serves with
observation time on a batch slot. Three re-scores a week apart, with no new
events, show the quiet accounts' silence growing and their median risk
higher at the last re-score than at the first. A
cutoff-ladder backtest, the sr11-7.v1 dossier and a published lead-time
result recomputed from its own appendix close the chain. CI runs every step
on each docs or API change, and runs it once more with observation time
off, where it must fail at the re-score step. The book is generated, so the
results are fixture results.
No API or response changes.
Bundling an archived model is refused
POST /v1/models/{model_id}/bundles now refuses to bundle a model that is
archived, a shared-checkpoint import included. The call answers 400
failed_precondition, and details[0] is a precondition_failure with
reason model_archived and the model_id.
Until now it promoted the archived model again, silently undoing the archive.
POST /v1/event-models/{model_id}/bundles already refused an archived event
model.
POST /v1/models:import-shared(importSharedCheckpoint) bundles the newest prepared candidate the same way, so it now refuses a candidate you archived (model_archived) instead of bundling it. A candidate whose model files are being deleted is refused with reasonmodel_files_purged.- A replay of an import whose candidate was already bundled, and later archived or had its files deleted, is refused the same way.
- An import that sends
expected_preview_fingerprintis refused by the preview check first, with its existing reason (stale_preview, orlocal_artifact_mismatchif the preview already showed the archived candidate), before any model file is read. - One candidate exists per dataset: preparing the import again over the same dataset returns the archived candidate. A new candidate needs a new dataset build (a different dataset fingerprint).
- Bundles created before a model was archived are unchanged and stay listed (a model cannot be archived while a slot serves one of its bundles).
- To promote that version again, bundle a model that is not archived (train or import it again).
- No response schema changes: the reason travels in the open
ErrorDetail, as the event-model refusals do.
Training defaults that give small and medium datasets real updates
A run that omits trainer.global_batch_tokens now sizes its batch from the
dataset: clamp(pow2_floor(train_tokens / 100), 16384, 1048576) tokens, so
an epoch has about 100 optimizer steps. Before, every run got a fixed
1,048,576-token batch. On a 3.8 million-token train split that gave 4 steps
per epoch and 40 updates in total, too few for the model to learn its labels.
Datasets of 104,857,600 train tokens or more keep the old batch.
When max_tokens_per_microbatch is omitted too on a single-process run, the
microbatch cap follows the batch, so each step really holds about one batch.
The cap needs a dataset built by dataset builder 1.2.0 or later, which records
its longest training row: a dataset built earlier keeps the 131,072-token cap
until it is rebuilt.
Single-process wsd runs now plan the learning-rate cooldown on the updates
the data really supports. The rate reaches zero on the last update, and the run
reports stop_reason: budget_cap instead of data_exhausted.
When overrides.label_weights is omitted, each binary label weighs 10 in the
training loss. After training, a single-process run with a binary label refits
each binary head on the frozen representation of the checkpoint it ships,
using training rows only. The model version uses the refit checkpoint, which
appears last in the run's checkpoint_uris. A head whose refit would not fit
in the trainer's memory keeps its trained weights, and the run still
completes.
Explicit settings are honored exactly. A run's recipe from
GET /runs/{id}/recipe still reproduces its original configuration byte for
byte, including runs created before this change. Request and response shapes
are unchanged, so existing API clients keep working.
Runs ship the checkpoint that predicts their labels best
A training run with label heads now picks the checkpoint it ships by its labels. The pick is the validation with the greatest mean primary metric: AUC for binary labels, macro-F1 for multiclass labels and R² for regression labels. An exact tie goes to the lower total validation loss. Before, a run shipped the validation with the lowest total loss. That loss also counts the next-event terms, which can keep improving after a label has started to overfit, so a run could ship a checkpoint that predicted its label worse than an earlier one. A run without validation label targets still ships the lowest-loss checkpoint, and so does every run created before this change.
best_step on a run names the checkpoint the model ships. best_val_loss
keeps its meaning, the lowest total validation loss seen, so the two can belong
to different steps. Early stopping still reads the total validation loss. The
model card in the console now shows "Shipped checkpoint" and "Lowest validation
loss" as separate facts.
Response shapes are unchanged, so existing API clients keep working, and a run's recipe still reproduces its original configuration exactly.
Event models: train, evaluate and promote scorers of single events
Tessera now trains event models: supervised scorers of one event from that event's own contracted fields, with no entity history (WP-M25). They take the same lifecycle as sequence models, through new operations only. The released model, label, dataset and bundle responses are unchanged.
- Event labels:
POST /v1/event-labelsdeclares an event-level target fed by a connected table whose rows name the event they are about (a chargeback naming the payment it disputes). A required horizon,maturation_days, defines the target as the label known attimestamp + maturation_days. Labels are archived and restored like entity labels. - Event dataset builds:
POST /v1/event-datasetsassembles the labeled events point in time. You choose an entity-hash split, or a time split with an embargo before each cut. The build masks every event that cannot be supervised, giving the reason, and freezes the feature layout. It measures how late each event type's events arrived, and flags or excludes backfilled events whose fields may postdate their outcome. - Training and evaluation:
POST /v1/event-modelstrains one deterministic, calibrated gradient-boosted tree model per label on the CPU pool. It is cancellable, and counted against your concurrent-training limit. The evaluation report and gates G1–G6 follow automatically. G4 checks that the serving path reproduces the stored features and scores exactly; G5 re-audits every target mask.GET /v1/event-models/{model_id}and/eval-reportread them. - Bundles and slots:
POST /v1/event-models/{model_id}/bundlespromotes under your promotion policy. Activating the bundle on a slot of its entity type switches the slot to the event family. While a slot is event-active, operations that need entity state answermodel_family_not_supported. - Serving: the serving image carries the native runtime that loads and scores event bundles, with exact TreeSHAP attributions that cite the event's own field values. Real-time event scoring (WP-V17) follows in a later release; until then an event-active slot serves no scores, and batch scoring refuses it.
- Limits:
- explanations are budgeted per bundle: a bundle too large to explain
within 50 ms is marked to serve scores without attributions
(
runtime.explanationis{top_k_default: 0, top_k_max: 0}); - a bundle too large for the serving process to load is refused at
creation (
bundle_exceeds_serving_memory), and a training whose largest possible bundle would be refused is refused at the training request with the same reason (an identical request still returns its model). Over a small feature layout that admits up to 11 binary labels, one multiclass label of up to 7 classes, or one binary label over about 504 numeric attributes seen in training; - activation refuses, with the same reason, a swap the serving process could not hold: the new bundle loading while the slot's current event bundle is still loaded;
- the hybrid recipe that adds sequence features, and model cards and dossiers for event models, come later.
- explanations are budgeted per bundle: a bundle too large to explain
within 50 ms is marked to serve scores without attributions
(
- The generated clients and the API reference are regenerated for the new operations.
Bind a source to a raw-source preset
PUT /v1/sources/{src_id}/raw-preset binds a source to a packaged raw-source
preset and GET reads the binding (mcc-canonical-event maps
canonical_event_v1 JSON records). The source's raw receipts are then mapped by
the preset instead of being profiled: mapped events need their event type
included in your approved contract, records the preset cannot map are
quarantined with their retained raw record and re-evaluated later, and each
canonical event_id is derived from the source's own event id, which is kept as
the external_event_id attribute. A source carries either a preset or an
inferred binding, never both.
Two existing operations now refuse a request that would give one source two
interpretations: PUT /v1/sources/{src_id}/mapping answers
failed_precondition (source_has_raw_preset or source_has_binding) for a
source that a preset or an accepted inferred binding already interprets, and
PATCH /v1/sources/{src_id}/onboarding answers source_has_raw_preset for a
preset-bound source, whose mode a project-wide switch also leaves alone. Sources
without a preset or binding behave as before, and no response shape changes, so
existing API clients keep working.
A published-results format and a design-partner runbook
The docs gain a design-partner runbook under Playbooks, and a public format for
publishing results: api/jsonschema/published_results.json with a matching
template. A results document names its provenance class (fixture,
design-partner or production) in the headline, gives every number a
fingerprint that points at an archived report, cites the protocol it was judged
under, and ends with the calls that reproduce it. The docs lint checks these
rules on every document that uses the format.
No API or response changes.
A payments-fraud domain pack
GET /v1/domain-packs now lists a second packaged pack, payments-fraud
(version 1), and POST /v1/domain-packs/payments-fraud:install installs it.
It declares account, card, customer, device and merchant entity types; payment,
refund, chargeback, login, payout and account_change event types with
amount_minor (integer minor units), currency (ISO 4217) and
transaction_id attributes; a draft contract that still needs your approval;
and five derived labels that are true when an entity's ledger records a
chargeback event within 90 days. The labels come only from the chargeback
events you send.
Existing packs, labels, contracts and API responses are unchanged, so existing API clients keep working.
MCC event types now reach the payments pack
Raw-source presets can carry an optional event_type.map that replaces exact
source event types with canonical ones. Version 2 of mcc-canonical-event maps
the 14 event types MCC sends (card.authorization.approved to payment,
chargeback.filed to chargeback, and so on) and keeps the original in
context.source_event_type. Version 2 of the payments-fraud pack records every
event against the customer, adds the ip, location and merchant_category
entity types and the event types those sources send, so the pack's
customer_chargeback_90d label now fires on that data.
Label histories are read per primary entity, so on customer-primary data only the customer label sees chargebacks; the account, card, device and merchant labels apply to sources whose events have those entities as primary. Secondary-entity label histories (planned) are not built.
Version 1 of both is unchanged and stays available. Event ids do not change between the versions, so moving a source from version 1 to version 2 and sending an event again stores it once. The first stored copy wins: events ingested under version 1 keep their version 1 types and are not retyped by a re-send, so bind version 2 before the first ingest (or use a new source or project). A request to bind the preset that names no version binds the latest release of that preset, and keeps the current version of a source already bound to the same preset; existing bindings and installs keep the version they pinned.
Marker previews answer on large projects
POST /v1/markers:preview used to read a project's entire event history, so
on large projects it could run past the 30-second request limit. It now reads
the newest days of the entity type's events, back as far as the pattern looks
(for example 30 days for a 30-day count). Within that reach the result is the
same as reading the whole history, except that an entity with exactly 1,024
events in reach may be marked truncated to be safe. A pattern that can match
silence, such as "not active in 30 days", samples from every entity seen in
the whole range it reads, so entities that went quiet show up in the preview. A pattern with a sequence step looks at
all history, so on a large project the preview reads what fits in its budget
and marks those entities' windows as truncated instead of guessing.
The preview shares the label previews' limits: a 503 unavailable with reason
marker_preview_deadline_exceeded and a request id after 25 seconds, and a
503 with Retry-After when too many previews run at once. The examples now
show both active and inactive entities when both occur. Request and response
shapes are unchanged.
Derived-label previews answer on large projects
POST /v1/labels:preview and POST /v1/pattern-labels:preview used to read
a project's entire event history. On a project with about 100,000 streamed
events this took longer than the 30-second request limit, and the label wizard
showed "service temporarily unreachable" with no request id.
A preview now grades the rule over the newest days of the entity type's
events: twice the horizon plus one day, at least 28 days, back from that
type's newest event and never later than today. It reads further back only
to find enough entities. Each anchor in those days gets the same value as a
full-history build at the preview's snapshot time. The examples show every
outcome the preview found: positive, negative, and not yet observed when a
horizon is still open. If the entity type has no events in those days, the
preview says so with the warning entity_type_has_no_recent_events.
A preview that can't finish within 25 seconds returns a 503 unavailable
error with reason label_preview_deadline_exceeded and a request id. When
too many previews run at once, a preview returns a 503 with reason
preview_capacity_exhausted and a Retry-After header. The console cancels
a preview you have already edited past.
The anchor and observation counts describe that window rather than the whole history; the dataset build still counts the whole history. Request and response shapes are unchanged, so existing API clients keep working.
Decision metrics no longer report placeholder lift and capture when no entity is positive
Decision metrics rank each entity by its latest test target. When none of
those targets is positive, lift, capture and recall are undefined, but
reports showed lift@k = 1.0 next to capture@k = 0, two placeholder values
that no ranking can produce, and baseline-first verdicts graded them as
measurements. Those three metrics are now left out (the label reports zero
positive entities); precision at the list size, alert precision
and the band-assignment matrices are still reported. A protocol criterion on
an undefined metric now shows that reason instead of a value and can never
make the protocol verdict pass; criteria on the measured metrics are graded
as before. Comparators and multiclass classes follow the same rule. Whenever
positives exist, capture@k always equals lift@k × n_top / N.
Starting a dataset build answers promptly on large projects
POST /v1/datasets used to read every commit of a project's event history
before answering. On a project with about 724,000 events that took longer
than the 30-second request limit, so the console showed a gateway error even
though the build had started. The request now answers in a few seconds at
that size, and the build itself is unchanged: the same snapshot, the same
fingerprint, and the same reuse of an identical earlier build.
Author your own raw-source preset
PUT /v1/sources/{src_id}/raw-preset now also accepts a preset you author
({"document": …}) in the same closed, data-only format as the packaged
presets. POST /v1/sources/{src_id}/raw-preset:validate checks a document and
an optional sample record without storing anything, returning typed errors
(a JSON pointer, a reason code and a message) and the canonical event the
sample would become, and GET /v1/sources/{src_id}/raw-preset/document reads
the stored definition. Each source keeps its presets as immutable, fingerprinted
versions; event ids derive from the preset name and your own event id, so an
event sent again after an edit is stored once. A preset maps up to 128
attributes in at most 32 KiB. Paths are literal and bounded, and value shapes
use a short bounded pattern language instead of regular expressions.
Existing sources, presets, receipts and API responses are unchanged, so existing API clients keep working.
The validation reports what a save would refuse on the source, never repeats a value from your sample record, and answers a document it cannot read (a number that is not finite, an unpaired surrogate, nesting deeper than 8 levels) with one typed error at the root. A version is a whole number up to 2147483647.
Observation-time serving: scheduled re-scoring of quiet entities
Deployment slots can serve state as a function of observation time (WP-V14): with an observation-time model active, a quiet entity's served risk moves on a schedule without a new event, and every state row records the cutoff it was computed at.
- Slot configuration:
GET/PUT /v1/slots/{slot}/observation-time—enabled,silence_thresholds,sweep_scheduleandmaturation_lag. Enabling requires the slot's active bundle to pin an observation-time model; sweeps require a realtime slot. - Scheduled re-score sweeps: on each tick, entities whose silence crossed
a configured threshold are re-scored at
scheduled_for − maturation_lagthrough the normal realtime scorer. as_ofandquiet_state: recorded with every state write of an enabled slot; read them withGET /v1/slots/{slot}/entities/{entity_type}/{entity_id}/observation. The existing state-read responses and v1 export records are unchanged. On enabled slots, sweep re-scores are state writes like any other (they emitscore.updated), and alert payloads'as_ofis the observation time.- Lagged cutoffs: on an enabled batch-only slot, a refresh or export
without
cutoffusesnow − maturation_lag(sweep ticks use the lag on realtime slots); warehouse exports that include theas_ofcolumn carry each row's storedas_of. - Slots that do not enable observation time are unchanged in every byte.
- The generated clients and the API reference are regenerated for the new operations.
Evaluation: baseline evaluations before any model
New API operation: POST /evaluations:baseline {dataset_id, protocol_id, comparator_ids?, seed?} returns 202 with an evaluation index row
(basis: "baseline_eval"). It grades a pre-registered protocol's baseline
arms (gbm_default, gbm_tuned, rfm_gbm, bgnbd_gg) and your registered
comparators on a dataset build's test split, with no training run, model or
bundle. The protocol must be declared strictly before the build
(protocol_after_data otherwise), and criteria are never restated in the
request. Read results with GET /evaluations/{report_id}, and list them with
GET /evaluations?protocol_id=&dataset_id=.
The stored report follows the new baseline_eval_report.json schema: per-arm
metrics with 95% entity-level bootstrap confidence intervals, decision
metrics, comparator gradings, evaluation_basis (with the
target_set_fingerprint a later model evaluation under the same protocol
shares) and a bar_only verdict that records the model as absent. Existing
evaluation reports and GET /models/{model_id}/eval-report are unchanged.
A succeeded baseline evaluation is metered once as build_cpu_seconds:
wall-clock seconds × the 8 reserved vCPUs of the cpu-standard class, with
dims.basis = "baseline_eval". Failed or retried attempts are not metered.
Deploy metering-service (which accepts the new dims.basis key) before or
together with eval-service; an older metering-service rejects the event as an
unknown dims key.
Holdout experiments use the protocol's pre-registered salt
POST /slots/{slot}/experiments with a protocol_id now creates the
holdout experiment with the protocol's pre-registered holdout_salt, so the
held-out group is fixed before any data exists. An unknown protocol is
not_found, and a protocol that declared no holdout_salt is refused with
failed_precondition (protocol_without_holdout_salt). Experiments without a
protocol_id keep minting their own salt. Only the salt fingerprint is ever
returned.
Protocol evaluations with pass, fail and not-evaluated verdicts
POST /evaluations {model_id, protocol_id, seed?} evaluates a model against
a declared protocol and returns 202 with the evaluation's status. The
report gains a verdict with one row per declared criterion
(label, metric, arm): pass only when the lower bound of the paired 95%
confidence interval of (model − arm) clears the declared margin, fail when
it does not, and not_evaluated with a reason when the report has no
evidence for it. The overall verdict is pass, fail or incomplete. An
evaluation_basis block names the protocol, the dataset build and the
fingerprint of the evaluated test targets, and a recompute appendix lists
every fingerprint, seed and API call behind the numbers.
Read evaluations with GET /evaluations (filter by protocol_id or
dataset_id) and GET /evaluations/{report_id}, which serves the stored
report after re-verifying its fingerprint. Verdicts are advisory: promotion
gates, serving and deliveries never read them. A protocol evaluation becomes
the model's latest evaluation report; its gates equal those of the model's
other reports for the same seed, and a gate.failed webhook is no longer
sent again when a newer report of the same model repeats the same failed
gates.
Detection-quality verification harness: planted and control books
Tessera's verification now has a harness for protocols' detection_quality
thresholds. It is a test and verification harness, not an API operation:
the acceptance tests run it today, and the phase driver and the evaluator's
real-image check will run it next. It scores two books built from one
seeded stream: one with a planted compromise or flight sequence appended to
a fixed set of entities, and the identical book without it. An entity is
alerted when it ranks within the protocol's alert_budget and its score
reaches the banding policy's highest band; alert_precision@budget counts
the top-budget ranking. The planted run must meet every declared threshold
and alert at least one planted entity (every planted entity when no
threshold is declared). The control run must alert no entity. A one-sided
pass is a fail, with typed reasons per direction. Every declared label is
judged and listed with its rule, and a label with no planted entities is
not_evaluated. A bar no model could reach on the books stops the harness
with harness_fixture_cannot_satisfy:<bar>. Results record the protocol id
and fingerprint, the seeds and both books' fingerprints, and are
byte-identical for the same inputs. No API change.
Evaluation protocols: declare your success criteria before the data
New API resource: POST /protocols declares an immutable evaluation protocol
in one call — the labels, metrics, baseline arms and comparators your model
must beat, a success margin per metric, the banding policy decision metrics
are measured under, and a provenance claim (fixture, design-partner or
production). The server fingerprints the declared content and records it
together with its audit event; declared_at is the server time of that
event. GET /protocols and GET /protocols/{protocol_id} read protocols
back verbatim. There is no update or delete: declare a new protocol to
supersede one.
Declare a protocol before building the dataset it will judge: protocol
evaluations and backtests judge only dataset builds whose manifest was
created strictly after the protocol's declared_at, and refuse any other
with failed_precondition (protocol_after_data). Those operations are
listed in the API reference and answer 503 unavailable until they ship.
Protocol backtests: lead-time curves per arm
POST /backtests {protocol_id, model_id, cutoffs?, seed?} replays a ladder
of past cutoffs over the test split of the model's dataset and reports, for
the model, the protocol's baseline arms and its comparators, the share of
positives alerted at least d days before their outcome at the protocol's
alert budget, on the fixed grid d ∈ 90, with median lead days and entity-bootstrap 95% confidence intervals.
Eligibility at each lead day is the same for every arm, so the curves
compare like for like. For arms whose scores only change with new events, an
alert counts only once the entity has new events, and each arm reports how
many first alerts came during silence. Without cutoffs, six
evenly spaced cutoffs over the dataset window are used. Read backtests with
GET /backtests and GET /backtests/{backtest_id}. A backtest never
refreshes a slot or writes served scores, and reruns are byte-identical.
Backtests by slot or bundle
POST /backtests now accepts a deployment slot or a bundle_id in place
of model_id. A slot backtests the model of its active bundle at request
time, and a bundle backtests its own model. The backtest is identified by
the bundle it resolved to (with its fingerprint), not by the addressing: the
same bundle by slot or by id is one backtest, and activating another bundle
later starts a new one. The slot is recorded as metadata. A slot with no active bundle is refused with
failed_precondition (slot_without_active_bundle).
Turn on prediction evidence and peer frames with a signals evaluation
POST /models/{model_id}/signals-evaluations evaluates a model with
prediction evidence (evidence: {enabled: true, top_k?}) and/or an
attribute peer frame (peer_frame: {kind: attribute, event_type, attribute}) and returns 202 with the evaluation's status; read it with
GET /models/{model_id}/signals-evaluations/{report_id} (running,
succeeded with gates_passed, or failed with a typed failure_reason).
When it succeeds it becomes the model's evaluation of record, and
POST /models/{model_id}/bundles with signals.enabled: true carries the
evidence and peer-frame settings into the bundle, so scored states include
the top transitions and attribute values behind each surprise and are
calibrated against the entity's peers. Gates G1–G6, label metrics and
baselines equal those of the model's default evaluation; bundles created
earlier keep their pins. One signals evaluation runs per model at a time
(failed_precondition with reason evaluation_in_progress). Starting one
needs a member role or an admin-scoped key; reading one needs read.
Semantic preparation resumes after pausing and after a raised budget
Waiting semantic preparation of accepted JSON events (WP-C11) now follows source control changes instead of being dropped:
- Pause and resume: switching a source to manual pauses its waiting work. Resuming onboarding, or switching the source (or its project) back to guided or automatic, picks the paused work up again from its original pins. Work whose source records have expired, or that was cancelled, invalidated or already published, is never revived, and a manual mapping keeps the source paused.
- Daily budget edits: raising the source policy's daily inference budget makes budget-deferred work due right away; lowering it applies from the next admission. A budget edit or a mode switch no longer stops waiting work. The worker accepts the resumed request with its original timeout and binding.
- Other policy edits: any other change, such as a new inference timeout, still stops waiting work instead of preparing it under different rules.
- No API response or schema changes.
Rollout order: run the contract-service migrations first, then roll the semantic-enrichment workers (they must accept a resumed grant above the original budget), then the contract-service automatic workers, before any producer of the new recovery wake format. An older contract-service worker that receives the new wake format rejects it; the durable due index re-drives the wake once a new worker runs, so no work is lost.
Governance evidence: pinned card exports, review dossiers and the model inventory
Tessera now packages the evidence it records for model risk, security and works-council reviews (WP-A7). Nothing is written about your model that the platform did not record, and evidence that does not exist is stated as missing.
- Governance fields:
GET/PATCH /v1/models/{model_id}/governanceand/v1/slots/{slot}/governancehold the intended use, limitations and usage restrictions you author. Every change is audited with the values before and after. - Pinned model cards: promoting a model pins its model card, governance
fields included, next to the bundle. A card source that fails verification
refuses the promotion.
GET /v1/models/{model_id}/card:export?format=markdown|pdfrenders the pinned card byte-identically every time. It adds a labelled current-governance block when the fields changed after promotion, and marks unpinned cards UNPINNED. - Dossiers:
GET /v1/dossier-templateslists four fingerprint-locked templates (sr11-7.v1,soc-security-review.v1,eu-ai-act-annex-iv.v1,works-council.v1).POST /v1/dossiersassembles an immutable, fingerprinted dossier for a model or a slot. Each section embeds evidence that re-verifies against its bytes, or states why it is missing.GET /v1/dossiers/{dossier_id}:download?format=json|markdown|pdfdownloads it. The audit extract states its cap, and the label-free anomaly section is always present for signals-enabled bundles. - Model inventory:
GET /v1/orgs/{org_id}/model-inventorylists every model version (shared-checkpoint imports included) and slot of the organization exactly once, with owners, gate and promotion facts, baselines, pinned card fingerprints, monitors and who last activated each slot. A source that fails verification withholds only its own row's facts, andfacts_withheld_reasonsays which and why. It needs the organization admin role or a custom role grantingmodel_inventory.list. - The released
GET /v1/models/{model_id}/cardresponse is unchanged. - Two-person approval for promotions is planned and not included.
- The generated clients and the API reference are regenerated for the new operations.
Large uploads complete without reading the whole file
POST /v1/uploads/{id}:complete no longer reads the whole uploaded object, so
completing an upload of any size up to the 50 GiB limit returns promptly instead
of timing out. It checks the object's size, records its storage metadata, and
reads only the first 1 MiB to detect the CSV dialect. The sync that ingests the
file now confirms the object is unchanged since completion (a replaced object
fails the sync with upload_object_changed) and computes the file's sha256
fingerprint while it reads. GET /v1/uploads/{id} returns that value as
file_fingerprint once the sync has succeeded; before then it is null.
Response shapes are unchanged.
CPU Start scales its stopped workers on a real cluster
The first live CPU Start was refused when it raised the stopped CPU workers from zero to one replica. The scale request checked a replica count of zero that the cluster leaves out of its scale record, so every Start rolled back. The request now relies on the resource identity check that already pins the stopped worker and sets the replica count directly. Stop, status and the GPU controls are unchanged.
Provisional CPU discovery deadlines sized from the live measurement
The first live CPU measurement showed that a whole structural-discovery request
(the embedding pass plus four role questions) takes about 60 seconds on the hosted
CPU workers, at the old image's fixed 60-second budget. The discovery server now
gets a provisional 300-second request budget, at least four times that lower
bound, and the contract-service waits 330 seconds. These replace the
120-second server and 150-second client values from 2026-10-11. The final values
follow an uncensored latency measurement with the newly admitted image. A rebuilt
CPU semantic image (a85ac91b..., source cb9eb0ce) passed the 24-question
real-model CPU proof with recipes identical to the first one and is admitted with
these deadlines; the first image keeps running without them. GPU workers and the
60-second defaults are unchanged.
Rejected-row samples hide secret fields at every depth
GET /syncs/{sync_id}/rejections already replaced the values of columns whose names suggest secrets (password, token, API key, authorization, cookie and similar). It now does so at every depth. A secret-named field inside a nested object or an array is replaced, and so are the value half of a name/value pair such as {"name": "Authorization", "value": ...}, the rest of an array that starts with such a name, and JSON text held in a string value.
Redaction now happens before values are shortened, so a truncated value can no longer show part of a credential, and a column name longer than 256 characters is checked at its full length. redacted_columns also lists a column whose nested content was redacted, and nesting deeper than 32 levels is hidden. The response shape is unchanged.
Purpose allow-lists are enforced on every output
API keys and custom roles with a purpose allow-list
(PUT /v1/projects/{project_id}/api-keys/{key_id}/purposes,
PUT /v1/orgs/{org_id}/roles/{role_id}/purposes) are now refused any output
whose effective purpose is outside the list. Reads, exports, export
schedules, audiences, lookalike jobs, alert rules and episodes, destinations
and experiment control manifests answer 403 permission_denied with
details[0].reason purpose_not_allowed, naming the purpose. Nothing is
created or returned, and no signed URL is issued. Keys and roles without a
list, and built-in roles, are unaffected.
Background outputs check the allow-list of the credential that set them up each time they run: export and projection runs, scheduled exports, audience materializations (including the runs an audience's own interval schedule starts, which act for the audience), destination deliveries and alert webhooks. Updating a schedule, audience or alert rule, or enabling a destination, makes the caller the credential it acts for; a manual audience materialization acts for whoever triggered it.
In an organization that uses purpose restrictions (any key or custom role with an allow-list, or any slot with a declared purpose), a revoked key or a removed or deactivated member stops the outputs that act for it. Re-bind them before rotating or offboarding: update the schedule, audience or rule, or enable the destination, with a current credential. Organizations that use no purpose restrictions are unaffected: rotation and offboarding never stop their outputs.
A refused destination is disabled with disabled_reason: null, and the
organization audit log records a destination.auto_disable event naming the
purpose. Re-enable it with a credential that may use the purpose and run a
backfill.
A slot purpose change now reaches serving reads and per-entity webhooks
(score.updated, segment.migration, marker.fired) within 5 seconds:
those webhooks apply suppression sets and holdouts under the slot's purpose.
If the slot purpose cannot be read, the output answers 503 unavailable, and a
per-entity webhook is not sent.
PostHog destinations deliver under the slot's band binding
Destinations now read the slot's band binding (PUT /v1/slots/{slot}/bands).
A slot with a binding can get a PostHog destination, and band changes and
backfills deliver tessera_band, tessera_score_<label> and the provenance
properties under the bound policy. A slot without a binding still answers
failed_precondition.
A destination's purpose must now be a name in the project's purpose
registry (GET /v1/purposes); any other name is 400 invalid_argument.
Deleting an entity also removes its person properties from deliveries that are still waiting to be retried, so they are never sent after the deletion.
Observation-time pattern labels, compile_pattern type, markers on score.updated, rollout order
Observation-time datasets now carry a permanent end-to-end check with a WP-C7
v2 pattern label: a churn rule written as a window_agg pattern produces
exactly the hand-computed as-of targets of the equivalent v1 rule, G5
recomputes every as-of target, row and accounting entry from the manifest,
and the evaluation job passes G4 through G6.
tessera_core.labels.compile_pattern returns an EventTypeGlob.
.match() works as before; regular-expression members (.search,
.fullmatch, and .pattern read as a regular expression) are not
available. No caller in this repository uses them.
score.updated does not carry behavioral markers. Read an entity's active
markers with GET /v1/slots/{slot}/entities/{entity_type}/{entity_id}/markers
(getSlotEntityMarkers), or subscribe to the marker.fired webhook.
Rollout order: deploy scoring-service before an eval-service that produces behavioral-evidence or peer-frame signals artifacts, so every scorer can load the bundles those evaluations promote.
Serve hazard horizons from scored state
Hazard builds, training and calibration are available, and the serving path for
hazard horizons is in place; enabling it on a bundle is not yet possible. A slot
whose active bundle serves hazard heads writes, together with each entity's
scores, the calibrated probability that each target happens within every
horizon of the grid, the calibration status of each horizon and the expected
time given the event happens within the last horizon. The three hazard reads
answer from that state: GET /slots/{slot}/entities/{entity_type}/{entity_id}/hazard
(404 no_hazard for a row written without hazard values, 404
hazard_cells_dropped when the entity's hazard values failed validation at
scoring time and were dropped while its scores still wrote),
GET /slots/{slot}/hazard (the targets, grid, calibration status and holdout
ECE of the active bundle, or enabled: false), and
GET /slots/{slot}/entities:top-hazard (a ranking by one horizon; suppressed
entities never rank). Existing state and score responses do not change.
Enabling hazard heads on a bundle (hazard: {"enabled": true}) fails closed
with failed_precondition and a reason naming the missing precondition:
hazard_observation_time_required, hazard_dataset_disabled,
hazard_dataset_invalid, hazard_model_mismatch, hazard_not_evaluated, or
hazard_ablation_missing. The hazard-ablation verdict lookup is not wired yet,
so every enablement currently ends with hazard_ablation_missing: no bundle can
serve hazard heads until the hazard-ablation protocol and evaluation land.
Declare the hazard and horizon interface
The API contract declares the discrete-time hazard flag chain: "when, not just
whether". POST /datasets accepts an optional hazard config with typed
targets ({"label_id": ...} for a derived label of the build, or
{"event_type": ...} for a label-free horizon propensity) and a horizon grid
(horizons_days, default 7/14/30/90). A hazard build requires
observation_time.enabled: true, so served probabilities rise as an entity's
silence accumulates. A new operation, POST /models/{model_id}/bundles:hazard,
creates a bundle exactly like POST /models/{model_id}/bundles with a required
hazard: {enabled} toggle, whose preconditions include a passing pre-registered
ablation protocol; the existing bundle operation and its body are unchanged.
Three new read operations serve the values: the calibrated probability that a
target fires within each horizon for one entity
(GET /slots/{slot}/entities/{entity_type}/{entity_id}/hazard), the hazard
heads and per-horizon calibration status of a slot
(GET /slots/{slot}/hazard), and a ranking of a slot's entities by one
horizon (GET /slots/{slot}/entities:top-hazard).
Existing responses do not change. Without hazard, every request payload,
fingerprint and artifact is byte-identical to before. A hazard build is
answered only by the new GET /datasets/{dataset_id}/hazard-recipe; the
recipe and extended recipe refuse it with failed_precondition reason
hazard_recipe_required, and the eligibility preview refuses a hazard request
with hazard_preview_unsupported. Hazard builds are accepted: they materialize the
per-bucket targets, train the hazard heads and are checked by gate G5. Enabling
hazard heads on a bundle is not yet possible: hazard: {"enabled": true} is
refused with failed_precondition until the hazard-ablation verdict lands (see
"Serve hazard horizons from scored state"), so the slot read answers
enabled: false and the entity and ranking reads answer 404 no_hazard and 400
hazard_not_enabled.
Generated-client note: hazard is carried by a new request model, DatasetBuildCreateRequest
(the existing DatasetBuildRequest plus hazard), which is now the declared
body of create_dataset_build and preview_dataset_eligibility in the
generated Python and TypeScript clients. DatasetBuildRequest keeps its name
and its shape (the released fields plus the request-only temporal_holdout
and expected_base_fingerprint, which the new body carries too); the hazard
recipe returns its own frozen request model, DatasetBuildHazardReplayRequest.
Code that passes a DatasetBuildRequest keeps working at runtime (both
serialize to the same JSON). A Python static type checker now expects DatasetBuildCreateRequest, so
construct that model with the same fields to type-check cleanly; TypeScript
callers are unaffected (the new body type accepts every DatasetBuildRequest).
One request-side change: the generated Python DatasetBuildRequest, no longer a
request body, no longer refuses an explicit null itself (that guard moved to
DatasetBuildCreateRequest), so a request built from it with, for example,
split=None reaches the server, which answers 400 invalid_argument, instead of
raising a ValueError in the client.
Require the hazard censoring bound at train time
Every hazard dataset manifest now records hazard_supervision.snapshot_time, and
the field is required: a hazard manifest without it is refused (fail closed). Gate
G5 checks it equals the pinned snapshot it recomputes from. The horizon buckets are
censored at the build's censor clock: the manifest's censor_clock.censor_time
(the earlier of the snapshot time and the data's last event), else snapshot_time.
The trainer re-checks every hazard target as it collates each training row: a
target whose bucket closes after that clock, or a hazard manifest with a missing or
unparseable snapshot_time or a malformed or late censor_clock, ends the run as
failed with error_code failed_precondition and a message starting
hazard_censoring_violation: on the run. Datasets without hazard targets are
unchanged.
Export schedules run on a cadence
/v1/slots/{slot}/export-schedules now creates, lists, reads, updates and
deletes export schedules instead of refusing them. A schedule binds one full
warehouse export (destination, column groups, write mode, pseudonymization,
an optional chained refresh and a cutoff lag) to a UTC cadence: every N hours
from an anchor, or daily at a fixed time.
- The export spec is validated when you create or change the schedule, exactly
like
POST /v1/slots/{slot}/warehouse-exports, including the destination grant probes. - Due ticks are picked up within about 30 seconds. Each tick runs at most one chained refresh and creates exactly one export, even when several serving-api replicas poll at once.
- After downtime only the latest missed tick runs; earlier ones are skipped.
PATCHwith"status": "paused"stops ticks, and"active"resumes the schedule. On resume only its latest grid point runs.- Each schedule's
last_runnames the newest export one of its ticks created, with that export's status. Scheduled exports carryschedule_idandscheduled_foron the warehouse export record and in their manifest.
Every schedule in one list
GET /v1/schedules lists every schedule of a project and environment in one
place: source syncs, slot exports, observation-time rescore sweeps and
alert-rule evaluation. Each item shows its target, cadence, whether it is
enabled, the next tick time computed by Tessera, the newest tick, and the
newest outcome (succeeded, failed or skipped, with the schedule's own
reason code). The list is read-only; each item links the resource where that
schedule is paused, resumed or edited. Filter by kind, enabled state or
target; results page with cursor and limit, newest first.
The console lists every schedule kind in one place
Settings, then Schedules, now reads the cross-kind GET /v1/schedules instead of assembling its list from each slot. It shows source syncs, exports, re-score sweeps and alert rules, and any kind added later, with each schedule's cadence, the next run the server computed, the last run, and the last outcome of its own kind.
- Filters: kind, active or paused, and a target (a source or slot), kept in the page address so a view can be shared or linked from a source or slot.
- Paging: the list is paged by the server's cursor. When a target filter matches nothing on a page it keeps reading further pages, and says so if it stops.
- Actions stay where they are: each row links to the page that owns the schedule (the source, the slot's Exports tab, its observation-time panel or its alert rules). Nothing is changed from the list.
Console overview reports failed syncs and builds, and leads with the next step
The console home page now shows what failed in the pipeline, with each fact on its own and a link to the record.
- Failed syncs: the Sources panel reads the latest sync of each batch source (at most 50 sources, six requests at a time) and names the sources whose latest sync failed. Stream sources have no syncs and are not checked; a history that cannot be read is named, not counted as healthy.
- Failed builds and runs: the Dataset builds panel links the newest failed build, and "Failed training runs" appears only when a run has actually failed.
- Separate facts: the latest trained model, the last successful score refresh of each batch-only slot, and freshness are three separate lines. None is combined into an "end to end" claim.
- Freshness: the freshness read is limited to the last 24 hours, and the overview reads at most six slots; the rest point to Monitoring.
- Layout: the next step leads on the page. Before the first events arrive, the "Send your first events" guidance comes before the still-empty activity panels, which matters on a phone. Once setup is complete the checklist summary moves below the activity, with a link back to it. A project with a stream source counts as having events.
- Checklist: the slot step is done as soon as the first page of slots shows an active bundle, even if a later page cannot be read.
Batch sources sync on a schedule
A batch source can now sync on a schedule. PUT /v1/sources/{id}/schedule
sets an interval (every_hours 1–168, optional anchor) or a daily UTC time;
GET reads it, DELETE removes it, and GET /v1/source-sync-schedules lists
the project's schedules. Each due tick starts one sync through the same path
as POST /v1/sources/{id}/syncs, incremental when the source supports it.
A tick that finds a sync still running records skipped_overlap instead of
starting a second one, and after downtime only the latest missed tick runs.
Every schedule reports its newest tick, outcome (succeeded, failed,
launch_failed or skipped_overlap), newest sync, error and consecutive
failure count, and its next tick time. Schedules are never paused
automatically. GET /v1/syncs/{sync_id}/trigger and
GET /v1/sources/{id}/sync-triggers tell scheduled syncs from manual ones;
the sync resource itself is unchanged. Stream sources cannot be scheduled.
Pattern labels can be authored through the API
A derived label's outcome can now be a behavioral pattern: a windowed count
or sum threshold, an ordered sequence within a time limit, or a rise against
trailing baselines, evaluated over each anchor's future window. The new
/v1/pattern-labels operations create, list, read, update, restore and
preview them (POST /v1/pattern-labels:preview); archive, stats and
provenance use the existing /v1/labels/{label_id} routes. Every trailing
window must fit inside the horizon, and the rule can be replaced only until
the label's first materialization.
/v1/labels keeps its released shape: it never lists pattern labels and
answers 400 failed_precondition (pattern_label) when asked to read,
update or restore one. A dataset eligibility preview reports a pattern
label's derived counts without its rule. Domain packs whose label recipes
use a pattern rule now install those recipes as pattern labels.
Batch catalogs can read local timestamps with no offset
A batch-catalog mapping can now carry an optional assume_timezone, an IANA
zone such as Europe/London, beside catalog and overrides (and beside
pack_catalog in the pack-catalog form). With it, a timestamp that has no UTC
offset, in the entry's timestamp_pointer value or in a timestamp attribute, is
read in that zone and stored as the corresponding UTC instant. Without it,
nothing changes: such rows are still rejected with naive_timestamp.
Clock changes are not guessed. A local time that does not exist (clocks go
forward, for example 2026-03-29T01:30 in Europe/London) or that occurs twice
(clocks go back, for example 2026-10-25T01:30 in Europe/London) is rejected
with the existing naive_timestamp reason and a message that says which.
Values that carry an offset, epoch numbers and bare dates are unaffected.
The zone applies to rows mapped after the new mapping version is stored, and is
part of the version's fingerprint. Events already ingested are not
reinterpreted, incremental syncs skip files already in the ledger (run a full
sync to re-read rows rejected earlier), and changing a zone on a source whose
entries have no event_id_pointer lands re-read rows as new events at shifted
instants. An unknown, host-local or non-IANA zone is 400 invalid_argument at
/assume_timezone. No response schema changes.
Entity alerts: rules, episodes, dispositions and measured precision
Deployment slots gain entity alerting (WP-V13) — rules as data, alert episodes with dedup and cooldown, triage dispositions that are audited and can feed labels back, and measured precision per rule.
- Alert rules:
GET/POST /v1/slots/{slot}/alert-rules,GET/PATCH/DELETE …/alert-rules/{rule_id}. A rule is a predicate in the platform predicate language pluscooldown_secondsandauto_resolve_after_seconds; at most 64 active rules per slot. - Episodes:
GET /v1/slots/{slot}/alert-episodesand…/alert-episodes/{episode_id}— one open episode per rule and entity, with an immutable opening snapshot (scores, bands, behavioral evidence, provenance). - Dispositions:
POST …/alert-episodes/{episode_id}:dispositionrecordstrue_positive | false_positive | benign | duplicatetogether with its audit record; with a rulelabel_bridge, true/false positives become label events observed at the disposition time. - Measured precision:
GET /v1/slots/{slot}/monitors/alert-precision. - Bands:
GET/PUT /v1/slots/{slot}/bandsbind a fingerprinted banding policy per label. - Webhooks:
alert.raised,alert.resolved,score.band_changedandentity.went_quietpayload schemas (api/events/alert_webhooks.json); exactly one envelope per transition, each passing the delivery suppression filter right before it is sent. Existing payloads (score.updated, state reads, exports) are unchanged. - Episodes auto-resolve on a 30-second sweep; entity data deletion also erases the entity's episodes, dispositions and alert deliveries.
- The generated clients and the API reference are regenerated for the new operations.
Warehouse exports \u2014 signals, Snowflake and table write modes
Warehouse exports (POST /slots/{slot}/warehouse-exports) land predictions
as governed tables in your own warehouse:
-
Warehouse records, classic record unchanged.
/slots/{slot}/warehouse-exports(create, list, get) answers with aWarehouseExportrecord stating each state-snapshot export's destination, the column groups it wrote, the write mode it applied and, for scheduled exports,schedule_idandscheduled_for. The classic export record of/slots/{slot}/exportsand of theexport.*webhook data keeps its original shape, so existing API clients keep reading it: it never carriesmodeor schedule fields;POST /slots/{slot}/exportsrefuses a Snowflake destination or an extended column group with400 invalid_argumentnaming the warehouse endpoint, its by-id read refuses such an export with400 failed_precondition, and its list leaves such exports out. -
Opened column aperture.
includegrows to the closed setembedding,scores,signals,segments,propensities,evidence,as_of:behavior_surprise, onepropensity__{event_type}column per shortlist event type plus thepropensity_statusmap, theevidenceblock and itsframe,segment_id, and a nullableas_of. Per-row absence isNULL, never an error; values are byte-for-byte the stored state. Signal-bearing groups need a signals-enabled bundle (400 failed_preconditionotherwise); colliding propensity column names fail with400 invalid_argumentnaming both event types. Omittingincludekeeps the previousembedding+scoreslayout unchanged. -
Snowflake destination.
{kind: "snowflake", credential_id, database, schema, table}writes as a Snowflake source's key-pair user (same project and environment) — no second credential store — through a per-export temporary stage and staging table that are always dropped, even on failure. Failures are typed and name the Snowflake user. -
Vaulted destinations in deployments.
s3andsnowflakeexports decrypt the referenced source's vaulted credentials in serving-api: a deployment enables them by settingTESSERA_CONNECTOR_KMS_KEYfor serving-api and applying its decrypt-only grant on the connector credentials key (Terraformconnectorsmodule); until then they answer400 failed_precondition. Ans3credential_idnow resolves only to a source of the export's own environment, and a disabled source authorizes no export. -
Table write modes.
mode: replace | merge | appendfor BigQuery and Snowflake:replace(the default) keeps today's behavior (atomic in Snowflake),appendaccumulates(entity, computed_at)score history without ever duplicating an unchanged state,mergeupserts one row per entity. Forappend/merge, column growth is additive and a type conflict fails with a typedschema_conflict; BigQuery job failures are typeddestination_write_failed. -
Governance. Every exported row passes the delivery suppression filter first;
manifest.jsongains the mode, resolved groups, the propensity column mapping, suppression counts and the declared freshness claim — "as-of-cutoff tables on a cadence": until a slot enables observation-time re-scoring, quiet entities' value columns are byte-identical across cutoffs andas_ofisNULL. -
Export schedules (interface).
/slots/{slot}/export-schedulesdeclaressch_schedules that bind one export spec to a UTC cadence. They activate with the platform tick scheduler; until then creation answers400 failed_preconditionand no schedule exists.
New JSON Schemas: export_schedule.json and export_manifest.json;
export_job.json gains the snowflake destination and the extended
include groups, which appear only in the webhook data of warehouse
exports. See Warehouse exports.
Bus bridge: AWS Kinesis
Correction (2026-10-21): the Kinesis bridge is planned, as is the Kafka bridge it shares its mapping config with, and the integration catalog lists Amazon Kinesis and Kafka as Planned; this release was written up as if the Kinesis bridge could be used today. The details below are kept as originally written.
The customer-side tessera/bus-bridge container (0.2.0) now reads AWS
Kinesis streams. Point it at a stream with consumer: {kind: kinesis, …} and
the same declarative mapping config as the Kafka bridge — any mapping that works
for Kafka works unchanged for Kinesis.
Shards are read with KCL-style leases kept in a DynamoDB checkpoint table in
your account: replicas sharing a bridge_id divide the shards, a crashed
replica's shards are taken over within three lease TTLs, and graceful
shutdown hands leases over immediately. Checkpoints advance only past records
Tessera accepted, and event ids derive from (bridge_id, stream, shard id, sequence number), so at-least-once reading becomes exactly-once in Tessera.
After a split or merge, child shards start only once their parents are fully
delivered, preserving per-partition-key order. Rate limits and ingest outages
stop reading on every shard while leases keep renewing (bounded memory, no
loss). Unmappable records go to an optional rejects stream as JSON envelopes
and never block a shard.
Credentials come from the default AWS provider chain (environment, EKS IRSA,
instance profile) — never the config file — and --check-config validates a
config without credentials or AWS calls. New Prometheus metrics:
bridge_shard_lag_ms, bridge_leases_held, bridge_checkpoint_age_seconds
and bridge_data_expired_total, plus an importable Grafana dashboard.
New guide: Bridge AWS Kinesis to Tessera.
Evaluation: decision metrics, baseline zoo & comparators
New API resource: POST /comparators registers a customer-supplied
(entity_id, as_of, score) CSV or Parquet file and returns 202 while an
asynchronous job validates it; GET /comparators and
GET /comparators/{comparator_id} read status, counts, the as_of range and
per-row violations. Comparator files are evaluation inputs only and never
enter ingestion: upload with POST /uploads (no source_id), PUT the bytes,
and register the session instead of completing it. Registration claims the
session, so POST /uploads/{upload_id}:complete now refuses a claimed session
(upload_claimed_by_comparator).
The evaluation report schema gains optional, advisory proof-engine blocks:
decision_metrics (lift@k, capture@k, precision/recall at list size,
alert-precision-at-budget and band-assignment matrices over entities under a
fingerprinted banding policy, with 95% entity-level bootstrap confidence
intervals), beats_baseline_at_margin verdicts that claim a win only when the
paired-delta CI lower bound clears a declared margin, a baseline zoo beside
the unchanged gbm_v1 (gbm_default, a budgeted tuned GBM with a
fingerprinted trial log, a BG/NBD + Gamma-Gamma timing family and an
RFM-feature GBM), and comparator gradings. The evaluation-request options
that produce these blocks (include_zoo, banding_policy, margins,
comparator_ids) are accepted only by the internal evaluation service for
now: no public API operation exposes them yet, and automatic evaluations do
not set them, so reports you can fetch today do not carry these blocks and
registered comparators are not graded yet.
All extensions are additive: existing reports, and evaluations without the new options, are byte-identical to before. Promotion gates G1–G6 are untouched.
Custom roles, project role overrides and SCIM 2.0 provisioning
Organizations can define custom roles as explicit permission sets over the
generated operation catalog (/v1/orgs/{org_id}/roles), assign exactly one role
per scope to a member, and override it per project
(/v1/projects/{project_id}/members/{user_id}/role). Built-in roles keep their
existing decisions and are evaluated by the same authorization path. Unknown or
wildcard permissions are rejected, an administrator cannot grant permissions it
does not hold, cannot change, remove or deactivate a member (or edit a role, or
cancel an invitation) that holds permissions it lacks, and an assigned role
cannot be deleted. Roles store permission names and are evaluated against the
running release's catalog, so adding an endpoint later changes no existing
grant.
Identity providers can provision users and groups through SCIM 2.0 at
https://api.staging.tessera.zone/scim/v2 (Users, Groups, ServiceProviderConfig)
with an organization SCIM token minted at /v1/orgs/{org_id}/scim-token. SCIM
uses its RFC 7644 wire format (application/scim+json), documented separately in
api/scim-openapi.yaml. Deactivating a user deactivates that organization's
membership without deleting identity or audit records; mapped groups assign
organization roles, with the most recently applied mapping winning conflicts.
Existing v1 schemas are unchanged. Membership.role, UserOrgMembership.role,
Invite.role, InviteCreateRequest.role, MemberUpdateRequest.role and the SSO
default_role remain the closed built-in enums, so existing API clients and the
console keep working; a member, invitation or SSO default that holds a custom
role is reported there as viewer. The exact role is read and written through
additive operations: GET/PUT /v1/orgs/{org_id}/members/{user_id}/role,
GET/PUT /v1/projects/{project_id}/members/{user_id}/role,
POST/GET /v1/orgs/{org_id}/role-invites and
GET/PUT /v1/orgs/{org_id}/sso/default-role. API-key credential restrictions
and existing operation IDs are unchanged. SCIM token creation never replays its
one-time bearer: a repeated Idempotency-Key returns a non-secret
scim_token_already_issued acknowledgment instead.
Send webhook samples and replay finished deliveries
POST /webhook-endpoints/{endpoint_id}:send-sample queues one signed test
event (its data is a fixed test marker) for a single registered endpoint. The sample
appears in that endpoint's delivery history immediately and follows the normal
signing and retry schedule; it is never broadcast to other endpoints. Each sample
is recorded in the audit trail as webhook_endpoint.sample, with a fingerprint of
the destination rather than the URL.
POST /webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}:replay re-sends a
finished delivery as a separate replay run with its own attempts and outcome. The
replay keeps the original envelope ID and bytes, so receivers can deduplicate it,
and it never changes the original delivery's history. Only one replay per delivery
can be active, and retries of the same idempotency key return the same run. The
replay request is recorded in the audit trail with its reason, and the end of the
run is recorded separately.
GET /webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}/replays lists the
runs for a delivery.
Both operations require the endpoint's current URL as expected_url and refuse
with endpoint_destination_changed if it changed, so an old confirmation can
never send to a new destination. Subscription changes are audited with the event
types before and after the change. The console's endpoint drawer adds both
actions, shows retry timing and final outcomes, and explains that a successful
HTTP response does not prove signature checking or downstream effects. The
webhooks guide documents both operations.
Sync history accounts for every row and lists rejected rows
Two new reads report how every input row of a batch sync ended up: GET /syncs/{sync_id}/diagnostics, and GET /sources/{source_id}/sync-diagnostics, which pages like the sync list. Each returns rows_read, which equals rows_accepted + rows_rejected + rows_skipped + rows_duplicate, along with the request_id of the call that started the sync and when each stage began (phase_started_at). Syncs created before this change report these fields as null, meaning not recorded rather than zero. The existing sync responses (POST /sources/{source_id}/syncs, GET /syncs/{sync_id} and GET /sources/{source_id}/syncs) are unchanged, so existing API clients keep working.
A row whose event_id repeats an earlier row of the same sync is now counted as a duplicate and committed once, instead of twice. This applies whether the event_id was supplied or derived by the platform. Rows that are identical in every mapped value therefore collapse into one event. To keep repeated occurrences as separate events, map an event id column.
GET /syncs/{sync_id}/rejections pages through the rows a sync rejected, and is available to organization members and to API keys with the read scope. Each row includes its typed reason, the column that caused it, and its position among the rows landed in the snapshot. Rows are returned in sanitized form: long values are shortened, and values in columns whose names suggest secrets are hidden.
In the console, each sync in a source's history links to /sources/{source_id}?sync={sync_id}. That page shows a copyable sync ID, the request ID, a stage timeline, counts that add up, the mapping and snapshot used, and the duration. For members, it also lists rejected rows, with a link to the mapping step that fixes each one, and a CSV download.
Suppression sets and holdout experiments
Suppression sets (/v1/suppression-sets) exclude entities from the outputs
of the purposes they cover, and no API accepts a bypass. Every set and
entries change is audited in the same transaction. Predicate suppression
sets evaluate the shared predicate language against each entity's state.
Holdout experiments (/v1/slots/{slot}/experiments) define a deterministic,
salted share of a slot's entities to withhold from deliveries for one
purpose. The incrementality readout endpoint is published but answers
failed_precondition (readout_unavailable) until its point-in-time join
ships.
What changes today:
- State reads and
scores:batchpass through the suppression filter. A suppressed entity reads like an entity without state, and held-out entities stay visible. If suppression state cannot be read, the request fails with503 unavailableinstead of returning unfiltered data. - Exports (every destination kind) run under the
generalpurpose. They leave out entities in covering suppression sets, and the held-out entities of active experiments on the export's slot. The export manifest counts both. If suppression state cannot be read, the export fails withunavailableand writes no manifest (parts already written stay in the bucket, unreferenced). A project with no suppression sets and no experiments gets exactly the export it got before. Export cycles do not record control manifests yet. - Alert webhooks (
alert.raised,alert.resolved,entity.went_quiet,score.band_changed) are decided when they are published. The alert of a suppressed entity, and of a held-out entity on the experiment's slot, is withheld, and its episode still exists. An episode is delivered or withheld as a whole: the holdout verdict is made once, when its opening webhook is published, and its resolve follows it, so an experiment that starts or ends mid-episode never leaves a raised alert that is not resolved. Suppression is checked on every event, so an entity suppressed after its alert was sent has the resolve withheld. If suppression state cannot be read, the alert stays queued and is retried with back-off; nothing is sent undecided. - Audiences (previews, materialized member artifacts, deliveries and
lookalike artifacts) leave out suppressed and held-out members and count
them in the audience manifest. If suppression state cannot be read, the
output fails with
suppression_unavailable. - PostHog destinations leave out suppressed and held-out entities of the
destination's slot and purpose (else
general), re-checking before every send attempt. The delivery receipt records every suppression set and holdout experiment that covered the purpose. If suppression state cannot be read, nothing is sent and the batch waits. - Export, alert, audience and destination deliveries record no control
manifests yet. The
score.updatedandsegment.migrationwebhooks do not apply suppression or holdouts yet.
Read exact per-source processing counts
GET /v1/sources/{source_id}/processing-counts (getSourceProcessingCounts,
read scope) returns raw, canonical, semantic and scoring counts for one source
as of one server instant, with the binding and automatic slot generation they
refer to. Raw counts partition received records into duplicate, quarantined or
excluded (with recorded reasons), expired, pending and distinct canonical events;
semantic counts partition durable semantic work into available, pending,
timeout, budget exhausted, unavailable, expired and stopped. Every count comes
from facts the owning stage persisted. A count without evidence is null with an
unavailable_reason, never zero. Existing responses, including the onboarding
status, are unchanged.
Shared schedule and predicate foundations
The platform gains one shared predicate evaluator and one tick scheduler for
future clock-driven features. Predicates follow the published predicate.json
grammar, reject unknown fields and limit violations with typed errors, compare
numbers exactly, and never match on absent data. Schedules run on an hourly
interval grid or at a fixed UTC time of day; after downtime or a pause only
the latest missed tick runs, and each tick is claimed exactly once per
project, even with concurrent workers.
Scoring requests accept an optional as_of observation time, which the tick
reason requires. Requests that omit it keep their existing wire bytes. Project
purge now pauses a project's schedules and fails its pending ticks.
Nothing runs on these schedules yet; this release adds the shared foundation only.
Score history, ranked reads and realized-quality monitors
Deployment slots can now keep an append-only history of every served score
beside the current entity state. GET /v1/slots/{slot}/entities/{type}/{id}/history
returns an entity's trajectory newest first, and with threshold reports
deviation_started_at — when the current run at or above the threshold
began. GET /v1/slots/{slot}/entities:top ranks a slot's entities by score,
by the behavior_surprise signal or by score change over a window, from
persisted snapshots whose cursors never reorder between pages. History is
plan-gated (limits.state_history: sampling interval, retention and daily
downsampling) and metered as state_history_rows; recording never delays or
fails scoring.
GET /v1/slots/{slot}/monitors/realized-quality serves, per binary label and
ISO week, realized AUC and lift@k computed from matured labels joined to the
score each entity had at the label's anchor time, with 95% bootstrap
intervals. Current state reads are unchanged; erasing an entity also removes
its score history and its ranked-read entries.
Find scored entities that have no recorded event history
Entities that were ingested in batch and scored, but whose events never
reached the event history, no longer open as unknown. A new read operation,
GET /v1/entities/presence, looks entities up by exact ID or prefix across
recorded event history and current scores. Each item carries the lifetime
summary when one is recorded and the served_slots (deployments) that hold
scores for the entity. Without a summary, first seen, last seen and event
counts are not recorded; they are never reported as zero. Exact and prefix
lookups return the same entities, ordered by entity ID.
GET /v1/entities and its response types are unchanged. The generated clients
add listEntityPresence (TypeScript) and list_entity_presence (Python).
The console uses the presence lookup for entity search, the entity page and deployment entity browsing. An entity is shown as unknown only when it has neither recorded events nor scores. Storage, authorization and network failures show an error with its request ID and a retry action.
Bounded failure diagnostics for the CPU Start tooling
A failed manual CPU Start now reports why before its rollback removes the evidence. Admission refusals name the object and the differing field paths, and a readiness or install failure prints a bounded summary of the CPU Deployments, Pods and Warning events using a fixed vocabulary. Environment values, Secrets, logs and free-form cluster messages are never printed, and failed commands report at most one fixed token. This changes tooling output only: no live operation has run, and CPU workers stay inactive.
Read-only CPU smoke through the contract-service pod for the semantic workers
The manual CPU operator gains a smoke action that checks a running CPU
deployment from the pod that really calls the private discovery Service. A
fixed relay confirms health, readiness, enforced authentication and closed
request validation, and optionally runs one metadata-only structural discovery
and its cached replay, reporting only statuses, latencies and role statuses. It
takes no lock, changes nothing in the cluster apart from the discovery result
cache, and never reads or prints the token. No live operation has run and CPU
workers stay inactive.
Manual CPU Start, Stop and Status tooling for the semantic workers
Operations tooling for our hosted deployment can run the semantic worker and private discovery
service on the existing CPU batch pool with the explicit cpu-float32 profile,
derived from the reviewed deployment templates. Start accepts only reviewed CPU
evidence for the pinned image and verifies that every consumer runs a release
containing the CPU recipe change. CPU and GPU operation share one lock and are
mutually exclusive. This does not activate CPU workers: no live
operation has run yet, and activation remains a separate reviewed Cloud Build
operation.
Explicit deadlines for the CPU semantic workers
The measured CPU worker takes 44-60 s per event, and under host contention 18.7%
of attempts exceeded the 60-second default and restarted the model process. The
semantic worker accepts an optional --minimum-job-timeout-seconds floor that
raises, never lowers, the project policy's per-event budget, and the hosted
contract-service waits 150 seconds for structural discovery. The CPU
operator passes the floor (150 s) and the discovery server budget (120 s) only to
images built with those flags. GPU workers, project policies and the 60-second
defaults are unchanged.
Explicit structural-discovery deadlines for CPU semantic workers
The automatic onboarding worker reads its structural-discovery HTTP deadline
from TESSERA_SEMANTIC_DISCOVERY_TIMEOUT_SECONDS, and the private discovery
server accepts a matching bounded --timeout-seconds request budget. Both keep
the existing 60-second default and reject values outside 1-600 seconds. The
semantic worker documentation lists every CPU deadline with the measured
canonical CPU latency. GPU behavior is unchanged.
Add audited segment explanations to the segments read surface
WP-V7: audited AI segment explanations
GET /v1/slots/{slot}/segments and GET /v1/slots/{slot}/segments/{segment_id}
gain an additive, optional explanation object on every segment item:
{status: explained | fallback, name, narrative, evidence, registry_id}. An
explained entry carries an audited name, a narrative and 3–5 evidence bullets,
each citing one profile statistic with its embedded stat_snapshot; every other
segment renders fallback with its existing Segment #N name. Explanations are
generated offline by a separate, audited batch job (generator → adversarial
skeptic → auditor) into an immutable, fingerprinted registry; the read path never
calls a model and an unavailable or unverifiable registry never fails the
request.
No generation provider is configured yet, so every segment currently renders
fallback with registry_id: null — explanations are unavailable until a
provider is decided.
PostHog destinations API and delivery service (inactive until band bindings ship)
New /v1/destinations endpoints list, inspect, update, delete, test, backfill
and re-enable PostHog destinations for a deployment slot. Creating a destination
requires a band binding on the slot (WP-V13), which is not available yet: until
it ships, POST /v1/destinations returns failed_precondition and nothing is
delivered.
When active, the destination-writer service delivers tessera_band,
tessera_score_<label> and provenance person properties in batches under a
per-destination rate limit. Event UUIDs are deterministic, so retries and
replays never write twice. The PostHog API key is write-only and stored only
as KMS envelope ciphertext. A revoked key disables the destination until it is
re-enabled. Each finished delivery cycle emits one
destination.delivery.completed webhook, and one export_rows usage record
when people were updated.
The writer calls the suppression filter before cutting each batch and again before every send attempt, and it drops entities whose state was erased. In this release that filter lets every entity through: suppression sets and holdouts are not yet enforced on destination deliveries. Enforcement turns on when WP-V12's suppression sets ship, with no change to destinations.
Declare the observation-time (elapsed-silence) model interface
The API contract declares the per-project, default-off observation-time flag
chain: an optional observation_time config on POST /datasets
({enabled, between_offsets, tail_offsets}), its verbatim echo in dataset
manifests and promotion-bundle runtime.observation_time, an optional
observation_time body on POST /models/{model_id}/bundles, and the optional
top-level observation_time_ablation evidence block of evaluation reports. A
model that consumes the elapsed-silence token sets
overrides: {observation_time: true, readout: last_event} on POST /runs.
Absent or enabled: false leaves every request payload, fingerprint, artifact
and score byte-identical to before, and existing reports keep their
ablations shape. Until the matching runtime is available, enabled dataset
builds and enabled bundles are refused with failed_precondition reason
observation_time_unavailable; they cannot silently ignore the flag. An
explicit null is rejected like any other malformed value.
Existing responses do not change. DatasetBuildRecipe and DatasetEligibility
echo the unchanged v1 request shape (DatasetBuildReplayRequest), so existing
API clients keep parsing them. A build that used observation time has no v1 recipe:
GET /datasets/{dataset_id}/recipe answers 400 failed_precondition (reason
observation_time_extended_recipe_required) and the new
GET /datasets/{dataset_id}/extended-recipe returns its complete recipe, which
can be posted back to POST /datasets to reuse or upgrade the build with the
option kept. POST /datasets:eligibility refuses a request that enables
observation time (reason observation_time_preview_unsupported); preview the
same request without it.
Name API keys and replace them without downtime
API keys can carry an optional integration name. Key lists keep their existing
response shape and include names only when the caller passes include_name=true;
a creation response includes the name only when one was supplied. Secrets and
hashes are never returned after creation.
The API keys page shows each key's integration name, prefix, environment, creator and last use, and calls out Live and admin-scoped keys before they are created. Its replacement flow copies the original key's kind, environment, scopes and browser origins; the original stays active until you revoke it separately. Revocation still targets one key ID, so keys that share a name are never confused.
Key creation and revocation audit events are now attributed to the key's own environment rather than the environment selected in the session, so Test and Live key lifecycles stay separate in the audit trail.
Training runs can span multiple nodes
POST /runs accepts trainer.nodes (1–8, default 1) to spread a run's data-parallel
ranks over several nodes, and trainer.world_size now counts ranks across every node
(up to 64: at most 8 per node and at most the compute class's GPUs per node). Node
topology at a fixed world_size never changes the training trajectory, but it is part of
the fingerprinted run config, so two runs differing only in nodes are distinct runs.
A multi-node run pins an exact split of global_batch_tokens into per-rank micro-batches
and accumulation steps. A batch with no exact split within the micro-batch bounds, an
uneven or oversized topology, or a dataset with fewer non-empty training shards than ranks
(too_few_shards) is refused with invalid_argument when the run is created, never on the
cluster. Single-node runs, one rank or several, are unchanged: the same fingerprints, the
same batch packing and every request they accepted before. The one new refusal is a
topology no compute class could ever start (more ranks per node than the class has GPUs),
which now fails at creation instead of at launch.
Nothing about the existing run reads changes, for any run. nodes and the wider
world_size are request-only: TrainingRun, its resolved trainer and the reusable
recipe stay exactly the shapes existing strict API clients already validate. A multi-node run reads back
in that same shape from GET /runs/{id}, POST /runs and POST /runs/{id}:cancel, with
trainer.world_size holding the ranks of each node once the total exceeds 8 (the
top-level world_size stays the total). Its nodes, total world_size, ranks per node,
pinned batch geometry and per-node node_status (pending, running, succeeded,
failed, lost or unknown) are the new GET /runs/{run_id}/topology, which also
carries the exact POST /runs request that replays the run; the recipe of a multi-node
run is not available (its response shape cannot carry nodes) and points to that read.
Multi-node runs are left out of the default GET /runs page, so an existing API client's
listing never meets one; pass include_multi_node=true (and keep it on cursor requests)
to include them. A run that loses a node fails within 10 minutes as node_lost; one whose
nodes never form a group fails as rendezvous_timeout. Both tear down every remaining pod
and keep the latest checkpoint, from which support can resume the run.
Model detail leads with the decision: summary, gate states and serving
Model detail opens with a summary of what the model predicts, the data it was trained on, its held-out quality and where it serves. Serving comes from your deployment slots, and the bundle in service shows its recorded policy, acknowledged warnings and reasons beside that status. Test-environment evaluations are labelled as workflow checks, not real-world performance evidence.
Gate cards now distinguish passed, failed, waived (acknowledged on a bundle that pins the same report), not-applicable and not-evaluated results; a missing gate result still counts as failed. Once a bundle exists, the decision panel describes the requirements for a new bundle separately from the bundle already created, and a waiver is never offered for a gate the current policy blocks. The bundle record shows only the values that bundle measured. G1 names its actual reference — the estimate from the newest promoted bundle that did not acknowledge a G1 failure, or the trivial predictor — and is never presented as a LightGBM comparison.
The model card shows key facts with units, meaning and bounded precision, with extra digits when a value would otherwise read the same as its threshold. Each section with rounded values keeps an Exact values disclosure, and the JSON download is unchanged. Section links for Lineage, Artifacts and Bundles stay at the top of the page, and deep links open their section after it loads. The gate-failure history and the models list name the failing gates in each row.
Check published copy against the honest-claims policy
Every change to the docs and the marketing site is now checked against the honest-claims policy. Copy may not claim capabilities the product does not have, and a published results document must state in its headline whether its numbers come from a test fixture, a design partner or production, with every number tied to the fingerprint of the archived artifact that holds it. Results from test fixtures are never described as measured outcomes and are not published on the marketing site.
The documentation, the API reference introduction and the docs site tagline now describe Tessera as training behavioral models on your own event streams. No endpoint, schema or response changed.
Pick and preview shared checkpoints before importing
GET /shared-checkpoint-exports lists the shared checkpoint exports that the
current project and environment are authorized to import, and
POST /models:preview-shared-import checks a reviewed binding against current
authorization, artifact evidence and promotion policy without importing anything.
A preview reports readiness, the specific reasons an export cannot be imported
yet, and a fingerprint of the evidence it used. Previews never create a model,
bundle, readiness record or gate acknowledgement.
POST /models:import-shared accepts an optional expected_preview_fingerprint.
When it is supplied, the import rechecks authorization and evidence and refuses
with a specific reason if anything changed since the preview, including when the
same idempotency key is retried. Imports without it keep their existing behavior.
The console's shared import panel can now pick an authorized export instead of requiring a typed export ID, summarizes the uploaded binding, and shows the preview's readiness and rejection reasons before import. A guided import is pinned to the previewed evidence fingerprint. Manual export-ID entry remains available.
Choose a field when automatic onboarding cannot resolve a structural role
When automatic onboarding stops because it cannot tell which field identifies
the entity, the record, the event type or the event time, the new
GET /sources/{source_id}/semantic-exceptions lists the candidates Tessera
already shortlisted for that role with metadata-only evidence (classifier
probabilities, types and counts). An admin can confirm one candidate with
POST /sources/{source_id}/semantic-exceptions:resolve. The choice is audited,
fingerprinted and becomes a locked field of the next binding; every
deterministic check still runs and guided sources still require approval.
Nothing is chosen automatically and activation thresholds are unchanged. The
source page shows the candidates and a confirm step. Existing responses are
unchanged.
Name, tag, search and exactly clone experiments
Training runs and dataset builds gain editable display names and tags
(GET/PATCH …/annotations). Run and dataset lists accept a whole-tag tag
filter and an explicit include_annotations=true opt-in; default responses are
unchanged. GET /sources accepts q to search source names or an exact source
ID.
GET /runs/{run_id}/recipe and GET /datasets/{dataset_id}/recipe return the
exact retained configuration of a run or build. Creating from a recipe with its
expected_config_fingerprint / expected_fingerprint either reuses exactly the
pinned versions or fails with typed pin_drift details before anything is
created; upgrading to current versions is an explicit, separate request.
Generated clients append the new list options after their existing positional
arguments.
Domain packs and entity-key crosswalks
Domain packs are optional, platform-authored bundles of data — an event
ontology, batch catalogs, derived-label recipes, propensity shortlists, band
presets and generated-corpus parameters — that you can browse with
GET /v1/domain-packs and install with
POST /v1/domain-packs/{pack_id}:install. Every release is fingerprinted
and verified against its pinned fingerprint on every read. An install runs
through the existing machinery in one transaction: the pack's entity types
are registered as drafts, its event types become one draft contract that
still needs your approval, its recipes become derived labels through the
normal label validation (GET /v1/labels/{label_id}/provenance reports the
pack each came from), and the install record (pak_…) pins the pack
fingerprint. Nothing a pack declares reaches a model
until you approve the contract; re-installing the same version is a no-op
and a newer version supersedes the previous install while keeping it.
Batch sources can map through an installed pack's batch catalogs by name
({"pack_catalog": {"pack": …, "catalog": …}} on
PUT /v1/sources/{src_id}/mapping); the stored mapping version is the
ordinary batch-catalog form and pins the install's pack_fingerprint, which
GET /v1/sources/{src_id}/mapping/provenance reports.
Batch sources and webhook adapter sources accept an optional customer-supplied
entity-key crosswalk (GET/PUT /v1/sources/{src_id}/crosswalk): up to
100,000 exact raw_id → canonical_id entries per immutable, monotonic,
fingerprinted version. It rewrites entity ids of events mapped after it is
stored — existing events keep their keys — and unmapped ids pass through.
Syncs record the crosswalk version they applied
(GET /v1/syncs/{sync_id}/crosswalk), and the active version reports
applied and passed-through counts.
Existing sources, mappings, labels, syncs and contracts behave exactly as before: their API responses are unchanged, so existing API clients keep working.
Named delivery purposes per project
GET /v1/purposes and PUT /v1/purposes manage a project's named delivery
purposes (WP-V12). Every project starts with general; a registry holds up to
16 lowercase-kebab names, all is reserved, and a purpose that another
resource still references cannot be removed. Replacements are audited in the
same transaction as the change. Reads require the read scope and
replacements the admin scope.
Purposes are the names that suppression sets, holdout experiments, slots and principal allow-lists will bind to as those features ship. This release adds the registry only; it does not yet change which outputs any credential can read.
Console names, tags, source search and clone
Training runs and dataset builds show their names and tags in lists and can be filtered by an exact tag. Run and build pages let members edit the name and tags; a concurrent edit is reported instead of overwritten. The Sources page searches names or an exact source ID on the server.
Clone on a run or build page opens the launcher prefilled with the exact retained configuration and an explicit choice: reuse the pinned versions, or review and acknowledge every version an upgrade changes. A clone never adopts newer versions silently; if exact reuse is no longer possible, the changed inputs are listed and nothing is created.
Connection tests name the failed stage and the exact permission to grant
The new POST /sources/{id}/connection-diagnostics runs a source's
connection test and tags every check of the built-in connectors with the
diagnostic stage it verifies (network, authentication, authorization,
object, schema). A failed check gets a typed failure cause, so a refused
password, a missing grant and an unreachable host read differently. For
database and warehouse sources, checks after a failure report
not_attempted; Cloud Storage and S3 stop after a failed listing. The
response carries the server's checked_at time.
POST /sources/{id}/test-connection keeps its original response shape
(status, and each check's name, status and detail), so existing API clients
keep working. Both operations now return check details that name the
least-privilege grant or setting that fixes a failure and never repeat
provider error text or credential material, and both record each test in the
audit log as source.test_connection (check names, stages, statuses, causes
and check time only).
The console shows the stage, the cause and the check time, and the BigQuery,
Postgres, Cloud Storage and S3 forms render a copy-ready least-privilege
permission template from the values you type (never from your credentials).
The setup guides now list exactly what each connector calls: BigQuery's
dataViewer can be granted on the single table, the S3 policy can be limited
to the prefix, and Snowflake roles need CREATE STAGE on the schema, which a
new stage_writable check proves. A MySQL table_readable check now runs a
zero-row SELECT, the Databricks connection test checks the access token
with the workspace, and an empty GCS or S3 prefix reports the unverified
object read as a warning.
This release does not add a pre-ingestion data sample, and connector success against real cloud accounts is verified separately per environment.
Mapping catalogs gain value maps and timestamp formats
Adapter mapping catalogs and their per-source overrides accept two new entry
fields. value_map normalizes vendor values per mapped attribute through a
bounded exact-match table of {from, to} pairs: matching never coerces types,
and a value without a matching from passes through exactly as sent.
timestamp_format (rfc3339, unix_s, unix_ms or unix_us) declares how the value at
timestamp_pointer is read, so epoch-millisecond sources map without guessing
units. Leaving it out keeps the existing default (RFC 3339 strings and
epoch-second numbers); an explicit rfc3339 accepts strings only.
Existing catalogs and overrides behave exactly as before. The catalog schema also declares the batch-catalog arm and the passthrough block of upcoming work; catalogs that use them are refused until those engines land.
Batch catalogs map nested source rows
Batch sources can store a declared batch catalog — the kind: batch arm of the
shared adapter-catalog format — as their mapping instead of a per-column
mapping. Rows are addressed as {"row": …} JSON Pointers after Parquet
struct/list decoding, so nested fields map directly. Every pointer is bounded
by the catalog's flatten.max_depth (a deeper pointer is refused when the
mapping is stored), and declared key/value parameter arrays promote only their
declared keys; an array longer than its max_items rejects the row with
flatten_bounds_exceeded instead of being truncated. Unmapped discriminators
and skip_if matches are counted as skips on the sync's details, and every
sync records the catalog mapping's fingerprint. The new
POST /v1/sources/{id}/catalog-mapping:preview reports a typed reason and the
offending pointer for each skipped or rejected row; the existing
mapping:preview response is unchanged.
A sync that uses a batch catalog reports the same row counts as a
column-mapped one in GET /v1/syncs/{sync_id}/diagnostics: every row read is
accepted, rejected, skipped (a catalog skip) or a duplicate event, and the four
counts add up to rows_read.
Column mappings, webhook adapter catalogs and manual/guided/automatic onboarding behave exactly as before. Batch catalogs apply to file and object sources; warehouse sources keep column mappings. Scheduled syncs are not part of this release.
Behavioral markers and pattern-based outcomes
Named markers let you describe a behavior once (for example, a losing bet
followed by a deposit within two hours) and have every scorer of a slot evaluate
it at scoring time. Manage markers with /v1/markers (create, list, get,
PATCH, delete). Use POST /v1/markers:preview to dry-run an unsaved pattern.
Changing a marker's pattern bumps its version and pattern_fingerprint. A slot
evaluates at most 32 active markers.
A marker that turns active sends one marker.fired webhook per episode. The
full marker state is available from the new
GET /v1/slots/{slot}/entities/{entity_type}/{entity_id}/markers, including
whether the evaluated window was truncated. The existing state read keeps its
response shape and lists only active markers in signals.markers.
Markers are outputs only: they never change datasets, embeddings or scores.
Audit streaming, hash-chain verification, legal hold and read audit
Organizations whose plan includes audit v2 can stream their audit log to a
SIEM: signed audit.recorded webhooks for project events and zstd JSONL batch
objects in a GCS or S3 sink for every event. Every audit event is sealed into a
per-organization hash chain that POST /orgs/{org_id}/audit-events:verify
recomputes, reporting the exact first tampered sequence or pending unsealed
events, and the daily export anchors each organization's chain head outside the
database.
GET/PUT /orgs/{org_id}/audit-settings configure a raise-only retention
keep-minimum of at least 396 days, an audited legal hold that blocks partition
removal, and streaming. GET/PUT /projects/{project_id}/audit-settings turn
on sampled read audit of entity-state reads. Settings beyond the plan are
refused with failed_precondition; sink credentials are write-only.
Audiences and lookalikes: deliver segment, predicate and lookalike member lists
New additive audiences endpoints (serving-api) turn a slot's entity states
into member lists for ad, CRM and warehouse tools:
GET/POST /v1/slots/{slot}/audiences,GET/PATCH/DELETE /v1/slots/{slot}/audiences/{audience_id}: declarative audiences over a segment, a frozen lookalike result, or a predicate over scores, behavioral signals and segment membership, validated against the slot's active bundle.POST /v1/slots/{slot}/audiences:preview: a side-effect-free size estimate with up to 10 hashed sample members.POST /v1/slots/{slot}/audiences/{audience_id}:materialize, plusGET …/materializationsandGET …/kpis: versioned CSV + Parquet member files with entered/exited counts, delivered to GCS or BigQuery and announced by oneaudience.syncedoraudience.failedwebhook carrying a 7-day signed CSV URL. Members pass the suppression/holdout seam first; the record reports the suppressed and withheld counts.POST/GET /v1/slots/{slot}/similar: exact cosine lookalikes over the stored model embeddings (1-100 seeds, topkup to 100,000).GET/PUT /v1/audience-settings:pseudonymize_audiencesreplaces raw entity ids with the per-projectentity_hashin every audience output.
Not active yet: interval schedules and s3 audience destinations are refused
with failed_precondition (scheduling_unavailable,
s3_destination_unavailable), and segment audiences are not refreshed
automatically on segment.migration — materialize on demand.
Amplitude and Mixpanel webhook adapters
Two new webhook adapters stream product-analytics events into Tessera with
zero code. Create an adapter source with "provider": "amplitude" or
"provider": "mixpanel" and point the provider's webhook at
https://ingest.staging.tessera.zone/v1/adapters/{provider}/{src_id}. Amplitude
authenticates with a Bearer shared credential and Mixpanel with HTTP Basic
(tessera:<secret>); the secret is sealed with Cloud KMS and compared in
constant time before the body is read.
Because these providers carry your own event taxonomy, their catalogs map
every event by a passthrough rule: Song Played becomes
amplitude.song_played, identity goes to entities (never to attributes),
properties become attributes with one level of nesting flattened, and
identity-management events such as $identify are acknowledged and
dropped. A request may carry one event, a JSON array, an {"events": [...]}
wrapper or NDJSON, up to 500 events, which also makes it the backfill path
for exported history.
Explicit per-event overrides, a field-wise passthrough override and
mapping:preview work exactly as for Stripe and Shopify. Stripe and Shopify
behavior is unchanged.
Project purge includes webhook replay cancellation
Project purge waits for webhook transaction owners before disabling endpoints and abandoning pending replay work. Replay completion audits commit with the cancellation, and retries preserve terminal replay history without duplicate completion records. The purge work census now accounts for replay states and continues to reject unknown states.
Existing provider drain and project writer fences still apply. This source change does not activate a release or establish complete project-purge verification across every producer and external store.
Explain form limits and connection recovery
Source, mapping and label forms now explain identifier examples and existing limits. BigQuery guidance includes legacy project IDs and table names with Unicode or spaces. Invalid ports and sync timeouts show their allowed ranges, while malformed JSON explains how to correct its syntax without losing the entered text.
Connection exceptions and internal requirement references now lead with recovery guidance, with original diagnostics available under Technical details. Existing actionable errors, request IDs, field error links and entered values remain available. These presentation changes use the existing validation rules and API contracts.
Known label errors preserve their corrective action, including when a new label is required. Field-level diagnostics receive the same guidance as form-level errors, while names that happen to contain an internal-looking reference remain unchanged.
Clarify empty upload recovery and source sync receipts
Empty files now explain what to replace before opening an upload session. Completed uploads distinguish stored bytes from usable data: missing rows or schema keep mapping unavailable, known server headers remain visible, and failed file processing shows the sync reason. Replace file starts a fresh upload without carrying forward the previous preview.
Disabled sources explain how to re-enable syncing. Rejected sync requests retain their error and request ID inline, while accepted requests show the exact sync ID even before history catches up. Re-enabling a source preserves its existing configuration. These console changes use the existing API and leave server enforcement unchanged.
Separate source readiness from sync and interpretation status
Source summaries now show source enablement, the saved mapping version, and the latest sync result alongside interpretation status. Manual mode identifies its mapping and contract workflow without implying an automatic discovery or model-preparation job.
A completed sync with zero normalized events explains which input and mapping checks to make next. Unknown event counts remain unknown, historical results remain visible for disabled sources, and mapping or sync lookup failures retain their request IDs and read-only retry controls. Sync counts describe that sync rather than lifetime ingestion, label readiness, or model readiness. Stream sources do not request batch sync or mapping discovery.
Declare named key, webhook recovery and guided import interfaces
The API contract declares optional integration names with explicit named-list opt-in, targeted webhook samples and independent replay history, and authorized shared-checkpoint discovery and read-only import preview. Existing unnamed-key responses, webhook delivery records and omitted-expectation imports retain their contracts. Generated-client list arguments retain their released positional prefixes.
Additive storage declarations support optional key names and separate webhook replay lineage. Runtime handlers, scheduling, cancellation and actual shared artifact eligibility remain separate implementation and verification gates; these interfaces alone do not establish working delivery or import readiness.
Existing key lists continue to work when the optional named-list flag is absent or false. Until the matching runtime is available, named key requests and preview-bound imports fail explicitly before key creation or import transactions; they cannot silently ignore the new fields. Authorization still runs first.
Define trusted original replay payload and mapping adapters
WP-S1 / WP-S5 / WP-O3: replay provider prerequisites
An internal typed adapter now specifies exact-generation compressed dead-letter reads and current-source mapping of retained original bus input. The service owns construction and authenticated scope; callers cannot supply authority through payload shapes, mapped events or missing owner lookups. Existing schemas and v1 stay unchanged.
This interface keeps methods inactive. It supplies no GCS/broker implementation, first-owner minting, service activation or cleanup claim. Real PostgreSQL and provider proofs remain required before dependent replay and writer activation.
Define transient replay provenance and publication lineage storage
Add internal schema-aligned, effect-free replay authority boundaries and empty project/environment-scoped storage for pinned dead-letter origins, original bus delivery evidence, replay bytes and exact publication-attempt links. Existing rows are not backfilled as never-issued. Public v1 and original replay behavior are unchanged; producer admission, durable replay, retention and erasure runtime remain pending and service writers are not activated by this interface release.
Clarify full quotas and capacity limits
The console distinguishes a quota at its limit from usage above the limit while preserving critical warnings and enforcement. Project and concurrent-training capacity limits no longer imply a monthly reset. Historical usage cards label the current quota separately from the selected period's usage.
Define exact original producer result encoding
Define versioned internal pull, pin and acknowledgement result evidence with lossless original bytes and exact registered identity. Schema validation grants no producer or replay authority. This contract does not activate workers or provider effects; runtime verification and lifecycle gates remain required.
Define original producer erasure registration
WP-S5 / WP-O3: inactive nonempty producer erasure boundary
Adds closed target/attempt/residue models, trusted provider interfaces and one empty transient registration table. Exact capture/epoch and owner/cut relations, forced tenant/environment isolation and original unknown outcomes are retained. A verified operation response remains distinct from fresh physical-absence proof; provider-retained objects cannot be declared erased by an elapsed deadline.
This is interface-only. It does not execute erasure, activate a writer or complete nonempty producer retirement, whole-project deletion or real-provider validation.
Define producer ownership before original ingest issuance
Add inactive source admission, original producer generation and closure/drain contracts with empty tenant-scoped storage. Unknown old workers and raw inputs with unresolved entity membership remain explicit blocking obligations. Existing lease scheduling, public v1 and replay behavior are unchanged; no worker or provider operation is activated by this interface foundation.
Authenticated raw capture precedes the first object pin and replay row. Exact capture-target links register pin attempts before provider effects while keeping foreign keys acyclic for physical erasure. Pinned replay evidence still requires a verified original ObjectPin; unpinned output grants no issuance authority.
Define original ingestion ownership and complete-cut contracts
Define the internal original issuance and complete-cut contract with an empty, project/environment-scoped SQL schema and typed contract tests. This interface release prepares coordinated durable ingestion and erasure work; it does not activate service writers or change public v1 acceptance. Existing runtime audit failures remain open until the documented implementation and provider gates pass. The contract preserves authorized dead-letter replay lineage and distinguishes first issuance of an unissued bus mapping failure from replay of an issued owner.
Clearer batch freshness ages and monitoring coverage
Monitoring shows batch state age as a readable duration alongside the recorded seconds, with full UTC observation windows and a paginated table of exact samples. The reported monitor status is distinguished from batch freshness, which remains unknown when the API supplies no age objective. Batch input drift coverage is stated explicitly.
An empty latest observation no longer reuses an older latency or queue result as current. A failed monitor-summary request shows its error and retry action while the independently available freshness panel remains usable. Observation windows are not presented as last-successful-refresh or source-cursor timestamps.
Keep full monitoring chart axis values visible
Keep full monitoring chart values visible when axis labels contain six or more digits. The shared chart measures rendered tick text and reserves its left margin in SVG units, including after fonts load. Recorded samples, scales, tooltips and accessible data tables keep their existing values.
Keep model evidence readable on narrow screens
Nested model-card facts size their columns to the available space, and long numeric values and long diagnostic identifiers can wrap without changing their precision, report messages or the downloaded JSON. Gate guidance distinguishes passed and not-applicable checks from failed checks; failed gates retain their repair instructions and report diagnostics.
Explain label definitions before creation
The label wizard review now explains the submitted target, entity population, source binding, observation time, maturation and missing-outcome behavior. Binary, category and numeric examples are explicit, and derived rules describe their filters, cadence, positive and negative outcomes, and open horizons.
The review keeps the exact request JSON in an advanced disclosure, shows the existing server-evaluated sample for derived rules, and updates after Back/edit without replacing the user's rule. Samples for an earlier rule disappear immediately when the definition changes or becomes invalid; delayed responses cannot restore them. Rule evaluation and API contracts are unchanged.
Keep served entity scores visible without a ledger summary
Predictions now wait for an entity ID prefix before searching and can open a selected entity's existing state directly. Clearing the prefix returns to the search prompt without an unfiltered API request.
The full entity page checks event history and deployment state independently when no lifetime summary is available. Existing scores retain their model and bundle identity. An unknown entity requires confirmed absence; pending, denied, unavailable, or incomplete reads do not become an absence claim. Read errors keep their request IDs and retry controls.
Filter training runs and dataset builds before pagination
Correction (2026-10-21): the Python and TypeScript SDKs are planned, and the integration catalog lists both SDKs as Planned; this note refers to their calls. The filters below are available through the HTTPS API.
Run and dataset list APIs now filter by lifecycle status, stored label ID and creation-date range before pagination. Runs also support an exact dataset ID filter. Results remain scoped to the selected project and environment, and existing Python and TypeScript SDK positional calls retain their original cursor and limit arguments.
Explain equal-count evaluation in the G3 guide
The count-invariance guide now explains the evaluator's equal-count exception, including its shuffled baseline comparison and numerical tolerance. It also explains why a passing result on an equal-count sample cannot establish invariance across different event counts, matching the console and downloaded model card.
Declare experiment discovery filter interfaces
The API contract and generated clients declare optional date, label and status filters for dataset and run lists, plus a dataset filter for runs. Existing generated-client cursor and limit positional arguments remain compatible. This interface release preserves unfiltered requests; server filter execution is pending the separate implementation release.
Clarify contract review and bulk edit scope
Contract review now separates saved draft changes from version differences. Valid PII settings stay neutral, with restriction guidance when their editor is focused.
Bulk edits show the affected attributes and protected rows before applying. Unchanged values and unavailable rows are excluded from the update, and Apply waits for pending edits to finish saving. The console preserves identifier and direct-PII restrictions and prevents explicit target-only labels from becoming feature inputs.
These changes use the existing contract API and role information. They do not change role inference or backend enforcement.
Improve console audit access and accessible data workflows
Console audit and accessibility improvements:
- Settings gains a scoped audit viewer and bounded JSON export with permission, date, paging, and context-change safeguards.
- Source lists show copyable IDs and UTC creation times; training links resolve the exact eligible dataset without replacing a later user choice.
- Connector forms explain access requirements and the limits of connection tests. Form errors, correction links, upload transitions, wizard steps, and projection camera controls support keyboard use.
- Chart disclosures retain complete sample values, report tables support keyboard scrolling, and long report identities reflow on narrow screens.
This frontend group uses existing APIs. Receipt history, named API-key integration workflows, experiment discovery filters, shared import readiness, and webhook sample/replay controls remain separate pending their server prerequisites. Quota boundary browser coverage complements the separately released quota presentation change. Audit completion still requires the release and its post-release checks.
Add bounded canonical CPU artifact export implementation
Implement the two fixed canonical CPU and base-image export profiles with bounded command execution, generation-bound delivery, original OCI byte verification and durable same-build recovery. Include the reviewed main-only workflow source after the artifact interfaces landed; retain the original positive acceptance fixtures.
Source on integration does not activate a trusted-main export route. Live artifact acquisition, installed-image and real-model verification, and validation on the hosted deployment remain pending normal trusted-main integration and authorized execution. No IAM, WIF, credential-transfer or main-lock change is included.
Define canonical CPU artifact export contracts
Canonical CPU validation now has documented artifact, recovery-state, and preparation contracts for the fixed semantic image and its separate Python base image. The contracts distinguish their provenance and exclude credentials from export control metadata.
This is an interface-only release. Its export entry points are inert stubs; it does not enable an export workflow, retrieve or execute an image, validate a model, or change the running release. The implementation and trusted-main workflow follow in a separate release after these contracts land.
Restore entity prefix and filtered Bigtable reads
Entity prefix lookup and other filtered Bigtable reads use a column-family expression accepted by the provider. Exact family selection, escaping, tenant row bounds and read limits are preserved.
Bigtable forbids colons in column-family expressions, including the colon in a noncapturing group. The internal exact-family builders now use an ordinary anchored group. User-supplied predicate patterns and authorization are unchanged.
Record policy changes and source status transitions in the audit trail
Automatic leakage-policy versions created by label changes now carry correlated audit events in the same transaction. Source status changes record their previous and resulting status, and denied source actions retain the target source ID. Configuration and credential values remain excluded from these details.
Applying or clearing audit filters preserves keyboard focus while resetting the feed's pagination and pending exports for the new filter context.
Preserve entity erasure during delayed stream marker writes
Delayed scoring markers no longer recreate an erased ledger row. Manifest completion compares the original staged batch before updating its marker. The stream router now retains unresolved work when a required marker does not transition, while recognizing an existing entity deletion explicitly.
Guided shadow comparison setup
Shadow comparison explains missing active selections, absent compatible shadows, and incompatible entity types separately. A guided slot-creation link prefills the matching Shadow role and entity type, then returns to the saved comparison within the same project and environment. Existing creation permissions still apply, and unavailable services retain their error and retry state.
Semantic training preserves raw targets after token sorting
Semantic training preserves raw targets after token sorting
Semantic training now selects bounded next-event targets using token-kind flags in canonical token order. Previously, reordering the same input tokens could change losses and gradients or reduce the raw-evidence target reservation.
New semantic runs pin the corrected sampling recipe. Start a fresh run when upgrading from the old recipe; its checkpoints cannot silently resume under changed optimization. Existing checkpoints remain readable for inference, and model quality still requires the usual evaluation and publication gates.
Automatic discovery filters incomplete structural candidates
Automatic discovery filters incomplete structural candidates
Automatic source interpretation now excludes fields missing or null in sampled records before classifying structural roles. When no field qualifies, discovery reports unknown. Updated discovery uses a new cache recipe while preserving accepted bindings, manual locks and the existing approval thresholds.
Explicit CPU execution for semantic preprocessing
Semantic workers and discovery support an explicit CPU float32 execution profile using the same pinned models. CPU provenance and caches remain separate from CUDA/BF16 artifacts. Existing GPU recipes and default execution are unchanged. CPU validation does not establish GPU numerical parity or readiness.
Run saved leakage-policy tests before editing rules
The leakage-policy screen now offers an explicit test of the saved policy against the approved contract. Results show matched and unmatched attribute names, distinguish known locked defaults from saved user rules, and keep unsaved previews separate. Missing prerequisites, empty results and failed requests have clear states with retry and request IDs. Large contracts are tested within the existing API request limit. Name tests do not replace contract roles or the other leakage checks used during dataset builds.
Distinguish filtered training logs from missing logs
An empty severity filter now identifies the selected level, shows matching and loaded line counts, and offers Clear filter. Successfully empty runs retain their separate waiting or no-logs message. Loading and failed reads no longer assert a zero log count, and existing retry and backoff behavior is preserved.
Browse large projection selections and export their analysis context
Embedding selections now stay in a bounded table with search, ordering, page navigation and keyboard selection. The table exposes coordinates and saved score/attribute values even when WebGL is unavailable. Selection outlines preserve score colors and can be hidden without clearing the selection.
The existing IDs-only CSV is unchanged. An optional analysis JSON download includes every selected row and its saved projection, project, environment, filters and timestamps. New projection artifacts preserve each scored row's observed generation, bundle, model and update time, including mixed generations. Older artifacts explicitly report unavailable scoring provenance.
Foundation schema consistency updates the encoder
Foundation schema consistency updates the encoder
Foundation training now backpropagates verified schema-equivalence penalties through the shared encoder. Previously the penalty used the inference embedding API, so a nonzero penalty changed the reported loss without updating weights.
Positive-weight consistency runs use a versioned optimization recipe. Start a fresh run after this fix; old detached-penalty checkpoints cannot silently resume under a different objective. Zero-weight run fingerprints and resumes remain compatible. Model quality and publication still require their existing gates.
Preserve equal-count evaluation caveats in model card downloads
Model cards now explain the limits of count-invariance evaluation when the report records that every sampled entity had the same event count, including when the reported gate passes. The JSON download retains all gate decisions and numerical evidence and includes the existing count-invariance caveat.
Show readable errors for incomplete entity history
Entity lookups now return a readable error with a request ID when stored history is incomplete or malformed. The console can show the error and retry without reporting the entity as missing.
Keep entity storage errors visible and retryable
Entity searches and timelines return a readable service-unavailable response with a request ID when their history store fails. Console clients retain access to the error response and can retry; storage failures are not reported as missing entities.
Keep projection views when exploring entities
Completed embedding projections now have bookmarkable export URLs. Returning from an entity or reloading reopens the existing result without creating another projection job. The browser tab retains color, camera, selection and tool choices for 24 hours, scoped to the signed-in account, project, environment, slot and export. Unavailable exports show an explicit retryable error.
Entity lifetime-summary failures now show their typed error, request ID and retry action while the timeline and scoring sections remain available independently.
Read-only inspection of saved entity history
An operator diagnostic can compare one entity's exact saved canonical snapshot with ledger observations within explicit resource limits. It reports unavailable objects, changed fingerprints or pins, accepted event identity, and ambiguous lifetime counts. It does not repair data, activate ingestion changes, or claim that retained rows prove complete lifetime history.
Edit derived-label descriptions without changing maturation
Editing a derived label's description preserves its horizon-bound maturation and no longer submits an immutable maturation change. The form explains the read-only value. Labels with editable maturation continue to support changes.
Keep label context when starting a dataset
Starting a dataset from a label now selects that label and its entity type, including after reloading the link. The launcher resolves the label directly even when it is beyond the first page of available labels. Missing, archived or incompatible labels explain how to recover, and lookup failures retain their request ID and retry. Changing the selection remains possible, and background refreshes do not overwrite those choices.
Explain count-invariance evaluation consistently
Count-invariance charts now display the evaluation report's decision and its shuffled comparator. Equal-count samples explain why the usual neighbor-share threshold does not apply and why this diagnostic cannot establish invariance across different event counts. The same explanation appears in the model card, whose downloaded report bytes remain unchanged. Missing metrics are explicit.
Preserve accepted event identity in ledger preparation
An inactive connector adapter can prepare entity history from the exact committed canonical manifest, preserving saved event IDs, receipt times and contract versions across retries. It shares bounded object validation with the read-only history inspector and rejects missing files, scope mismatches and incomplete batches. Ledger activation and historical repair still require the separate durability, retention and deletion-fence contracts.
Working help links and explicit integration availability
Console help links now follow the deployment's documentation host and use the current state-read route. Gate guides preserve their section anchors.
The website, documentation, and source gallery share a capability matrix that distinguishes console setup, API setup, preview, and planned integrations. Snowflake, Databricks, Redshift, and MySQL link to their supported API guides while their console forms remain planned. Planned integrations are separate from available setup paths. The website also clarifies batch refresh, streaming scoring requirements, and policy-controlled quality-gate acknowledgements.
Keep source syncs alive during storage commits
Source syncs now renew their heartbeat independently of blocking extraction and storage operations. The configured sync deadline still applies. Timeout cleanup checks the current lease before deleting a worker, and late worker responses cannot overwrite a terminal sync status.
The console distinguishes a source time limit from a missing worker heartbeat in its recovery guidance. Sync failures and timeout cleanup now emit structured logs with project, source and sync identifiers for operational investigation.
Release images use the scanned October 8 platform base and patched PyJWT, urllib3, Sharp, source-map-js, PCRE2 and Perl packages. The existing semantic-worker checkpoint mitigation was re-reviewed and remains limited to its guarded loading paths, with an October 15 expiry.
September 2026
Reviewed platform checkpoints for automatic customer sources
Reviewed platform checkpoints for automatic customer sources
Adds an operator-controlled, versioned release catalog and explicit release allowlist for automatic projects. Eligible sources use their existing durable preparation lease to mirror a bounded, generation-pinned complete generated checkpoint through the existing tenant publisher. Project-local preparation, evaluation and activation gates remain required. Customer data is never pooled.
Platform grant checks also apply to existing mirrors, evaluation dispatch and report publication, and shared bundle activation. SQL recipient revocation and catalog revocation both deny further use. Normal operator grants and v1 payloads remain unchanged. Already-running serving tensors require the existing explicit slot/runtime shutdown controls for immediate cessation.
The release feature is disabled until an operator supplies genuine checkpoint,
quality, training and licensing evidence, an exact release allowlist and the
trusted catalog configuration to registry and eval runtimes. No fabricated
publication metadata is included. See specs/PLATFORM-SEMANTIC-ENROLLMENT.md for
limits, deployment requirements and the real-artifact verification contract.
Automatic onboarding accepts customer event category names
Automatic raw-source onboarding can assign a neutral source-record category when
customer JSON has no operation field. Noncanonical operation names are preserved
in an immutable, source-scoped mapping instead of requiring customers to rename
them to Tessera's canonical spelling. This introduces the optional
SemanticBinding.event_type_adapter contract.
These categories describe source identity, not inferred business outcomes. Missing or ambiguous entity identity and event time still require evidence. Mappings retain exact raw strings, reject collisions, and allow at most 128 entries of up to 256 characters. Accepted strategies and locked choices survive rediscovery; new values require a validated binding and contract revision.
Existing bindings omit the new property and retain their fingerprints. The
canonical /v1/events API and manual mappings keep their existing behavior.
Pinned self-hosted semantic preprocessing worker
The semantic worker now executes the pinned SemIf/Qwen3.5 direct-logit scorer against approved causal projections, persisting actual immutable distributions and inference traces. Project-scoped retries reuse accepted results; bounded timeouts preserve explicit unavailable states without blocking ingestion.
Separate metadata-only discovery uses cached Qwen embedding vectors to shortlist field paths and classify structural roles. It preserves unknowns, manual locks and deterministic approval gates. The GPU dependencies remain isolated from the trainer, and serving makes no teacher/model calls.
Native real-model verification passed for three generated schema families. The prod-uid GPU-container verification remains blocked by local registry access and the missing Docker NVIDIA runtime; the runnable harness is included.
Automatic semantic onboarding: versioned API contracts
Phase 6 now defines source-scoped raw JSON ingestion, automatic/guided/manual
onboarding controls and progress, immutable semantic artifacts, authorized shared
checkpoint import and causal feature-view/trajectory reads. Canonical
POST /v1/events retains its existing contract. New generated clients expose
these interfaces ahead of runtime implementation; interface availability alone
does not indicate a deployed feature.
Artifacts pin the self-hosted SemIf/Qwen recipes, project ownership, immutable input/output provenance, locked overrides and exact representation semantics. Scoring messages gain optional complete revision pins while preserving legacy cursor-only requests. Shared checkpoint imports retain local ownership and existing evaluation/promotion gates.
July 2026
Short training runs now validate and stop adaptively
New short-horizon WSD runs with early stopping validate at actual packed epoch boundaries and use a fingerprinted proportional cooldown minimum. This prevents small datasets from entering cooldown on the first step or reaching the budget without enough validation checks for the configured patience—even when token packing yields fewer updates than nominal geometry. Existing run fingerprints and checkpoint resumes keep their original schedule exactly; explicit validation cadence and fixed-horizon mode are unchanged.
Training logs now retain structured progress, validation, checkpoint, and early-stopping
fields in the API and console. The API returns its legacy three-field entries by default for
strict older API clients; pass structured=true to opt into the additive context. Switching between
runs clears the prior run's log cursor and rows, terminal metric catch-up retries transient
failures, and an in-progress timeline labels its pending terminal stage as complete instead
of succeeded. A pre-update line now makes the first CPU step visible before its first metric,
and partial trainer requests preserve omission so CPU runs do not accidentally enable compile.
The release also pins patched transitive docs and console dependencies identified by the
required dependency audit.
Console: promotion gates now explain the decision and next step
Model evaluation now leads with each gate's plain-language name and keeps its purpose, impact, remediation, observed report values, threshold, and verbatim diagnostics together. Optional LightGBM comparison deltas and confidence intervals appear when the report contains them.
The console uses the registry's three-state policy verdict throughout. Clean
models are ready to promote, advisory quality failures show an explicit
warning and require per-gate acknowledgement plus a reason, and integrity or
strict-policy failures remain non-waivable blockers. Acknowledged bundles show
the recorded actor, reason, policy mode, gate name, and gate.override
provenance.
Owners and admins can now manage warn or strict mode and raise G1-G3 to
blocking under Settings → Promotion policy. Other roles get a guided
read-only view, while G4-G6 stay visibly locked for every role.
Verifiable model cards unite training and promotion evidence
Every evaluated model now has a schema-versioned model card assembled on read. It brings fingerprinted lineage and data provenance together with resolved trainer settings, adaptive-stop history, per-label confidence intervals, G1–G6 evidence, loss curves, and fact-derived limitations. Promoted models also show the bundle and any recorded advisory-gate acknowledgement.
The console renders the card on model detail and downloads the same JSON served
by GET /models/{model_id}/card. Historical runs degrade gracefully when
newer training, baseline, or signals fields are absent. Cards reflect current
promotion state; the underlying source artifacts and fingerprints remain the
immutable evidence.
Every artifact used to assemble a card is fingerprint-verified. A mismatch fails the request closed with typed source details instead of returning a partial report, and missing, unevaluated, or cross-project model ids remain indistinguishable.
Training runs now explain adaptive completion
Run responses now echo every resolved trainer setting and report the actual
completed step, stop reason, best checkpoint step, and cooldown start when
available. The console frames total_steps as a maximum budget, derives live
ETA from the immutable token batch, and shows an early-stop summary with the
best-step marker and cooldown band.
The run launcher exposes maximum epochs, patience, minimum improvement, and a
fixed-horizon off switch. An untouched form keeps the existing minimal request
body; edited controls are fingerprinted in the run's trainer block.
Training runs can stop early after validation plateaus
New training runs use validation-driven early stopping with a warmup-stable-decay learning-rate schedule. The default epoch count is now a maximum budget of 10 epochs; patience defaults to five validation evaluations, and every run finishes through a deterministic learning-rate cooldown.
The fingerprinted trainer settings now expose early_stopping, patience,
min_delta, cooldown_ratio, and lr_schedule. Explicit
early_stopping: false, lr_schedule: cosine, and epochs: 3 preserve the
former behavior, and existing fingerprinted run configs remain readable.
Training logs load reliably in the run monitor
The run monitor now loads structured trainer logs through the supported Cloud Logging pagination interface. Previously, log reads could report that the stream was unavailable even though the trainer entries existed.
Batch refresh is available after slot activation
Newly activated slots can now load their batch-refresh history and start the
first refresh from the console. Serving operations nested beneath a deployment
slot are routed to the serving API instead of returning Not Found from the
control-plane gateway.
The same correction restores slot exports, stored entity-state reads, batch scores, and segment operations. Authentication, project isolation, rate limits, idempotency, and request tracing remain enforced by the serving API.
Advisory gate warnings can be acknowledged in the console
Models with a G1-G3 quality warning can now create a promotion bundle from the console after every warning is acknowledged with a reason. Integrity failures and quality gates made blocking by project policy remain disabled.
The recorded actor and reason remain visible with the bundle, and acknowledged bundles stay available in the deployment-slot picker even though their raw evaluation report still contains a failed gate.
Evaluation: automated LightGBM baseline comparisons
Evaluation reports can now include a reproducible, point-in-time-safe
LightGBM comparison for every label. The comparison is informational and
does not change promotion gates; callers that disable it and older reports
continue to omit the optional baselines block.
New dataset builds now retain per-event timestamp metadata for exact causal rolling windows. This additive shard field bumps the dataset builder to 1.1.0, so newly built shard and derived-manifest fingerprints intentionally differ from builder 1.0.1 and earlier artifacts; existing shards remain readable.
Promotion gates distinguish quality tradeoffs from integrity failures
Promotion no longer treats every failed evaluation gate as the same decision. G1-G3 are quality judgments that warn by default: a bundle can be created only when the request acknowledges the exact failing advisory gates with a non-empty reason. G4-G6 are integrity checks and remain unconditional blockers.
Projects now have a warn or strict promotion policy. Admins can restore the
all-failures-block behavior with strict, or raise individual G1-G3 failures
to blocking; integrity severity can never be lowered. Model list and detail
responses include the server-resolved promotion verdict, and the new
/gate-definitions endpoint supplies the plain-language catalog.
Acknowledged bundles preserve the eval report's gate facts verbatim and add the reason and actor as immutable provenance. Each acknowledgement and every bundle creation are recorded transactionally in the audit log. Acknowledging a G1 regression never lowers the baseline the next model must beat.
Distributed validation and best-checkpoint integrity
Validation metrics from multi-rank training runs now cover the full validation
split, including rows assigned beyond the shortest rank's shard tail. As a
result, GET /runs/{id}/metrics may report corrected val_loss values for
existing configurations with world_size > 1; single-rank values are
unchanged.
Whenever validation runs, the checkpoint registered on the resulting model is now guaranteed to be the complete best-by-validation-loss checkpoint at the recorded best step. The newest checkpoint is used only for runs where no validation could run, and that fallback is explicitly recorded in trainer logs.
Training log retries are now bounded
When trainer logs are temporarily unavailable, the run monitor now backs off and pauses automatic retries after four failed reads. Existing log lines stay visible, the run continues normally, and Retry logs resumes the stream on demand instead of producing an endless series of failed requests.
CUDA startup failures now include recovery guidance
When a training worker cannot initialize its assigned GPU, the run page now explains that training stopped instead of falling back to CPU and links directly to a retry where another GPU compute class can be selected.
Model & data monitoring: drift, freshness, and score webhooks per slot
Serving is now observable as a product feature. Every realtime deployment
slot gets hourly drift monitors comparing its live traffic against the
serving bundle's evaluation-time baselines, 5-minute freshness rollups,
and two new outbound webhook event types — all under
GET /v1/slots/{slot}/monitors….
- Drift monitoring: per contracted feature attribute, presence-state and
value-distribution PSI against the promoted bundle's pinned baselines;
per label, predicted-score distribution PSI. Statuses are flap-damped
(two consecutive windows in, two out) and windows with too few
observations report
insufficient_data— low-volume projects never get paged by noise. drift.alertwebhook: published exactly once when a monitor entersalert, carrying the triggering drift point and the thresholds in force.score.updatedwebhook: opt-in per slot — every live state write can notify your systems (CRM sync, caches) with the entity's fresh scores and full provenance (bundle_id,model_fingerprint,event_cursor,computed_at), throttled per entity to a configurable interval.- Configurable:
PUT /v1/slots/{slot}/monitors/config(admin scope) sets PSI thresholds, the minimum window size, muted attributes (points keep flowing; webhooks stay quiet), andscore.updatedemission. - Freshness API:
GET /v1/slots/{slot}/monitors/freshnessserves event→state latency percentiles and queue age for realtime slots, and state staleness for batch-only slots; monitor summaries with per-kind status live atGET /v1/slots/{slot}/monitors. - Drift history is retained for 180 days and served with cursor pagination;
reads require the
readscope.
Console monitoring: drift dashboards, alerts, freshness SLOs, gate failures
The console gains a Monitoring section over the WP-V4 monitoring API — drift, freshness, and alert status per deployment slot, rendered exactly as the platform computes them.
- Overview: every slot with its worst monitor status (
alert>warn>ok) and last evaluation time; slots awaiting their first hourly run show a clear "first evaluation pending" state. - Per-slot dashboard (
/monitoring/{slot}?tab=&range=&attribute=&label=, deep-linkable): presence / value / score drift with threshold bands at your configuredpsi_warn/psi_alert, worst-first attribute tables, labeled bundle-rotation markers, and low-volume windows shown neutrally as insufficient data. Freshness panels track event→state latency percentiles against the 2 s p95 target (state staleness for batch-only slots). - Alerts + acknowledgement: drift alert episodes are now first-class —
GET /v1/slots/{slot}/monitors/alertslists firing/resolved episodes with peak PSI, andPOST …/monitors/alerts/{alert_id}:acknowledgerecords triage acknowledgement (idempotent; thresholds, muting, and webhooks are untouched). The console's Alerts tab acknowledges optimistically and offers one-click attribute muting with the consequence spelled out. - Gate-failure history:
GET /v1/monitors/gate-failureslists evaluations whose promotion gates failed — the console links each record to the model's full evaluation report. - API additions:
kind=filter onGET …/monitors/drift; alert and gate-failure endpoints above (allread-scoped; key-based acknowledgement needsadmin). - Shadow vs active: a read-only comparison overlaying a shadow slot's score drift on the live slot serving the same entity type.
Console: usage dashboards, billing & platform quota states
The console gains its commercial surface (WP-U7):
- Usage dashboards (
/usage): one card per billing metric (the closed 002 §11 set) with period totals summed client-side from the flatGET /usagerollups, unit-appropriate formatting (bytes → GiB, compute seconds → h:mm), a daily chart per metric (dims rows folded, gaps render as zero days), quota consumed % where a plan quota exists, and defined empty states for zero-usage metrics. A period picker (current billing period / previous / last 30 days) re-queries everything and round-trips through the URL. Usage is project-scoped: the page names the displayed project/environment and re-fires with the tenancy headers on switch. - Billing (
/billing): current plan + subscription state +current_period_end, per-quota meters that visually distinguish hard (enforced) from soft (record-only) limits, and the upcoming-invoice preview (quantities from the same usage rollups; amounts from Stripe, rendered from integer minor units viaIntl.NumberFormat). "Manage billing" hands off to the Stripe customer portal in the same tab and the return redirect re-fetches the subscription; Free orgs get an explicit plan card with their limits and upgrade via Stripe Checkout — no portal call is ever attempted for portal-ineligible orgs. - Platform quota communication: a global shell banner warns at ≥ 80%
of any hard quota (dismissable per billing period) and turns critical at
≥ 100% (non-dismissable), linking to
/billing.useQuotaStatus()andQuotaExceededStateship via@console/uifor every feature page: liveresource_exhausted(429) responses anywhere in the console flip the affected quota to exceeded immediately — org owners/admins see an upgrade CTA, other roles are pointed at an org admin.
No API changes.
Console: embedding explorer
See your whole population at once: the new Embeddings tab on every deployment slot renders a server-side 2-D UMAP projection of the slot's entity embeddings — up to 200,000 points in an interactive WebGL scatter.
- Request a projection with a sample size (default 50,000, capped at
200,000) and color columns — scores from the slot's bundle labels plus
contracted feature attributes. Projections are asynchronous exports
(
POST /slots/{slot}/exportswithkind: projection_2d); the console polls until done and downloads the artifact from a signed URL with a progress bar. - Color-by any requested column: numeric scores/attributes get a perceptually ordered sequential ramp with a min/median/max legend; categorical attributes (and segment ids) get a color-blind-safe categorical palette with click-to-hide legend entries.
- Lasso, rectangle and shift-additive selection turn regions of the scatter into entity lists: a live count, a virtualized id list that deep-links into the entity explorer, "copy ids" and "download CSV" — all client-side; selections are never sent to the API.
- Honest by design: a persistent (non-dismissable) caveat explains that 2-D projections distort distances and cluster shapes, alongside the projection's provenance — bundle, model fingerprint, sampled/total entity counts and creation time. Slots without an active bundle explain that embeddings exist only after activation and batch scoring.
Console: settings grow up \u2014 guided role gates, reveal-once secrets, and a webhooks admin surface
The settings cluster (organization, members, projects, API keys) moves onto the guided-experience foundation, and the platform's outbound-webhook subsystem finally gets a console surface:
- Webhooks admin surface (new):
Settings → Webhookslists your endpoints with subscribed event types and status; create an endpoint by picking event types from the real envelope enum (or"*"), get thewhsec_…signing secret exactly once, rotate it with a 24-hour dual- signature grace window, and delete with the consequences spelled out. An auto-disabled endpoint explains the sustained-failure cause and offers one-click re-enable. - Deliveries log with human guidance: each endpoint's drawer shows its deliveries newest-first (cursor-paginated, 30-day retention) with status and event-type filters kept in the URL so views are shareable. Failed deliveries lead with the plain-language cause and next step (timeout, receiver 4xx/5xx, TLS, blocked address, redirect); the raw attempt ladder sits behind "Show details", and a retrying delivery states its position on the retry ladder. A signing-help popover summarizes HMAC verification and links the docs guide.
- Role gates explain themselves: members and viewers no longer hit a flat "admins only" wall on API keys — the page explains what keys do, which role manages them, and links Members to find those people. The API-keys tab is no longer hidden, and admin-only actions on shared screens (role changes, invites, webhook mutations) render disabled with their reason exposed to assistive technology.
- Reveal-once done right: API-key creation and webhook-secret create/rotate share one reveal panel — copy affordance, explicit "never shown again" warning, confirm-dismiss — and the plaintext is provably gone from the page afterwards.
- Structured layouts & richer feedback: all four settings screens move to Card/DescriptionList layouts with scope glossary help on key creation; revoking a key or invite ends in a toast with the follow-up action ("Create replacement key" / "Send a new invite"), and every zero state (projects, keys, endpoints, deliveries) says what the object is for and how to create one.
No API changes: all organization, member, project, API-key, and webhook requests keep their existing generated-client shapes, scoping, and idempotency behavior.
Console: slots & serving explain activation
The slot screens adopt the guided-experience foundation — operationally identical, no longer opaque:
- Slot state with a why: every slot (list rows and the detail page) explains its serving state — a slot with no active bundle says "not serving yet — no bundle activated" with the next step, instead of a bare chip. Mode (live/shadow) and scoring (batch/realtime) chips carry inline glossary help.
- Activation history as a timeline: the append-only history renders as a lifecycle timeline — who activated which bundle, when, and how long each generation was (or has been) active, newest first with the current generation marked. The exact table stays one disclosure away.
- Guided empties route to promotion: an empty slots list explains what a slot is and leads with "Create a slot"; an activation flow that finds no promotable bundle says why and links straight to the models page's promote flow — never an empty picker.
- Activation context: selecting a bundle shows the same promotion-decision gate summary as the models page plus all six pinned fingerprints — what you are activating and why it is safe — before the unchanged typed confirmation.
- Failures name the artifact: a fingerprint re-verification failure
explains which pinned artifact drifted (e.g.
encoders_fingerprint), what that means, and what to do next; the raw error envelope and request id sit behind "Show details". - No dead ends: after activation the page points at verifying scoring via the read-API quickstart; without a bundle it points at promoting a model. Rollback keeps its previous-bundle preselect and typed confirmation, and the timeline refreshes in place.
No API changes: every read and mutation (listSlots, createSlot,
activateSlot, rollbackSlot, listModels, listModelBundles) keeps its
exact shape and sequencing; slot creation keeps the batch_only
restriction and typed-confirmation semantics are untouched.
Console: labels explain themselves \u2014 jargon help, guided support & a forward path
The label screens adopt the guided-experience foundation — the jargon-dense governance surface now explains every term, shows label health at a glance, and always offers the next step:
- Every jargon field gets help:
maturation_days, bothnull_semanticsarms (including whatguard_min_positive_rateprotects against), point-in-time observation, task types, label source kinds, and the derived-outcome vocabulary (horizon, anchors, censoring, predicate matches, rule freeze) each carry a keyboard-operable popover — a plain-language one-liner first, the precise spec definition one click deeper. No monospace field name renders unexplained. - Zero labels is a guided start, not an empty table: with no labels yet, the list explains what labels are for and that they are optional — a dataset can be built self-supervised without any — with "Create a label" and "Build a dataset" actions.
- Support at a glance: the support dashboard renders KPI tiles and the weekly buckets as a theme-aware chart (screen-reader table twin, explicit empty and loading states); the exact numbers stay one click away and unchanged. Derived labels additionally show their materialization accounting (anchors, censored horizons, predicate matches) with help attached.
- Zero support explains itself: a label with no positives leads with its likely cause — nothing ingested yet, a maturation/horizon window still open, or a derived rule whose anchors matched nothing — each with a fix link, instead of three zero tiles. Causes derive only from fields the stats endpoint already returns, and say "likely".
- Preview errors humanized: a failed rule preview leads with the cause and the next step; the raw code, message, and request id sit behind "Show details". Preview request composition and debounce are unchanged.
- A forward path: creating a label lands on its detail with a "Build a dataset with this label" next step; the detail always renders it, blocked with a named reason ("Approve a contract first", or "Ingest observations first" for ingested labels without support) until the prerequisites exist. A fresh derived label is never blocked on support — its observations are materialized by the first build itself.
No API changes: all reads and mutations keep their existing shapes; the wizard's step sequence and validation rules are WP-U3's contract.
Console: data-in screens explain errors and offer forward paths
The sources, mapping, dead-letter, contracts, and leakage-policy screens adopt the guided-experience foundation — the data-in half of the console now explains what went wrong, why, and what to do next:
- Mapping errors become guidance: a preview rejection such as
missing_timestampno longer prints as a raw server string. It renders as a plain-language cause naming the offending column, a next step, and a link that jumps the wizard straight to the step that fixes it; the raw code sits one click away under "Show details". Unknown codes fall back to the raw code + support guidance — never a blank panel. - Saving a mapping offers the forward path: after a successful save the screen proposes "Run a sync" (batch sources — the exact same sync trigger as the Syncs tab) or the kind-appropriate next step for streaming sources, plus "Review contract". No more dead-end success toast.
- Sync transparency: running syncs show their pipeline phase (extract → land → normalize → profile → commit) with elapsed time and an ordered stage tracker — never a bare spinner, and no invented percentages where the API exposes no row totals. Failed syncs lead with the plain-language cause and next step (timeouts, credential failures, mutated objects, rejection-rate aborts…); raw code/message/details are one disclosure away.
- Upload progress on the shared bar: real percent with per-stage copy,
role="progressbar"semantics, and typed failure guidance (interrupted, chunk rejected, byte-count mismatch, expired session) with resume steps. - Dead-letter rows explain themselves: every park reason renders in plain language with a forward path (fix the mapping and replay, include the event type in a new contract draft, …). Replay/discard are unchanged.
- Contracts name their prerequisite: with zero versions, the screen now explains that contracts are drafted from profiling, which runs after a source sync completes — with links to connect or sync a source. Contract jargon carries glossary help, and an approved contract offers "Define labels" / "Build a dataset" instead of dead-ending.
- Leakage policy explained: the page opens with a plain-language definition of leakage (glossary popover with the precise 002 §6 wording one disclosure deeper), "policy vN" explains versioning, and pattern-test results lead with what was denied and why.
No API changes: every request — preview, save, sync, replay/discard, diff, approve, policy test — keeps its exact shape, sequencing, debounce, and feature-flag gating.
Console: models & evaluation explain the promotion decision
The model screens adopt the guided-experience foundation — evaluation reports now show the promotion decision instead of asking you to derive it from spec-language tables:
- Promotion decision up front: the model page leads with a plain-language summary — the all-gates rule, passed / n/a / failed counts and a Promotable/Blocked verdict. A label-less model whose G1/G2 gates are n/a reads as promotable, not as "2 gates missing".
- n/a explained everywhere: every gate marked n/a carries inline help stating that "not applicable" means the gate had nothing to check — and that it does not block promotion. The precise spec definition sits one disclosure deeper.
- Charts over tables: G1 renders each label's estimate with its 95% CI against the baseline and threshold as an interval mark — how far above the bar you are is visible at a glance. G3, G4 and G5 render margin bars of metric vs threshold; G2 keeps its reliability sparkline on the shared chart kit. The full metric tables and raw report values stay one disclosure away — nothing was removed, and every number is the report's own value, verbatim.
- Failing gates stay loud: a failing gate keeps its prominent treatment and the report's actionable diagnostics; the promote button stays disabled naming the failing gate(s).
- Promote flow, retrofitted: the confirmation modal leads with the same promotion-decision summary, then the consequence (an immutable bundle pinning the six fingerprints). Success points forward — "Bundle created → Activate it on a slot".
- List rows informative: each model row shows its tier and a gates-passed chip derived from the list response itself (no extra requests), and a still-training model links to its training run.
No API changes: all reads and the single promote mutation keep their existing shapes, sequencing and polling behavior.
Console: dataset builds show real progress, 0-row builds explain themselves
The dataset screens adopt the guided-experience foundation — the console's flagship dead-ends now say why, how far along, and what next:
- Real 12-stage build progress: a building dataset shows the actual pipeline (resolve → pre-gate → extract → assemble → split → derive → labels → encoders → token stats → tokenize → post-gate → finalize) with the current stage highlighted, the completed prefix marked, and a plain-language one-liner for every stage — no more lone status chip while you wait. The datasets list shows the current stage on running rows ("running — encoders, stage 8 of 12") and why queued rows wait.
- 0-row builds explain themselves: a build that "succeeds" with zero rows no longer renders blank zero tiles. It states the likely cause (no events matched the contract window/entity type, or all rows were filtered) and links the fixes: the pinned contract version, the sources screen, and the launcher to widen the window — with the build's window parameters shown inline. A missing manifest gets the same guidance.
- Human failure guidance: failed builds lead with the plain-language cause and next step (insufficient label support links the offending label's page; a tripped label guard explains the broken-join signal), with the raw code and message one click away under "Show details". Unknown codes still get support guidance. Leakage-aborted builds keep their typed violation report, now introduced by a header explaining the fail-closed gate with a link to the leakage policy.
- No more dead end after success: a succeeded build offers "Start a training run", and the run launcher preselects that dataset.
- Manifests with meaning: per-split row counts render as KPI tiles, and every fingerprint carries inline glossary help (snapshot, fingerprint); the build launcher gains glossary help on window, split, seed and the pinned contract/leakage-policy versions.
No API changes: reads remain GET /datasets and GET /datasets/{id}, the
only mutation remains POST /datasets with an unchanged body, and the
≤ 5 s / terminal-stop polling behavior is untouched.
Complete object lifecycle: edit, delete, archive & restore everywhere
Every console object now has a complete, honest lifecycle (WP-O4) — edit or remove it, or see exactly why you cannot and what to do instead:
- New API operations —
DELETE /orgs/{org_id}(soft deactivate; refused with a typed blocker list while any project is not fully deleted),DELETE /labels/{label_id}+POST /labels/{label_id}:restore(archive: hidden from lists and future dataset builds, observations retained),POST /contracts/{version}:discard(hard-delete a never-approved draft; the version number is never reused),PATCH /webhook-endpoints/{endpoint_id}(url / event_types / description), andPOST /slots/{slot_id}:decommission+:restore(the slot name stays reserved and its activation generation is never reset). All idempotent, role-gated per the lifecycle matrix, tenancy-scoped, and audited. - Console lifecycle actions — project rename/delete/restore, org deactivation, per-row member role changes, label edit/archive/restore, model archive (with the blocking slot enumerated), slot decommission/restore, draft-contract discard, and source deletion — each behind a confirmation dialog that names the consequence, requires typing the object name for high-blast-radius deletes, and renders the server's typed blocker list with Confirm disabled until blockers are resolved.
- Two new console surfaces — Settings → Webhooks (list, create, edit, enable, rotate secret, delete, delivery history) and Settings → Profile (edit your display name).
- Immutable objects explain themselves — approved contracts, leakage policies, mappings, datasets, bundles, API keys and events never dead-end in an error: the console states the invariant and offers the real alternative (supersede / rebuild / rotate / re-ingest).
- Changed —
models:archivenow requires the project admin role (previously member); archived labels are excluded from new dataset builds with a typedlabel_archivedblocker; deleting a source is refused while labels still reference it.
Deletion never touches already-emitted metering or audit history, and project purges now snapshot in-period billable usage before sweeping tenant rows, so invoices can never regress.
First-party growth instrumentation (dogfooded on Tessera)
Tessera now measures its own funnel with Tessera — and nothing else:
- First-party only, off by default. A thin browser package over the code of our planned TypeScript SDK powers funnel analytics for the marketing site and console; events flow through the normal ingest API into a dedicated internal Tessera project. No third-party trackers, tag managers, session replay, or pixels — ever. Emission ships disabled in every environment and is enabled per-environment as a gated rollout step.
- Privacy posture as disclosed. The behavior matches the privacy
policy's first-party analytics section exactly: a random
visitor_idinlocalStorage(no cookies, no fingerprinting), first-touch attribution, full GPC/DNT honoring (a privacy signal disables the package entirely), and contact-form funnel events that carry ids and coarse categories only — never form contents. The subprocessor list is unchanged. - A closed, PII-free event taxonomy (
growth_taxonomy.json) validated on every emission: ids, enum values, and attribution fields only; PII-shaped attributes are rejected by construction.
Nothing changes for customer data or the public API surface: milestone emission is fire-and-forget, adds no requests to customer-facing hot paths, and cannot affect request latency or outcomes.
Console: self-serve upgrade \u2014 hitting a plan limit is never a dead end
The conversion path from Free to Pro now exists end to end, with zero payment friction before you need it:
- No payment step anywhere in signup: account creation, email verification, organization/project setup and the guided first-run never ask for a card or a plan — every new organization starts on Free.
- Upgrade prompts at the exact limit moment: when a hard cap returns 429 (included monthly events, concurrent runs), a training tier is plan-gated, or the Free compute credit runs out, the affected screen shows an inline, dismissible prompt naming the exact cap — current usage, the limit, and when it resets — instead of a bare error. Your work stays on screen; dismissing snoozes the prompt for the billing period.
- One click to checkout: org owners and admins go straight from the
prompt to the new
/upgradepage — a Free-vs-Pro comparison rendered from the same machine-readable plan catalog and rate card as everything else (drift-tested; no hand-typed prices) — and from there into Stripe-hosted checkout. Members see plain-language guidance to ask an owner or admin, never a dead button. - Confirmation that tolerates webhook latency: after checkout, the console polls until the plan flips to Pro, confirms what's now unlocked, and returns you to where you left off. A slow webhook shows "payment received — activation in progress", never an error. Canceling changes nothing.
- Enterprise path: prompts and the upgrade page offer "Contact us" for teams that need SSO, audit exports, or SLAs — custom plans have no public price.
- Marketing can deep-link
signup?plan=pro: after the new organization is created, the console continues straight to the upgrade page.
Public marketing site: homepage, transparent pricing, contact us
Tessera has a front door. The public marketing site ships on
www.staging.tessera.zone (the www.staging.tessera.zone apex follows
the gated production flip):
- Homepage — what Tessera is (predictive ML on your event stream), a code snippet for the planned Python SDK (ingest an event → read a score), how it works in three steps, real console screenshots, and the shipped-integrations strip.
- Transparent pricing at
/pricing— the Free/Pro/Enterprise grid and an interactive usage estimator, both rendered from the same published plan catalog and rate card the platform bills from (drift-tested; never hand-copied). - Solutions pages for churn, recommendations, fraud & abuse, lifetime value, and segmentation — each backed by a runnable quickstart.
- Contact us at
/contact— a guided pilot on your data; the full platform remains self-serve. - New legal pages on the docs site: terms of service and privacy policy — the first legal documents for the platform, linked from the site footer.
Contract history handles discarded draft gaps
Contract history now comes from a cursor-paginated API that lists only real, existing versions. Discarded draft numbers remain permanently consumed but no longer hide newer drafts or produce broken history links in the console.
Public changelog, status page & API policies
The docs site gains the trust surfaces of a production API vendor:
- Changelog at
/changelog/— generated from schema-validated release notes, newest first, grouped by month, filterable by component — plus an Atom feed at/changelog.xml(latest 50 entries). - Status page at
/status/— live states for the API, event ingestion, and the console with 90-day uptime history and public incident history, backed by machine-readable/status/status.jsonpublished every minute by an independent prober. - API deprecation & versioning policy at
/security/deprecation-policy/— rendered from the same policy file CI uses to validate every deprecation announcement, so the published notice window and the enforced one are always identical. - Security pages — data handling (encryption in transit and at rest, tenant isolation, retention, credential handling) and the schema-validated subprocessor list.
Behavioral segment discovery
Deployment slots gain label-free behavioral segments (serving-api +
scoring-service) — discovered from entity-state embeddings with zero
labeling effort:
- Rebuilds:
POST /slots/{slot}/segments:rebuild {min_cluster_size?, min_samples?, sample_size?, seed?, radius_quantile?, match_max_distance?, max_segments?}starts a build job on thecpu-batchpool: a deterministic stratified sample of the slot's usable entity-state embeddings is clustered with HDBSCAN in the full embedding dimension (cosine geometry over L2-normalized vectors), and the result is persisted as an immutable, fingerprinted SegmentRegistry (https://spec.tessera.dev/schemas/segment_registry.json). Segment ids andSegment #Nnames survive rebuilds and retrains via Hungarian centroid continuity matching; name numbers are strictly monotonic and never reused. Identical inputs rebuild to identical fingerprints, segments and member sets. Build status is polled viaGET /slots/{slot}/segment-builds(/{id})(https://spec.tessera.dev/schemas/segment_build.json); there is no build webhook. - Reads (
readscope):GET /slots/{slot}/segments— the current registry summary with astaleflag (registry bundle ≠ the slot's active bundle);GET /slots/{slot}/segments/{id}— centroid, KPIs and ≤ 10 contrast statistics ordered by |z|;GET /slots/{slot}/segments/{id}/members— the build-time membership snapshot (cursor-paginated, each member with its cosine distance);GET /slots/{slot}/segments/{id}/kpis— per-head KPI blocks (binary/regressionmean/p50/p90, multiclass argmax shares, plusbehavior_surpriseand per-event-type propensity means when label-free signals are present) with whole-population baselines. - Scoring-time assignment: state writers now set
entity_state.segment_idby nearest-centroid-within-radius against the slot's current registry (nulled when unsegmented, when no registry exists, or when the registry pins a different bundle) inside the same fenced write, and segment changes emit throttledsegment.migrationwebhooks (https://spec.tessera.dev/schemas/segment_migration_event.json; at most one per entity per hour plus a per-slot flood guard; receivers dedup on the envelopeid).
Rebuilds require the admin scope and honor Idempotency-Key; a slot with
no active bundle or a live build is rejected (failed_precondition), and a
slot with too few usable states fails typed (insufficient_states — run a
refresh first). Succeeded builds meter build_cpu_seconds
{dims: {slot_id, job: "segment_build"}}; segment reads meter
state_reads. Segments are name-only (Segment #N) — audited AI
explanations, lookalikes and audience activation are Phase 3.
Embedding projection exports (projection_2d)
You can now ask Tessera for a server-side 2-D map of a deployment slot's entity embeddings.
POST /v1/slots/{slot}/exports accepts a new request variant discriminated
by a top-level kind: "projection_2d" (bodies without a kind remain
state-snapshot exports, unchanged): a deterministic stratified sample of up
to 200,000 entities (sample_size, default 50,000), an optional seed
(default 42), and up to 8 color_columns — a label head's score
(binary/regression), a contracted role=feature attribute
(event_type.attr_key, or the bare key when unambiguous), or segment_id
once behavioral segments exist for the slot. Attribute colors are read
as-of each entity's state cursor, so a value that changed after the entity
was last scored never leaks into the picture.
The job runs asynchronously and produces a single JSON artifact
(documented as ProjectionArtifact, schema
https://spec.tessera.dev/schemas/projection_artifact.json): columnar
entity_id/x/y arrays plus one array per color column, and a metadata
block carrying the bundle and model identity, exact sampling and UMAP
parameters, and counts. Poll GET /v1/slots/{slot}/exports/{export_id} —
once succeeded the record carries the artifact's metadata and an
artifact_url, a 15-minute signed URL minted freshly on every poll (fetch
it plainly; the object is gzip-encoded and decompresses transparently).
Job records now carry kind everywhere, export.completed /
export.failed webhooks fire as usual with data.kind: "projection_2d",
and each export meters export_rows at the sampled point count.
Same inputs, same seed, same platform version reproduce byte-identical point coordinates. One caveat ships inside every artifact, verbatim: points are a 2-D UMAP projection of high-dimensional embeddings — distances and cluster shapes are projection artifacts; treat clusters as hypotheses, not conclusions. The interactive embedding explorer building on these artifacts is coming to the console next.
Real-time scoring: events update entity state within seconds
Events flowing through the streaming path now update entity state in near
real time. Create a deployment slot with scoring: "realtime" (or flip an
existing one) and every ingested event for its entity type triggers a
rebuild of that entity's recent window and a fresh embedding + scores write
— target p95 event→state latency is 2 seconds at 1,000 events/s per
project.
- Freshness is visible:
GET /v1/slots/{slot}/entities/{type}/{id}/statereturns the advancingevent_cursor,bundle_id,model_fingerprintandcomputed_atafter each incorporated event — no more waiting for a batch refresh. - Shadow slots score on the identical path into their own state rows, so a candidate bundle can be compared against the active one on live traffic before activation. Shadow scoring is never billed.
- Billing: each realtime state write for a live slot meters one
scores_servedunit (dims: {slot_id, mode: "realtime"}), exactly like batch scoring metersmode: "batch". - Degradation is graceful: bursts queue (Pub/Sub redelivery is the buffer) and per-slot autoscaling absorbs them; a poison event is retried 5 times, then parked and counted — the entity's state self-heals on its next event or a batch refresh.
Console: entity explorer
The per-entity inspection surface ships — the primary debugging tool for "why does this entity have this score":
- Entity search (
/entities): raw-id lookup by entity type + exact id or id prefix (never full-text search), with first/last seen, lifetime event counts, cursor pagination, and a direct "open" action whenever the query matches an id exactly. - Entity detail (
/entities/{type}/{id}— a stable, shareable deep-link route): a newest-first, virtualized event timeline with full attribute inspection (model-input attributes and non-modelcontextrendered as separate labeled sections),event_typemulti-select +since/untilfilters that live in the URL so support teams can share exact views, and infinite scroll over cursor pages. - State per slot: one card per deployment slot scoring the entity's type — live/shadow mode, score per label, bundle id and model fingerprint as copyable badges, and honest embedding freshness: "up to date" when the state row's event cursor matches the newest event, "stale by N events (last scored at T)" when scoring lags, "never scored" before the first activation-driven refresh.
- Dead-letter awareness: when any of the entity's events failed contract
validation, a warning banner deep-links into the dead-letter browser
pre-filtered to that entity (link appears once the
deadletter_uiflag is enabled; the dead-letter page now seeds its filters from URL parameters).
Console: training runs explain themselves \u2014 queued reasons, progress, human errors
The training-run screens adopt the guided-experience foundation — the console's most opaque surfaces now say why, how far along, and what next:
- Never a bare "queued": every queued run explains itself. When the
requested compute class is scarce or plan-gated, the run says so by name
("Waiting for compute capacity … requested
gpu-a100… falls back togpu-l4"); otherwise it explains that runs start when a node frees up — in the runs list and on the run page. - Scarcity fallbacks surfaced: a run moved to a cheaper class shows a callout naming both the requested and effective class, with inline glossary help on what a compute class is.
- Live, truthful progress: the run page shows a stage tracker (queued → running → done) and a bounded percent-complete bar — no more spinner-only waits. The API does not return the run's overridable global token batch, so the console does not turn the trainer default into a false time-remaining estimate; throughput remains visible in its chart.
- Human error explanations: failed runs lead with the plain-language cause and the next step (out-of-memory, preemption, diverged loss, data unavailable, pre-flight refusal); the raw code and message sit one click away under "Show details", with a request id only when the payload actually carries one. Unknown codes still get support guidance.
- Real links + what next: the run's model id links to the model page, and a finished run offers "Review the model & evaluation". The launcher gains glossary help on tiers and compute classes, and classes you can't pick explain their reason, cost, and fallback.
- Charts on the shared kit: loss/learning-rate/throughput/GPU charts get a legend, hover tooltips, and screen-reader table twins, in both themes.
No API changes: all reads and mutations keep their existing shapes and polling behavior.
Console: grouped navigation, pipeline rail & breadcrumbs
The console shell now shows the pipeline's shape instead of a flat nine-item list — no routes changed, every deep link resolves exactly as before:
- Grouped navigation: Overview, then Data (Sources, Contracts, Labels), Modeling (Datasets, Training, Models), Serving (Slots) and Settings, with non-interactive group headers and a brand-primary accent on the active entry (its group is highlighted too).
- Pipeline stage rail: a compact "you are here" rail under the nav links all seven stages (Sources → Slots); once onboarding derivation lands it marks each stage done / current / to-do (accessible names announce "stage 4 of 7"), and it degrades to plain links if the derivation is unavailable.
- Breadcrumbs with stage context: nested pages render a route-derived trail (e.g. Modeling / Training / run_01H… with the full id on hover, or Settings / API keys) plus a "Stage N of 7" marker for pipeline screens; top-level list pages stay breadcrumb-free.
- Animated mobile drawer: the navigation drawer now slides/fades with the
motion tokens, keeps focus trapped while open, returns focus to the menu
button on close, and mounts instantly under
prefers-reduced-motion. - Warm shell restyle: top bar and side nav adopt the MANTIKA elevation, radius and type tokens in both themes (WCAG-AA enforced).
No API changes.
Console: guided first-run \u2014 onboarding checklist, warm auth screens & SSO sign-in
The console home is now a guided first-run experience, with warmer and more helpful first-touch screens:
- Getting-started checklist on the Overview home: ten steps from create a project to activate a slot, with each step's state derived live from the API — done, current (with a deep-link CTA), or blocked with a plain-language reason and a retry. The optional labels step never blocks dataset builds, and a failed read degrades only its own step. When every stage is live the checklist collapses into a compact "Pipeline complete" card with per-stage links.
- Role-aware onboarding: organization owners/admins get a create-API-key shortcut; members see who can issue keys instead of a dead-end link into an admin-only page. The ingestion curl example is one click away behind "Show ingestion example".
- No more "No project selected" dead ends: every screen that needs a project now explains the cause, points at the project switcher, and offers owners/admins a Create-project action.
- First-visit coach-marks introduce the tenancy switcher, the navigation, and the checklist (or its pipeline-complete summary) — dismissible (button or Esc), never shown again once dismissed (including across sign-out), and instant under reduced motion.
- Sign in with SSO: the login screen discovers your organization by email
domain (
POST /auth/sso:discover) and runs the Identity Platform tenant sign-in against your IdP (Okta, Azure AD, any SAML 2.0/OIDC provider). Domains without SSO keep password and social sign-in, with a plain explanation instead of an error. - The login, signup, and invite screens got the warm redesign treatment with guided error copy (e.g. expired invites now say what happened and what to do), accessibility-checked in light and dark themes. Existing password and social auth fields, requests, providers, and redirects are unchanged; SSO is additive.
Console: MANTIKA design system & guided-experience foundation
The console gets its redesign foundation — new visual identity plus the shared primitives that make every screen explain why and what next (screens adopt them in follow-up releases):
- MANTIKA brand token layer: electric-blue primary (
#4a6cfftonal scale) on warm navy-ink neutrals, Safiro display + Manrope UI typefaces (self-hosted), accent + gradient tokens, theme-aware elevation, rounder radii, display type scale, tokenized spacing and motion — light and dark, every text/chart pair WCAG-AA-enforced. - Guided-experience patterns: status-with-reason (a queued run now says
why and what to expect), human error explanations (server codes like
missing_timestampbecome guidance naming the offending column, raw details one click away), live job progress (stages + percent + ETA), guided empty states (a 0-row dataset explains its likely cause with fix links), an inline jargon glossary (tiers, gates G1–G6,n/a— which never blocks promotion), "what next" affordances, onboarding checklist + coach-marks. - Data-viz layer: dependency-free SVG charts (multi-series with hover, CI interval marks, threshold margin bars, KPI tiles, sparklines, donut, lifecycle timeline) — theme-aware, screen-reader-accessible, CSP-safe.
- Motion: CSS-only entrance/exit transitions on dialogs, drawers,
dropdowns and toasts, fully disabled under
prefers-reduced-motion.
No API changes. No screen layouts change in this release; the /styleguide
gallery shows every primitive against real pain-case fixtures.
SaaS webhook adapters: connect Stripe and Shopify with zero code
Point your SaaS tools' webhooks at Tessera and their events become canonical, model-ready facts about your entities — no code, no ETL. This release ships the adapter framework plus the first two providers: Stripe and Shopify.
- New receiver on the ingestion host:
POST https://ingest.staging.tessera.zone/v1/adapters/{provider}/{src_id}. Every delivery is authenticated with the provider's own signature scheme (StripeStripe-Signature, ShopifyX-Shopify-Hmac-Sha256) against a KMS-encrypted signing secret — verified on the raw bytes, in constant time, before anything is parsed. - Create an adapter source with
kind=stream,config.subtype=adapter:{provider, webhook_secret}. The secret is write-only and encrypted at rest; rotation takes effect within a minute. - Versioned mapping catalogs translate provider payloads into canonical
events (Stripe charges, invoices, subscriptions and checkout sessions;
Shopify orders, checkouts and refunds — see the per-app guides for the
generated coverage tables). Catalogs are immutable once released; your
sources pin a
catalog_versionand new sources default to the latest. - Per-source overrides on the standard mapping routes
(
GET/PUT /v1/sources/{id}/mapping): rename canonical event types, re-point attributes, add skip rules, or disable a type — validated field by field.POST /v1/sources/{id}/mapping:previewwith asample_payloadreturns the exact events the live path would publish, with zero side effects. - Provider retries are safe by construction: the provider's delivery id
becomes the canonical
event_id, so redeliveries dedup downstream. Unmapped event types are acknowledged and dropped (never guessed); mapping failures park in the replayable dead-letter view withreason=mapping_failed. - New docs: "Connect Stripe" and "Connect Shopify" guides with catalog-generated event coverage tables.
New JSON Schema: adapter_catalog.json
(https://spec.tessera.dev/schemas/adapter-catalog.json). Amplitude and
Mixpanel adapters follow in a later release on the same framework.
TypeScript SDK + safe browser ingestion
Correction (2026-10-21): the TypeScript SDK is planned for Phase 2, and
the integration catalog lists it as Planned; this release was written up as
shipped and published to npm. The planned package name is
@mantika-ai/tessera. Do not install a package called @tessera/sdk or
tessera from npm: the bare name tessera belongs to an unrelated project.
Send events over the HTTPS ingestion API. The server-side browser
ingestion described below (origin-locked CORS, publishable keys, the beacon
path) is part of that API. The SDK builds and the CI publish step described
below belong to the planned package and are kept as originally written.
@tessera/sdk now ships the full ergonomic layer (003 §6) with first-class
Node and browser builds, and browser ingestion is origin-locked
server-side.
- Node build (
@tessera/sdk, ESM + CJS): bufferedtessera.events.track(entity, eventType, attributes?, options?)— flush at 100 events or 2 s, bounded buffer, gzip batches, per-batchIdempotency-Key, exponential-backoff retries honoringretry_after_seconds, 207 per-item rejects viaonReject(never retried) — plus full typed resource clients (tessera.sources,tessera.projects, …) and the raw generated core attessera.api. - Browser build (
@tessera/sdk/browser, also thebrowserbundler condition): event tracking only,fetch-based, no Node built-ins, ≤ 12 KiB min+gzip. Pending events are delivered onpagehide/visibilitychange: hiddenviafetch(…, { keepalive: true })with anavigator.sendBeaconfallback (payloads split under 60 KiB). - Key safety: with a publishable (
tpk_) key, any non-ingest method throwsTesseraScopeErrorlocally, before any network call; the constructor refuses secret (tsk_) keys wherewindowexists unlessallowSecretKeyInBrowser: true. - Server-side CORS on
ingest.staging.tessera.zone: preflightOPTIONS /v1/eventsanswers 204 withAccess-Control-Allow-Origin: <origin>+Vary: Originonly when the presented publishable key'sallowed_originsexactly allowlists the request origin (scheme+host+port, no wildcard subdomains); actualtpk_POSTs from missing or non-allowlisted origins are rejected withpermission_denied. An empty allowlist rejects all browser traffic (deny by default); allowlist edits take effect within 60 s. Secret keys are unaffected by origin checks. - Beacon-path auth:
POST /v1/events?key=tpk_…authenticates publishable keys via query parameter (sendBeaconcannot set headers); secret keys in URLs are refused withinvalid_argument.
Published to npm with provenance from CI on sdk-ts/vX.Y.Z tags.
Bus bridges: Kafka + native Google Pub/Sub intake
Correction (2026-10-21): the customer-side bridge container
(tessera/bus-bridge) for Kafka is planned, and the integration catalog lists
the Kafka bridge, the Google Pub/Sub bridge and the Kinesis bridge as Planned;
this release was written up as if both paths were available. The native Google
Pub/Sub intake is available today: create the source through the API, as the
Connect Google Pub/Sub guide describes. The Kafka details
below are kept as originally written.
Message buses now have a no-code path into Tessera.
Kafka — tessera/bus-bridge is a lightweight container you run next to
your cluster: it consumes a consumer group, maps each JSON message to a
canonical event with a declarative mapping config, and delivers batches to
POST /v1/events. At-least-once from Kafka becomes exactly-once in Tessera
via event ids derived from (bridge_id, topic, partition, offset); offsets
commit only after Tessera accepted the events; rate limits and ingest
outages pause consumption (bounded memory, no rebalance storms, no loss).
Unmappable messages republish to a rejects topic you configure — they never
block the stream. Prometheus metrics, health endpoints and a
--check-config validation mode are built in.
Google Pub/Sub — Tessera pulls a subscription you own: create a
kind: stream source (subtype: pubsub_subscription) carrying the
subscription path and a mapping config, and grant Tessera's intake service
account roles/pubsub.subscriber on that one subscription. No container,
no credentials. Mapping failures park in the replayable dead-letter view
(reason: mapping_failed); replay re-runs the mapping with your current
config. A subscription that becomes unreadable marks only that source
errored — new Source.status value — and never affects other sources.
Both paths share one mapping-config schema (bus_mapping.json, documented
as BusMappingConfig in the API reference) and one mapping engine, so a
config behaves identically wherever it runs — including the Kinesis bridge
planned for Phase 3.
New guides: Bridge Kafka to Tessera and Connect Google Pub/Sub.
CDP destinations: Segment + RudderStack
Correction (2026-10-21): the Segment and RudderStack destinations are planned, and the integration catalog lists both as Planned; this release was written up as if they could be turned on today. The destination implementations exist in the repository, and their public catalog listings are pending. Send events over the HTTPS ingestion API. The details below are kept as originally written.
Turn Tessera on without writing code if you already route product data through a CDP:
- Segment destination (Actions framework, cloud mode) — actions
trackEvent,identifyEntity,pageViewwith default subscriptions; 429/5xx retry signaling honoringretry_after_seconds; per-event failures visible in Segment's delivery UI without failing whole batches. - RudderStack destination (cloud transformer + config definition) —
identical translation, batched to
POST /v1/eventswithin the 500-event / 5 MiB limits; ignored calls surface as filtered-not-failed with counted reasons. - One shared mapping template (
api/jsonschema/cdp-mapping.json, versioned): entity type + id precedence, event-name rules (renames, lowercase, space replacement), attribute renames/drops and flattening — translated by a single reference implementation (integrations/cdp/mapping), so both destinations map identical inputs identically and the console can later edit the same document. - Guarantees:
event_id= CDPmessageId(retries dedup downstream); the CDP's event timestamp is used verbatim (never delivery time, never fabricated); CDP context never becomes attributes; calls with no usable entity id are dropped with counted reasons;alias/groupare acknowledged and ignored (identity resolution is Phase 3).
Setup guides: docs Connect → Segment and Connect → RudderStack.
Vendor catalog listings are pending partner review; the packages, contract
tests and submission checklists ship in integrations/cdp/.
Data deletion: project purge & entity-level deletion
Deletion is now a real, verifiable capability instead of a status flag:
- Project purge:
DELETE /projects/{project_id}now schedules an asynchronous purge with a cancelable 7-day grace window. After the window, a purge worker physically deletes every project-prefixed record across object storage (every generation, including noncurrent and soft-deleted), the entity ledger and state stores, the control database (child-first along the live foreign-key graph), and usage metering — then freshly re-enumerates every store and completes only on a zero-residue pass. The project lands in the new terminalstatus="deleted"as a scrubbed tombstone row;project.purge_started/project.purge_completedaudit events carry per-store deletion counts and the verification result. - Restore:
POST /projects/{project_id}:restore(orgowner) cancels a pending purge during the grace window — the project returns toactive(revoked API keys stay revoked). Once purging starts, restore fails withfailed_precondition.GET /projects/{project_id}/purge(orgadmin+) returns the purge record and completion report. - Entity-level deletion (data-subject requests):
POST /entities/{entity_type}/{entity_id}:delete(orgadmin+ session oradmin-scoped key) erases one entity across both environments: ledger + per-slot state rows are deleted, a tombstone prevents any resurrection (later events for the entity are dropped at ingest and it is excluded from future dataset builds), and the canonical event store is rewritten by an atomicdeletionmanifest supersession verified by row accounting. Physical file deletion respects pinned snapshots (awaiting_snapshot_release). On completion the rawentity_idis scrubbed from the record; the pseudonymousentity_hashremains the stable reference, and the audit trail (entity.delete_requested,entity.delete_completed) references the entity only by hash.GET /entity-deletions(?status=)andGET /entity-deletions/{deletion_id}page the records. - Policy: the normative data deletion policy documents scope, the grace window, what is retained and why, and how aggregate artifacts, dataset shards, and secondary-entity references are treated.
New JSON Schemas: project_purge.json, entity_deletion.json under
https://spec.tessera.dev/schemas/. New id prefixes: prg_ (purge),
edel_ (entity deletion).
Security hardening & disaster recovery
Supply-chain and DR posture is now enforced by CI and proven by drills:
- Security checks (
.github/workflows/security.yml): required contextssecurity/dependency-audit(pip-audit over every uv lockfile +pnpm audit --prod; HIGH/CRITICAL or unknown severity fails closed),security/image-scan(Trivy),security/sbom(syft CycloneDX 1.5, schema-validated, published toplatform/sbom/on main) andsecurity/policy(conftest: digest-pinned base image, base-image age, security-doc lint). Waivers live in ONE expiring exceptions file,infra/security/audit-exceptions.yaml— expired entries fail the build. - Pinned base image (
infra/docker/base/): every service image builds FROM one digest-pinned platform base; a weekly workflow (base-image.yml) rebuilds it from the upstream digest channel, scans it and opens the bump PR; a base older than 30 days turnssecurity/policyred platform-wide. - Backups (
infra/terraform/modules/backup+ WP-F1 module wiring): Cloud SQL PITR (7 d WAL) + daily backups retained 30 d, Bigtable daily backups ofledger_events/entity_stateretained 30 d, GCS versioning + 7-day soft delete on the data/artifacts/exports buckets — all policy-enforced against every terraform plan (infra/policy/terraform). - Restore drill (
tools/restore_drill, quarterlyrestore-drill.yml+ dispatch): unattended Cloud SQL/Bigtable/GCS restore verification in isolated throwaway resources, signed report perapi/jsonschema/drill_report.jsontoplatform/drills/; failures alert viaRestoreDrillFailed(ticket). The identical orchestration runs against the tessera-testing fakes in PR CI. - Key rotation (
tools/rewrap_credentials): KMS keys rotate ≤ 90 d (policy-enforced); the rewrap tool re-encrypts every connector-credential envelope to the newest key version — idempotent, resumable, decrypt-verified — withKeyRotationOverdue(ticket) on stale rows. - Security documents (
infra/security/): threat model (STRIDE + tenant-isolation attack paths), incident-response runbook and pen-test readiness checklist, all section-linted in CI. - Defensive deny (tessera-core): a mounted route with no OpenAPI
operationId now fails closed with
permission_deniedbefore any handler runs (004 §2 deny-by-default, WP-A2 review follow-up).
No public API surface change.
Behavioral signals: sequence surprise & next-event propensities
Every entity can now carry two label-free signals, computed from the same forward pass that produces its embedding — zero labels, zero extra training, zero extra scoring passes.
behavior_surprise — a population-calibrated [0, 1] measure of how
atypical the entity's recent event sequence is under the model's own
next-event objective. Calibration is count-stratified: 0.91 means "more
surprising than 91% of entities with similar history length", never a proxy
for event count. The key is omitted while the entity has fewer than 2
events in its scored window.
propensities — calibrated probabilities that the entity's next event
is each of a configurable shortlist of your event types (up to 32), keyed
by your event-type names verbatim. Scores are calibrated independently
against held-out empirical rates and read as unconditional
next-event probabilities; they need not sum to 1.
How it rolls out through the existing lifecycle:
- Evaluation now (by default) calibrates signals after the promotion
gates: the eval report gains a
signalsblock — the resolved config, a fingerprintedsignals_calibration.jsonartifact, and two non-mandatory quality checks (S1 surprise/count invariance, S2 propensity calibration) that never affectgates_passed. The evaluation request accepts an explicit shortlist and surprise window; defaults are the top-16 next-event types andK = min(64, W). - Bundles:
POST /models/{model_id}/bundlesaccepts an optional body{"signals": {"enabled": true}}. It copies the report's signals config verbatim intoruntime.signalsand pins the calibration artifact, which slot activation re-verifies byte-for-byte like every other artifact. A model whose report has no signals block is refused with typed detailsignals_not_evaluated. No body — no signals: existing clients are unaffected. - State reads will surface the
signalsobject on scored entities once a signals-enabled bundle is active on the slot (written in the same fenced state update as the embedding).
See the new Behavioral signals guide for how to read the numbers.
Derived outcome labels (label-free supervision)
Labels can now be synthesized from the event stream itself — zero labeling effort, full auditability:
source.kind: "derived"onPOST /labels: declare aDerivedOutcomeRule(churn|conversion|recurrence|custom) with an event-type pattern, optional attribute filters, an outcome (presence/absence) and a horizon. Template presets fill sensible defaults (churn→absence;recurrence→min_count: 2); contradictions are rejected, never silently corrected.maturation_daysdefaults to the rule'shorizon_days.- Deterministic materialization: dataset builds evaluate each anchor
event's predicate over the entity's future events in
(t, t + horizon_days]from the same pinned snapshot — anchors whose horizon has not elapsed are censored (horizon_open), never negative — and rebuilds are byte-identical. A rule matching zero events fails the build withrule_matches_nothinginstead of producing degenerate labels. POST /labels:preview: dry-run an unsaved rule against a sample of your latest events before creating the label (anchor/observation counts, positive rate, examples; entity ids appear only asentity_hash).GET /labels/{id}/statsfor derived labels serves the latest materialization's support stats plus an additivederivedblock (anchors, censoring, predicate matches, source dataset).- After a label's first materialization its rule is frozen
(
derived_rule_frozen) — a changed rule is a different outcome; create a new label.
Connectors: Snowflake, Databricks, Redshift and MySQL
The four Phase-2 warehouse/database connector kinds land on the WP-C1
framework — every /sources, /test-connection, /mapping, /syncs
endpoint now works with kind: snowflake, databricks, redshift and
mysql, each behind a connectors.{kind} feature flag (default on):
- Snowflake (
SnowflakeSourceConfig/SnowflakeCredentials): key-pair auth only — the platform generates the RSA-2048 pair at source creation, vaults the private key, and returns the public key exactly once in the new reveal-onceSource.setupobject together with the exactALTER USER … SET RSA_PUBLIC_KEYstatement; replacing credentials mints a fresh pair (rotation = re-registration). Extraction unloads through a per-sync temporary internal stage (COPY INTO @tessera_sync_{sync_id} … TYPE=PARQUET, driver GET,DROP STAGEeven on failure).test-connectionrunsauthenticate(JWT),warehouse_usable,table_readable; an auth failure surfaces theALTER USERstatement. - Databricks (
DatabricksSourceConfig/DatabricksCredentials): SQL warehouse via the Statement Execution API (disposition=EXTERNAL_LINKS, format=ARROW_STREAM), mapped columns only, cursor predicate pushed down. Results estimated over 4 GiB (or reported truncated) unload viaCOPY INTO '/Volumes/{unload_volume}/tessera/ {sync_id}/' … FILEFORMAT = PARQUET+ Files-API downloads, with the directory deleted even on failure; nounload_volume⇒ the sync failsunload_volume_required. - Redshift (
RedshiftSourceConfig/RedshiftCredentials):UNLOADto the customer's S3 (IAM_ROLE '…' FORMAT PARQUET PARALLEL ON MAXFILESIZE 256 MB), ingested back through the S3 Parquet reader and deleted; a deletion failure never fails the sync — it lands asredshift_unload_cleanup_failedin the new non-fatalSyncJob.detailsarray.test-connectionrunsauthenticate,table_readableand anunload_probefull round-trip (UNLOAD ('SELECT 1')→ read back → delete). - MySQL (
MysqlSourceConfig/MysqlCredentials): read-only keyset extraction over TLS (ssl_mode: required|verify_ca), optional SSH tunnel (config.tunnel+ vaultedtunnel_private_key), pages of 50,000 ordered by(cursor, primary key)— a PK-less table fails mapping withkeyset_tiebreak_unavailable(MySQL has noctidfallback). Sessions forceTRANSACTION READ ONLYandtime_zone = '+00:00'; MySQL < 8.0.16 automatically gets the expanded OR-form keyset predicate. - Normative type maps per kind (documented on the docs site, generated
from code):
TINYINT(1)→boolean,SET→"a,b", SnowflakeNUMBER(p≤18,0)→integer,ARRAYlimited to homogeneous string/number elements (mixed rows are rejected), naiveTIMESTAMP_NTZ/DATETIME/timestampcolumns require the mappingtimezone, andVARIANT/OBJECT/SUPER/MAP/STRUCT/JSON/BLOB/BIT/spatial types failPUT /mappingnaming the column. - API shape additions:
Source.setup(reveal-once platform-minted credential material) andSyncJob.details(non-fatal typed warnings on any terminal status).
Stripe billing: self-serve Pro, transparent metered invoices, dunning
Stripe billing: self-serve Pro, transparent metered invoices, dunning
Organizations can now put a card on file and pay for exactly what they use:
- Self-serve checkout —
POST /v1/billing/checkout-sessionopens a hosted Stripe Checkout onto the Pro plan; the subscription's metered items (writes, reads, export rows, and per-second compute pass-through by class) are created automatically, with tax handled by Stripe Tax.POST /v1/billing/portal-sessionopens the Stripe customer portal for payment methods, invoices and billing details. Free requires no payment method, ever. - New pricing plans — the v2 plan catalog ships the published pricing: Free (100k writes / 50k reads / 100k export rows included, hard caps), Pro ($0 base + usage with 1M writes / 500k reads / 1M export rows included monthly), Enterprise (custom). Existing organizations keep their current plan (grandfathered); new organizations start on the new Free.
- Transparent invoices —
GET /v1/billing/subscriptionreturns the org's plan, billing state, current period and an upcoming-invoice preview whose line quantities are computed from the same usage rollupsGET /v1/usageserves — every line is independently verifiable (billed_quantity = round_half_up(usage / divisor),amount = billed_quantity × unit price). Usage reaches Stripe hourly withsetsemantics, so re-syncs never double-count. - Payment-failure handling — a failed payment starts a 7-day grace
window (
past_due), then restricts new usage to Free-plan limits (restricted— data keeps flowing), and downgrades to Free only after 30 more unpaid days or subscription cancellation; any successful payment restoresactiveimmediately. Transitions emit the newbilling.payment_failed/billing.state_changedwebhooks.
Plans, quotas & plan-driven rate limits
Plans, quotas & plan-driven rate limits
Every organization is now pinned to an immutable version of a published plan (Free / Pro / Enterprise), and usage is continuously evaluated against it:
- Plan catalog — orgs are assigned
free@latestat creation; published plan versions are immutable and fingerprint-verified, and publishing a new version never changes existing assignments (migrations are explicit and audited; a migration that lowers a limit warns immediately but hard-enforces only from the next monthly period). - Quotas —
events_per_month,max_projectsandconcurrent_training_runsare evaluated at least every 60 s over the usage rollups with a soft-warn → hard-enforce ladder (warnings at 80% and 95%); crossing a threshold publishesquota.warning/quota.exceededwebhooks exactly once per period. Over the events cap,POST /v1/eventsreturns 429resource_exhaustedwithretry_after_seconds; training runs above the plan tier or concurrency and projects beyond the cap get typedplan_limitdenials. GET /usage/quotas— a new read-scoped endpoint returning each evaluated limit's{metric, limit, used, period_end, enforcement};GET /usagenow embeds the same evaluatedquotastates.- Rate limits — per-API-key token buckets per endpoint class
(
ingest/read/admin) driven by the org's plan (ingest_rps,read_rps,admin_rps, burst ×burst_multiplier);X-RateLimit-Limit/Remaining/Resetheaders now ride on every response, and 429s carryretry_after_seconds. - Retention — data retention now follows the plan (Free 90 d, Pro 400 d, Enterprise custom), surfaced to the platform's retention sweeps as a single per-project cutoff.
Quota checks fail open: if quota state is ever unavailable, customer data is accepted and the platform alerts internally — billing protection never drops events.
Enterprise SSO: SAML/OIDC via Identity Platform tenants
Organizations can now connect their own identity provider (gateway-api):
- Provider config:
GET/PUT /orgs/{org_id}/sso— one GCP Identity Platform tenant per org with exactly one SAML 2.0 or OIDC inbound provider. Invalid material (unparsable certificate, non-HTTPSsso_url/issuer) is rejected before any Identity Platform call. OIDC client secrets are write-only: envelope-encrypted at rest and never returned by any read. - Domain capture:
POST/GET /orgs/{org_id}/sso/domains,DELETE …/domains/{domain},POST …/domains/{domain}:verify— DNS TXT verification (tessera-verification=<token>on_tessera.<domain>). One org owns a domain; public suffixes and shared consumer email domains (e.g.gmail.com) are rejected. - JIT provisioning: first sign-in through an org's tenant creates the
user (
provisioned_via="sso"; existing accounts map by email) and an org membership with the config'sdefault_role. SSO sessions count as email-verified — the IdP asserted the identity. - Enforced SSO:
status="enforced"(requires ≥ 1 verified domain and a completed test sign-in) rejects password/social sessions for captured domains with 403permission_denied(reason="sso_required"); orgowners stay exempt (break-glass) and API keys are unaffected.status="disabled"rolls back without deleting users or memberships. - Discovery:
POST /auth/sso:discover {email}(unauthenticated, per-IP rate-limited) returns{sso: true, tenant_id, provider_kind}for verified + SSO-enabled domains and a constant{sso: false}otherwise.
New JSON Schema: sso_config.json under https://spec.tessera.dev/schemas/.
Audit log: every security-relevant action, recorded and queryable
Every security- and configuration-relevant action on your organization is now recorded as an immutable audit event: logins and rejected credentials, org/member/invite changes, project and API-key lifecycle, schema-contract approvals, leakage-policy and label changes, deployment-slot activations and rollbacks, artifact fingerprint-mismatch overrides, and export creation.
New endpoints (cursor-paginated, newest first, all filters combinable):
GET /v1/orgs/{org_id}/audit-events— the organization's full feed (org roleadmin+, console sessions only).GET /v1/projects/{project_id}/audit-events— one project's feed (org rolemember+, or anadmin-scoped API key).
Filters: action (prefix match — api_key matches api_key.create and
api_key.revoke), actor_id, resource_type, resource_id,
occurred_after/occurred_before, outcome (success|denied|failure).
Denied mutating calls (403) are recorded with outcome=denied, so
permission probes are visible. Every event carries the acting principal,
the affected resource, the request id, IP and user agent, and validates
against the published schema
https://spec.tessera.dev/schemas/audit_event.json.
Audit events are append-only at the database level, retained for at least
one year, and exported daily as zstd-compressed JSONL to your project's
export prefix (projects/{project_id}/audit/dt=YYYY-MM-DD/; org-scoped
events under orgs/{org_id}/audit/…). Secrets never appear in audit
details — credential-shaped values are redacted before storage.
Phase 1 integration milestone harness
New tools/milestones (tessera-milestones): ONE scripted, unattended
end-to-end scenario proving the whole Phase 1 lifecycle over the public
surface — tenant bootstrap → CSV upload + HTTPS event ingestion → Postgres connector
→ contract approval (with a real rejected leaky-approval probe) → labels
(label_events + label_table) → leakage-gated dataset → tier-S training →
all six promotion gates → bundle/slot activation → batch scoring → BigQuery
export with signed export.completed webhook verification → docs
quickstarts. Runs on every PR against the fake stack
(--target local-fakes, .github/workflows/milestone-phase1.yml) and
nightly against our hosted deployment once it exists (spend-gated on a
repository variable; bring-up checklist in the infrastructure runbooks). Reports follow the new
api/jsonschema/milestone_report.json (reused by WP-I2/I3); failures alert
via MilestoneP1Failed (ticket). Phase 1 seams surfaced by this work — the
WP-M4 EvalDeps.previous_report G1 baseline wiring, the CUDA trainer image
and the eval-service entity-hash-master-key accessor — are proposed in the
owner-tagged follow-up PRs #37, #38 and #39. No public API surface change.
Docs: quickstart, concepts, connector guides & runnable examples
Correction (2026-10-21): the Python and TypeScript SDKs are planned, and the integration catalog lists both as Planned; this note described Python SDK forms of the quickstart and SDK guides as available. The SDK guides carry a Planned banner, and the curl form of each quickstart step works today. Segment and Kafka are also Planned in the catalog, and their connector pages carry the same Planned banner.
The docs site gains the content customers onboard with — and every executable code sample is a real file executed in CI against a seeded test project, so the docs cannot silently rot.
- Quickstart (
/quickstart/): eight steps from an empty project to a served, auditable prediction — first events, CSV upload + column mapping, contract approval, a binary label, a leakage-gated dataset build, a real training run through the six promotion gates, slot activation + batch scoring, and the state read. Every step in bothcurland Python SDK form, both executed in CI. A Phase 2 label-free quickstart page ships availability-bannered with statically validated configs. - Sample dataset:
tessera-sample-v1.zip(stable URL/downloads/tessera-sample-v1.zip) — three deterministic generated domains (e-commerce orders, media streaming, logistics) plus a label file;manifest.jsoncarries per-file sha256 and the dataset fingerprint; two generations are byte-identical. - Concepts (
/concepts/): entities & events, schema contracts, presence, labels & masking, leakage gates, fingerprints & reproducibility — each with a worked example from a different industry, plus a glossary. - Connector guides (
/connect/): file upload, BigQuery, Postgres, GCS, S3 and HTTPS ingestion (live), plus availability-bannered Segment/Stripe/Shopify/Kafka/Pub-Sub pages whose copy-paste configs are schema-validated againstapi/openapi.yamlon every change. - Train & Serve guides: labels, datasets, training runs; slots & bundles, batch scoring, state reads, exports.
- SDK guides (
/sdks/): the full Python guide (buffering, idempotent ingestion, batch error handling, reads) with executable samples; a Phase 2 banner for TypeScript.
For contributors: make docs-test runs every example suite against a
fake-wired local stack (tools/doctest), and make docs-lint enforces the
no-inline-snippets rule, live-key hygiene, connector-guide coverage, and
config-block schema validation. New integrations drop a suite under
apps/docs/examples/<area>/<name>/ and register in
apps/docs/connect-coverage.yaml.
Serving read API
The public read surface for scored entities is live (serving-api,
003 §4) — four endpoints, all requiring the read scope and metered as
state_reads per API key:
GET /v1/slots/{slot}/entities/{type}/{id}/state— the entity's current embedding + scores under a deployment slot (addressed by the slot's stable name), with label-freesignals/segment_idwhen the state row carries them. Every scored response includes the mandatoryfreshnessobject —event_cursor,computed_at,bundle_id,model_fingerprint(005 §7) — so batch-refreshed staleness is always visible. Embeddings serialize as JSON float arrays, or little-endian float32 base64 with?embedding_encoding=base64. No state row yet → 404 withdetails[0].reason == "no_state".POST /v1/slots/{slot}/scores:batch— up to 1,000 entities in one synchronous call (one batched Bigtable read); per-item results in request order withstatus: found|not_found, HTTP 200 even when some items are missing;includeselectsembedding/scores/signals/segment_id.GET /v1/entities/{type}/{id}/events— the entity's canonical event timeline, newest first, cursor-paginated (limit≤ 200) withevent_type,since,untilfilters; each item carries the canonical event plusreceived_atandsource_idonly. Works for any registered entity type, with or without a model.GET /v1/entities?type=…&id=…|id_prefix=…— exact-id/prefix entity lookup returning{first_seen, last_seen, event_count}per entity, ordered by id (raw-id lookup only; no free-text search).
Slot names resolve within the credential's project only (a foreign slot name is an indistinguishable 404), and a bundle activation is visible in responses within 30 s. Latency SLO: p95 ≤ 50 ms for state reads at 1,000 rps (verified nightly against our hosted deployment). Python/TypeScript client cores regenerated with typed models for all four endpoints.
Batch scoring & exports
Deployment slots gain their Phase 1 scoring path (serving-api +
scoring-service):
-
Refreshes:
POST /slots/{slot}/refreshes {cutoff?}bulk-recomputes every entity's state (embedding + scores) for the slot as of an explicit cutoffT— no event withtimestamp > Tinfluences any written state. Jobs re-verify every fingerprint pinned in the slot's active bundle before scoring (fail closed on any mismatch), write fenced, cursor-monotonicentity_staterows (a newer stored row is never regressed — skipped and counted inentities_skipped_stale), and resume idempotently from per-partition checkpoints.GET /slots/{slot}/refreshes(/{id})reports status, counters and typed errors. -
Exports:
POST /slots/{slot}/exports {destination, cutoff?, refresh_first?, pseudonymize?, include?}snapshots the slot's stored states to:- GCS — Parquet (zstd, ≤256 MiB parts) under a platform-hosted prefix
with 7-day signed URLs, or verbatim into a customer-supplied
gs://prefix (requires a writer grant for the project's export service account); - BigQuery — Parquet staging + load with
WRITE_TRUNCATE; the grant is verified with a dry run before the job is accepted (403permission_deniedwith the service-account email otherwise); - S3 — via stored, envelope-encrypted credentials referenced by
credential_id(ans3source), never exposed anywhere.
Columns:
entity_type,entity_id(orentity_hashwithpseudonymize: true),embedding(list<float32>), onescore__{label_name}per binary/regression head andscore__{label_name}__{class}per multiclass class, plusmodel_fingerprint,bundle_id,event_cursor_timestamp,event_cursor_event_id,computed_at, and amanifest.json. - GCS — Parquet (zstd, ≤256 MiB parts) under a platform-hosted prefix
with 7-day signed URLs, or verbatim into a customer-supplied
Terminal job states publish refresh.completed/failed and
export.completed/failed webhooks (payload = the job record); refreshes
meter scores_served, exports meter export_rows. POSTs honor
Idempotency-Key (repeats return the same rfr_/exp_ id). Job creation
requires the admin scope; status reads require read. New JSON Schemas:
refresh_job.json, export_job.json, export_webhooks.json under
https://spec.tessera.dev/schemas/.
Console: datasets, training, models & deployment slots
The model-lifecycle surface of the console ships — from dataset build to activated bundle, every artifact stays content-addressed and inspectable:
- Datasets (
/datasets,/datasets/new,/datasets/{id}): the build launcher requires an approved schema contract (blocking otherwise), pins the contract + leakage-policy versions visibly before submission, and takes labels (multi-select with entity type + task), window (default 1024 events/entity), time range, and split fractions/seed (defaults 0.8/0.1/0.1). Build detail polls while in flight (≤ 5 s, stops on terminal states), renders the manifest — per-split row counts, per-split per-label support, every fingerprint as a copyable badge — and renders failed/aborted builds as a typed violation table with human explanations for known codes (insufficient_label_support, leakage-gate denials) linking to the fix surface. - Training (
/runs,/runs/new,/runs/{id}): the launcher offers succeeded datasets only, shows the 005 §4 tier parameter table (S/M; L is Phase 3 — "coming soon"), and validates advanced ModelConfig overrides client-side against the closed key set (unknown keys are errors, deny by default). The monitor renders live loss/LR/throughput charts from incremental?after_step=metric fetches (≤ 10 s, idempotent merges, terminal-state stop), the run config (dataset fingerprint, tier, seed), a status timeline, and a virtualized log viewer with level filtering and tail-follow. - Models (
/models,/models/{id}): list with tier, dataset fingerprint, 002 §9 status and eval summary (gating labels' AUC); detail renders the full eval report — one section per gate G1–G6 with metric value, threshold, pass/fail chip and the report's diagnostics verbatim (failing gates are prominent and always actionable). - Slots (
/slots,/slots/{id}): slot creation (name, entity type, mode; Phase-1 scoring isbatch_only— realtime arrives in Phase 2), bundle activation listing promotable bundles and displaying all six pinned fingerprints with an explicit typed confirmation, fingerprint-mismatch refusals naming the offending artifact, and rollback to the previous bundle with the same confirmation pattern.
New shared design-system components (@console/ui): TimeSeriesChart
(lightweight SVG, streaming appends, min/max-envelope downsampling),
FingerprintBadge and GateChip — reused by upcoming explorer surfaces.
Console: contracts, leakage policy & labels
The governance surface of the console ships — deny-by-default (002 §4) becomes a usable human workflow:
- Contract review (
/contracts,/contracts/{version}): one collapsible table per event type showing every attribute's declared type, role, encoding, missingness, PII class, profiling stats (null rate, cardinality, examples) and inferred-vs-edited state. Drafts are editable inline (closed 002 §4 sets;pii: directand identifier attributes can never become features — blocked client-side, enforced server-side), with optimistic 2-second-batched saves, bulk edit across event types in a single PATCH, and per-attribute failure surfacing. A client-side diff view compares any version against its predecessor with a counts summary. Approval shows role totals + the fingerprint affordance, requires typing the project name, and is gated to org owners/admins (003 §3). - Leakage-policy editor (
/leakage-policy): platform default deny patterns render locked (002 §6 — never removable); customer regex patterns and exact{event_type, attr_key}denies are editable with theidentifier_value_scan/label_temporal_gatetoggles explained. The pattern tester dry-runs the contract's attribute keys server-side (POST /leakage-policy:test— authoritative; Python regex semantics), and saving warns when approvedrole=featureattributes newly match. - Labels (
/labels,/labels/new,/labels/{id}): a wizard covering name/task, source binding (label_events,label_table, orderivedlabel-free rules with churn/conversion/recurrence/custom template presets and a live server-side rule preview), maturation with a plain-language explainer, and explicit-opt-in null semantics with the mandatoryguard_min_positive_rate. The label detail page renders the support dashboard: observations, distinct entities, the task-specific stat, maturation coverage, a weekly observations/positive-rate chart, and the derived materialization block.
API surface note: POST /labels:preview (WP-C6 R18) and the additive
LabelStats.derived block are now in api/openapi.yaml, plus
api/jsonschema/derived_outcome_rule.json — the WP-C6 interface vendored
ahead of its Phase 2 implementation (see specs/ERRATA.md); the gateway
route answers NotImplementedError until WP-C6 lands.
Console: sources & ingestion
Correction (2026-10-21): the Python SDK is planned, and the integration catalog lists it as Planned; this note listed Python SDK snippets in the streaming setup. The console streaming setup shows the curl snippet, and events are sent over the HTTPS ingestion API. The console gallery now groups the later-phase connector kinds by setup path: Azure Blob Storage is planned and listed under Planned; Snowflake, Databricks, Redshift and MySQL are listed under API setup and are set up through the API once your operator enables the connector.
Getting data into Tessera is now a console flow (apps/console → Sources):
- Connector gallery: every connector kind from the platform's closed set, with Phase-1 kinds (BigQuery, Postgres, GCS, S3, file upload, streaming) enabled and later phases visibly marked "coming soon".
- Credential forms + connection test: typed, validated forms per kind; secrets are write-only and never shown again. Creating a source immediately runs its connection test — each check renders with pass/fail and detail, and "Continue to mapping" unlocks only when the test passes.
- File upload: resumable direct-to-storage upload (progress, pause, cancel, failure-resume) with a schema preview — column names, inferred types, and sample rows — before mapping.
- Mapping wizard: entity → event type → timestamp (explicit timezone handling) → attributes (auto-slugified keys) → review, with a debounced live preview of the resulting canonical events sampled server-side, including per-column rejection reasons. Saving stores an immutable, fingerprinted mapping version.
- Sync monitoring: trigger syncs and watch them move queued → running → succeeded/failed live (polling stops on terminal states); failures show the sync error verbatim.
- Streaming setup: the ingestion endpoint, key-management deep links, and copy-ready curl / Python SDK snippets scoped to your environment's key prefix — never a real secret.
- Dead-letter browser (feature-flagged for Phase 2): browse parked events with reasons and filters, inspect the stored payload, and replay or discard selections — replay re-validates against the current contract.
Python SDK (planned): ergonomic event tracking
The planned official Python SDK (tessera 0.2.0, Python 3.10–3.13) has its
ergonomic layer built on top of the generated API core:
- Buffered event tracking —
Tessera(api_key).events.track(entity, event_type, attributes)validates locally (canonical-event shape, 002 §2) and never blocks on network I/O; a background flusher batches toPOST /v1/eventsat 100 events / 2 s (configurable), gzip-compressed, oneIdempotency-Keyper batch. - Reliability — retries honor
retry_after_secondson 429 and back off exponentially with jitter on 5xx/connection errors, reusing the batch'sIdempotency-Keyso retries are exactly-once server-side; per-item 207 rejections are surfaced viaon_reject(event, code, message)and counted inFlushResult(accepted, rejected, dropped). - Flush guarantees —
flush(),close(), context-manager support and anatexithook drain the buffer on clean exit (10 s bound); the flusher is a daemon thread;fork()is detected and children start with a fresh buffer. - Full typed surface — thin resource namespaces (
client.sources,client.projects,client.deadletter, …) over the OpenAPI-generated core;context.library = {"name": "tessera-python", "version": …}is stamped on every event;User-Agent: tessera-python/{version}.
Correction (2026-10-20): the Python SDK is planned, and the integration
catalog lists it as Planned; this release was written up as installable from
PyPI. The planned package name is mantika-tessera, reserved on PyPI by a
0.0.1 placeholder that is not the SDK. The bare PyPI name tessera belongs to
an unrelated project, so do not install it. Send events over the
HTTPS ingestion API. Quickstart:
sdks/python/docs/quickstart.md.
Stream router: the streaming path is live end to end
Events accepted by POST /v1/events now flow all the way to durable,
queryable, scoreable state. The new stream-router service consumes the
ingest stream and, for every event:
- appends it idempotently to your entity's ledger (retries and redeliveries can never duplicate an event),
- lands it in the canonical event store as Parquet with exactly-once visibility, within five minutes end to end,
- validates it against your approved schema contract — excluded event types,
unregistered entity types and malformed payloads park in the replayable
dead-letter view (
GET /v1/events/deadletter) instead of disappearing, - auto-registers first-seen entity types as drafts for your review,
- flags attributes your contract has not reviewed yet for re-inference (they are stored untouched; the model never sees them until you approve them),
- and publishes a scoring request for every entity type with a realtime deployment slot, both live and shadow — the hand-off the upcoming real-time scoring service consumes.
New internal event schema: scoring_request.json (Pub/Sub
scoring.requests) is now built and validated by a single shared
implementation. No public REST surface changed.
Model registry: promotion bundles & deployment slots
Evaluated models can now be promoted and served through deployment slots:
GET /models— list model versions with?status=(002 §9 lifecycle) and?gates=passed|failed(derived from the latest evaluation report), plusGET /models/{id}andPOST /models/{id}:archive.POST /models/{id}/bundles— create an immutable, fingerprinted promotion bundle. Creation is gated: every mandatory evaluation gate (G1–G6) must PASS. The bundle pins the six reproducibility fingerprints (contract, leakage policy, encoders, tokenizer, dataset, eval report), copies the gate results verbatim, and carries the completeruntimeblock scorers read — model config, event window and label heads.GET /models/{id}/bundleslists a model's bundles newest-first.GET/POST /slots— deployment slots: a stable, per-project-unique name (prod,shadow-1, …) withenvironment: live|test,mode: live|shadowandscoring: realtime|batch_only; bundles rotate under the slot.POST /slots/{id}:activate— activation re-downloads and re-hashes every pinned artifact before flipping anything; any fingerprint mismatch aborts with the expected and observed hashes. The escape hatch (allow_mismatch: true+ a mandatoryreason) is fully audited.POST /slots/{id}:rollback— roll back to the previous bundle (itself fully re-verified; slot history is append-only).- Webhooks — subscribe to
bundle.created,slot.activatedandslot.rolled_back.
Every activation increments the slot's strictly monotonic
activation_generation, the fencing token that keeps concurrent scoring
writers consistent.
Evaluation reports: promotion gates G1\u2013G6
Trained models are now evaluated against the full promotion-gate suite before they can ever be promoted:
GET /models/{model_id}/eval-report— the stored evaluation report for a model: pass/fail per gate (G1 label quality with 95% bootstrap CIs, G2 calibration, G3 count invariance, G4 tokenizer/embedding parity, G5 masked-supervision audit, G6 drift baseline) with actionable failure diagnostics, per-label metric blocks, and the drift baseline serving monitors will consume. Reports are immutable, content-fingerprinted artifacts; the API re-verifies the fingerprint on every read.gate.failedwebhook — subscribe a webhook endpoint to be notified the moment any evaluation gate fails, with the failing gate ids and a one-line diagnostic each.
Reports are deterministic: re-running the identical evaluation reproduces the report byte for byte, so the fingerprint your promotion bundles pin is stable.
Training runs: checkpointed, resumable, deterministic model training
The public API gains its training surface (gateway-api → training-service):
POST /runs(200) — start a training run from{dataset_id, model_config_tier | overrides, labels?, seed?, trainer?}. The dataset build must have succeeded with an empty violation report (failed_preconditionotherwise); tier/overrides are validated against the model-config bounds (invalid_argument); the resolved inputs become a content-addressed run config (https://spec.tessera.dev/schemas/run_config.json) — identical requests reuse the in-flight run. Runs execute as Kueue-queued GPU Jobs on thegpu-trainpool (queuetraining).GET /runs,GET /runs/{id}— status (queued | running | succeeded | failed | cancelled), pinned fingerprints, retained checkpoint URIs,best_val_loss, and the createdmodel_idonce succeeded (a ModelVersion in statustraining; evaluation/promotion comes next).GET /runs/{id}/metrics— multi-series step time series{series: {name: [[step, value], …]}, cursor}with incremental?after_step=polling (live charts) and?metric=filtering.GET /runs/{id}/logs— the trainer's structured logs, paged.POST /runs/{id}:cancel— queued runs stop immediately; running runs stop gracefully within one checkpoint interval, leaving a final valid checkpoint.
Training implements the platform ML spec exactly: bf16 autocast with fp32
master weights, AdamW with gradient clipping, cosine LR with linear warmup,
gradient accumulation to a configured global token batch, single-node
multi-GPU DDP with lockstep collectives (uneven data ends epochs everywhere
together; oversized batches are skipped everywhere together), and
torch.compile on by default. Checkpoints are safetensors directories with
fingerprinted manifests (last 3 + best-by-val-loss retained); resume is
exact — stochastic streams are keyed PRNG(seed, rank, step), so no RNG
state is stored and the loss curve continues as if uninterrupted. Identical
(dataset, config, seed, world_size) reproduces evaluation AUC within
±0.002, enforced by a CI training smoke on a fixed generated dataset.
Terminal runs publish a run.completed webhook and meter
training_gpu_seconds (hourly while running + a terminal remainder, summing
to exact wall time × GPU count).
Model architecture
tessera_core.model implements the two-level behavioral foundation model of
005 §4 exactly, once, in the shared library — the same forward code serves
training (WP-M3), evaluation (WP-M4) and scoring (004 §2 rule 1):
ModelConfig: the verbatim 005 §4 tier table (S ~30M / M ~120M / L ~400M params, asserted within +-10%),tier("S"|"M"|"L")/with_overrides(dict)with range-validated bounds (typedConfigErroron any out-of-bounds or unknown key — unknown channel flags are rejected, not reserved), and a 002 §10fingerprint. Satisfies the WP-M1 collator'sSupportsBucketConfig.TesseraModel(config, labels): permutation-invariant event encoder (concat[attr, value, presence, etype, mlp(numeric)] featurizer -> set transformer with no positional encoding -> masked mean-pool), RoPE causal sequence encoder onscaled_dot_product_attention(is_causal=True, rotary tables precomputed once per max window and cached, no absolute/sinusoidal position tables anywhere — scanned by test), readoutsmean_pool/last_event/attention_pool, next-event heads (event-type + categorical-value sampled-softmax CE over in-batch negatives with the positive excluded from its own negative set; numeric Huber on a linear output — no sigmoid), and one linear head per label (BCE-with-logits / CE / Huber; masked targets contribute exactly zero).- Total loss
L = L_next_event + sum_k w_k L_label_kwith global observed-count normalization: counts and sums all-reduce (SUM) in a single collective whentorch.distributedis initialized, so gradients equal the true global mean under any per-rank mask imbalance and zero-count ranks never deadlock. - Bit-exact batching: events/sequences are processed in exact-size groups (no padded value ever enters a reduction), making causality and padding invariance bitwise properties (WP-M2 AC1/AC3), and within-event tokens are canonically ordered so token-set permutation is exact (AC2).
- Checkpoints:
save_checkpoint/load_checkpoint— safetensors (never pickle) + canonical-JSON manifest (api/jsonschema/checkpoint_manifest.json, new fragment) carrying the config payload/fingerprint, label definitions,TOKENIZER_VERSIONand per-filefile_fingerprint; loads re-hash and fail closed (FingerprintMismatch), with the auditedallow_mismatch=True, reason=...escape hatch.
Consumers declare the tessera-core[model] extra (torch, safetensors,
numpy). See libs/tessera-core/README.md and the 2026-07-08 WP-M2 entries
in specs/ERRATA.md.
Tokenizer & batch layout
tessera_core.tokenizer is THE single tokenization implementation of
005 §2–3 (004 §2 rule 1), imported by both dataset-builder and
scoring-service — gate G4 asserts parity between the two paths:
- Hash ids, two forms, one implementation:
hid_raw(63-bit int64-safe; dataset shards store raw ids, making shards model-tier-independent) andhid(% B+0 -> 1remap; bucket 0 = padding / no-value sentinel). Golden vectors inlibs/tessera-core/tests/golden/hid_vectors.json. - Five parallel channels per token (
attr_id,value_id,numeric_value,presence_id,event_type_id) over unordered per-event token sets, with all 002 §4 encodings (quantile CDF, categorical_hash, boolean, cyclic_time, text_hash with identifier hygiene, array_multihot), 002 §5 presence tokens (not_applicablenever materializes), and the structural/derived tokens of 005 §2 (event type, time-since-prev/first over log1p seconds with the window-boundary rule, position-in-day). - Encoder fit/serialize/load:
fit_encoders(TRAIN split only, deterministically seeded ≤ 100k reservoir per attribute, 1,024 quantile knots) andEncoderSetwhose canonical-JSON serialization makesencoders_fingerprintequal the sha256 of the file bytes. New JSON-Schema fragment:api/jsonschema/encoder_set.json. - Structural deny enforcement: the tokenizer runs THE leakage-policy
evaluator at construction and raises typed
ForbiddenAttributeRole/NotApplicableMaterialized— defense in depth behind dataset-builder's gate, deny by default. - CSR ragged batch layout + collator (005 §3): five flat length-T tensors
plus
event_offsets/sequence_offsets; bucketing happens only incollate(), parameterized by the model config'sB_attr/B_val/B_etype; byte-deterministic. TOKENIZER_VERSIONis pinned inPromotionBundle.pins.tokenizer_version(002 §9). Single-core throughput: ~440k tokens/s (floor 50k, enforced bybenchmarks/bench_tokenizer.pyand atest_acceptance_*test).
Consumers declare the tessera-core[tokenizer] extra (numpy). See
libs/tessera-core/src/tessera_core/tokenizer/README.md and the 2026-07-08
WP-M1 entries in specs/ERRATA.md (KLL determinism, sentinel configuration,
not_applicable vs deny precedence).
Datasets: leakage-gated, reproducible training shards
The public API gains its dataset surface (gateway-api → dataset-builder):
POST /datasets(202) — start a build from{entity_type, labels, window, split, snapshot_id?, contract_version?}(https://spec.tessera.dev/schemas/dataset_build_request.json). Every input is pinned — event-store snapshot (created + pinned when omitted), approved contract, leakage policy, label definitions and their label-store manifests — into a content-addressed build fingerprint. Re-POSTing a request that resolves to an existing fingerprint is idempotent;failedbuilds relaunch under the samedataset_idand resume from stage checkpoints.GET /datasets,GET /datasets/{id}— status (queued | running | failed | aborted_leakage | succeeded) + current pipeline stage, and, once succeeded, the full manifest (https://spec.tessera.dev/schemas/dataset_manifest.json) with row counts, per-split label support, token statistics, every pinned fingerprint and the typed violation report (empty = every gate passed).
Builds run as GKE Jobs on the cpu-batch pool (Spot-tolerant — preempted
jobs resume, not restart). Leakage gates are build-aborting: deny-pattern /
identifier-role violations abort before extraction, and identifier values
found inside feature text abort after tokenization — either way no shards
are published and the violation report is served in the manifest position.
Shards are 005 §3 CSR-layout Parquet (zstd, ~256 MiB target) storing raw
63-bit hash ids — model-tier-independent. Labels follow the 002 §6 masked-
supervision rule exactly; every candidate target is audited in
audit/supervision_audit.parquet (the gate G5 input). Encoders are fit on
the TRAIN split only and shipped as encoders/encoders.json
(encoders_fingerprint). Each successful build meters build_cpu_seconds.
Label ingestion & point-in-time label store
Declare supervision targets and get label observations into a point-in-time-correct store — labels are targets, never inputs, structurally:
- Labels API:
GET/POST /labels,GET/PATCH /labels/{id}(002 §6 shapes: binary/multiclass/regression tasks,label_table|label_events|derivedsources, maturation, opt-inassume_negative_after_maturationwith a mandatoryguard_min_positive_rate ∈ (0, 1)). Creation registers the label's name andvalue_column/value_attributeinto leakage deny-pattern scanning so contract inference marks matching attributestarget_only, and refuses alabel_eventsvalue attribute that isrole: featurein the approved contract.name/entity_type/taskare immutable after the first ingested observation; PATCH allows metadata,maturation_days,null_semantics,backdating_limit_daysand source column renames only. New JSON Schemahttps://spec.tessera.dev/schemas/label_observation.json. - Label-table ingestion rides batch syncs: sources referenced by a
label_tabledefinition sync without a mapping and their rows become observations (never canonical events). Typed rejections — missing/ unparseable/naiveobserved_at, the +1 h future guard, the optional per-label backdating limit, task parse failures — are counted in the newlabelsblock of sync details and written to the snapshot'srejected/path. Accepted observations older than 30 days are flaggedbackdated. - Label-events routing: events matching a
label_eventsdefinition tee an observation off the canonical flow (observed_at= the event's timestamp); the events themselves still land in the event store. Matched events lacking the attribute or failing parsing are dead-letter counted, never dropped. - Point-in-time store: append-only Parquet per label under
projects/{p}/labels/{label_id}/v1/observed_dt=…/, committed by fingerprinted snapshot manifests.LabelStore.read(project_id, env, label_id, as_of, manifest_fingerprint=None)returns observationsobserved_at ≤ as_of, deduplicated byobservation_key(earliestreceived_atwins), pinned reads re-hash and fail closed — the as-of join APIdataset-builderuses. - Support stats:
GET /labels/{id}/stats— observation counts, distinct entities, observed-at range, backdating, read-time maturation coverage, ISO-week rollups, and per-taskpositive_rate/class_counts/value_quantiles— maintained incrementally at ingestion (zero store scans per request; retries never double-count).
Public docs site & API reference pipeline
https://docs.staging.tessera.zone now exists as a Docusaurus 3 site (apps/docs),
deployed as static files to the per-env tessera-{env}-docs-site GCS buckets
behind Cloud CDN. WP-X2/WP-X3 can add content without touching build or deploy
machinery.
- Eight stable section routes:
/quickstart,/concepts,/connect,/train,/serve,/api-reference,/sdks,/security(placeholder pages; prose lands in WP-X2/WP-X3). - API reference generated from
api/openapi.yamlon every merge: one page per tag, one section per operation (method + path, auth per 003 §2, parameters, request/response schemas, pagination/idempotency notes, the 003 §1 error envelope + closed code set). Committed output is byte-drift checked (tools/codegen/api-reference/check_drift.py); hand edits fail CI. Every WP that edits the OpenAPI spec runspnpm --filter docs run gen:api-reference. - Per-API-version doc versioning: current docs are labeled
v1; cutting a version is one command (pnpm --filter docs run docs:version <label>) and snapshots prose + API reference together. - Fully local build-time search (all versions, same-origin), dark mode with a persistent toggle, custom 404, sitemap + robots; the built site makes zero third-party requests (proven by an offline Playwright test).
- CI:
docs-ci.yaml(build + drift + links + gitleaks + terraform validate) on every docs-affecting PR;make docs-check/make docs-devtargets;make checknow includes the docs gate. - Deploy:
docs-deploy.yamlon merge to main — build once, rsync per env with immutable caching for hashed assets andno-cachefor HTML andsearch-index*.json, HTML uploaded after assets, then CDN invalidation; supports--dry-run. Edge infra:infra/terraform/modules/docs-site(HTTPS LB + backend bucket + CDN, HTTP→HTTPS redirect, managed cert; DNS record deferred until a tessera.dev zone exists).
Console: authenticated shell & design system
The Tessera web console goes live (apps/console, Next.js 15):
- Sign in / sign up with Identity Platform email+password (Google/GitHub arrive with their OAuth apps); sessions refresh before expiry and any authenticated route visited signed-out returns you to it after sign-in.
- Tenancy switching: pick organization, project, and
live/testenvironment from the top bar. Selections persist per user; whiletestis active a persistent banner marks the environment, and every request carriesX-Tessera-Project/X-Tessera-Environment(003 §2). - Settings: organization rename, member management and invites, project list/create, and API keys — a new key's plaintext is shown exactly once with a copy affordance; lists only ever show the key prefix (003 §2).
- Design system (
@console/ui): tables with 003 §1 cursor pagination, zod-validated forms that map servererror.details[]onto fields and showrequest_id, wizards, JSON viewer, code blocks, status chips, toasts and more — light + dark themes meeting WCAG AA. - One API client (
@console/api): wraps the generated OpenAPI client with auth/tenancy headers,Idempotency-Keyon every mutation, typed 003 §1 error envelopes, and automatic single retry of idempotent GETs on 429 honoringretry_after_seconds.
Streaming ingestion: POST /v1/events + the dead-letter store
The public API gains the streaming intake (ingest-api, served on
ingest.staging.tessera.zone and proxied from api.staging.tessera.zone):
POST /v1/events(003 §5): up to 500 canonical events (002 §2) / 5 MiB per batch, one event ≤ 64 KiB. Per-item 207-style results — invalid items are rejected individually with field-precise messages while the rest of the batch is accepted. Accepted means durably enqueued (Pub/Sub publish acknowledged onevents.ingest); the server sets the envelope (project_id,environment,source_id,received_at,ingest_id,canonical_entity_key) and derives missingevent_ids per 002 §2 so retries are no-ops. Requires an API key with theingestscope — secret (tsk_…) or publishable (tpk_…). The first use of a key creates-or-reuses its implicitkind=streamsource (sdk:{key_prefix}). Per-key sustained event-rate limits (Phase-1 static defaults: 20,000 events/slive, 1,000test, 500 publishable) answer 429resource_exhaustedwithretry_after_secondsandX-RateLimit-*headers.Idempotency-Keydedups whole requests per project for 24 h. Each accepted request is metered asevents_ingested.- Dead-letter store: events that fail after acceptance (e.g. contract
violations at stream time) park in a project-scoped, replayable store —
GET /v1/events/deadletter(filter byreason,event_type,entity_type,entity_id; cursor-paginated;readscope),POST /v1/events/deadletter:replayand…:discard(bydlt_idsor{filter: {reason, before}};adminscope). Replay republishes the original ingest message unchanged exceptenvelope.replay_of; items are refused after 10 replays or once discarded. Retention: 30 days. - New schemas:
https://spec.tessera.dev/schemas/ingest-message.json(theevents.ingestmessage consumed by the stream router) andhttps://spec.tessera.dev/schemas/deadletter-item.json(the shape downstream consumers publish ontoevents.deadletter).
Observability stack & SLOs
The platform-wide observability layer is now code: any WP satisfies 004 §3
item 6 by appending one entry to infra/observability/services.yaml and
importing tessera_core.telemetry.
infra/terraform/modules/observability(wired into all three env stacks, plan-verified): PodMonitoring per registry entry, Managed Prometheus GlobalRules wrappinginfra/prometheus/rulesverbatim, in-cluster Grafana (dashboards file-provisioned via labeled ConfigMaps + sidecar), Alertmanager with severitypage/ticketwebhook routing (receiver URLs read at runtime from Secret Manager via Workload Identity +url_file— never in code or state), kube-state-metrics, and the PII-guard/cost-anomaly CronJobs + GCP identities (deny-by-default grants).tessera_core.telemetrynow emits the WP-O1 standard metric set:http_requests_total{service,method,route,code},http_request_duration_seconds{service,route},worker_iterations_total,ingest_publish_failures_total,state_write_conflicts_total,pipeline_records_total(+ the Phase-2event_to_state_latency_secondshistogram). Route labels are templated paths only; 404s labelunmatched. (Label change from WP-F3's scaffold:status→code,serviceadded — WP-F3 tagged for review.)tools/dashboards: one RED dashboard per service from one template intoinfra/grafana/dashboards(14 seeded); byte-for-byte drift check, alert convention + runbook lint, pinned promtool runner — all inmake checkvia the newobservability-checkgate.- Alerts + SLOs: paging rules (ApiHighErrorRate, IngestPublishFailures,
RouterBacklogAgeHigh, TrainingJobCrashLoop, StateWriteConflictSpike,
PiiGuardHit) and ticket rules (CostAnomalyDetected, PiiGuardStale), plus
three SLOs (API 99.9%/30d, ingest 99.95%/30d, event→state p95 ≤ 2s/28d) as
recording rules with 14.4×/6× multiwindow burn alerts; every alert has a
runbook (
infra/runbooks/) and promtool tests for firing/non-firing. tools/pii_guard: hourly log scan (canaryTSSRCANARY-+ email/digit-run detectors, allowlist for generic detectors only) proving attribute values never appear in service logs, and the daily cost-anomaly job whose 40%/$50 thresholds flow from Terraform variables into the job config.
Public docs: apps/docs/docs/platform/slos.md (public SLO summary). No
public API surface change (api/openapi.yaml untouched).
Deploy pipeline & environments
Merged code now becomes running services with no bespoke steps: any service WP ships by adding a kustomize base (and, only if customizing, a Dockerfile) and inherits the pipeline.
infra/k8s/: kustomize bases for all 13 cluster services (001 §3) + shared_libcomponents (probes, security context, OTEL env, Prometheus annotations), per-environment overlays (one GCP project per environment), one shared GKE Gateway with per-service HTTPRoutes, an Alembic migration Job template (db-migrate-<sha>), and a conftest policy suite over rendered output (resources, probes, non-root, read-only rootfs, SHA-pinned images, no inline Secrets, 001 §4 node placement, Kueue queue label).- Image pipeline (
.github/workflows/deploy.yaml): affected-only builds tagged with the 40-char git SHA, SPDX SBOM (syft) attached as a cosign attestation, keyless cosign signing via GitHub OIDC, verification gate with issuer/repo identity constraints + unsigned-copy negative check. - Staged rollout: each environment before production rolls out automatically
→
tools/smoke→ hold;promote-prod.yamlonplatform/v*tags requires a passing smoke result for the same SHA (fails closed) + GitHubproductionenvironment approval. - Release ordering (
tools/deploy): migration Job must reach Complete (10 min timeout) before any Deployment update; failed rollouts auto-undo; every Deployment carriesdeployment.tessera.dev/sha(queryable with kubectl). tools/smoke: post-deploy verification CLI (health, authenticated round-trip incl. 003 §1 pagination envelope, X-Request-Id) — exit 0 = release may proceed.- Make targets:
render,k8s-validate,deploy,rollback,smoke,migration-lint(additive-migrations CI lint, 004 §2 rule 6). - GCP: GitHub OIDC Workload Identity Federation +
tessera-ci-deployGSAs in all three env projects (infra/terraform/deploy); Artifact Registrytesserarepo live in the dev/CI project. No service-account keys.
No public API surface change.
tessera-testing fakes & golden data
libs/tessera-testing now carries the shared test substrate every downstream
WP builds on (004 §3.2):
- In-memory twins of the WP-F3 client protocols —
FakeGcs(generations +if_generation_match),FakePubSub(ordering-key FIFO, ack/nack, dead-lettering after 5 attempts),FakeBigtable(idempotent check-and-put, newest-first prefix scans, fencedentity_statewrites per 002 §8) — all raisingTenancyErroron un-prefixed keys exactly like the real clients. - One shared conformance suite (
tessera_testing.conformance) run against fakes (default CI) and live emulators (pytest -m emulatorCI job withdocker-compose.emulators.yaml); identical case list on both. - Deterministic scenario generators (
generate(domain, ...)) for e-commerce, gaming, and logistics: schema-valid canonical events, labels in both 002 §6 source shapes, presence-anomaly injection, and an exact ground-truth manifest with a new internal schemaapi/jsonschema/ground_truth_manifest.json. - Reusable pytest fixtures via a
pytest11plugin (fake_stores,scenario_*_small, emulator fixtures,emulatormarker). - A scenario seeding tool (a CLI under
tools/): seeds a scenario into fakes, emulators (idempotent ledger rows + a publish per event), orPOST /v1/eventsper 003 §5. - Cross-cutting fix in
tessera-core(WP-F3 tagged): single-boundValueRangePredicateno longer crashes the default Bigtable backend — the 002 §8 fence compare needs start-only value ranges.
No public HTTP API surface change (api/openapi.yaml untouched).
tessera-core library
libs/tessera-core now carries the single shared implementation of Tessera's
foundational contracts (004 §2 rule 1):
- Canonical JSON + fingerprinting (002 §10):
canonical_json,fingerprint,file_fingerprint,ArtifactRef,load_verified(fail-closed, audited override path). - Canonical event + label models (002 §2/§6) with new JSON Schemas
api/jsonschema/event.jsonandapi/jsonschema/label_definition.json; pydantic/JSON-Schema parity is fuzz-asserted.derive_event_id,validate_size,canonical_entity_key,entity_hash. - Presence resolution (002 §5) with explicit
reference_timeand never-materializednot_applicable. - Tenancy-enforcing GCS / Pub/Sub / Bigtable clients (
TenancyErrorbefore any network call;GcsClient.platformfor the reservedplatform/prefix;BigtableTransportis a raw guarded transport for WP-D4's ledger layer). - Telemetry:
configure_telemetry,bind_context,health_router,instrument_app, RED metric helpers. - Reserved import paths for
tessera_core.tokenizer(WP-M1),tessera_core.leakage(WP-D2),tessera_core.ledger(WP-D4).
No public HTTP API surface change (api/openapi.yaml untouched).
WP-F2: Monorepo bootstrap & CI/CD
Public-surface changes (004 §3.7):
- Seed
api/openapi.yaml(OpenAPI 3.1): servers per 003 §1, the closed error envelope (ErrorEnvelope, nine codes), the pagination envelope (PageEnvelope),X-Request-Idresponse header, security schemes fortsk_/tpk_bearer keys, andGET /healthz. - Generated API surface artifacts: the client cores of the planned Python and
TypeScript SDKs (
tessera._generated;@tessera/sdksrc/generated/), and the docs-site API reference (apps/docs/api-reference/), all produced bytessera-codegen.
GCP infrastructure baseline (Terraform)
New infra/terraform/ tree: reproducible per-environment deployments —
VPC + Cloud NAT + Private Service Access, GKE Standard (system / cpu-batch /
gpu-train a2+l4 / gpu-serve pools, Workload Identity, Managed Prometheus),
Kueue training queue, Cloud SQL Postgres 16 (control, private IP, PITR,
CMEK), Bigtable ledger (ledger_events, entity_state), the six Pub/Sub
topics with 5-attempt dead-lettering and a BigQuery metering subscription,
CMEK GCS buckets, per-env KMS keyring (90-day rotation), per-service GSAs with
least-privilege grants, Secret Manager placeholders, Artifact Registry, and a
conftest policy suite gating every plan. No public API surface change.
Schema contracts & leakage policies
The public API gains the contract lifecycle (gateway-api +
contract-service):
- Contracts:
POST /contractsinfers a draft from the latest profiling data per the 002 §4 defaults (deny by default — unreviewed attributes areexcluded);PATCH /contracts/{version}edits drafts (event-type status- per-attribute role/encoding/missingness/pii/description only);
POST /contracts/{version}:approve(org owner/admin) runs THE leakage evaluator with error-severity blocking and fingerprints the frozen document (002 §10) — the prior approved version flips tosuperseded.GET /contracts/current,GET /contracts/{version}decorate every attribute with read-onlyprofilestats + aninferredflag;GET /contracts/{version}/diff?against=returns a structured diff withwidening/narrowing/neutralrisk per change. New JSON Schemahttps://spec.tessera.dev/schemas/schema_contract.json.
- per-attribute role/encoding/missingness/pii/description only);
- Leakage policy:
GET/PUT /leakage-policy(immutable, fingerprinted versions; invalid regexes are rejected naming the pattern) andPOST /leakage-policy:test— a server-side dry-run through the exact evaluator regex engine. The platform default deny patterns ((^|_)(label|target|outcome|ground_truth)(_|$)+ identifier name heuristics) are always enforced and can never be removed. New JSON Schemasleakage_policy.jsonandviolation_report.json. - Internal plumbing: contract-service now serves the contract-head and
full-contract internal endpoints (ETag/If-None-Match) consumed for
contract_version_at_ingeststamping, accepts streaming attribute observations (attribute_observation.json), registers entity types idempotently, and builds the per-project identifier-value scan index consumed by dataset builds.
Canonical event store
tessera_core.eventstore is the durable, project-isolated canonical event
store on GCS (001 §5.1):
- Partitioned Parquet layout
projects/{p}/events/{event_type}/env={environment}/dt=YYYY-MM-DD/…with the 002 §2 canonical event schema + server-set envelope as flat columns (zstd). - Staging-then-manifest atomic commits: data files are invisible until a
create-only manifest object lands (
if-generation-match: 0); crashes before the manifest write leave the store logically unchanged; retries with the samemanifest_idare idempotent. - Snapshot cursors: immutable, fingerprinted consistent points that
dataset builds pin (
pin_snapshot/release_snapshot); resolution is independent of later commits. Reader.scan→pyarrow.RecordBatchReaderwith event-type andtimestamp_microrange pushdown; partition listing is structurally impossible for readers.- Verified small-file compaction, per-plan retention with deferred
physical deletion, and daily
storage_bytes_dayusage metering (002 §11).
New interface schemas: api/jsonschema/event_store_manifest.json,
api/jsonschema/event_store_snapshot.json. New tessera-testing module
eventstore_fixtures (in-memory GCS twin + golden logistics/media event
generators). New dataset-builder CronJobs (compact hourly, retain + meter
daily). GcsClient gained a create() (create-only) operation and a typed
PreconditionFailed error.
No public HTTP API surface change (api/openapi.yaml untouched).
Connectors: BigQuery and Postgres
The first two warehouse/database connector kinds land on the WP-C1
framework — every /sources, /test-connection, /mapping, /syncs
endpoint now works with kind: bigquery and kind: postgres:
- BigQuery (
BigQuerySourceConfig/BigQueryCredentials): customer service-account key (vaulted) or cross-project grant to Tessera's per-project service account (roles/bigquery.dataViewer+roles/bigquery.readSessionUser; no credential stored). Extraction uses the Storage Read API (Arrow) with column projection limited to mapped columns and incremental cursors pushed down as arow_restrictionliteral; read sessions are capped at 4 streams.test-connectionrunsauthenticate,table_exists,storage_read_sessionand names the exact missing grant on failure. - Postgres (
PostgresSourceConfig/PostgresCredentials): read-only user over TLS (ssl_mode: require|verify-full), optional SSH tunnel (config.tunnel+ vaultedtunnel_private_key), keyset-paginated extraction ordered by(cursor, primary key)(orctid) — never OFFSET — with 50k-row pages, a 5-minute per-page statement timeout, ≤ 3 retries with backoff, anddefault_transaction_read_only = on.test-connectionrunstcp_or_tunnel,authenticate,table_readable,read_only(a user holding write privileges reports the new check statuswarn). - Normative type maps per kind (documented on the docs site, generated
from code): mapping an
unsupportedcolumn (e.g. BigQueryJSON, Postgresjsonb) failsPUT /sources/{id}/mappingwith 400invalid_argumentnaming the column and its type; cursor columns are restricted to cursor-capable types. NaiveDATETIME/timestampcolumns require the mappingtimezone;DATE/datebecome midnight UTC; decimals with > 15 significant digits flagprecision_lossin the sync's profiling report (new optionalflagsfield). - API shape additions:
ConnectionCheck.statusgainswarn;SyncErrorgains optionaldetails—extraction_page_failedcarries the failing page's keyset position.
File upload + object-storage connectors (GCS, S3)
Two new ways to get files into Tessera, both landing through the WP-C1 connector framework (mapping, profiling, normalization and sync status work identically to warehouse connectors):
- Direct resumable uploads —
POST /uploads {filename, format: csv|parquet, source_id?}returns a signed GCS resumable session URL for one CSV or Parquet file up to 50 GiB; the client PUTs bytes straight to GCS without transiting Tessera services. Sessions expire after 24 h.POST /uploads/{id}:completeverifies the object (oversize ⇒invalid_argumentwith the observed size), records itsfile_fingerprint, sniffs the CSV dialect + encoding from the first 1 MiB (delimiter, quote char, header presence, UTF-8/UTF-16/Latin-1 — explicitconfig.csvoverrides win), attaches the object to afile_uploadsource (created on the spot when absent) and enqueues a sync.GET /uploads/{id}then serves a schema preview —columns: [{name, inferred_type}]plus the first 20 sample rows — straight from the landed snapshot, so the console can configure a mapping without another scan. Headerless CSVs synthesizecol_1…col_n;.gz/.zstCSVs decompress transparently. - Bucket connectors — source kinds
gcs({bucket, prefix?, glob?, manifest_object?}, no stored credential: access is the per-project service account the customer grantsroles/storage.objectViewer; the connection test returns the exact grant command on permission failure) ands3({bucket, region, …}with vaulted{access_key_id, secret_access_key}). Object selection is full-key glob with**support, or an exclusive newline/JSON manifest object. Bucket syncs are file-incremental: each object is ingested at most once (keyed by etag/generation); a changed object followsconfig.on_object_change: skip|reingest|fail(defaultfail⇒object_mutated);mode=fullre-ingests everything. - Reserved: kind
azure_blobships as a Phase-2 stub behind theconnectors.azure_blobfeature flag (creation isfailed_preconditionwhile disabled; the connection test reportsnot_implemented).
Connectors: sources, mappings, batch syncs, profiling
The public API gains the batch-ingestion surface (gateway-api +
connector-runner):
- Sources:
GET/POST /sources,GET/PATCH/DELETE /sources/{id},POST /sources/{id}/test-connection. Connectorkindis the 003 §4 enum; per-kindconfig/credentialsvalidate against the plugin's registered JSON Schemas. Credentials are envelope-encrypted (per-credential AES-256-GCM data key wrapped by the project's KMS key) and write-only — reads exposecredentials_setonly.kind=streamrecords carry aconfig.subtypeenvelope (pubsub_subscription|adapter|implicit_sdk) for the streaming path. - Mappings:
GET/PUT /sources/{id}/mappingstores immutable, fingerprinted column→canonical-event mapping versions (new JSON Schemahttps://spec.tessera.dev/schemas/source_mapping.json);POST /sources/{id}/mapping:previewapplies a candidate mapping to a sample (from_snapshot|sample_payload, one operation shared with adapter stream sources) and returns ≤ 50 mapped events plus per-column rejection counts with typed reasons. - Syncs:
POST /sources/{id}/syncs(full | incremental) runs aconnector-runnerGKE Job on thecpu-batchpool: extract → immutable landed Parquet snapshot (_manifest.jsonwritten last) → normalization into canonical events (WP-D1 manifest-atomic commit) → post-normalization profiling per(event_type, attr_key)(https://spec.tessera.dev/schemas/profiling_report.json). Incremental syncs extractcursor > watermark; the watermark advances only at commit.GET /syncs/{id}reports status/phase/counts/profile; terminal syncs publish thesync.completedwebhook event and meterevents_ingested+build_cpu_seconds.
Usage metering pipeline
Every metered occurrence on the platform is now recorded exactly once and queryable by the tenant that generated it:
GET /usage— flat usage rollup rows for the credential's project:?metric=&granularity=hour|day&start=&end=&limit=&cursor=→{"data": [{metric, unit, period_start, quantity, dims}], "next_cursor"}(003 §1 pagination; scoperead; defaults to the last 30 days atgranularity=day; ranges over 366 days are rejected). Phase 1 metering is record-only — quotas and billing arrive with WP-B2/WP-B3.- Metering event contract — new JSON Schema
https://spec.tessera.dev/schemas/usage_metering_event.json(api/events/usage_metering.json): 002 §11 plus a deterministic idempotency idmeter_id; fixed metric→unit mapping; closed dims key set. Services emit through the singletessera_core.metering.emit_usagehelper. - Pipeline —
metering-serviceconsumesusage.metering, dedups on(project_id, meter_id), and aggregates into hourly/daily rollups (row-level-secured per project); invalid messages dead-letter with a typed reason. Raw events land in BigQuerytessera_metering.usage_raw(occurred_at-day partitions, clustered by project/metric). A daily job reconciles rollups against the deduplicated raw stream; the daily storage sweep meters GCS bytes per project (storage_bytes_day) — Bigtable ledger bytes are not yet metered.
Outbound webhooks
Your systems can now receive signed, retried HTTPS notifications for platform events:
- Endpoints:
GET/POST /webhook-endpoints,GET/DELETE /webhook-endpoints/{endpoint_id},GET /webhook-endpoints/{endpoint_id}/deliveries,POST /webhook-endpoints/{endpoint_id}:rotate-secret,POST /webhook-endpoints/{endpoint_id}:enable. Up to 16 active endpoints per project; HTTPS-only with public-unicast address checks at registration and before every attempt. - Envelope:
{id, type, created_at, project_id, data}perhttps://spec.tessera.dev/schemas/webhook_envelope.json, with a closed event-type enum (sync.completed,run.completed,gate.failed,bundle.created,slot.activated,slot.rolled_back,score.updated,export.*,refresh.*,drift.alert,quota.*,billing.*). Delivery is at-least-once and unordered — dedup onid. - Signatures: every delivery carries
Tessera-Signature: t=<ts>,v1=<hex>wherehex = HMAC-SHA256(secret, "{t}." + body). Secrets arewhsec_…, shown exactly once at creation/rotation; for 24 h after a rotation headers carry both the new and old signature so receivers can roll over without loss. Verify with a 300 s timestamp tolerance. - Retries + auto-disable: failed attempts retry at +1 m, +5 m, +30 m,
+2 h, +21 h (6 attempts over ~24 h). Endpoints failing 20 consecutive
attempts are disabled automatically and org owners/admins are emailed;
re-enable with
:enable. A 30-day per-endpoint delivery log records every attempt.
New JSON Schemas: webhook_envelope.json (under api/events/),
webhook_endpoint.json, webhook_delivery.json under
https://spec.tessera.dev/schemas/. Docs: the new
Webhooks guide covers
verification snippets and receiver best practices.
Projects, environments, API keys, authorization
Every request now resolves to exactly one tenancy context
{org_id, project_id, environment, scopes} before any handler runs:
- Projects:
GET/POST /projects,GET/PATCH/DELETE /projects/{project_id}— the data + model boundary. Every project has exactly the environments["live", "test"]; project names are unique per organization.DELETE(orgowneronly) marks the projectdeletingand immediately revokes all of its API keys; the physical purge is asynchronous. - API keys:
GET/POST /projects/{project_id}/api-keys,DELETE …/api-keys/{key_id}. Secret keystsk_{env}_…(server-side, scopes ⊆ingest/read/admin) and publishable keystpk_{env}_…(client-side ingestion; always exactly["ingest"], optionalallowed_origins). The plaintext appears exactly once, in the create response; storage keeps only a lookup prefix and a peppered SHA-256. Revocation takes effect within 5 seconds. - Console project scoping: session data-plane calls select tenancy with
the
X-Tessera-Project+X-Tessera-Environmentheaders, validated against org membership. - Authorization matrix: every operation is declared in
api/authz.yamland enforced by generated middleware — session role floors per org, key scope intersections, deny by default. Cross-tenant resource ids always yield 404 (no existence leak). - Rate limits (Phase 1 static): 600 requests/min per credential;
X-RateLimit-Limit/Remaining/Reseton every authenticated response; 429resource_exhaustedincludesretry_after_seconds. - Idempotency:
Idempotency-Keyon POSTs now dedups for 24 h per tenant scope, replaying the original response.
New JSON Schemas: project.json, api_key.json under
https://spec.tessera.dev/schemas/. Internally, tenant tables are protected
by Postgres row-level security keyed to per-request tenancy GUCs.
Identity: users, organizations, invites
The public API gains its identity surface (gateway-api):
- Sessions:
Authorization: Bearer <Identity Platform JWT>(email+password with verification; Google/GitHub once their OAuth apps are provisioned). Users are provisioned just-in-time on first sign-in. - Profile:
GET/PATCH /users/me,GET /users/me/orgs. - Organizations:
POST /orgs,GET/PATCH /orgs/{org_id}— creator becomesowner; rolesowner ⊃ admin ⊃ member ⊃ viewer; an org always keeps at least one owner. - Members:
GET /orgs/{org_id}/members(cursor-paginated),PATCH/DELETE /orgs/{org_id}/members/{user_id}. - Invites:
POST/GET /orgs/{org_id}/invites,DELETE /orgs/{org_id}/invites/{invite_id},POST /invites:accept— single-use tokens, 7-day expiry, delivered by email; only a hash of the token is stored.
Mutating endpoints require a verified email (400 failed_precondition,
reason="email_unverified"). New JSON Schemas: user.json,
organization.json, membership.json, invite.json under
https://spec.tessera.dev/schemas/.