Repository navigation
Releases: toshish/traza
Release list
v0.26.0-preview.1
[0.26.0-preview.1] - 2026-09-06
Preview: an object-storage snapshot archive behind the new, default-off
object-storage cargo feature and the feature-gated traza-object binary.
A verified pin publishes to S3-compatible storage as an explicitly named,
immutable snapshot — manifest written last under a conditional create, every
object read back and SHA-256-verified before it is named — and reads back
three ways: queryable in place by chunk-verified range reads (exact engine
semantics: last-write-wins across segments, tenant scoping, content search,
cursors, payload retrieval), verified full restore into a fresh directory,
and snapshot administration (list, inspect, verify, delete, cleanup).
Added
object-storagefeature:traza::object_storage(Rust API) and
traza-object(CLI) for publish/query/restore/delete against S3-compatible
backends viaobject_store(env credentials, TLS verifying by default,
bounded per-request and per-transfer budgets with retries, plain HTTP only
by explicit opt-in), plus injectable in-memory and fault backends for
tests.- Deletion is fenced by permanent snapshot tombstones: a deleted (or
swept-after-abandonment) snapshot id is never reusable, which is what
makes delete and cleanup safe against concurrent or resumed publishers
across machines without a lock service. Sweeping an abandoned upload
requires an explicit publisher-quiescent acknowledgment, retires the id,
and the publisher re-checks the tombstone and its own upload marker
around its commit. - Every remote manifest is bound to the generation manifest it
archived before a query is served — an omitted or substituted segment
fails the open instead of shrinking answers — and the archived tombstone
log is strictly re-parsed for pending erasures. Store identity is
checked on every read, verify, delete and cleanup;publishreports a
manifest_sha256to retain externally and enforce via
--expect-manifest-sha, which is the stated trust root (the bucket is
not assumed Byzantine-proof). - Source pins are validated before anything they name is opened: canonical
file naming, symlink refusal, and refusal of unmanifested or live-store
files, so a stale manifest can never silently archive an older subset. segment::RangeSourceandSegment::open_from_source: local file opens
and remote opens now share one implementation, so a remote segment gets
the full v8 validation story (header CRC, section checksums, timestamp
grounding, per-read block/content-page CRCs) by construction. Remote
reads additionally verify a per-chunk SHA-256 table from the snapshot
manifest, including cache hits, under one global bounded chunk cache.Store::pin_for_object_archive(feature-gated): pin + verify + refuse
pending erasures — the pin publication requires and independently
re-checks from the pin's own tombstone log.- Operations guide:
docs/operations/object-storage.md, stating the scope
honestly — archives are historical immutable state, not tiering; local
TTL/erasure never reaches published snapshots (delete the snapshots, and
mind bucket versioning/object-lock); remote querying saves the archive
copy's disk, not the live store's.
Changed
pin_generationnow checkpoints and links under one maintenance/seal
acquisition, closing the between-locks gap in which compaction could
replace a just-manifested file and fail the pin.- Version metadata: crate and UI move to 0.26.0-preview.1. The standalone
MSRV stays 1.81; theobject-storagefeature's resolved toolchain floor
is recorded inpackage.metadata.traza.object-storage-rust-version.
Fixed
- Incremental SHA-256 preserves a buffered partial block across small or
empty updates. Object verification now remains correct across arbitrary
network chunk boundaries; one-shot digests and the storage format are
unchanged.
v0.25.0
[0.25.0] - 2026-09-06
A lossless storage-efficiency release: format v8 compacts persisted indexes
while preserving spans, payloads, query semantics, and retention settings.
Measured on the three bundled synthetic corpora (paired same-machine
before/after runs, identical ingested bytes; not a forecast for arbitrary
traffic): whole-data-directory size fell 44.2% on generic
(132,663,519 → 73,968,447 bytes for 1M service-trace spans), 31.4% on
llm (107,692,288 → 73,909,976 bytes for 200k LLM-call spans), and
38.1% on pinned-context (4,283,487 → 2,650,416 bytes for 10k
long-context spans). The full normalized export of all 1.21M baseline spans
is byte-identical after migration, and every migrated store verifies intact.
Evidence, latency pairs, and reproduction commands:
docs/benchmarks/storage-v8-comparison.md.
Changed
- Record offsets and trace/attribute postings use delta varints. Metadata
sections use LZ4 when it produces fewer bytes, with raw passthrough otherwise.
Record compression and content-addressed payloads retain their existing codecs. - Segment headers and all metadata used to exclude query results carry integrity
checks. Content-index pages are also checked when read after the segment opens. - Format v6 and v7 stores migrate automatically to v8, including pins and their
manifests. Migration preserves filenames and WAL fold boundaries and proves
complete coverage of legacy records before replacing a segment.
Fixed
- Corrupted persisted query indexes can no longer silently hide matching spans
at open. Malformed geometry and noncanonical varints are refused. - Legacy offset metadata that omits intact records is refused instead of
publishing an apparently healthy but incomplete migrated store.
Upgrade
Migration changes files in place through atomic replacements and resumes after
interruption. It is one-way from the first converted file. Stop the old server
and retain a complete cold backup plus a tested compatible binary before opening
existing data with this release. Downgrade requires restoring that backup;
replacing only the executable is insufficient. See
the format and migration contract.
v0.24.2
[0.24.2] - 2026-08-31
The follow-up an external review of the v0.24.1 line earned: the query
deadline now reaches the sidecar rebuilds and buffered sweeps it missed,
the v7 header's timestamp range is validated against real records instead
of trusted, and the filter benchmark refuses to publish a latency for an
answer it did not verify. Every finding was reproduced red before its fix.
Fixed
- The query deadline reaches the rollup sidecar rebuild. External
review of v0.24.1 found two query paths that check the budget per
segment and then rebuild a missing rollup sidecar INSIDE the segment
with no check at all — the scoped payload fetch's reachability probe
and the analytics fold's fast path. A 50k-span rebuild ran to
completion under a 1 ms budget (~1 s of decode) and answered as though
the probe had been cheap. The rebuild's record-decode loop now runs
under the calling request's budget at the standard cadence, with a
final check before it returns; second-round review found the sibling —
the supersede prefilter's key-hash rebuild, reachable per emitted span
on every query and fold — and it is threaded the same way. The
maintenance TTL sweep passes no budget, exactly as the deadline
contract exempts it, and the shadowed-tail scan never rebuilds at all
(a segment without a sidecar ends that scan). - Buffered spans no longer escape the query deadline. The same
review found every buffered-span sweep unchecked: an unflushed
20k-span corpus answered complete results under a zero budget on the
unlimited query, fold, session-union, and analytics-rollup paths, and
the limited query built its whole buffer source before the merge's
first check. Every buffer sweep on a query path now checks at the
standard cadence, and every budgeted query path performs one final
check before returning a completed answer — a request past its budget
is refused, never rewarded for finishing late. The deadline's cost
statement in docs/configuration.md is amended to match. - The segment header's timestamp range is validated, never trusted.
The same review demonstrated the harm: the two range words carry no
checksum,may_contain_timestampspruned whole segments on them
before any validation, and editing sixteen header bytes silently hid a
record the store could still decode — the false-negative predicate the
corruption contract forbids. Both opens now prove both declared bounds
against record bytes: the eager open against every record, the lazy
open by decoding the first block (whose fence-vs-first-record check
pins the declared min to the actual first record) and reading the last
record's timestamp — the true max, records being timestamp-sorted —
which must equal the declared max. Second-round review forced that
grounding: pinning the min to the first FENCE alone could be colluded
with, since the fence is unchecksummed metadata too — raising the
header word and the fence together still silently hid the head window.
A metadata-only forgery of the range is now refused at open on either
bound; a forgery must alter record bytes plus the block crc32 to get
past it, which the validation's own decode catches. An empty segment
must declare the canonical empty range, and any record read outside
the declared range still fails loud as corrupt. Deliberate amendment
#5 in docs/segment-format.md states the validation and its cost — two
bounded block decodes per lazy open, dropped from the cache after. - The canonical benchmark's filter gate gained a semantic oracle.
Review also noted the bench recorded filter latency on any 200 with
parseable JSON — a wrong or empty answer would have been timed and
published as a healthy number. Each filter sample now asserts the
exact span count the corpus construction defines, the requested
benchmark.groupvalue on every returned span, and the exact expected
span ids, before its latency is recorded; a wrong answer aborts the
run under the same refuse-to-publish rule as the gates.
v0.24.1
[0.24.1] - 2026-08-31
Fixed
- The compaction keeps-up test's tick deadline is sized for v7 merges.
The bound that proves a tick merges the backlog it found and returns —
rather than merging what arrives for as long as the writes do — was
twenty seconds, sized before v7 made every merge re-encode lz4 blocks.
The v0.24.0 tag run watched a starved shared runner push one legitimate
tick past it and fail release preflight, so the v0.24.0 tag exists but
published no artifacts on any channel; this release is the v7 line's
first shipped artifact. The tripwire is two minutes now, and the
comment says which failure the clock is for.
v0.23.0
[0.23.0] - 2026-08-26
The release where the production obligations land, and the first artifact to
carry them: everything between v0.22.1 and here ships for the first time,
including the [0.22.2] section below, whose tag was never cut. Generations and
checkpoints with hot backup and one-flag restore; erasure with a receipt that
re-checks every domain by name; tenant identity in the primary key; the eval
entity model; attribution that names the failing step; a compute deadline on
every query and a connection slot no panic can leak; and a six-demo tour that
CI holds green. The format pays for it once: segment formats 1–5 are spent,
and a pre-generation store migrates on first open, automatically.
Added
-
--query-deadline-ms(default 30000,0disables): a wall-clock budget
one query or aggregation may spend inside the engine, checked at segment
boundaries and every few thousand decoded records. Past it the request
answers400 {"error":"query deadline exceeded: ..."}with no rows — the
refusal that stops one runaway scan (an unindexable content search, a
whole-corpus fold) from holding a handler thread for minutes, and never a
partial aggregate, because a partial answer would look complete. A200
still means what it meant — every answer served is still the whole answer
— and durability is untouched: ingest, the tail, exports (bounded by the
socket timeout), and erasure work are all exempt. The worst measured
legitimate query in this repo is ~3 s, so the default never touches
measured use.traza_query_deadline_exceeded_totalcounts the HTTP
refusals; an MCP tool that exhausts the budget surfaces the refusal in its
tool result instead, uncounted.- Library surface:
Config::query_deadlineis a new field (None—
the unbounded behaviour every embedder had — by default; aConfig
built by struct literal must add it,..Config::default()need not),
andError::DeadlineExceededis a new variant of the exhaustive
Errorenum, so a downstream exhaustivematchmust add an arm.
- Library surface:
-
A demo tour in
examples/— six scripted proofs, one claim each. Five
new demos joinmcp-demo, andexamples/README.md
indexes the tour:swarmstreams a simulated agent platform into a live
dashboard in real time;crashSIGKILLs the server mid-ingest and proves
every acknowledged span survived, then corrupts a byte, watches
/v1/verifyname the file, and restores from a hot backup;needle
ingests a million spans over real HTTP and times the answers;incident
investigates a runaway session over MCP —diagnose_session, a defused
prompt injection,promote_failures_to_dataset, and an experiment diff
proving a fix;vanisherases one tenant's session next to an identical
twin tenant and walks the receipt, including the backup pin it refuses to
ignore. Every number each demo prints is measured during the run — a timed
request, a counter, a field from a live response — and each script asserts
its own claims and exits non-zero when one fails, which is whyci.shnow
runs all six at their smoke settings as merge gates.- Demos refuse to run against a port another server already holds:
readiness is the child's own startup line, not a probe that a squatter
can answer. This guard also landed inmcp-demo.
- Demos refuse to run against a port another server already holds:
-
Attribution: the server names the failing step, instead of the reader
doing it. A newdiagnose_sessionMCP tool answers why did this run
fail — the outcome, the step the failure is attributed to, and the
repetition behind it — with the evidence for each claim travelling beside
it. It replaces the two prompts that used to teach a model to read a trace
and judge its shape by eye ("repeated sibling names are a retry storm,
deep chains are a loop"), on the surface's own rule that a prompt wanting
a branch is a tool.- Every rule fires on telemetry that already exists. An earlier design
rested on asession.outcomeattribute and arelation: "retry-of"link
— both conventions Traza invented and nothing else emits — which would
have been a closed loop: seed the convention, detect it, pass CI, and
answercause: nullon every real store. A declared convention now
raises confidence and is never a precondition. The discriminators are
error density, serial fraction, self-similar depth and context growth. - Context growth is read from
context_tokens, not prompt tokens.
gen_ai.usage.cache_read_input_tokensandcache_creation_input_tokens
are recognized, because Anthropic reportsinput_tokensas the uncached
remainder: a growing conversation reports a prompt count that falls as
its cache warms, so reading the wrong field inverts the signal on the
configuration long-running agents actually use. Where the arithmetic is
unknown the trend is reported as unreadable rather than guessed. - It refuses to say more than the data supports. A shape is classified
only when a discriminator agrees; otherwise it is reported as
inconclusive with the missing signal named. Ordinary iteration is
reported as ordinary so a reader can see it was examined and set aside.
Absent token data can never satisfy a healthy classification. The
strongest test is silence: the analysis finds no fault anywhere in the
bundled corpus of healthy workloads. - Session outcome is first-class and provenance-tagged:
declaredwhen
a span said so (session.outcome,session.goal— Traza extensions,
labelled as honestly asllm.cost_usd),derivedfrom the run's own
spans otherwise, andunknown— never rendered as success — for a run
still in flight. A run that failed a call and recovered is a success that
reports its errors, not a failure.
- Every rule fires on telemetry that already exists. An earlier design
-
promote_failures_to_datasetcloses the loop into the M4 eval
entities: a session's attributed failures become a regression dataset
version, each example carrying its own copy plus provenance back to the
span. Re-promoting is idempotent. Gated by anrwtoken and a new
--mcp-promoteswitch, separate from--mcp-annotationsbecause an
annotation dies with its span while a promoted example deliberately
outlives its source. The caller names a session, never a span: the
promoted set is re-derived server-side from the diagnosis, so text injected
into the telemetry can change whether a promotion happens but never what it
copies — proven by a test that plants exactly that instruction. -
A runaway agent scenario in the seed corpus: an agent whose search tool
fails from the fourth turn on, so every later reflection appends the failure
to its context and asks again. Its steps are siblings under one root, the
shape real frameworks emit, and it carries no declared outcome and no retry
link — so finding it proves the analysis works on what a real pipeline
sends. -
Tenant identity in the primary key. Span identity is now
(tenant, trace_id, span_id)— everywhere: the write buffer's index, WAL
replay, segment supersede resolution, compaction's last-write-wins merge,
the tail ring's veils, cursors, the analytics key hashes, annotations and
erasure. Two tenants sharing a trace id can no longer silently upsert over
each other, which is the whole reason this ships now: keys cannot be
retrofitted after the format freeze. The default tenant is the empty
string and is never serialized, so a single-tenant store writes
byte-identical WAL frames, segment records and annotation lines to what it
wrote before tenancy existed — no segment or WAL format bump, proven by a
serialization-guard test. Tenant scoping reaches every surface the roadmap
names:- Credentials:
TRAZA_TOKENSentries take an optional binding,
scope@tenant:token. A bound credential ingests, queries, tails,
exports, annotates and erases exactly its own tenant; naming another
tenant is a 403, and the store-global operator surfaces (/v1/stats,
/v1/metrics,/v1/verify, checkpoint/flush/backups) refuse bound
tokens outright. OTLP exporters select a tenant with thetraza.tenant
resource attribute; MCP tools are scoped by the same binding. - Sessions are
(tenant, session_id)— the samesession.idunder
two tenants is two sessions, in the rollup sidecar (format v3,
self-healing rebuild), the session list, andgroup_by=sessionrows. - Retention takes per-tenant windows:
--tenant-ttl TENANT=SECONDS,
repeatable. A tenant's cutoff is its override, else the global TTL,
else never — and the segment retire-whole fast path only runs when a
global TTL covers everyone, because a whole-segment decision taken from
configured windows alone would delete an unswept tenant's data with the
segment. - Quota accounting:
GET /v1/tenantsreports per-tenant spans,
traces, serialized bytes and offloaded payload bytes from one exact
fold. Accounting, deliberately not enforcement. - Erasure: trace/span/session subjects carry a tenant (empty = the
default tenant, never "all"), and a newtenantsubject erases
everything a tenant owns — spans, annotations, scores, datasets,
experiments — with the same barrier, ordering, and receipt discipline
as M3. Reference-aware payload deletion now spans tenants: a blob two
tenants share survives one tenant's erasure and the receipt names why.
- Credentials:
-
The eval entity model — identity only, no workflow. The addressing the
product thesis requires, and nothing else: Dataset (stable id, name,
tenant), DatasetVersion (immutable, content-addressed manifest of
(example_id, digest)pairs with a parent version for lineage and the
promotion's provenance — re-POSTing identical content IS the same
version), Example (stable id across versions; input, optional
expected output, split...
v0.22.1
[0.22.1] - 2026-08-07
Fixed
- The release archive's dashboard now sits where the server actually
probes. v0.22.0 packaged the UI atui/distinside the archive, but
the binary-relative search — the packaging convention the source itself
documents — looks for<binary dir>/ui. Launched from inside the
extracted directory, the working-directory fallback hid the bug; launched
from anywhere else,/was a 404. The archive now places the build at
ui/, and every release archive is smoke-tested from an unrelated
working directory — dashboard 200, span accepted and read back —
before anything publishes, so this class of bug cannot ship twice. - The README's container quickstart no longer discards the token it
mints. The old one-liner generatedTRAZA_TOKENSinline, so the server
started and the reader had no way into their own dashboard. The token now
lands in a shell variable and is echoed before use.
Added
--version/-Vontraza-server, printing the crate version.- Third-party notices ship with the artifacts. Release archives and
container images now carryTHIRD_PARTY_NOTICES.md: the MIT and OFL
material for React, Inter and JetBrains Mono in the dashboard build, and
the dual-licensed Rust crates linked into the binary. - The release pipeline fails closed. A preflight job refuses a tag
whose version disagrees withCargo.toml,ui/package.json, or this
file, and runs the full./ci.shgate on the tagged commit before
anything builds; the crates.io job fails loudly when the registry token
is absent instead of skipping green. A new CI job checks the whole tree
on the advertised MSRV, holdingrust-versionto being true rather than
asserted — and its first runs proved the asserted 1.70 false twice over:
the locked dependency tree requires 1.71, and the test suite uses
File::set_times, stabilized in 1.75.rust-versionnow says 1.75,
because that is the number the gate can hold. SECURITY.md(private vulnerability reporting, supported versions,
and the threat model in one paragraph), a code of conduct, issue and PR
templates, and Dependabot coverage for Cargo, npm, and GitHub Actions.
Changed
- The container runs as uid 65534, with
/datashipped owned by that
uid so a named volume inherits it — a bind-mounted data directory must be
writable by 65534 — and images carry OCI source, version, and license
labels.
v0.22.0
[0.22.0] - 2026-08-07
Added
- An install, not a build. Tagged releases now ship what the README used
to ask you to compile:
traza-<version>-{linux-x86_64,linux-aarch64,macos-aarch64}.tar.gz, each
carrying thetraza-serverbinary (musl-static on Linux — no libc to
match) with the dashboard already built inui/dist, alongside
SHA256SUMSand GitHub build-provenance attestations
(gh attestation verify). The same binaries become the container image,
ghcr.io/toshish/traza, builtFROM scratch— an engine with two
dependencies does not need a distribution underneath it. A non-loopback
bind still refuses to start withoutTRAZA_TOKENS, in the container as
anywhere else. The tag also publishes to crates.io when the registry
token is present. - The merge bar runs in public.
./ci.sh— the same script, no drift —
now runs on Linux and macOS for every push and pull request, and the
README carries the badge. Until this release the script had only ever
gated one platform: the laptop it was written on.
Fixed
-
The dashboard now renders trace media wherever the trace actually put
it, instead of only the one lucky shape it recognized before. Found via
a production vision-review trace whose image showed as an empty frame: the
conversation view understood onlydata:/http(s):strings in a part's
data/uri/url, which missed most real emitters and — worse — poured
every other locator into the transcript as text (a bare-base64 screenshot
rendered as eight thousand lines of base64). The messages panel and the
trace inspector's payload modal now recognize, with tests: bare-base64
datalifted into adata:URI via the declared MIME type; Anthropic
source.{base64,url,file}parts; Google GenAI typelessinline_data/
file_dataparts; OpenAIinput_audio,image_url-object, andfile
parts; MCP-style tool-result content lists (screenshots included);
width/heightmetadata; and parts whose bytes were never captured,
which now say so with the emitter'sunavailable_reasoninstead of
presenting an empty frame that reads as a rendering bug. -
Offloading no longer demotes media to a JSON dump. A
gen_ai.{input,output}.messagesattribute past--payload-threshold-bytes
becomes a payload reference, and the old conversation view could only show
it as preview text — "load full" printed the raw JSON, megabytes of base64
included. The payload body is the original messages JSON, so the
conversation view now fetches it back (automatically up to 4 MiB, when the
turn nears the viewport; on demand above that, content-addressed and cached
across screens) and renders the parsed turns — an offloaded text-to-speech
span plays its audio again. The trace inspector's payload modal gained the
same understanding: message payloads render as turns, media payloads as
media, anything else as code, with the literal bytes onerawtoggle away. -
Every MCP tool now advertises
ToolAnnotations. The catalog carried
names, descriptions and schemas but noreadOnlyHint,destructiveHint,
idempotentHintoropenWorldHint— and absent hints are not neutral. Each
defaults to the pessimistic answer, so nine read-only tools were advertised
as potentially destructive and open-world. A host that gates on that asks for
approval on every call; one running non-interactively, with nobody to ask,
cancels them, which is what a Codex run did. The nine readers now declare
readOnlyHint: truewithopenWorldHint: false, andrecord_annotation
declares an additive, non-idempotent write. Tool nature is a required
argument of the builder rather than a step that can be skipped. -
describe_storereports the server version.initializecarried it in
serverInfo, but a host reads that once and need not pass it to the model,
so an agent asked which Traza it was talking to correctly answered that it
could not tell. The overview block — and thetraza://store/overview
resource that shares it — now names the version.
Changed
-
The measurement records moved out of the repository root into
docs/benchmarks/, and are named for what they
measure rather than shouted:BENCHMARKS.mdis now
docs/benchmarks/canonical-corpus.md, andINGEST-BENCHMARK.md,
STORAGE-BENCHMARK.md,QUERY-BENCHMARK.mdand
INDEX-MEM-BENCHMARK.{md,json}areingest.md,storage.md,query.md
andindex-memory.{md,json}. Six SHOUTING files at the root read as project
furniture on the level of README and LICENSE; they are generated
documentation, and they belong with the documentation that cites them.
Every benchmark binary writes to the new path, anddocs/README.mdnow
indexes all five records rather than two. Entries below this one name the
files as they were called at the time. -
query-benchpublishes its record only for the canonical configuration, the
rulebenchalready held itself to. Now that the record is a published
document, an experimental corpus or client count would otherwise have
overwritten the committed numbers with an answer to a different question. -
The advertised tool catalog is smaller than before the annotations were
added: 14,895 → 14,296 bytes, with 628 bytes of new annotations inside
that. The four accepted time formats were restated on ten properties while
theinitializeinstructions already spell them out once per session; that
repetition and some restated prose are gone. Every semantic warning stays —
word-not-substring, status-is-not-an-attribute, a missing key is kept, and
the ranking ceiling that sends a caller toslowest_spans. -
The published history was rewritten once, before the repository went
public, to remove an internal live-deployment analysis that had no
business in a public tree. Tagsv0.1.0throughv0.20.0are
bit-identical to the private originals; commits from thev0.21.0
release onward carry new hashes. Stated here because a lineage that
quietly changed under its tags would be worse than one that says so.