Skip to Content
Changelog

Every release, in order.

The server, the dashboard and the client packages ship together under one version number. This is what each of those numbers carried.

Current release
v0.14.11
Releases shipped
53
Changes logged
566
Last published
Sep 2, 2026
53 releases/hover the band to read one, click to jump
v0.14.11Sentry Grouping Parity
2026
v0.14.11Permalink to v0.14.11Patch
  • server
  • grouping
  • sentry-compat
  • dashboard
  • fix

Sentry Grouping Parity

Issue grouping reads the message Sentry renders, parameterizes event-specific values, and walks exception trees safely, so one bug stays one issue across upgrades

Grouping reads the message Sentry renders

An SDK sending logentry with a message template, params and a rendered formatted used to be grouped and titled by the template alone, because formatted was never read. Titles now use formatted, and when an SDK sends only the template, it is rendered with params the way the SDK would have.

  • Empty logentry fields are treated as absent, matching Sentry's or semantics, so an empty message no longer shadows a usable formatted
  • Grouping still keys on the template, so every rendering of one message lands in the same issue while titles show the real text

Message parameterization before grouping

Event-specific values in a message are replaced with placeholders before it is grouped, so one bug does not open one issue per id it mentions.

  • A port of Sentry's parameterization.py: emails, URLs, UUIDs, hashes, dates, durations, MACs, ints, floats, booleans and quoted strings, in Sentry's order. Applied to grouping only — titles keep the real text
  • Four patterns that need per-match validation callbacks are deliberately not ported, since a half-ported pattern that mangles text is worse than an absent one

Issues group like Sentry

  • Grouping resolves every exception in the chain, not just one, and picks the exception Sentry would title the issue by, including React Error wrappers and RxJava/Kotlin diagnostic wrappers
  • The tree walk is bounded at depth 1000 and detects cycles, where Sentry recurses unguarded
  • An issue's title, level and culprit follow its latest event

Grouping keys migrate instead of forking

The grouping key exactly as computed up to v0.14.10 is frozen in grouping_v1 and kept as a fallback. The digest tries the current key first, then the legacy one, then records the current key on the issue it finds — so each existing issue migrates itself on its next event instead of opening a fresh issue beside the old one.

Fixes

  • Issue search also matches the last frame's filename and module
  • The dashboard renders set_regression activity entries again, after the server-side enum rename left the component matching the old regression value
v0.14.10Permalink to v0.14.10Patch
  • server
  • security
  • dashboard
  • fix

Session Secret Validation

An unusable SESSION_SECRET_KEY stops the server before it touches the database, with a message that says what to do about it

An unusable session secret stops the server at startup

The cookie master key is a 256-bit signing key followed by a 256-bit encryption key, so SESSION_SECRET_KEY has to be at least 64 bytes. A shorter one used to panic deep in startup, after the database was open, the migrations had run and the workers were up, with a message about key length and nothing about the variable that set it.

  • The key is built in SecurityConfig::from_env, before any I/O, so a bad secret stops the process there
  • The error names the length it received and the command that produces a valid one: openssl rand -hex 32
  • openssl rand -base64 32 gives 44 characters and is refused with that same message, rather than a panic
  • SecurityConfig has a hand-written Debug that redacts the secret, so it cannot reach a log line through the derived Debug on Config
  • A valid secret still builds the same key it always did, so existing sessions survive the upgrade

The docs no longer publish a working key as their example value, and explain that changing the key invalidates every existing session.

Fixes

  • Webhook and Slack URLs are parsed with URL and checked for protocol instead of prefix-matched, so https:// on its own no longer passes validation and reaches the save
  • Enter or Space on a waterfall collapse chevron expands the span. The row handler calls preventDefault, which is what dispatches a native button's click, so keyboard use selected the row and never expanded it
  • A span whose reported end precedes the start of the view window, reachable when an SDK's clock runs backwards mid-span, no longer draws a bar with negative width. Both ends are clamped to the track
  • The toast progress value is normalized once and used for the bar, for aria-valuenow and for the caption. A caller reporting 150 drew a full bar and announced "150 %" against a declared maximum of 100
  • The data-table column header shows press feedback again. It opens a popup, and Base UI swallows :active on those, so it looked inert until the panel appeared

Improvements

  • Biome enforces a cognitive complexity limit across the workspace. The 28 violations it found were cleared by lifting their logic into modules that are tested directly: trace summaries, alert credentials and routing, integration forms, event jump targets, project setup snippets, transaction payloads, waterfall geometry and data-table query parsing
  • Vitest resolves the @/ alias, so a unit test whose subject imports a value through it no longer fails as if a package were missing
  • CI, CodeQL, cargo-deny and CodeRabbit run on pull requests to any base branch. A branches filter matches the PR's base, so a stacked PR or one onto a long-lived feature branch merged with no checks at all
v0.14.9Permalink to v0.14.9Patch
  • server
  • grouping
  • sentry-compat
  • fix

Empty Fingerprint Grouping

Fixes every error collapsing into one issue when an SDK sends an empty fingerprint.

Empty fingerprint falls back to default grouping

An event carrying "fingerprint": [] produced an empty grouping key, so every error in the project landed in the same issue. sentry-ruby sends that empty array on every event, and a fingerprint whose elements Relay drops (null, arrays, objects) ends up empty too.

  • An empty fingerprint now means "no custom fingerprint" and falls back to the default type, value and transaction key, the same as Sentry
  • A fingerprint that only contains droppable elements takes the same path
  • Non-empty custom fingerprints are unchanged

Existing issues created while the bug was live keep their empty key; new events group correctly from this version on.

v0.14.8Permalink to v0.14.8Patch
  • server
  • ingest
  • performance

Zero-Copy Ingest

The envelope body is parsed in place instead of copied per item, digest memory is bounded under bursts, and zstd joins the accepted encodings

The envelope is parsed in place

Ingest copied the request body once per envelope item, then copied it again to validate it, then a third time inside the digest. A single 4MB event cost several times its own size in transient allocations, and the read path was loading the full event payload for list views that never render it (@reneleonhardt).

  • The parser owns the body and hands out Bytes slices of the one allocation. A selected payload is copied only when keeping it would pin an envelope at least 16KB larger and four times its size, so a small item never holds a large buffer alive and a large item is never copied for a small saving
  • Event JSON is validated by walking it with IgnoredAny instead of building a serde_json::Value that is thrown away. Trailing garbage is still rejected, matching the previous full-parse semantics
  • The digest drops the raw file contents once the tree is parsed and the size check has run, so grouping and the database writes do not carry a second copy
  • The event list selects the columns it renders. SELECT * was loading and JSON-parsing the whole data blob per row only to drop it, the largest avoidable allocation in the read path

Bursts no longer spike memory

A burst of events spawned an unbounded number of digest tasks, each holding a full payload plus its parsed working set (@reneleonhardt).

  • Spawned digests pass through a gate of 16. Queued tasks wait holding only their metadata, and the file read, the JSON parse and the grouping working set all sit inside the permit. The gate lives on the app instance, not in a static, so each test server gets its own budget
  • The HTTP request path never waits on it: transactions and spans are still persisted inline regardless of the backlog
  • An envelope is capped at 1024 items, and log and span containers at 1024 entries each, bounding metadata amplification from many tiny items

zstd, and encodings that chain

  • zstd is accepted as a Content-Encoding, with the window log capped at 2^27, the smallest power of two that still covers the 100MB payload limit, so a frame header cannot make the decoder reserve an oversized history buffer
  • A list like gzip, zstd is decoded in reverse application order. identity is a no-op and coding names match case-insensitively
  • The header is rejected when it is not ASCII or carries a blank token inside a list. A utf-8 token, which some SDKs send, is ignored rather than treated as a coding
  • gzip reads multi-member streams, and deflate tries the zlib wrapper first and falls back to raw DEFLATE for older clients
  • Every codec enforces the 100MB decompressed ceiling while decoding instead of after, so a decompression bomb is stopped at the limit rather than allocated in full first

Improvements

  • Pending event records are written as a stream. Base64 is encoded in 48KB chunks straight into a buffered writer, so storing an event no longer builds a whole encoded copy of the payload in memory before writing it (@reneleonhardt)
  • A malformed pending record is preserved by hard link before the replacement is renamed into place, so a crash mid-publish can no longer leave recovery with neither the old record nor the new one. The temporary file is opened with create_new, a symlink at the canonical name is never treated as a complete record, and the parent directory is synced after publishing (@reneleonhardt)
  • The ingest directory is created once at startup and the path is shared, rather than being resolved and recreated on the request path
v0.14.7Permalink to v0.14.7Patch
  • server
  • ingest
  • alerts
  • security

Durable Ingest and Alert Delivery

Nothing acknowledged is lost, alerts send exactly once and off the digest path, and the instance version moves behind auth

Nothing acknowledged is lost

A 200 from the ingest endpoint is a promise that the data survived. Several paths returned it before the write landed, or treated a retry as a new delivery (@reneleonhardt).

  • Direct transaction, span, log and session items commit before the request is acknowledged
  • Replays are deduplicated by protocol identity: standalone spans by trace and span id, logs by a byte-stable container key, transactions before their child spans are extracted. An envelope with no event header derives its identity from the delivery UUID, so a headerless retry keeps the same durable key
  • SQLite keeps event files queued until the digest, the alert writes and a full WAL checkpoint all succeed. PostgreSQL and legacy storage are unchanged
  • Recovery no longer sweeps a temporary file that an atomic writer is still publishing, and a duplicate counts as idempotent only when both the stored record and the payload are valid
  • Session aggregate flush restores its in-memory state when the write fails, instead of dropping the window

Alerts send once, and off the digest path

  • Due retries are leased with UPDATE ... RETURNING, plus FOR UPDATE SKIP LOCKED on PostgreSQL, for 120s. A second worker tick or another process on shared PostgreSQL can no longer pick up a row mid-dispatch
  • trigger_alert stopped awaiting every dispatch inline, which stalled the digest up to 30s per channel on an unreachable webhook. History still commits synchronously and is born leased, so the retry worker takes over only once the lease expires and nothing is sent twice
  • Idempotency keys carry project identity, so the same event UUID in two projects no longer collapses into one alert
  • Retry backoff is bounded on every side: shared attempt limit, clamped exponent, clamped negative counts from corrupt state, jitter, and a cap of one hour. Retry payloads stay out of the public history response

A broken item no longer sinks the envelope

Relay discards an invalid item with an outcome and lets its siblings through. Returning 4xx here made the SDK drop that data permanently, so an envelope carrying an event plus one broken log container lost the event too.

  • Validation errors from direct-item processors drop the item and return 200. Database and storage failures still propagate as retryable 5xx
  • A failed quota refresh, typically SQLite busy under write load, degrades to the stale quota state with a warning instead of 500ing the envelope

The instance version is behind auth

GET /health/version sat inside the /health exemption and answered anyone, and the dashboard printed the same number on the login and error screens. A version turns "is this instance vulnerable to X" from a probe into a lookup.

  • The endpoint now takes ApiAuth, a session cookie or a Bearer token. /health and /health/ready stay open, since a probe has to work without credentials
  • The number lives on Settings, About. An architecture rule, version-behind-auth, keeps APP_VERSION out of everything above the gate

Breaking: monitoring that polls /health/version anonymously now gets a 401. Point it at /health or /health/ready, or give it a token.

Romanian, French and Spanish

Three more dashboard languages (@edideaur, #276), with fr, es and ro registered in the next-intl plugin, which still named only en and zh and so disagreed with routing.ts. The review pass fixed Romanian plural forms for fatal and configured counts, "téléversement" for a French upload since "dépôt" already names a repository here, and "journal" and "registro" for logs in prose, so the two empty states on the logs page agree.

Improvements

  • SQLite coalesces durability checkpoints across concurrent digests. PRAGMA wal_checkpoint(FULL) ran once per digested event and blocks readers, making it the dominant per-event cost under load. A digest whose commit predates a completed checkpoint is already durable and skips its own, so N concurrent digests cost at most two checkpoints
  • CI runs the PostgreSQL e2e suite. The default build is SQLite, which hid a column added as TEXT where AlertType decodes as varchar: every events-row decode failed at runtime and killed all digests, with green checks. The column is varchar now, and the suite that catches this runs on every change
  • The standalone span identity index builds CONCURRENTLY, outside the transactional migration, and the migration is re-runnable. An interrupted build leaves an INVALID index owning the name, so a DROP INDEX CONCURRENTLY IF EXISTS and a re-dedup run ahead of it, each as its own single-statement migration
  • Source map assembly tracks chunk ownership. A finished or exhausted job releases its rows and then deletes only the chunks no other job still owns, so a shared chunk survives and a terminal failure stops leaking storage. Empty manifest jobs finish instead of hanging
  • Pending-event recovery scans back off from 1 second up to 30 while there is work, and reset to 30 when idle. It was a fixed 30-second poll
  • HTTP/2 is disabled in actix-web until it moves to h2 0.4. That was the last path to the vulnerable h2 0.3, so cargo audit and cargo deny now run with no ignore list, across all features
  • The SQLite alert payload rollback rebuilds the table, since SQLite cannot drop a column in place, and v1 span items are written in 64-item transactions
v0.14.6Permalink to v0.14.6Patch
  • dashboard
  • docker
  • hotfix

Dashboard Image Hotfix

Fixes the v0.14.5 dashboard Docker image failing to start.

Dashboard image startup fix

The v0.14.5 dashboard image crash-looped on startup with Cannot find module '@swc/helpers/esm/_interop_require_default.js'. Next.js 16.3.1 bumped its internal @swc/helpers to 0.5.23, whose new module-sync exports condition makes require() on Node 22.10+ resolve to esm/ files that Next's standalone output never copies into the image.

  • Pin next>@swc/helpers to 0.5.15 via a pnpm override until Next traces the esm/ directory upstream (vercel/next.js#93852)
  • next dev was unaffected, which is why the break only surfaced in production

Improvements

  • The agent trace waterfall no longer grows past the viewport when a span label is very long; the pane truncates instead of pushing the span detail panel out of view
v0.14.5Permalink to v0.14.5Patch
  • server
  • sqlite
  • sourcemaps

SQLite Write Resilience and Hermes Source Maps

Bursts no longer lose events to a busy write lock, React Native stack traces symbolicate, and sentry-cli stops hanging on assemble

A busy write lock no longer costs you events

busy_timeout queues writers, but it drops the event when the lock is held past it. SQLite gives no safe way to resume a transaction that lost its snapshot, so the only correct recovery is to run the whole thing again.

  • A digest that hits a busy error retries its entire write transaction, up to three times with 50/100ms backoff, and reports the healed episode once it succeeds (@reneleonhardt)
  • Grouping, issue, event and the project's stored-event counter now commit or roll back together. A failure between them used to leave an issue counting an event nobody could open, with nothing to repair it, since a failed digest deletes the payload from the temp store
  • Quota state stays outside that transaction on purpose. It counts rows over time windows with a scan that cannot use the index, and holding the write lock across it would recreate the pathological holder the retry loop exists to survive
  • Pool exhaustion is deliberately not retried. It is a capacity signal, and retrying it only lengthens the queue that caused it
  • The SQLite issue delete moved onto BEGIN IMMEDIATE, since it reads counters before it writes. The write-first transactions that must keep a deferred BEGIN now say so in place: promoting them would hold the write lock across disk I/O and mass purges for no benefit (@reneleonhardt)
  • All of it is a no-op on PostgreSQL, where a per-project advisory lock serializes issue creation and no busy code is ever reported

Hermes and Metro source maps symbolicate

React Native bundles ship maps with x_facebook_sources, which the regular source map parser rejects outright, so every frame skipped the rewrite and stack traces stayed minified.

  • Maps are parsed with DecodedMap, which detects Hermes and regular maps alike (@roberteggl)
  • Original function names are resolved from the Hermes scope data, falling back to the token name for regular maps
  • Indexed maps are skipped rather than mis-parsed

sentry-cli stops hanging on chunk upload

  • POST .../files/difs/assemble always returns HTTP 200 and carries the outcome in the body. sentry-cli --wait polls until the state is ok and treats any non-200 as an unknown error; the missing-chunk case already reported not_found, only the status code was wrong (@roberteggl)
  • A poll that arrives after assembly finished is answered from the assembly job before the chunk check. The worker deletes consumed chunk rows on success, so checking chunks first made every post-completion poll report not_found and --wait never returned. Sentry's own blobs persist after assembly, which is why the order matters here and not there. Error jobs still fall through to the re-queue path

Improvements

  • WAL now runs with synchronous=NORMAL, removing one disk flush per commit from the write path (@reneleonhardt). The durability trade this makes, and when to pick PostgreSQL instead, is documented under Configuration → Database
  • busy_timeout stays at 5s, the value SQLite's own guidance pairs with WAL. Every writer outside the digest, sourcemap assembly, the storage purge, bulk issue updates, alert rules, the transaction, span and log processors, gets one shot at the write lock, so the timeout is their entire tolerance. A regression test now builds its pool through db::create_pool, pinning the settings the server actually ships instead of its own overrides
  • 18 dependencies updated across the JS and Rust workspaces, all pinned exact
v0.14.4Permalink to v0.14.4Patch
  • server
  • webview-ui
  • client
  • agents

Agent Trace Detail and Dashboard Totals

Every gen_ai attribute a span carries is now readable, and the agents dashboard reports numbers rather than only shapes

Spans carry their attributes, and now hand them over

Every gen_ai.* payload an SDK sent was already stored and simply unreachable.

  • GET /api/projects/{id}/spans/{span_id} returns a span with its attribute bag: prompts, responses, tool arguments and results, system instructions, tool definitions
  • Kept off the list response on purpose. spans.data is never trimmed, so a trace's worth of prompts would dwarf the waterfall they are drawn from
  • The two on-disk shapes are normalized: Spans Protocol v2 stores the flat attribute bag, while the legacy standalone and transaction-embedded producers store the whole span object with attributes under its own data key. Callers see one flat shape either way
  • Attributes whose declared type contradicts their value are dropped, as Relay does. An object-valued attribute could otherwise masquerade as a whole attribute bag and hide its siblings

The trace page has a details panel

  • Two panes: the waterfall beside a details panel for the selected span, with the selection in the URL so it is server-rendered and shareable
  • Model, agent, reasoning effort, conversation, token breakdown, available tools, input and output, and the raw attributes
  • It opens on the first LLM call rather than on nothing, matching Sentry
  • Providers disagree about whether input tokens include cached ones, so the reading is picked that brings input plus output closest to the reported total. Subtracting cached from an input that never contained it loses tokens that were billed. A warning fires when the parts miss the total by more than a percent, and a total is derived when the provider reports only the parts
  • The header gains LLM calls, errors and the models used

The dashboard reports numbers

It had six charts and no totals, and neither page could be scoped to a window or an environment.

  • /agents/summary, /agents/models, /agents/tools/stats and /agents/environments, surfaced as a totals row and two tables, per model and per tool
  • Every aggregate is filtered by window and environment, both held in the URL and preserved through trace pagination
  • The traces table gains started, LLM calls and errors
  • Cached-input and reasoning-output token counts are now stored, read under both attribute spellings since Relay renamed them and older data carries the old names

Cost stays deliberately excluded. gen_ai.cost.* is not SDK data, Relay computes it from a pricing table, so there is nothing to pass through.

Fixes

  • Platform, release and environment are stamped on transaction-embedded spans and on the promoted agent root. Both were left NULL to be recovered by a JOIN, but the dashboard filters spans directly, so an environment filter dropped every one of them and agent runs read zero beside a non-zero LLM call count for the same traces
  • The collapse control in the waterfall was an interactive element nested inside another, and is now a sibling of the row link
  • The selected span is constrained to the loaded trace, and a network failure is no longer reported as a missing span
  • Spans with missing timestamps sort deterministically
  • Three endpoints stopped publishing a limit parameter they ignore

Improvements

  • 33 dependencies updated across the JS and Rust workspaces, all pinned exact
  • Two hydration flags moved from useState plus a mount effect to useSyncExternalStore, so the correction lands before the first paint instead of after it
v0.14.3Permalink to v0.14.3Patch
  • webview-ui
  • server
  • client
  • i18n

Internationalization, and Language on the Account

The dashboard ships in English and Chinese, and language and timezone are stored on the user account instead of a cookie

The dashboard is translated

Built on next-intl and contributed by @LiJoeAllen.

  • 1117 message keys across two locales, English and Chinese, with every UI string extracted from more than 250 files
  • The portable core stays framework-free: model and lib take a translator parameter rather than reaching for a hook

Language and timezone belong to the account

Both are chosen in /settings/account and stored on the user, not in a cookie, which does not follow a reader to another browser and is lost with site data.

  • Two nullable columns, and nullable is the point: NULL means "has not chosen", a different state from "chose English". A NOT NULL DEFAULT 'en' would make the Accept-Language fallback unreachable and force English on anyone who never opened the setting
  • PATCH /auth/me takes either field. Absent leaves the stored value alone, explicit null clears it
  • Validation is by shape, not membership. pt-BR is accepted even though no dashboard here renders Portuguese, because this API is usable without the dashboard and hard-coding that dashboard's locale list would mean redeploying the server to add a language to a frontend. Same for zones, which the tz database changes without this server being rebuilt. What it refuses is anything not shaped like a tag or a zone
  • The browser's timezone is adopted onto the account once, only when unset. A reader who deliberately picked UTC while sitting in Madrid must not have it silently overwritten
  • A UTC reading is labelled and a local one is not, since a UTC time looks exactly like a local one and is silently hours out
  • The locale is never in the URL. A shared link opens in the reader's language rather than the sender's, which is what a team tool wants, and an internal dashboard behind a login has no use for indexable per-language URLs or per-locale caching
  • The language control sits with the other preferences rather than in the header, where it would earn permanent space for an annual click
  • @rustrak/client exposes both fields on the user

Dates and numbers follow the reader

  • 19 files formatted dates through date-fns with no locale, and 13 more called a bare toLocaleString(), which resolves the locale of whatever process runs it. A Server Component therefore used the container's and disagreed with the browser that hydrated it
  • All of it now goes through next-intl's useFormatter and getFormatter, with the option sets named once
  • date-fns is no longer a dependency

Improvements

  • Messages are split into a shell set and a dashboard set, so a route is served only the namespaces it renders rather than all thirty
  • Two architecture suites keep the translation from rotting: locale-completeness fails on a file that formats without the request locale, and message-keys fails on a translator bound to a namespace that does not exist, on a second English dictionary living in a .ts file, and on a key that no dictionary resolves
v0.14.2Permalink to v0.14.2Patch
  • webview-ui
  • server
  • client
  • mcp
  • tables
  • errors

A Shared Data Table, and Quieter 5xx

Six dashboard lists move onto one shared table, server errors stop putting their internals on the wire, and the MCP handshake reports the version it is actually running

One table behind six lists

Every list in the dashboard drew its own table. Issues kept a map of Tailwind widths that the header row and the row component each applied by hand, and its own docblock recorded the two having drifted apart once already.

A shell in shared/ui/components/data-table/, built on TanStack Table v9, now backs issues, logs, tokens, alert rules, project members and team members. issue-row.tsx and the ISSUE_COLUMNS map are gone: a header and its cells read one column declaration, so there is nothing left to keep in step.

  • A column either declares a fixed size in pixels or is marked meta.grow and takes what is left over. The distribution is computed once and handed to the browser as explicit numbers, because under table-layout: fixed a browser spreads any surplus across every column and declared widths stop meaning anything
  • Batch actions live inside the header row. Ticking a row slides the column titles down and drops the actions in from above, in the same space, with the tick column keeping its head so select-all survives the swap. The old bar above the table pushed everything below it down by its own height
  • Five features are registered. Sorting, filtering, resizing, pinning and grouping are absent because no table asks for them, and v9 features are opt-in and tree-shakable, so leaving them out is a bundle decision rather than a config one. Logs in particular cannot sort at all, since the API accepts only page, per_page, level and trace_id
  • A clickable row is a real tab stop with Enter, Space and a focus ring, and the hidden bulk bar is out of the tab order, so the row-is-the-control model holds for the keyboard too
  • settings/storage is deliberately left alone: it is a Server Component rendering five unpaginated rows with no client JavaScript, and converting it to gain a shared shell is a straight downgrade

A 5xx stops describing itself

The body of a server error carried the error's own Display. AppError::Database renders sqlx::Error, whose Postgres arm names the constraint, table and column of the failed query, and AppError::Internal interpolates whatever string its call site had to hand. Neither is safe on a wire that a caller holding nothing but a public DSN can read.

  • Every 5xx body now carries a fixed message, and the detail goes to a log::error! line keyed by an incident id
  • The id travels in the body as incident_id and in an X-Rustrak-Incident response header. The header is what lets one grep return both the access-log line naming the route and the log line carrying the detail, since the access logger is the only place that knows the method and path
  • It is present on 5xx and on nothing else. A 4xx says what went wrong in message, logs nothing, and so has no line for an id to point at
  • @rustrak/client exposes it as incidentId on server_error, omitted entirely when the response carries none: a proxy-generated 502, an older server, or a body that is not JSON at all. The client keeps discarding 5xx prose on its own side rather than trusting the server to have redacted it

Fixes

  • The MCP handshake advertised a hardcoded 0.1.0 while the package was at 0.14.1, so every AI client saw a version thirteen minors stale. It now reads the number from package.json, which the fixed group already bumps on every release, making the advertised version a consequence of that machinery rather than a second copy nobody was updating (#149)
  • A narrow viewport could paint a frame at the wide table's width before correcting itself
  • The log level filter now routes through the pager's transition

Improvements

  • Dependencies updated to their latest exact versions across the monorepo, including the docs site

10 of 53 releases