Skip to main content

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

addedapidocsv1

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

fixeddocsconsoleapi

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

fixedapiconsole

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

changedapimodelsconsoledocs

Dossier downloads (GET /dossiers/{dossier_id}:download) and model card exports (GET /models/{model_id}/card:export) in markdown and pdf now come from governance renderer version 3 (renderer_version: "3").

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

The API responses, the stored reports and the json dossier download are unchanged and keep every criterion and the stored overall. Stored dossiers keep their bytes and fingerprints. A new render of an unchanged dossier or card can have a different sha256; renderer_version tells the renderers apart.

Dossiers now include the model's settled backtests

changedapidocs

The backtesting section of a dossier (sr11-7.v1, sr11-7.v2) now embeds the settled backtests of the documented model, newest first, instead of reading evidence: missing with source_not_bound.

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

Dossiers assembled before this change are unchanged. No API response gains a field.

A refresh of a realtime slot now writes the realtime scorer's exact scores

changedmodels

A refresh of a slot that scores in real time now writes exactly the scores and embeddings that the slot's realtime scorer writes for the same events. Before, the refresh ran the model in full precision while a tier S realtime scorer runs it int8-quantized, so the two disagreed slightly on every entity. To make the two equal on any node, realtime scorers and every refresh Job now run the same CPU kernels (AVX2, with the vendor-neutral branch of the math library). Tested on a real AMD AVX-512 CPU, and under emulation on Intel CPUs with and without AVX-512 (qemu, Intel SDE) and an AMD AVX2 CPU (qemu).

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

Rollout​

  • Ship scoring-service and serving-api in the same release: the realtime scorers get the kernel pin from the scoring controller, refresh Jobs from serving-api, and until both are live the two sides run different kernels.
  • Before rollout, read each live realtime slot's int8 state (the scoring_int8_fallback gauge) and the CPU platform of the node its scorer runs on, to know which cost row applies.
  • The release restarts every realtime scorer (rolling, no downtime), and each restart re-runs the int8 self-validation on the pinned kernels. After the rollout, check scoring_int8_fallback for every slot (a bundle near the validation bounds can switch between int8 and full precision), scoring_cpu_kernel_pin_mismatches (0 on every scorer) and the forward p95 (scoring_stage_seconds{stage="forward"}).
  • Expect a one-time wave of band transitions on live int8 slots: entities near a band edge can cross it once as they are re-scored after the scorers restart (and on the next refresh of a realtime slot). For those entities the wave can trigger alerts, destination triggers and segment migrations. Plan the release in a quiet window and tell the slot's operator in advance.
  • Re-run the realtime-versus-refresh equality check only after both services are live and every scorer has restarted.

Availability wording corrected across the docs, website and console

fixeddocsconsole

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

fixedchangedapisdks (planned)

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

fixedapi

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

addedapiconsoledocs

The customer map now keeps itself up to date. Maps with customer groups update every night at 03:00 UTC by default, and the map you see is swapped for the new one when it is ready. A map with no standing credential to run as waits for an Admin.

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

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

Four new operations (refreshCustomerMap, getCustomerMapSettings, putCustomerMapSettings and listCustomerMapEditions) are additive and change no existing response; getCustomerMap now fills in the schedule, the pin and the latest change. The new permissions are customer_map.settings.get and customer_map.editions.list for readers, customer_map.refresh for Members and customer_map.settings.put for Admins; a custom role made earlier needs them added. See Customer map.

A five-step story on the segment map

addedconsole

"Present", the P key or the presenter bar on a map chapter now starts a five-step story on the segment map: the whole bank, the regions that emerge on their own, a high-churn region chosen by rule, one of its customers with their stored predictions, and the region explained. Every number in a caption is read from the map on screen.

  • The map page is read-only while the story runs: write controls show themselves disabled with the reason, and the console sends no write.
  • ?story=1 to ?story=5 in the address opens a step directly.
  • A step with nothing to show, such as a region with no stored predictions, is left out.

The trajectory inspector reads large prefixes in seconds

fixedapi

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

addedconsole

The segment map now draws each segment as a region: a soft outline around where its customers sit on the map, on a dark map surface.

  • Region names: a segment named by a person, or by the AI (with its AI badge), keeps that name. A segment with neither is named by its computed descriptor, such as "Higher churn risk", tagged Computed, with "Segment #N" as a small id under it. Descriptors come from a fixed rule over the map's prediction scores, never from AI, and the region list shows the sentences behind each one.
  • Four zoom levels: Bank (one title for the whole map), Regions, Sub-regions and Customers. The wheel, the zoom buttons, the level buttons and the keys +, -, 0 and 1-4 move between them; each level shows its own labels.
  • Sub-regions: dense areas inside a region, lettered by size (for example "4a"), each described against the rest of its region.
  • Region list: the regions and their sub-regions as a keyboard tree beside the map. Enter selects one, opens the selection panel with its customers and moves the map to it.
  • Dark or page theme: the map is dark in both page themes; "Match page" follows the page instead, and the choice is remembered in the browser.
  • A map built before segment ids were attached shows up to eight density regions, "Map region A" onward, and says that no segment build is attached.

The segment map opens with its customers settling into place

addedconsole

The first time an export's segment map opens in a browser tab, its points start as one cloud and settle into their positions over about two seconds. The segment names then fade in, and screen readers hear how many customers the map drew.

  • Once per export per tab: going back to the map, or reloading it, draws it at once.
  • Skipped when reduced motion is on, when the map opens on one customer, on a story or tour step or at a camera kept from earlier in the tab, in a hidden tab, on a software graphics renderer, or with ?nointro=1 in the address. The map then draws its final positions in its first frame.
  • Any input ends it: a click, a scroll, a touch or a key shows the map as it ends.
  • Large maps: above 20,000 points the same even sample a pan draws moves, and the full map takes its place at the end.

Ask the map answers verified questions about the segment map

addedconsole

"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

changedmodels

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

changedsecurityapiconsole

Suppression sets and model and dataset file retention belong to the project and apply in both its live and test environments. A request acting in the test environment (a tsk_test_ key, or the console switched to test) can now only add to them:

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

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

Make these changes with a live key, or from the console in the live environment. A registered application may still turn retention on or shorten it, and erase entities, in the projects it owns, also when it acts in test. In the console, these actions now show the refusal in test mode; disabling them there is a follow-up.

Retried requests no longer keep one-time secrets in plaintext

securityfixedapi

A POST sent with an Idempotency-Key stores its successful response for 24 hours, so that a retry after a lost response gets the same answer. The console sends a key with every write. Until now that stored copy was plain, so the one-time secrets these operations return stayed readable in the platform's database, in its backups and, when storing the copy failed, in its logs:

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

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

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

Response shapes are unchanged.

Transition. Stored copies made before this release are deleted when it deploys. Retrying such a request within its 24-hour window therefore runs it again instead of replaying it: you get a second API key, webhook endpoint or rotation, a new upload session or a new Stripe session, and a repeated invite returns already_exists. While the release rolls out, a request answered by a server still on the previous release is stored the old way, and a retry that reaches an updated server gets the 409 above for a few minutes, until that copy is deleted; a retry after that runs again. If a response was lost around the release, check your API keys and webhook endpoints and revoke or delete the ones you did not receive. If gateway-api is rolled back, the previous release cannot read a copy this release stored and returns an unreadable body for that retry; retry with a new Idempotency-Key.

A queued training run says when it is waiting for compute

fixedmodels

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

fixedsecurityapidocs

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

fixedsecurityapi

Every contract version that automatic onboarding approves is now recorded as a contract.approve audit event, in the same transaction as the approval. Before, only approvals made through POST /contracts/{version}:approve were.

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

Automatic onboarding also stops approving a new contract version when nothing changed. A re-proposal from more records of the same shapes, or from a batch without an optional field, used to mint a version whose diff was empty. It now approves nothing and records nothing. See Schema contracts.

Marker previews of silence patterns sample their read range evenly

fixedapi

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

fixedapiconsole

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

fixedapi

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

fixedsecurityapi

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

addedchangedapimodelsdocs

Model card exports (GET /models/{model_id}/card:export) and dossier downloads (GET /dossiers/{dossier_id}:download) in markdown and pdf now come from governance renderer version 2 (renderer_version: "2"). These are documents for people to read:

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

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

The API responses, including GET /models/{model_id}/card, and the json dossier download are unchanged. A new render of an unchanged card or dossier can have a different sha256; renderer_version tells the two renderers apart.

Every credential now acts only in its own environment

fixedsecuritychangedapiconsole

An API key holds exactly one project and environment, and a console session works in the environment it selects. Requests and the outputs they set up now stay in that environment:

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

The console has the title bar it shares with Argus, and every page names itself in its tab

changedaddedconsoledocs

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

securitychangedapiconnectorsdocs

Reads of a Stripe, Shopify, Amplitude or Mixpanel adapter source returned the encrypted form of its webhook signing secret in config.webhook_secret. This affected GET /sources, GET /sources/{source_id} and the create and update responses. It was never the secret itself, and only Tessera's own services can decrypt it, but any caller with read access could see it. Every Source response now shows {"set": true} in its place.

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

The response shape is unchanged: config is an open object, and only the value of webhook_secret changes.

Score one event synchronously against a sequence-model slot

addedapidocs

POST /slots/{slot}/scores:compute now answers for slots whose active bundle is a sequence model scored in realtime (WP-V17). The slot's realtime scorer scores the entity's recent history with the event included, exactly as it will once the event is ingested, and stores nothing.

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

See Synchronous scoring.

Score a raw source record synchronously

addedapidocs

POST /slots/{slot}/scores:compute now accepts raw_source_event: one record in your raw source's own shape, for a source bound to a raw-source preset (WP-V17 with WP-C13). The event is derived with the source's bound preset release, exactly as raw-source intake derives it, so it has the same event id and content as the event intake writes for that record. It is then scored like a canonical event.

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

See Synchronous scoring.

Realtime scores no longer depend on the events scored beside them

changedmodels

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

changedapisdks (planned)docs

POST /models:import-shared accepts an optional acknowledge_gates body member (WP-V15/WP-U23), the same acknowledgement createModelBundle takes. A shared checkpoint whose local evaluation fails only advisory gates can now be imported when you acknowledge exactly those gates with a reason; before, any advisory failure refused the import.

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

See Acknowledge at a shared-checkpoint import.

Description-channel models acknowledge their own quality gates like any model

changedapimodels

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

addedchangedapimodels

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

addedapimodelsdocs

You can now train, evaluate and serve an entity baseline model on any sequence dataset build (WP-M28), to compare with the sequence models trained on the same data and to serve when you choose it. It scores each entity from aggregate features of its recent history, with one calibrated gradient-boosted tree model per label.

  • Training: POST /entity-baseline-models trains on an existing sequence dataset build (any tokenizer format). Any succeeded build within the admission bound is admitted, and what Tessera found on the dataset (a training study, or a sequence model with a recorded G1 failure: quality, competitiveness or support) is recorded as admitted_by for context. Each label's arm (gbm_default, gbm_tuned or rfm_gbm) is chosen on validation or named explicitly. Taking it from a training study (study_id) takes effect when training studies ship.
  • Evaluation: the report uses the standard shape and gates. G3 does not apply, and G4 checks input parity between the dataset and serving. G1 and G2 follow the project's promotion policy, as for every model: under the default policy a failure needs an acknowledgement with a reason, recorded in the bundle and the audit log. G4, G5 and G6 block bundle creation.
  • Bundles and serving: POST /entity-baseline-models/{id}/bundles creates the bundle, which activates on a batch slot like any bundle; a non-v1 build's bundle also pins its tokenizer inputs, read with GET /entity-baseline-bundles/{bundle_id}/tokenizer-inputs; adding its pins and inserting each artifact at its position rebuilds the stored manifest that fingerprint covers. A refresh writes each entity's scores, and the new GET /slots/{slot}/entities/{type}/{id}/scores reads them. Batch score reads, ranked reads by score and delta, history, bands, score exports, destinations and the freshness monitor serve a baseline slot as well.
  • Custom roles: the family's operations, the tokenizer inputs read included, are new permissions in the role catalog. A growing catalog grants nothing implicitly (ERRATA 2026-10-12, WP-A5), so a custom role reaches them only once an administrator adds them; the built-in roles carry them at the floors of their sequence counterparts.
  • Unchanged: the released model, dataset, bundle and state responses. GET …/state and the other embedding-based reads refuse a baseline slot with model_family_not_supported.

Training studies: the default way to train a model

addedapimodelsconsolesdks (planned)docs

Correction (2026-10-21): the Python and TypeScript SDKs are planned, and the integration catalog lists both SDKs as Planned; this note said the SDKs have the four training-study operations. Use the HTTPS API for createTrainingStudy and the list, read and cancel operations.

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

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

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

The final confirmation of this work on our hosted deployment needs the WP-M26 release gate and its time holdout, so the evaluation of a study winner on our hosted project is confirmed after that gate ships.

Out-of-time holdouts for dataset builds, and labels censored at the end of the data

addedchangedapimodelsconsole

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

changedapimodels

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

changedapidocs

Each POST /evaluations:baseline run now gets a runtime estimate (WP-M22). It is based on the declared 45-minute target for the full baseline zoo plus one comparator on 100,000 entities with up to 1,024 events and 5 labels, and it scales up for larger work.

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

See Runtime estimate.

Full-book backtests stop at the end of the data

fixedapimodelsdocs

A full_book_out_of_time backtest now ends its window where the dataset's data ends, not only at its event-store snapshot. The window ends at the earliest of the snapshot instant, the time of the dataset's last event and the request instant, minus the protocol labels' longest maturation. Before this fix, a dataset exported before its snapshot was taken could be graded past its last event: churn-style labels counted positives that never happened, and conversion-style labels counted misses.

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

See Protocols and backtests.

See what a bundle needs before you create it

addedapimodels

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

changedsecurityapiingestconsole

A key or custom role with a purpose allow-list now needs the event-feed purpose to read raw event data. event-feed is already the purpose the committed event feed uses. Without it, these reads answer 403 permission_denied, with details[0].reason purpose_not_allowed naming event-feed:

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

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

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

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

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

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

Who is not affected:

  • keys and roles without an allow-list;
  • built-in roles;
  • applications.

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

To restore access:

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

A purpose registry that cannot be read now answers 503 unavailable, with reason purpose_registry_unavailable, on every service.

Dataset build retention: delete idle builds' files, and the erasure bound

addedapiconsolemodelsdocs

Dataset builds used to keep their files, and the event-store snapshot they pinned, forever. They can now be deleted whole and verifiably:

  • Retention: PUT /v1/dataset-file-retention (putDatasetFileRetention, org admin or the admin scope) sets how many days after a build became idle (finished, every model trained on it archived, nothing reading it) its files are deleted, from 1 to 365, or null (off, the default). GET /v1/dataset-file-retention reads it; it is versioned like the model file setting.
  • Explicit deletion: POST /v1/dataset-files/{dataset_id}:purge (purgeDatasetFiles) requests the deletion of a finished build's files, due 24 hours after the request at the earliest; with include_archived_models: true it also requests the deletion of its archived models' files. POST /v1/dataset-files/{dataset_id}:cancel-purge (cancelDatasetFilePurge) withdraws the build's request until the deletion starts.
  • State: GET /v1/dataset-files and GET /v1/dataset-files/{dataset_id} show each build's file lifecycle, its typed holds, its event-store snapshot pin, and how many entity deletions wait for that snapshot (blocking_erasures).
  • After the deletion starts, training, evaluating or rebuilding the same request on the build is refused with reason dataset_files_purged; the dataset and recipe reads keep answering from the frozen manifest. When the files are deleted the build's snapshot is released once no other build holds it, so the erasures waiting on it complete.
  • The erasure bound. With unpinned snapshots expiring after 7 days (see the snapshot TTL note of 2026-10-14) and builds deleted whole, the data deletion policy now states how long an erased entity's data can remain: R + D days with no readable copy and R + D + 7 days with no copy at all, for a replacement made R days after the request and both retentions at D days (41 and 48 days at 35 and 6).
  • Console: Settings → File retention gains the Dataset builds card and a partner preset that sets both retentions; each dataset page gains a Files section with Delete files now (and its archived models) and Cancel deletion for admins.
  • Docs: the data deletion policy 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

changedsecurityapiingest

Contract reads decorate each attribute with a profile whose examples and top_values come from attribute values sampled during ingest. Until now, those samples were kept with no link to the entity whose events held them. As a result, an erased entity's values (an email, say) could still appear in contract profiles after its deletion completed. They no longer can.

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

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

Operators. The entity-deletion role needs SELECT and UPDATE (samples, sample_attribution) on attribute_observations before the first run of this release's entity-deletion worker (the bring-up runbook, §3). On our hosted deployment they are lines of the deletion-maintenance grant manifest, which the grant step after the release applies (the default privileges that used to cover them are gone). Elsewhere, until the grant lands, each deletion pass and sweep that has samples to remove fails closed and retries on the next run.

Score one event synchronously against an event-model slot

addedapidocs

POST /slots/{slot}/scores:compute now answers for slots whose active bundle is an event model (WP-V17). It scores one canonical event in the same request, with per-label scores, bands and TreeSHAP explanations, and stores nothing: the event is not ingested and no state is written.

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

Slots serving a sequence model answer 503 scorer_unavailable from this endpoint for now. See Synchronous scoring.

Unpinned event-store snapshots now expire after 7 days

changedsecurityapimodelsdocs

An event-store snapshot that nothing pins now protects the files it reads for 7 days after it is taken, instead of 90 (WP-O7). So once nothing pins the event-store files an entity deletion superseded, they are deleted within 7 days. Snapshots taken before this change keep their 90-day expiry.

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

Released schemas are unchanged; the request descriptions name the new reason.

Leakage reports and new comparator checks no longer repeat identifier values

changedsecurityapimodelsdocs

An aborted build's leakage report no longer carries raw identifier values, and neither does a comparator validation run from this release on (WP-O7). The released schemas are unchanged: the affected values sit in fields they already type as free objects or strings.

  • Leakage evidence: when a build aborts because known identifier values were found inside a text feature (identifier_value_hit), the evidence now counts the values found (value_count) and lists up to five of them only as keyed digests (value_digests, each hash: followed by 64 hex characters). Before, it listed the values themselves.
  • Stored reports: reports stored before this release are rewritten into the same form, and each one's report_fingerprint is recomputed so it stays self-consistent. For such a report, value_count is the number of values it listed, at most five. GET /datasets/{id} and GET /event-datasets/{id} then serve the new values in the same shape; the build's updated_at does not change.
  • Comparator validation: a comparator validated from this release on still lists each offending row and column, but the message never repeats the cell's value.

Evaluation reports name entities and events by pseudonym

changedsecuritymodelsconsoledocs

Evaluation reports written from this release on no longer hold raw entity ids, canonical entity keys or raw event ids, and evaluations no longer store per-entity feature matrices (WP-O7). The released report schema is unchanged: the affected values are strings inside blocks it already passes through.

  • Pseudonyms: an entity is named by hash: followed by its keyed entity hash, the same per-project hash Tessera uses everywhere else. Comparator point-in-time violations carry it in entity_id. Gates G4 and G5 name an entity as {entity_type}:hash:{entity_hash} (plus #{instant} for an observation-time row). Event-model gates G4 and G5 name an event by hash: followed by a keyed hash of its id. Your files and ids never appear in a new report.
  • G4 divergences say where the two paths differ (the event, the channel, the first differing token position) and how many token positions differ. They no longer print the tokens themselves, which are the entity's behavioural data.
  • G5 bound: G5 lists at most 100 violations. The new violations_total metric keeps the full count, and the console shows it on the report, the gate guidance and the model card.
  • No stored feature matrices: the LightGBM baseline keeps its feature matrices in memory and stores only the fitted booster. A model card over an older report still lists that report's baseline_feature_matrix references, but no longer verifies them. Every other reference is still verified, and the card fails closed on a mismatch.
  • Older reports are immutable records and are not rewritten.

Deleting an entity removes it from comparator files

changedsecurityapimodelsdocs

Deleting an entity now also removes its rows from every comparator file of the project (WP-O7). The released schemas are unchanged.

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

Dataset builds record each split's longest row

changedapimodels

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

addedapimodels

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

addedchangedapisdks (planned)docs

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_version refuses a put when the declarations changed since you read them. A changed type for an approved field, a direct privacy class on a feature, or an identifier proposed as a feature is refused with a typed reason, and nothing is stored.
  • Reads: GET /v1/field-declarations (current) and GET /v1/field-declarations/{version}, at the same access level as contract reads. Replacing the declarations needs the admin role or an admin-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

addedapidocs

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

addeddocs

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

addeddocs

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

addeddocs

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

addedapiconsolemodelsdocs

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, org admin or the admin scope) sets how many days after the archive an archived model version's files are deleted, from 1 to 365, or null (off, the default). GET /v1/model-file-retention reads it. The setting covers both environments and is versioned: expected_version refuses a stale write with reason version_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 reason purge_already_started.
  • State: GET /v1/model-files and GET /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

changedapidocs

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

addedchangedapidocs

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

addedapimodels

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

addeddocs

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

addeddocs

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

changedapimodels

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 reason model_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_fingerprint is refused by the preview check first, with its existing reason (stale_preview, or local_artifact_mismatch if 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

changedapimodels

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

changedapimodelsconsole

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

addedapimodelssdks (planned)docs

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-labels declares 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 at timestamp + maturation_days. Labels are archived and restored like entity labels.
  • Event dataset builds: POST /v1/event-datasets assembles 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-models trains 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-report read them.
  • Bundles and slots: POST /v1/event-models/{model_id}/bundles promotes 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 answer model_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.explanation is {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.
  • The generated clients and the API reference are regenerated for the new operations.

Bind a source to a raw-source preset

addedapidocs

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

addeddocs

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

addedapidocs

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

addedapidocs

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

fixedapi

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

fixedapiconsole

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

fixedmodels

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

fixedapi

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

addedapidocs

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

addedapisdks (planned)docs

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_schedule and maturation_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_lag through the normal realtime scorer.
  • as_of and quiet_state: recorded with every state write of an enabled slot; read them with GET /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 emit score.updated), and alert payloads' as_of is the observation time.
  • Lagged cutoffs: on an enabled batch-only slot, a refresh or export without cutoff uses now − maturation_lag (sweep ticks use the lag on realtime slots); warehouse exports that include the as_of column carry each row's stored as_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

addedmodelsapi

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

changedapi

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

addedmodelsapi

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

addedmodels

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

addedmodelsapi

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

addedmodelsapi

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

addedmodelsapi

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

addedmodelsapi

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

fixedapidocs

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

addedapisdks (planned)docs

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}/governance and /v1/slots/{slot}/governance hold 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|pdf renders 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-templates lists four fingerprint-locked templates (sr11-7.v1, soc-security-review.v1, eu-ai-act-annex-iv.v1, works-council.v1). POST /v1/dossiers assembles 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|pdf downloads 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-inventory lists 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, and facts_withheld_reason says which and why. It needs the organization admin role or a custom role granting model_inventory.list.
  • The released GET /v1/models/{model_id}/card response 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

changedapiconnectors

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

fixedmodels

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

changedmodels

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

securitychangedapiconnectors

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

changedapi

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

changedapi

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

changedapimodelssdks (planned)

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

addedapimodels

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

addedapisdks (planned)models

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

changedmodels

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

addedchangedapidocs

/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.
  • PATCH with "status": "paused" stops ticks, and "active" resumes the schedule. On resume only its latest grid point runs.
  • Each schedule's last_run names the newest export one of its ticks created, with that export's status. Scheduled exports carry schedule_id and scheduled_for on the warehouse export record and in their manifest.

Every schedule in one list

addedapidocs

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

changedconsoledocs

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

changedaddedconsole

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

addedapiconnectorsdocs

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

addedapimodelssdks (planned)docs

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

addedapiconnectorsdocs

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

addedapisdks (planned)docs

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 plus cooldown_seconds and auto_resolve_after_seconds; at most 64 active rules per slot.
  • Episodes: GET /v1/slots/{slot}/alert-episodes and …/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}:disposition records true_positive | false_positive | benign | duplicate together with its audit record; with a rule label_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}/bands bind a fingerprinted banding policy per label.
  • Webhooks: alert.raised, alert.resolved, score.band_changed and entity.went_quiet payload 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

addedapisdks (planned)docs

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 a WarehouseExport record stating each state-snapshot export's destination, the column groups it wrote, the write mode it applied and, for scheduled exports, schedule_id and scheduled_for. The classic export record of /slots/{slot}/exports and of the export.* webhook data keeps its original shape, so existing API clients keep reading it: it never carries mode or schedule fields; POST /slots/{slot}/exports refuses a Snowflake destination or an extended column group with 400 invalid_argument naming the warehouse endpoint, its by-id read refuses such an export with 400 failed_precondition, and its list leaves such exports out.

  • Opened column aperture. include grows to the closed set embedding, scores, signals, segments, propensities, evidence, as_of: behavior_surprise, one propensity__{event_type} column per shortlist event type plus the propensity_status map, the evidence block and its frame, segment_id, and a nullable as_of. Per-row absence is NULL, never an error; values are byte-for-byte the stored state. Signal-bearing groups need a signals-enabled bundle (400 failed_precondition otherwise); colliding propensity column names fail with 400 invalid_argument naming both event types. Omitting include keeps the previous embedding + scores layout 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. s3 and snowflake exports decrypt the referenced source's vaulted credentials in serving-api: a deployment enables them by setting TESSERA_CONNECTOR_KMS_KEY for serving-api and applying its decrypt-only grant on the connector credentials key (Terraform connectors module); until then they answer 400 failed_precondition. An s3 credential_id now resolves only to a source of the export's own environment, and a disabled source authorizes no export.

  • Table write modes. mode: replace | merge | append for BigQuery and Snowflake: replace (the default) keeps today's behavior (atomic in Snowflake), append accumulates (entity, computed_at) score history without ever duplicating an unchanged state, merge upserts one row per entity. For append/merge, column growth is additive and a type conflict fails with a typed schema_conflict; BigQuery job failures are typed destination_write_failed.

  • Governance. Every exported row passes the delivery suppression filter first; manifest.json gains 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 and as_of is NULL.

  • Export schedules (interface). /slots/{slot}/export-schedules declares sch_ schedules that bind one export spec to a UTC cadence. They activate with the platform tick scheduler; until then creation answers 400 failed_precondition and 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

addedingestconnectors

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

addedchangedmodelsapi

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

addedapisdks (planned)docs

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

addedapiconsoledocs

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

addedchangedapiconsoleconnectors

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

addedchangedapidocs

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:batch pass 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 with 503 unavailable instead of returning unfiltered data.
  • Exports (every destination kind) run under the general purpose. 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 with unavailable and 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.updated and segment.migration webhooks do not apply suppression or holdouts yet.

Read exact per-source processing counts

addedapisdks (planned)

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

addedapi

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

addedapimodelsdocs

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

addedfixedapiconsolesdks (planned)

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

changedmodels

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

addedmodels

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

addedmodels

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

changedmodels

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

changedmodels

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

addedapi

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)

addedapisdks (planned)

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

addedapisdks (planned)models

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

addedapiconsole

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

addedchangedapimodelssdks (planned)docs

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

changedfixedconsole

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

addedchangeddocsapi

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

addedapiconsolemodels

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

addedapiconsole

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

addedapisdks (planned)

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

addedapiconnectorsingestdocs

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

addedapi

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

addedconsole

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

addedchangedsecurityapiconnectorsconsoledocs

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

addedapiingest

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

addedapiconnectorsdocs

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

addedapimodelsdocs

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

addedsecurityapidocs

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

addedapisdks (planned)docsv1

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, plus GET …/materializations and GET …/kpis: versioned CSV + Parquet member files with entered/exited counts, delivered to GCS or BigQuery and announced by one audience.synced or audience.failed webhook 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, top k up to 100,000).
  • GET/PUT /v1/audience-settings: pseudonymize_audiences replaces raw entity ids with the per-project entity_hash in 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

addedapiingest

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

fixedapi

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

fixedconsole

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

fixedconsole

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

fixedconsole

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

addedapisdks (planned)

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

addedingest

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

addedingest

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

fixedconsole

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

addedingest

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

addedingest

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

addedingest

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

addedingest

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

fixedconsole

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

fixedconsole

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

fixedconsole

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

changedfixedconsole

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

fixedconsole

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

addedfixedapi

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

fixeddocs

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

addedapisdks (planned)

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

fixedconsole

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

fixedconsole

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

addedmodelsdocs

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

addedmodelsdocs

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

fixedapi

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

fixedapiconsole

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

fixedingest

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

fixedconsole

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

fixedmodels

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

fixedconnectors

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

addedapi

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

fixedconsole

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

fixedconsole

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

addedfixedconsoleapi

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

fixedmodels

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

fixedapiconsole

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

fixedapi

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

fixedapi

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

fixedconsole

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

addedapi

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

fixedconsole

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

fixedconsole

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

fixedconsole

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

addedapi

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

fixedconsoledocs

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

fixedapiconsole

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

addedapi

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

addedfixedapiingest

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

addedapi

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

addedapisdks (planned)

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

fixedchangedmodelsapiconsolesdks (planned)docs

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

addedchangedconsolemodelsdocs

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

addedapimodelsconsoledocs

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

addedchangedmodelsconsoleapi

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

addedchangedmodelsapi

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

fixedapiconsolemodels

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

fixedapiconsole

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

fixedconsolemodels

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

addedchangedmodelsapi

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

addedchangedapimodelsdocs

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

fixedchangedapimodels

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

fixedconsole

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

fixedconsole

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

addedapi

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.alert webhook: published exactly once when a monitor enters alert, carrying the triggering drift point and the thresholds in force.
  • score.updated webhook: 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), and score.updated emission.
  • Freshness API: GET /v1/slots/{slot}/monitors/freshness serves event→state latency percentiles and queue age for realtime slots, and state staleness for batch-only slots; monitor summaries with per-kind status live at GET /v1/slots/{slot}/monitors.
  • Drift history is retained for 180 days and served with cursor pagination; reads require the read scope.

Console monitoring: drift dashboards, alerts, freshness SLOs, gate failures

addedconsoleapi

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 configured psi_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/alerts lists firing/resolved episodes with peak PSI, and POST …/monitors/alerts/{alert_id}:acknowledge records 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-failures lists evaluations whose promotion gates failed — the console links each record to the model's full evaluation report.
  • API additions: kind= filter on GET …/monitors/drift; alert and gate-failure endpoints above (all read-scoped; key-based acknowledgement needs admin).
  • 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

addedconsole

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 flat GET /usage rollups, 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 via Intl.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() and QuotaExceededState ship via @console/ui for every feature page: live resource_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

addedconsole

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}/exports with kind: 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

addedchangedconsole

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 → Webhooks lists your endpoints with subscribed event types and status; create an endpoint by picking event types from the real envelope enum (or "*"), get the whsec_… 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

changedconsole

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

changedconsole

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, both null_semantics arms (including what guard_min_positive_rate protects 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

changedconsole

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_timestamp no 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

changedconsole

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

changedconsole

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

addedchangedapiconsolesdks (planned)

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), and POST /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:archive now requires the project admin role (previously member); archived labels are excluded from new dataset builds with a typed label_archived blocker; 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)

addedconsoledocs

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_id in localStorage (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

addedconsole

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 /upgrade page — 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

addeddocs

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

fixedaddedapiconsolesdks (planned)

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

addeddocs

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.json published 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

addedapi

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 the cpu-batch pool: 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 and Segment #N names 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 via GET /slots/{slot}/segment-builds(/{id}) (https://spec.tessera.dev/schemas/segment_build.json); there is no build webhook.
  • Reads (read scope): GET /slots/{slot}/segments — the current registry summary with a stale flag (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/regression mean/p50/p90, multiclass argmax shares, plus behavior_surprise and 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_id by 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 throttled segment.migration webhooks (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 envelope id).

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)

addedapimodels

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

addedapi

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}/state returns the advancing event_cursor, bundle_id, model_fingerprint and computed_at after 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_served unit (dims: {slot_id, mode: "realtime"}), exactly like batch scoring meters mode: "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

addedconsole

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-model context rendered as separate labeled sections), event_type multi-select + since/until filters 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_ui flag is enabled; the dead-letter page now seeds its filters from URL parameters).

Console: training runs explain themselves \u2014 queued reasons, progress, human errors

changedconsole

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 to gpu-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

changedconsole

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

addedchangedconsole

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

addedconsole

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 (#4a6cff tonal 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_timestamp become 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

addedingestapidocs

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 (Stripe Stripe-Signature, Shopify X-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_version and 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:preview with a sample_payload returns 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 with reason=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

addedsdks (planned)ingest

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): buffered tessera.events.track(entity, eventType, attributes?, options?) — flush at 100 events or 2 s, bounded buffer, gzip batches, per-batch Idempotency-Key, exponential-backoff retries honoring retry_after_seconds, 207 per-item rejects via onReject (never retried) — plus full typed resource clients (tessera.sources, tessera.projects, …) and the raw generated core at tessera.api.
  • Browser build (@tessera/sdk/browser, also the browser bundler condition): event tracking only, fetch-based, no Node built-ins, ≤ 12 KiB min+gzip. Pending events are delivered on pagehide / visibilitychange: hidden via fetch(…, { keepalive: true }) with a navigator.sendBeacon fallback (payloads split under 60 KiB).
  • Key safety: with a publishable (tpk_) key, any non-ingest method throws TesseraScopeError locally, before any network call; the constructor refuses secret (tsk_) keys where window exists unless allowSecretKeyInBrowser: true.
  • Server-side CORS on ingest.staging.tessera.zone: preflight OPTIONS /v1/events answers 204 with Access-Control-Allow-Origin: <origin> + Vary: Origin only when the presented publishable key's allowed_origins exactly allowlists the request origin (scheme+host+port, no wildcard subdomains); actual tpk_ POSTs from missing or non-allowlisted origins are rejected with permission_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 (sendBeacon cannot set headers); secret keys in URLs are refused with invalid_argument.

Published to npm with provenance from CI on sdk-ts/vX.Y.Z tags.

Bus bridges: Kafka + native Google Pub/Sub intake

addedingestapi

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

addedconnectorsingest

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, pageView with default subscriptions; 429/5xx retry signaling honoring retry_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/events within 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 = CDP messageId (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/group are 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

addedapidocs

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 terminal status="deleted" as a scrubbed tombstone row; project.purge_started / project.purge_completed audit events carry per-store deletion counts and the verification result.
  • Restore: POST /projects/{project_id}:restore (org owner) cancels a pending purge during the grace window — the project returns to active (revoked API keys stay revoked). Once purging starts, restore fails with failed_precondition. GET /projects/{project_id}/purge (org admin+) returns the purge record and completion report.
  • Entity-level deletion (data-subject requests): POST /entities/{entity_type}/{entity_id}:delete (org admin+ session or admin-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 atomic deletion manifest supersession verified by row accounting. Physical file deletion respects pinned snapshots (awaiting_snapshot_release). On completion the raw entity_id is scrubbed from the record; the pseudonymous entity_hash remains the stable reference, and the audit trail (entity.delete_requested, entity.delete_completed) references the entity only by hash. GET /entity-deletions(?status=) and GET /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

securityapi

Supply-chain and DR posture is now enforced by CI and proven by drills:

  • Security checks (.github/workflows/security.yml): required contexts security/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 to platform/sbom/ on main) and security/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 turns security/policy red 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 of ledger_events/entity_state retained 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, quarterly restore-drill.yml + dispatch): unattended Cloud SQL/Bigtable/GCS restore verification in isolated throwaway resources, signed report per api/jsonschema/drill_report.json to platform/drills/; failures alert via RestoreDrillFailed (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 — with KeyRotationOverdue (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_denied before 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

addedmodelsapi

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 signals block — the resolved config, a fingerprinted signals_calibration.json artifact, and two non-mandatory quality checks (S1 surprise/count invariance, S2 propensity calibration) that never affect gates_passed. The evaluation request accepts an explicit shortlist and surprise window; defaults are the top-16 next-event types and K = min(64, W).
  • Bundles: POST /models/{model_id}/bundles accepts an optional body {"signals": {"enabled": true}}. It copies the report's signals config verbatim into runtime.signals and 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 detail signals_not_evaluated. No body — no signals: existing clients are unaffected.
  • State reads will surface the signals object 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)

addedapimodels

Labels can now be synthesized from the event stream itself — zero labeling effort, full auditability:

  • source.kind: "derived" on POST /labels: declare a DerivedOutcomeRule (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_days defaults to the rule's horizon_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 with rule_matches_nothing instead 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 as entity_hash).
  • GET /labels/{id}/stats for derived labels serves the latest materialization's support stats plus an additive derived block (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

addedconnectorsapi

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-once Source.setup object together with the exact ALTER USER … SET RSA_PUBLIC_KEY statement; 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 STAGE even on failure). test-connection runs authenticate (JWT), warehouse_usable, table_readable; an auth failure surfaces the ALTER USER statement.
  • 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 via COPY INTO '/Volumes/{unload_volume}/tessera/ {sync_id}/' … FILEFORMAT = PARQUET + Files-API downloads, with the directory deleted even on failure; no unload_volume ⇒ the sync fails unload_volume_required.
  • Redshift (RedshiftSourceConfig/RedshiftCredentials): UNLOAD to 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 as redshift_unload_cleanup_failed in the new non-fatal SyncJob.details array. test-connection runs authenticate, table_readable and an unload_probe full 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 + vaulted tunnel_private_key), pages of 50,000 ordered by (cursor, primary key) — a PK-less table fails mapping with keyset_tiebreak_unavailable (MySQL has no ctid fallback). Sessions force TRANSACTION READ ONLY and time_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", Snowflake NUMBER(p≤18,0)→integer, ARRAY limited to homogeneous string/number elements (mixed rows are rejected), naive TIMESTAMP_NTZ/DATETIME/timestamp columns require the mapping timezone, and VARIANT/OBJECT/SUPER/MAP/STRUCT/JSON/ BLOB/BIT/spatial types fail PUT /mapping naming the column.
  • API shape additions: Source.setup (reveal-once platform-minted credential material) and SyncJob.details (non-fatal typed warnings on any terminal status).

Stripe billing: self-serve Pro, transparent metered invoices, dunning

addedapiconsole

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-session opens 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-session opens 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/subscription returns the org's plan, billing state, current period and an upcoming-invoice preview whose line quantities are computed from the same usage rollups GET /v1/usage serves — every line is independently verifiable (billed_quantity = round_half_up(usage / divisor), amount = billed_quantity × unit price). Usage reaches Stripe hourly with set semantics, 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 restores active immediately. Transitions emit the new billing.payment_failed / billing.state_changed webhooks.

Plans, quotas & plan-driven rate limits

addedchangedapiingestsdks (planned)

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@latest at 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_projects and concurrent_training_runs are 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 publishes quota.warning / quota.exceeded webhooks exactly once per period. Over the events cap, POST /v1/events returns 429 resource_exhausted with retry_after_seconds; training runs above the plan tier or concurrency and projects beyond the cap get typed plan_limit denials.
  • GET /usage/quotas — a new read-scoped endpoint returning each evaluated limit's {metric, limit, used, period_end, enforcement}; GET /usage now embeds the same evaluated quota states.
  • 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/Reset headers now ride on every response, and 429s carry retry_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

addedapi

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-HTTPS sso_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's default_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 403 permission_denied (reason="sso_required"); org owners 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

addedapi

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 role admin+, console sessions only).
  • GET /v1/projects/{project_id}/audit-events — one project's feed (org role member+, or an admin-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

addedapi

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

addeddocs

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 both curl and 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.json carries 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 against api/openapi.yaml on 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

addedapi

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-free signals/segment_id when the state row carries them. Every scored response includes the mandatory freshness object — 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 with details[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 with status: found|not_found, HTTP 200 even when some items are missing; include selects embedding/scores/signals/segment_id.
  • GET /v1/entities/{type}/{id}/events — the entity's canonical event timeline, newest first, cursor-paginated (limit ≤ 200) with event_type, since, until filters; each item carries the canonical event plus received_at and source_id only. 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

addedapi

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 cutoff T — no event with timestamp > T influences 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-monotonic entity_state rows (a newer stored row is never regressed — skipped and counted in entities_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 (403 permission_denied with the service-account email otherwise);
    • S3 — via stored, envelope-encrypted credentials referenced by credential_id (an s3 source), never exposed anywhere.

    Columns: entity_type, entity_id (or entity_hash with pseudonymize: true), embedding (list<float32>), one score__{label_name} per binary/regression head and score__{label_name}__{class} per multiclass class, plus model_fingerprint, bundle_id, event_cursor_timestamp, event_cursor_event_id, computed_at, and a manifest.json.

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

addedconsole

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 is batch_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

addedconsole

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: direct and 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 the identifier_value_scan / label_temporal_gate toggles 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 approved role=feature attributes newly match.
  • Labels (/labels, /labels/new, /labels/{id}): a wizard covering name/task, source binding (label_events, label_table, or derived label-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 mandatory guard_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

addedconsole

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

addedsdks (planned)

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 to POST /v1/events at 100 events / 2 s (configurable), gzip-compressed, one Idempotency-Key per batch.
  • Reliability — retries honor retry_after_seconds on 429 and back off exponentially with jitter on 5xx/connection errors, reusing the batch's Idempotency-Key so retries are exactly-once server-side; per-item 207 rejections are surfaced via on_reject(event, code, message) and counted in FlushResult(accepted, rejected, dropped).
  • Flush guarantees — flush(), close(), context-manager support and an atexit hook 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

addedingest

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

addedmodelsapi

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), plus GET /models/{id} and POST /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 complete runtime block scorers read — model config, event window and label heads. GET /models/{id}/bundles lists a model's bundles newest-first.
  • GET/POST /slots — deployment slots: a stable, per-project-unique name (prod, shadow-1, …) with environment: live|test, mode: live|shadow and scoring: 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 mandatory reason) 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.activated and slot.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

addedmodelsapi

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.failed webhook — 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

addedmodelsapi

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_precondition otherwise); 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 the gpu-train pool (queue training).
  • GET /runs, GET /runs/{id} — status (queued | running | succeeded | failed | cancelled), pinned fingerprints, retained checkpoint URIs, best_val_loss, and the created model_id once succeeded (a ModelVersion in status training; 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

addedmodels

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 (typed ConfigError on any out-of-bounds or unknown key — unknown channel flags are rejected, not reserved), and a 002 §10 fingerprint. Satisfies the WP-M1 collator's SupportsBucketConfig.
  • 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 on scaled_dot_product_attention (is_causal=True, rotary tables precomputed once per max window and cached, no absolute/sinusoidal position tables anywhere — scanned by test), readouts mean_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_k with global observed-count normalization: counts and sums all-reduce (SUM) in a single collective when torch.distributed is 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_VERSION and per-file file_fingerprint; loads re-hash and fail closed (FingerprintMismatch), with the audited allow_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

addedmodels

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) and hid (% B + 0 -> 1 remap; bucket 0 = padding / no-value sentinel). Golden vectors in libs/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_applicable never 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) and EncoderSet whose canonical-JSON serialization makes encoders_fingerprint equal 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 in collate(), parameterized by the model config's B_attr/B_val/B_etype; byte-deterministic.
  • TOKENIZER_VERSION is pinned in PromotionBundle.pins.tokenizer_version (002 §9). Single-core throughput: ~440k tokens/s (floor 50k, enforced by benchmarks/bench_tokenizer.py and a test_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

addedapi

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; failed builds relaunch under the same dataset_id and 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

addedapiconsole

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 | derived sources, maturation, opt-in assume_negative_after_maturation with a mandatory guard_min_positive_rate ∈ (0, 1)). Creation registers the label's name and value_column/value_attribute into leakage deny-pattern scanning so contract inference marks matching attributes target_only, and refuses a label_events value attribute that is role: feature in the approved contract. name/entity_type/task are immutable after the first ingested observation; PATCH allows metadata, maturation_days, null_semantics, backdating_limit_days and source column renames only. New JSON Schema https://spec.tessera.dev/schemas/label_observation.json.
  • Label-table ingestion rides batch syncs: sources referenced by a label_table definition sync without a mapping and their rows become observations (never canonical events). Typed rejections — missing/ unparseable/naive observed_at, the +1 h future guard, the optional per-label backdating limit, task parse failures — are counted in the new labels block of sync details and written to the snapshot's rejected/ path. Accepted observations older than 30 days are flagged backdated.
  • Label-events routing: events matching a label_events definition 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 observations observed_at ≤ as_of, deduplicated by observation_key (earliest received_at wins), pinned reads re-hash and fail closed — the as-of join API dataset-builder uses.
  • Support stats: GET /labels/{id}/stats — observation counts, distinct entities, observed-at range, backdating, read-time maturation coverage, ISO-week rollups, and per-task positive_rate / class_counts / value_quantiles — maintained incrementally at ingestion (zero store scans per request; retries never double-count).

Public docs site & API reference pipeline

addeddocs

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.yaml on 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 runs pnpm --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-dev targets; make check now includes the docs gate.
  • Deploy: docs-deploy.yaml on merge to main — build once, rsync per env with immutable caching for hashed assets and no-cache for HTML and search-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

addedconsole

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/test environment from the top bar. Selections persist per user; while test is active a persistent banner marks the environment, and every request carries X-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 server error.details[] onto fields and show request_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-Key on every mutation, typed 003 §1 error envelopes, and automatic single retry of idempotent GETs on 429 honoring retry_after_seconds.

Streaming ingestion: POST /v1/events + the dead-letter store

addedingestapi

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 on events.ingest); the server sets the envelope (project_id, environment, source_id, received_at, ingest_id, canonical_entity_key) and derives missing event_ids per 002 §2 so retries are no-ops. Requires an API key with the ingest scope — secret (tsk_…) or publishable (tpk_…). The first use of a key creates-or-reuses its implicit kind=stream source (sdk:{key_prefix}). Per-key sustained event-rate limits (Phase-1 static defaults: 20,000 events/s live, 1,000 test, 500 publishable) answer 429 resource_exhausted with retry_after_seconds and X-RateLimit-* headers. Idempotency-Key dedups whole requests per project for 24 h. Each accepted request is metered as events_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 by reason, event_type, entity_type, entity_id; cursor-paginated; read scope), POST /v1/events/deadletter:replay and …:discard (by dlt_ids or {filter: {reason, before}}; admin scope). Replay republishes the original ingest message unchanged except envelope.replay_of; items are refused after 10 replays or once discarded. Retention: 30 days.
  • New schemas: https://spec.tessera.dev/schemas/ingest-message.json (the events.ingest message consumed by the stream router) and https://spec.tessera.dev/schemas/deadletter-item.json (the shape downstream consumers publish onto events.deadletter).

Observability stack & SLOs

addedapi

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 wrapping infra/prometheus/rules verbatim, in-cluster Grafana (dashboards file-provisioned via labeled ConfigMaps + sidecar), Alertmanager with severity page/ticket webhook 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.telemetry now 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-2 event_to_state_latency_seconds histogram). Route labels are templated paths only; 404s label unmatched. (Label change from WP-F3's scaffold: status→code, service added — WP-F3 tagged for review.)
  • tools/dashboards: one RED dashboard per service from one template into infra/grafana/dashboards (14 seeded); byte-for-byte drift check, alert convention + runbook lint, pinned promtool runner — all in make check via the new observability-check gate.
  • 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 (canary TSSRCANARY- + 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

addedapi

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 _lib components (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.yaml on platform/v* tags requires a passing smoke result for the same SHA (fails closed) + GitHub production environment approval.
  • Release ordering (tools/deploy): migration Job must reach Complete (10 min timeout) before any Deployment update; failed rollouts auto-undo; every Deployment carries deployment.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-deploy GSAs in all three env projects (infra/terraform/deploy); Artifact Registry tessera repo live in the dev/CI project. No service-account keys.

No public API surface change.

tessera-testing fakes & golden data

addedapi

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, fenced entity_state writes per 002 §8) — all raising TenancyError on 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 emulator CI job with docker-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 schema api/jsonschema/ground_truth_manifest.json.
  • Reusable pytest fixtures via a pytest11 plugin (fake_stores, scenario_*_small, emulator fixtures, emulator marker).
  • A scenario seeding tool (a CLI under tools/): seeds a scenario into fakes, emulators (idempotent ledger rows + a publish per event), or POST /v1/events per 003 §5.
  • Cross-cutting fix in tessera-core (WP-F3 tagged): single-bound ValueRangePredicate no 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

addedapi

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.json and api/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_time and never-materialized not_applicable.
  • Tenancy-enforcing GCS / Pub/Sub / Bigtable clients (TenancyError before any network call; GcsClient.platform for the reserved platform/ prefix; BigtableTransport is 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

addedapi

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-Id response header, security schemes for tsk_/tpk_ bearer keys, and GET /healthz.
  • Generated API surface artifacts: the client cores of the planned Python and TypeScript SDKs (tessera._generated; @tessera/sdk src/generated/), and the docs-site API reference (apps/docs/api-reference/), all produced by tessera-codegen.

GCP infrastructure baseline (Terraform)

addedapi

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

addedapiconsole

The public API gains the contract lifecycle (gateway-api + contract-service):

  • Contracts: POST /contracts infers a draft from the latest profiling data per the 002 §4 defaults (deny by default — unreviewed attributes are excluded); 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 to superseded. GET /contracts/current, GET /contracts/{version} decorate every attribute with read-only profile stats + an inferred flag; GET /contracts/{version}/diff?against= returns a structured diff with widening/narrowing/neutral risk per change. New JSON Schema https://spec.tessera.dev/schemas/schema_contract.json.
  • Leakage policy: GET/PUT /leakage-policy (immutable, fingerprinted versions; invalid regexes are rejected naming the pattern) and POST /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 Schemas leakage_policy.json and violation_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_ingest stamping, 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

addedingest

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 same manifest_id are idempotent.
  • Snapshot cursors: immutable, fingerprinted consistent points that dataset builds pin (pin_snapshot/release_snapshot); resolution is independent of later commits.
  • Reader.scan → pyarrow.RecordBatchReader with event-type and timestamp_micro range pushdown; partition listing is structurally impossible for readers.
  • Verified small-file compaction, per-plan retention with deferred physical deletion, and daily storage_bytes_day usage 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

addedconnectors

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 a row_restriction literal; read sessions are capped at 4 streams. test-connection runs authenticate, table_exists, storage_read_session and 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 + vaulted tunnel_private_key), keyset-paginated extraction ordered by (cursor, primary key) (or ctid) — never OFFSET — with 50k-row pages, a 5-minute per-page statement timeout, ≤ 3 retries with backoff, and default_transaction_read_only = on. test-connection runs tcp_or_tunnel, authenticate, table_readable, read_only (a user holding write privileges reports the new check status warn).
  • Normative type maps per kind (documented on the docs site, generated from code): mapping an unsupported column (e.g. BigQuery JSON, Postgres jsonb) fails PUT /sources/{id}/mapping with 400 invalid_argument naming the column and its type; cursor columns are restricted to cursor-capable types. Naive DATETIME/timestamp columns require the mapping timezone; DATE/date become midnight UTC; decimals with > 15 significant digits flag precision_loss in the sync's profiling report (new optional flags field).
  • API shape additions: ConnectionCheck.status gains warn; SyncError gains optional details — extraction_page_failed carries the failing page's keyset position.

File upload + object-storage connectors (GCS, S3)

addedconnectorsapi

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}:complete verifies the object (oversize ⇒ invalid_argument with the observed size), records its file_fingerprint, sniffs the CSV dialect + encoding from the first 1 MiB (delimiter, quote char, header presence, UTF-8/UTF-16/Latin-1 — explicit config.csv overrides win), attaches the object to a file_upload source (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 synthesize col_1…col_n; .gz/.zst CSVs decompress transparently.
  • Bucket connectors — source kinds gcs ({bucket, prefix?, glob?, manifest_object?}, no stored credential: access is the per-project service account the customer grants roles/storage.objectViewer; the connection test returns the exact grant command on permission failure) and s3 ({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 follows config.on_object_change: skip|reingest|fail (default fail ⇒ object_mutated); mode=full re-ingests everything.
  • Reserved: kind azure_blob ships as a Phase-2 stub behind the connectors.azure_blob feature flag (creation is failed_precondition while disabled; the connection test reports not_implemented).

Connectors: sources, mappings, batch syncs, profiling

addedconnectorsapi

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. Connector kind is the 003 §4 enum; per-kind config/credentials validate 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 expose credentials_set only. kind=stream records carry a config.subtype envelope (pubsub_subscription | adapter | implicit_sdk) for the streaming path.
  • Mappings: GET/PUT /sources/{id}/mapping stores immutable, fingerprinted column→canonical-event mapping versions (new JSON Schema https://spec.tessera.dev/schemas/source_mapping.json); POST /sources/{id}/mapping:preview applies 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 a connector-runner GKE Job on the cpu-batch pool: extract → immutable landed Parquet snapshot (_manifest.json written 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 extract cursor > watermark; the watermark advances only at commit. GET /syncs/{id} reports status/phase/counts/profile; terminal syncs publish the sync.completed webhook event and meter events_ingested + build_cpu_seconds.

Usage metering pipeline

addedapi

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; scope read; defaults to the last 30 days at granularity=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 id meter_id; fixed metric→unit mapping; closed dims key set. Services emit through the single tessera_core.metering.emit_usage helper.
  • Pipeline — metering-service consumes usage.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 BigQuery tessera_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

addedapi

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} per https://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 on id.
  • Signatures: every delivery carries Tessera-Signature: t=<ts>,v1=<hex> where hex = HMAC-SHA256(secret, "{t}." + body). Secrets are whsec_…, 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

addedapiconsole

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 (org owner only) marks the project deleting and 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 keys tsk_{env}_… (server-side, scopes ⊆ ingest/read/admin) and publishable keys tpk_{env}_… (client-side ingestion; always exactly ["ingest"], optional allowed_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-Environment headers, validated against org membership.
  • Authorization matrix: every operation is declared in api/authz.yaml and 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/Reset on every authenticated response; 429 resource_exhausted includes retry_after_seconds.
  • Idempotency: Idempotency-Key on 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

addedapiconsole

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 becomes owner; roles owner ⊃ 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/.