Skip to content

v0.30.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 23:18
· 130 commits to main since this release

[0.30.0] - 2026-11-03

Fixed

  • A resume position whose values do not have the order terms' types is
    refused
    (issue #1623). plan_page now checks every value of a resume
    tuple against the type it proved for the order term at that position, and
    refuses a mismatch with PagePlanError::ResumeValueTypeMismatch, naming the
    position, the term and both types, instead of rendering a literal of another
    type into the keyset predicate
    (a_resume_value_of_another_type_is_refused_at_any_position). A fixed-width
    binary value of another width is a mismatch. The MCP ravel_query_sql and
    ravel_search_logs tools answer such a cursor as cursor_invalid
    (a_cursor_whose_tuple_has_another_type_is_invalid).
  • A MetricsBench memory-step failure no longer stops the folded read-back
    (issue #2821). After an --rss-band breach or an OOM-killed container,
    run.sh lists memory in stamps.json as deferred_failures, runs stop,
    fold, start and readback-folded, and then exits as before: 75 naming
    memory, or 70 naming ingest after a failed ingest verification. A folded
    step that fails after the memory failure exits 70 naming that step.
  • The MetricsBench start step no longer reruns the one-shot services
    (issue #2821). After the fold, run.sh starts the stopped ravel-server
    container with docker compose up -d --no-deps --no-recreate ravel-server
    instead of docker compose start ravel-server, which also started its
    dependencies: on the real-S3 lane createbuckets reran its empty-bucket
    check against Ravel's bucket, which by then held the run's objects, exited
    1, and failed the step. The ravel_restart stamp and
    the step's failure message name the new command.
  • MetricsBench's Mimir comparator can now list the cardinality profile's
    1,000,000 series
    (issue #2821). In deploy/metricsbench/config/mimir.yaml,
    the gRPC message ceilings go from the 100 MiB default to 512 MiB: the
    server pair, and every client pair on the query path between Mimir's own
    components (six sections, listed in the README's frozen-limits table). At
    the default, the harness read-back's /api/v1/series answered 500 ("grpc:
    trying to send message larger than max (133231002 vs. 104857600)"), which
    made both cardinality runs of 2026-11-03 diagnostic. A response under
    100 MiB is unaffected.
  • A declared typed attribute column whose key starts with __ravel_ is
    refused
    (issue #3730, epic #1374). The prefix names Ravel's own
    synthetic columns (__ravel_attr[k], __ravel_row_ref, and the MCP
    tools' __ravel_group, __ravel_group_attrs, __ravel_depth and
    __ravel_parent), and before this nothing
    stopped a declaration from taking one of those names. The check is
    case-sensitive, so __Ravel_x is still an ordinary key
    (validation_rejects_reserved_prefix,
    validation_accepts_keys_that_only_resemble_the_reserved_prefix). It
    applies to ravel-cli typed-attr-column set, to ravel-server's
    --typed-attr-column and --typed-attr-column-tenant flags, which now
    fail startup on such a key, and to every durable tenant config write
    (set_tenant_config_refuses_a_new_reserved_key). A tenant config record
    stored before this rule still loads and its typed attribute columns are
    still served (a_stored_record_with_a_reserved_key_still_loads,
    a_stored_reserved_prefix_key_is_still_applied), and a durable write that
    carries such a stored key through unchanged, such as a retention change,
    still succeeds, while one adding a new reserved key is refused
    (set_tenant_config_keeps_a_stored_reserved_key_and_refuses_a_new_one).
    ravel-cli typed-attr-column set replaces the whole list, so it refuses
    any reserved key, a stored one included.
  • An MCP response that a row cap stopped stays inside its byte cap
    (issue #3733). The byte fit now measures the envelope with the row cap's
    rows_omitted count and row_cap_hit already written, and under the
    status finish gives it (ok_bounded whenever a cap took something), so
    neither can add bytes after the fit. Before, a response packed to its cap
    could end up a few bytes over it once the status changed from ok to
    ok_bounded. When the byte cap cuts, the kept rows or the one cut cell can
    now be a few bytes smaller than before.
  • A PromQL read no longer holds every object's samples outside the memory
    budget
    (issue #3747). The local sample prefetch folds each object's scalar
    and histogram runs into one per-series accumulator as the object's fetch
    completes, with the result merge_soa_runs gives in any completion order
    (scalar_fold_equals_the_merge_in_any_completion_order), and reserves what
    the fold holds against the engine memory budget. A series carried by many
    objects is held and charged once
    (a_sample_read_holds_one_copy_of_each_series_across_objects), and a read
    whose merged samples do not fit is refused with FetchMemoryExhausted
    against the budget's limit instead of reaching the host-memory floor
    (a_sample_read_that_outgrows_the_budget_is_refused_against_it). The
    charge is 24 bytes per held scalar sample, plus an overhead per series and
    per distinct provenance prefix, and shows under
    ravel_memory_reserved_bytes{component="fetch"}. A native-histogram sample
    costs substantially more, because its spans and buckets are charged too. A
    large read now either answers or is refused earlier, with "fetch memory
    exhausted", against the budget itself, so a deployment with a tight
    --memory-budget-bytes may see more refusals on very large reads.
    TooManySeries can now be raised partway through a fetch, at the series
    that crosses the limit.
  • The MCP compact text block escapes line separators, C1 controls and bidi
    controls in every caller string
    (issue #3750). String cells, column names,
    the failure message, warnings and next_steps text now escape U+2028,
    U+2029, the bidi controls U+202A to U+202E and U+2066 to U+2069, and the
    marks U+200E, U+200F and U+061C as \u{XXXX}, beside the control
    characters they already escaped. Map cells,
    the # shared line and each row's diff are serialized as JSON, and each of
    those characters the serializer left raw is written as JSON's own \uXXXX,
    so U+007F, U+0085 and U+009B no longer reach the text raw
    (no_caller_path_writes_a_hostile_character_raw) and each stays JSON that
    parses back to the original map
    (json_cells_parse_back_to_the_original_map). The warnings:
    line now quotes each warning and writes its " as \", so a warning
    containing ; no longer reads as two
    (one_warning_containing_the_separator_is_not_read_as_two). A
    next_step: line quotes its action the same way, so an action containing
    - is not read as a split
    (one_next_step_action_containing_the_separator_is_not_read_as_a_split).
    structuredContent is unchanged, but a consumer that parses the text block
    sees the new escapes and quoting.
  • The MCP docs no longer misstate accuracy.lower_bound_count (issue
    #3751). The reference and guide now say it is reserved and always false;
    0.29.0's guide said it was true on a COUNT over logs or spans. The
    reference also
    says that a budget_exceeded names its counter in failure.message, with
    failure.counter set only for the analyze-timeseries series cap.

Added

  • ravel_get_trace reports the span tree (#1382, epic #1374). Every span
    carries __ravel_depth, its count of returned ancestors, and
    __ravel_parent: root, present, cycle, or, for a parent id no
    returned span has, missing when the result is complete and not_returned
    when it is not, because a row or byte cap fired or coverage is partial
    (missing_parent_is_disclosed,
    a_row_capped_result_never_says_missing,
    get_trace_reports_the_span_tree_and_a_dropped_root). The tree is labelled
    after the byte fit over the rows that survive it, and the envelope stays
    inside the cap
    (a_byte_capped_result_labels_after_the_fit_and_stays_inside_the_cap). A
    child that starts before its parent still resolves
    (a_child_that_starts_before_its_parent_is_present), a parent loop ends
    the walk as cycle with a null depth
    (a_parent_cycle_ends_and_is_labelled), and a duplicated span id resolves
    to its first row (a_duplicated_span_id_resolves_to_its_first_row). A
    missing parent under a time_range adds a widen_time_range next step,
    and a not_returned one adds raise_max_rows
    (next_steps_appear_exactly_for_an_absent_parent). include_logs: true is
    still refused as unsupported, and its message now names
    ravel_search_logs (include_logs_names_search_logs).
  • benchmarks/mcp-eval holds the MCP agent evaluation corpus and runner
    (issue #1385, epic #1374). It has 24 questions across six journeys, their
    expected answers and evidence, the bands, and a seeded OTLP fixture.
    run.sh --check validates the corpus offline, --fixture loads the
    fixture into an empty tenant, and --run baseline|full runs every case
    through $MCP_EVAL_AGENT_CMD and scores correctness, evidence, tool calls,
    invalid queries, retries, store requests and bytes. No results are
    recorded yet, and a run refuses an --out inside the repository.
  • MetricsBench counts Ravel's objects after ingest (issue #2821). The
    new objects step of deploy/metricsbench/run.sh lists Ravel's bucket on
    either lane and writes ravel-objects.json with the count and bytes of data
    objects, commit records, store-qualify probes and everything else, and the
    optional --ravel-objects-band MIN:MAX fails the run when the data object
    count, and not the whole bucket, falls outside it.
  • The MCP server serves ravel_query_sql (epic #1374). It runs one
    read-only SELECT over one table, with its rows filtered to the required
    time_range rather than only the segments listed for it
    (query_sql_excludes_a_row_outside_time_range_from_an_overlapping_segment),
    and pages by keyset. The statement is checked before anything is read:
    two tables are invalid_argument with a split_the_statement next step,
    a write is unsupported, and a statement the page planner refuses, such
    as one with its own LIMIT or a float ORDER BY term, is
    invalid_argument naming the reason
    (statement_refusals_are_answered_before_the_backend). An ORDER BY term
    computed from other rows (an aggregate, a window function such as
    count(*) OVER (), or a subquery) carries no cursor, since a row committed
    between pages could move a group past it: the statement runs as written, a
    capped result is ok_bounded, and an order_by_the_rows_own_columns next
    step says that paging needs an order on the row's own columns (issue
    #3749). A full page of a
    statement with no ORDER BY to page by ends ok_bounded with no cursor
    (a_capped_statement_with_no_order_is_bounded). Under a total order
    the cursor resumes after the page's last row
    (query_sql_walks_a_total_order_three_pages_by_keyset); under an order that
    is not total, a page ends before an open group of equal rows
    (a_grouped_page_ends_before_an_open_group). A later page resolves at page
    one's instant and audits at its own clock
    (a_later_query_sql_page_resolves_at_the_mint_instant_and_audits_at_the_callers_clock).
    A full page with no cursor position (no complete group, a boundary value
    that has no cursor encoding, or a cursor over its 4 KiB bound) also ends
    ok_bounded with
    no cursor and a next step
    (no_complete_group_fits_is_bounded_with_no_cursor,
    a_boundary_with_no_cursor_position_is_bounded_with_a_next_step). A newer
    erasure expires the cursor (a_newer_erasure_expires_the_cursor), and a
    cursor sent with another statement or time_range is cursor_invalid
    (a_cursor_for_another_query_or_time_range_is_refused). max_rows defaults to
    200 with a ceiling of 5,000. An evidence_ref is refused as unsupported.
  • metricsbench_ingest --readback-only reads one system back from a kept
    ingest.json without replaying anything
    (#3685). It rebuilds the row's
    read-back plan from the same stream through the replay's own recording
    path, reads the row back with its commit tokens, and exits 0 on a match, 1
    on a mismatch, a read-back error or another setup failure, and 64 on a
    refused report or flag, including a stream that drifted from the kept one.
  • MetricsBench reads Ravel back on a folded catalog (#3685). After the
    memory step, deploy/metricsbench/run.sh stops ravel-server, folds the
    metrics catalog with every writer stopped, starts the server again, and
    repeats the Ravel read-back. It fails the run when the folded read-back
    does not match, or when its catalog GETs exceed
    --folded-catalog-get-floor (default 64) plus
    --folded-catalog-get-max-fraction (default 0.05) of the data objects the
    objects step listed; an unfolded read costs about one per object.
  • The MCP server serves ravel_query_promql (epic #1374). It runs an
    instant evaluation (evaluation_time) or a range evaluation
    (time_range with step) and answers one row per series, the series'
    ts_ns and value as parallel arrays, with NaN and -0.0 kept
    (query_promql_answers_both_modes_with_exact_samples). The mode is checked
    before anything is read: both modes, neither, or half of the range pair is
    invalid_argument (both_modes_or_neither_is_refused_before_the_backend),
    and so is a step without a unit or an empty range
    (a_bad_step_or_an_empty_range_is_refused_before_the_backend). A time or
    step that is not a whole millisecond is refused rather than rounded
    (sub_millisecond_inputs_are_refused_not_truncated). The range is
    half-open: steps run from start_ns while below end_ns, and the scope
    reports the step alignment and the 5 m lookback. A result that skipped
    part of its coverage is refused as unavailable unless the call sets
    allow_partial_coverage: true
    (partial_coverage_is_refused_without_consent_and_returned_with_it); a
    partial result returned with that consent reports accuracy.exact: false,
    with an approximation saying it omits the data the query could not reach
    (issue #3751).
    max_rows counts series, defaults to 200 with a ceiling of 5,000, and a
    capped result is ok_bounded with no cursor
    (query_promql_max_rows_caps_series_and_is_ok_bounded). A range of more
    than 11,000 steps, the engine's own limit, is invalid_argument before the
    backend call, with a next step naming the smallest step that fits
    (a_range_over_the_step_limit_is_refused_before_the_backend). A series
    whose samples do not fit max_response_bytes keeps a prefix of them as
    parallel arrays of one length, with samples_omitted counting the rest,
    and the result is ok_bounded
    (a_series_over_the_byte_cap_keeps_an_aligned_prefix,
    query_promql_bounds_a_long_series_by_steps_and_bytes). ravel_log_lines
    and ravel_log_bytes work over a logs tenant
    (query_promql_counts_log_lines_and_bytes). An evidence_ref is refused
    as unsupported.
  • ravel-cli load writes batches into a range of past ingest hours
    (issue #3700). --ingest-hour-stride <hours> --ingest-hour-start <unix hour> writes batch i into ingest hour start + i x stride through a shifted
    clock, so a fresh tenant can span many ingest hours for read-path
    measurement (ADR-3692 decision 8). The hours are synthetic, so the lane is
    for fresh measurement tenants only. It forces one fully acked batch per
    hour (--pipeline-depth 1, --read-cursors 1, --target-bytes 1), and
    refuses with exit code 2 before any write for --signal metrics, on a
    tenant that already has a HEAD or commit records, when the range reaches
    the current hour, or when the start is before 2020-01-01. Before each
    batch it fails the load if the catalog HEAD has appeared (a fold ran
    during the load) or if the batch's
    event times reach past its hour plus max_ingest_lag; a failed strided
    load is not resumable. The load report names the synthetic hours.
    ravel-cli catalog fold --snapshot-part-max-entries <n> overrides the
    snapshot part cap for one fold, so a small cap gives hour-ranged parts.
  • Ingest holds at the head while the flush backlog is over a bound
    (ADR-3718, issues #3720 and #3756). A background detector reads each ingest
    router's queued flush count and the routers' deferred-flush-trigger counts
    every 100 ms. The node counts as congested while any one router's queued
    flush count is above that router's bound and at least 1 flush trigger was
    deferred across all routers over the last full second. Neither signal alone
    counts: a healthy node at its queue cap defers triggers, and a node with
    many tenants can sit above the bound with no backlog. A router's bound is its
    shard count times --max-queued-flushes (32 at the defaults), unless
    --ingest-hold-queued-flushes sets one bound for every router. While it is
    congested, every OTLP and Remote Write request waits at the head, right after
    the in-flight permit and before the tenant is resolved, for up to
    --ingest-hold-max-wait (default 1s, at most 20s, the listener-join
    share of the default --shutdown-timeout). A request whose wait ends with
    the congestion cleared is admitted; one
    still congested at the max wait gets 429 with Retry-After: 1 on HTTP and
    RESOURCE_EXHAUSTED on gRPC. An OTAP stream holds once per
    BatchArrowRecords batch, after that batch's own in-flight permit; its
    tenant was resolved when the stream opened, and a refused batch ends the
    stream. Held requests are counted on ravel_ingest_head_hold_total{mode, outcome} and timed on the histogram ravel_ingest_head_hold_seconds{mode};
    the gauge ravel_ingest_head_congested{mode} reads 1 while the hold is on.
    --ingest-hold-max-wait 0 turns the hold off and spawns no detector task.
    On GKE at a fixed 160k log lines/s offered on 8 cores, the hold kept two
    overloaded nodes at 141k to 150k lines/s stored, where unheld collapses
    settled at about 112k.
    It does not make a node that locked into overload recover (#3718 comment
    6610502055).
  • The MCP server serves ravel_analyze_timeseries (epic #1374). It
    runs change_point or summary over each series of a PromQL range result
    and answers one row per series, through the same service method as
    POST /api/v1/analytics, so admission and the analytics audit record are
    the route's (analyze_timeseries_summary_is_exact_and_matches_the_http_route).
    A change point lands where the series changes
    (analyze_timeseries_places_a_change_point_where_it_was_built). Both ops
    are reported as heuristics, accuracy.exact: false with the method named,
    and every row carries the minimum point count and whether the series was
    downsampled; change_point bucket-averages a series over 2,000 points and
    summary never downsamples
    (analyze_timeseries_reports_downsampling_and_summary_never_does). A series
    under the minimum is a row marked too_few_points, not a failed call
    (analyze_timeseries_reports_too_few_points_per_row). The range arguments
    are checked by the same function as ravel_query_promql's, before anything
    is read, and both tools refuse the same inputs with the same message
    (both_tools_refuse_the_same_bad_ranges_before_the_backend,
    analyze_timeseries_refuses_bad_ranges_before_the_store). A result that
    skipped part of its coverage is refused as unavailable, with a next step
    to read the partial series through ravel_query_promql. max_rows counts
    series; the scan budgets and an evidence_ref are refused as
    unsupported.
  • The MCP server serves ravel_search_logs (epic #1374), the last of the
    nine tools. Typed predicates (eq, ne, in, exists) on typed attribute
    columns and an indexed trace_id, a has_word body search, a minimum
    severity and a trace id compile to one SELECT over logs, with every
    caller value rendered as a typed literal (text single-quoted, bytes as
    X'...')
    (caller_values_reach_the_statement_only_as_literals,
    search_logs_filters_and_pages_a_logs_tenant). A predicate key that is
    neither a typed attribute column nor trace_id with trace_id indexed is
    refused before any statement runs, an indexed key that is not a typed
    attribute column included, with a next step naming ravel_describe_data
    (an_unknown_key_is_refused_before_the_statement_runs,
    an_indexed_key_other_than_trace_id_is_refused_unless_declared). Results
    order and page by (ts, observed_ts), and a page never ends inside a group
    of rows equal on that pair (a_group_straddling_the_cap_is_not_split); a
    group larger than the page comes back ok_bounded with no cursor
    (a_group_larger_than_max_rows_is_bounded_with_no_cursor). Rows with the
    same attribute key set form a group, and the attributes the whole group
    shares are listed once in __ravel_group_attrs
    (shared_attributes_are_hoisted_once_per_group). The tool's two columns,
    __ravel_group and __ravel_group_attrs, cannot collide with a declared
    attribute column named group or group_attrs
    (search_logs_group_columns_cannot_collide_with_a_declared_column). An
    evidence_ref is refused as unsupported. All nine tools are now served
    and none is only catalogued: 0.29.0 served five, and this release adds
    ravel_query_sql, ravel_query_promql, ravel_analyze_timeseries and
    ravel_search_logs, and the span tree in ravel_get_trace, whose logs leg
    (include_logs) is still deferred.

Changed

  • The MCP tools reference is generated from the tool catalog. Each tool's
    title, description, annotations and arguments (JSON type and whether
    required) in docs/reference/mcp.md now come from the catalog the server
    advertises in tools/list, and a test fails when the page drifts from it.
    The reference and the guide now state, tool by tool, how an evidence_ref
    argument is handled.
  • MetricsBench runs Ravel at fetch concurrency 32 (issue #2821).
    deploy/metricsbench/ravel.yml sets --fetch-concurrency 32 in place of
    the derived default, max(8, 2 x cores), which is 8 in the benchmark's
    2-CPU container. It is disclosed in the README's settings list and in an
    ADR-0927 amendment. On real S3 the cardinality read-back is bound by GETs
    in flight. On the folded 1,000,000-series tenant, its /api/v1/series
    listing took 264 to 277 s at 8, past the 120 s deadline, and 71 to 74 s at
    32, with the same GETs and bytes. Ravel's own derivation is unchanged.
  • Cached log string columns hand SQL their values without a copy (issue
    #3231). With the decoded-column cache on (it is off by default:
    --decoded-cache-max-bytes defaults to 0), each cached string column keeps
    one Arrow values array over its own buffers, built once. A scan served
    from the cache, or the scan that admits the entry, now reuses that array
    instead of copying
    every cell: a plain column whose block no filter narrowed and whose rows
    are all present also skips the per-row key loop, and a block's own
    dictionary skips its per-scan values copy. Results are unchanged. The
    cache's memory reservation now lasts until the last Arrow buffer over an
    entry's bytes is gone, so the memory pressure evictor no longer counts as
    freed an entry a query result still holds. Each cached string column that
    can carry the array is charged 512 bytes more when admitted. A ClickBench
    run at an 8 GB cache measured 20% more evictions and 4% more misses per
    round, which the counters attribute to the larger charge (#3326 comment
    6607706943). Building the array counts
    once per cache entry in strCellsValidated. An offer refused because the
    column already carries a filled reservation (admitted concurrently under
    another key, or evicted while something outside the cache still held it)
    is counted in the new stats.decodedCache.filledCellRefusals.
  • The whole-segment fast path's serve-if-fits check stops early (issue
    #3642). With the decoded cache on, a LIKE '%...%' scan whose decoded-set
    estimate passes 0.95 of --decoded-cache-max-bytes stops reading segment
    directories at that point, since the verdict can no longer change. Its
    stats.decodedCache.deferredSetEstimateBytes is then the sum read so far,
    a lower bound, and the new deferredSetEstimatePartial: true says so. A
    scan that fits reads and reports exactly as before.

What's Changed

  • feat(ravel-logseg): write STREAM_DIR and FIELD_DIR after PAGE_DIR by @pmoust in #3232
  • perf(ravel-codec): decode integer pages in bulk by @pmoust in #3239
  • feat(ravel-sql): prune logs blocks for an integer <> predicate by @pmoust in #3247
  • fix(sql): keep string values charged while a downstream operator holds them by @pmoust in #3249
  • fix(parquet): walk page headers before the decoder skips a corrupt list by @pmoust in #3258
  • feat(server): export column-stats cache and fold drop metrics by @pmoust in #3259
  • fix(load): derive the pipeline depth a quarter past the rows bound by @pmoust in #3141
  • perf(sql): load column statistics only for the shapes the metadata paths answer by @pmoust in #3257
  • Measure bbtree and hash-then-sort against std maps on two hot paths by @pmoust in #3264
  • docs(adr-3217): withdraw decision 2 for the competing entry by @pmoust in #3255
  • feat(server): size the column-stats cache from the memory budget by @pmoust in #3260
  • fix(parquet): serve whole column chunks only in the scan phase by @pmoust in #3265
  • Measure hash-then-collect postings on stream-sorted rows by @pmoust in #3268
  • feat(ingest): expose the RLOG row-group size as a loader flag by @pmoust in #3267
  • perf(catalog): cache column statistics per part and per dictionary by @pmoust in #3272
  • feat(logseg): place declared columns first in the RLOG chunk order by @pmoust in #3277
  • perf(codec): reuse a per-thread zstd context on read-path decodes by @pmoust in #3278
  • fix(parquet): report a decoder error while scanning a file as Corrupt by @pmoust in #3266
  • perf(catalog): stream the column-statistics decode one segment at a time by @pmoust in #3279
  • fix(cli): run the depth flush with an explicit --target-bytes by @pmoust in #3282
  • feat(metricsbench): capture each comparator's effective version and engine by @pmoust in #3286
  • perf(sql): cast group keys back to dictionaries above the TopK by @pmoust in #3287
  • fix(cli): scale the load ack deadline with the flush target by @pmoust in #3290
  • fix(catalog): degrade a refused column-stats reservation to a scan by @pmoust in #3291
  • docs(adr-0927): amend the MetricsBench contract before the first portable run by @pmoust in #3284
  • chore(metricsbench): bump the Mimir comparator to 3.2.3 by @pmoust in #3285
  • feat(bench): anchor and stream the metricsbench ingest replay by @pmoust in #3288
  • feat(difftest): bound every PrometheusClient query by a complete-operation deadline by @pmoust in #3292
  • feat(sql): push LIKE '%literal%' on a declared str column as Contains by @pmoust in #3293
  • fix(ravel-sql): keep partitioned hash join inputs co-partitioned by @pmoust in #3294
  • fix(guards): anchor AI attribution trailers in authorship guard by @manulpatel in #3256
  • chore(metricsbench): freeze comparator ingest windows and query limits by @pmoust in #3296
  • feat(ravel-bench): move MetricsBench workload and corpus to version 2 by @pmoust in #3297
  • chore(release): prepare 0.27.0 by @pmoust in #3302
  • chore(metricsbench): bound Mimir query memory at 2 GiB by @pmoust in #3305
  • feat(bench): require the MetricsBench anchor and record stream provenance by @pmoust in #3298
  • feat(bench): classify MetricsBench outcomes into the seven statuses by @pmoust in #3303
  • chore(deps): clear the zstd advisories cargo deny reports by @pmoust in #3310
  • feat(server): export the catalog's decoded-cache bytes by @pmoust in #3307
  • feat(bench): add MetricsBench report schema version 3 and validate by @pmoust in #3315
  • perf(logseg): cut the contains kernel's per-row work outside the search by @pmoust in #3308
  • perf(ravel-catalog): keep each cached L0 record's derivation by @pmoust in #3311
  • perf(query): place only the filter sections the reader reads by @pmoust in #3318
  • perf(sql): lower skip-partial aggregation probe ratio to 0.1 by @pmoust in #3322
  • docs: state the probe and filter-section bounds as the code applies them by @pmoust in #3324
  • perf(cache): widen the S3-FIFO ghost window to 30% of the cache by @pmoust in #3323
  • docs: correct ghost-window figures and stale ADR-0046 references by @pmoust in #3325
  • feat(metricsbench): deploy Ravel in the portable lane at the comparators' envelope by @pmoust in #3309
  • feat(bench): send stale markers, native histograms and Ravel's row over Remote Write by @pmoust in #3314
  • docs(adr): amend ADR-0927 for warnings, Mimir limits, band 6 and acks by @pmoust in #3316
  • docs: add ADR-3331 Ravel Agents durable agent runtime by @pmoust in #3332
  • feat(catalog): bound the decoded caches' held bytes by @pmoust in #3365
  • docs(adr): propose ADR-3326, a decoded column-block cache by @pmoust in #3329
  • feat(sdk): add the TypeScript Agents SDK workspace skeleton by @pmoust in #3366
  • docs(metricsbench): size the max-samples limits by what each one counts by @pmoust in #3328
  • feat(server): add --max-series and --max-samples query ceilings by @pmoust in #3333
  • feat(bench): read each receiver's accepted set back and enforce band 6 by @pmoust in #3327
  • docs: name the region cost and the column id rule for chunk order by @pmoust in #3367
  • test: pin chunk order reads through POSTINGS, NumRange and records by @pmoust in #3369
  • feat(server): charge catalog decodes to the process memory budget by @pmoust in #3319
  • feat(bench): run the PromQL corpus over HTTP against every engine by @pmoust in #3368
  • docs: bring two #3244 changelog entries in line with the server wiring by @pmoust in #3371
  • fix(cli): bound --target-bytes and give every target its allowance by @pmoust in #3372
  • test(tla): add the agents protocol area by @pmoust in #3375
  • feat(metricsbench): freeze Ravel's per-query ceilings in the portable lane by @pmoust in #3377
  • feat(cli): warn about --chunk-order names no mapping key carries by @pmoust in #3378
  • feat(bench): collapse the full-range corpus entry under corpus version 3 by @pmoust in #3374
  • feat(bench): retry an unavailable oracle query before voiding the run by @pmoust in #3379
  • fix(cli): name a clamped derived target and reword the target bound by @pmoust in #3381
  • feat(metricsbench): drive a profile end to end and stamp what ran by @pmoust in #3384
  • feat(bench): write a diagnostics sidecar with ULP gaps and oracle retries by @pmoust in #3385
  • feat(agents): add ravel-agents-core contracts and skeleton crates by @pmoust in #3383
  • feat(bench): let the ingest run skip its in-process row by @pmoust in #3386
  • feat(metricsbench): run the large profiles without the in-process row by @pmoust in #3387
  • feat(metricsbench): write the query diagnostics sidecar on every run by @pmoust in #3388
  • feat(agents): add the ravel-agents-store publication skeleton by @pmoust in #3389
  • test(agents-sim): add the reference machine, history checker and scheduler by @pmoust in #3390
  • fix(metricsbench): repeat a short read-back until the receiver settles by @pmoust in #3391
  • docs(adr): amend ADR-3326 so admission keeps a headroom margin by @pmoust in #3380
  • feat(sdk): add the TypeScript SDK core for Ravel Agents by @pmoust in #3392
  • feat(server): add the agents feature seam and --agents flags by @pmoust in #3394
  • docs(tla): add the agents traceability table by @pmoust in #3396
  • docs(adr): amend ADR-3326 so the evictor frees shortfall plus margin by @pmoust in #3395
  • feat(agents-runtime): add the turn coordinator over AgentStore by @pmoust in #3397
  • docs(agents): add the Ravel Agents docs, key rows and IAM grants, rename the MCP guide by @pmoust in #3400
  • feat(metricsbench): keep replay requests in flight per series shard by @pmoust in #3398
  • test(ravel-server): re-pin mcp_e2e completed-call cost counters by @pmoust in #3402
  • feat(metricsbench): bound summation residue with ULP tolerances by @pmoust in #3405
  • feat(agents-sim): milestone 1 and 2 simulation schedules by @pmoust in #3406
  • docs(adr): accept ADR-3326 after Stage 0 and record the entry decision by @pmoust in #3407
  • chore(deps): bump pulldown-cmark to 0.13.5 for RUSTSEC-2026-0414 by @pmoust in #3409
  • ci: add agents, mcp and TypeScript SDK gate lanes by @pmoust in #3403
  • feat(server): serve the agents client and worker routes under --agents by @pmoust in #3411
  • feat(agents-runtime): journal tool steps and coordinate their effects by @pmoust in #3410
  • fix(metricsbench): equal nofile limits, a read-back that fits 1M series by @pmoust in #3414
  • perf(logseg): decode plain string pages into one buffer plus offsets by @pmoust in #3413
  • feat(tla): model the agents effect journal and one agent end to end by @pmoust in #3418
  • test(agents-sim): simulate steps, effects and reconcile by @pmoust in #3420
  • feat(sdk): run the agents SDK end to end against ravel-server by @pmoust in #3417
  • feat(agents-runtime): fire timers from HEAD and sweep wake hints by @pmoust in #3419
  • fix(metricsbench): read back in day windows and keep failed reports by @pmoust in #3423
  • feat(ravel-server): serve the agents step, effect and reconcile routes by @pmoust in #3422
  • refactor(logseg): hold every decoded column form behind an Arc by @pmoust in #3424
  • ci(metricsbench): run the nightly on a Ravel built from its own commit by @pmoust in #3426
  • feat(agents-sim): run every seed batch nightly against fresh windows by @pmoust in #3427
  • docs(tla): measure and band every agents configuration by @pmoust in #3428
  • test(logseg): make the chunk-order prune comparison prune partially by @pmoust in #3429
  • feat(sdk): scaffold a starter agent project and generate client types by @pmoust in #3430
  • fix(cli): state the target bound as absolute and warn on string resource keys by @pmoust in #3432
  • feat(agents): add ravel-agents-client and ravel-cli agents commands by @pmoust in #3440
  • feat(agents): serve timer, sweep, event resume and inspector routes by @pmoust in #3436
  • feat(metricsbench): run the stack's object store on s3s-fs by @pmoust in #3441
  • fix(metricsbench): pull the AWS CLI from Docker Hub by @pmoust in #3447
  • test(agents): check the store on RustFS and add an agents chaos lane by @pmoust in #3445
  • feat(query): add the ADR-3326 decoded column-block cache, off by default by @pmoust in #3439
  • feat(mcp): serve the Ravel Agents tools over MCP by @pmoust in #3446
  • feat(agents): page HEAD indexes into bounded immutable objects by @pmoust in #3450
  • test(agents-core): seed the kernel sequence proptest by @pmoust in #3455
  • docs(adr): run the MetricsBench Ravel row at a 500 ms flush delay by @pmoust in #3449
  • feat(metricsbench): run the Ravel row at a 500 ms flush delay by @pmoust in #3462
  • docs(adr): add ADR-3456 native GCS object store backend by @pmoust in #3457
  • test(metricsbench): pin the stated S3 request budget to its derivation by @pmoust in #3464
  • feat(agents): report how close each agent is to its lifetime caps by @pmoust in #3465
  • fix(query): keep the ghost credit when a decoded-cache admission is refused by @pmoust in #3466
  • docs(adr): keep cache-served columns off the tenant ceiling by @pmoust in #3461
  • feat(agents): project committed agent history into logs and traces by @pmoust in #3469
  • feat(agents-sim): measure agents cost per message, turn and pass by @pmoust in #3470
  • feat(object-store): add GcsStore, a read-write GCS backend by @pmoust in #3467
  • fix(query): hold one copy per series in a series listing by @pmoust in #3471
  • feat(server): run ravel-server on a GCS bucket with --store gcs by @pmoust in #3472
  • feat(cli): accept --store gcs by @pmoust in #3474
  • feat(server): run ravel-server on a GCS bucket with --store gcs by @pmoust in #3477
  • feat(agents): ship agents in the published image with an operator guide by @pmoust in #3480
  • docs(readme): add a Ravel Agents section by @pmoust in #3489
  • chore(release): prepare 0.28.0 by @pmoust in #3488
  • docs(readme): name 0.28.0 as the pin in the Agents section by @pmoust in #3491
  • docs(adr): add ADR-3492 ingest CPU per log line by @pmoust in #3493
  • feat(logseg): choose RLOG page encodings by estimate on request by @pmoust in #3500
  • feat(codec): tokenize without per-token allocation by @pmoust in #3501
  • perf(otlp): move values out of the logs request in normalize by @pmoust in #3502
  • test(sql): attribute the unfolded logs count poll's GETs by @pmoust in #3503
  • feat(clickbench): invalidate kit runs whose boxes differ in speed by @pmoust in #3504
  • test(server): hold dead-endpoint ports so parallel tests cannot bind them by @pmoust in #3505
  • feat(logseg): encode a block's column pages on a caller's executor by @pmoust in #3508
  • feat(ingest): cut per-record work on the OTLP logs ingest path by @pmoust in #3509
  • feat(ingest): seal logs flush column pages in parallel on the write gate by @pmoust in #3512
  • docs: add ADR-3513 per-tenant record cache byte budget flag by @pmoust in #3514
  • feat(sql): compact only each batch's top k rows under a TopK by @pmoust in #3516
  • feat(catalog): add per-tenant record cache byte budget flag by @pmoust in #3518
  • feat(sql): compile a constant regexp_replace pattern once per session by @pmoust in #3520
  • perf(logseg): index block pages per column chunk in PageDir by @pmoust in #3524
  • fix(query): skip the catalog of an object whose series a listing saw by @pmoust in #3507
  • fix(logseg): validate plain Str columns once at decode by @pmoust in #3515
  • fix(metricsbench): keep measuring every row after a read-back fails by @pmoust in #3438
  • docs(adr): make LIKE-deferred columns cacheable behind frequency-aware admission by @pmoust in #3526
  • fix(sql): keep NULL dictionary entries NULL in GROUP BY keys by @pmoust in #3534
  • feat(logseg): refuse decoded-cache admissions that would evict a more used entry by @pmoust in #3536
  • fix(agents): destroy in the runtime and null state before first turn by @pmoust in #3538
  • test(logseg): pin the admission gate's production-shaped traces by @pmoust in #3542
  • feat(ingest): time logs flush and strict-ack phases on /metrics by @pmoust in #3537
  • fix(agents): hand over only acknowledged reconcile writes by @pmoust in #3543
  • feat(sdk): record agent steps and effects on the server by @pmoust in #3544
  • fix(catalog): charge the fold's previous snapshot part decode by @pmoust in #3546
  • perf(logseg): build the page index per chunk on first lookup by @pmoust in #3549
  • revert: drop the decoded-cache frequency admission gate by @pmoust in #3550
  • fix(query): keep a listing's seen set under a shared half of the budget by @pmoust in #3551
  • fix(catalog): refuse a previous snapshot part naming another tenant by @pmoust in #3553
  • feat(agents): add the POST /api/v1/agents/destroy route by @pmoust in #3554
  • feat(agents): add AgentsClient::destroy and ravel-cli agents destroy by @pmoust in #3545
  • fix(metricsbench): say an oracle failure ends the run at query by @pmoust in #3555
  • docs: add a runbook entry for a fold the memory budget refuses by @pmoust in #3558
  • feat(sdk): kill worker A as a real process in kill-and-recover by @pmoust in #3560
  • docs(adr): record the ADR-3492 baseline as built and the next cuts by @pmoust in #3559
  • docs(adr): amend ADR-3331 to serve destroy over HTTP by @pmoust in #3527
  • fix(server): set TCP_NODELAY on every accepted connection by @pmoust in #3567
  • test(sql): cover every TopK pre-filter key type in the exactness property test by @pmoust in #3566
  • fix(guards): let only a collidable PR make the dispatch guard refuse by @pmoust in #3570
  • perf(logseg): build a chunk's page index only on its second lookup by @pmoust in #3571
  • docs(adrs): serve LIKE-deferred columns only when a scan fits by @pmoust in #3562
  • fix(sdk): abort a turn whose lease renewals fail past the lease by @pmoust in #3581
  • fix(agents-client): print a refused request's safe ids by @pmoust in #3582
  • feat(metricsbench): run s3s-fs 0.17.0-ravel.2, which sets TCP_NODELAY by @pmoust in #3573
  • docs: add ADR-3541 for a subject on the principal in audit records by @pmoust in #3574
  • perf(logseg): build the OTLP flush batch from rows by reference by @pmoust in #3585
  • feat(tenant-resolve): give Principal an optional bounded subject by @pmoust in #3584
  • docs(adrs): keep a zero-copy Arrow values array per cached Str column by @pmoust in #3587
  • feat(sql): accept a table-qualified min_commit_token over Flight SQL by @pmoust in #3589
  • fix(sql): refuse at mint a Flight ticket its surface refuses at decode by @pmoust in #3593
  • feat(ravel-server): record the principal subject on agents audit records by @pmoust in #3595
  • fix(catalog): check the tenant of every snapshot part read by @pmoust in #3592
  • fix(catalog): charge snapshot part reads to the memory budget by @pmoust in #3596
  • feat(audit): name the caller's subject on query audit records by @pmoust in #3597
  • feat(sql): refuse table-name shadowing and run logs LIKE as logs-only by @pmoust in #3599
  • fix(sql): report the TopK pre-filter's row bound and lost order by @pmoust in #3591
  • test: assert the quietest window in the bloom and tokenizer alloc pins by @pmoust in #3601
  • feat(auth): read the mTLS subject from --mtls-subject-header by @pmoust in #3600
  • docs: sweep stale one-table and Flight statements after ADR-2904 by @pmoust in #3602
  • fix(sql): close the carried multi-table review nits by @pmoust in #3604
  • perf(logseg): hash internally keyed encode maps with foldhash by @pmoust in #3603
  • feat(tools): add bin/agent-cli for the lookups sessions repeat by @pmoust in #3583
  • fix(codec): hash bloom staging sets with SipHash again by @pmoust in #3610
  • Nudge sessions toward agent-cli and measure its use by @pmoust in #3613
  • feat(segment): prune the sparse chunked catalog decode by equality by @pmoust in #3615
  • feat(mcp): split cursor redemption around the resolve by @pmoust in #3617
  • fix(maintain): cut RSPAN L1 parts before a KEY_IDX bucket overflows by @pmoust in #3618
  • fix(sql): enforce scan budgets on logs, spans, alerts and audit by @pmoust in #3619
  • feat(sql): report the pre-reserve-ahead peak in the SQL stats by @pmoust in #3621
  • feat(server): add QueryService::describe_data by @pmoust in #3624
  • feat(sql): add refuse_several_tables, the MCP SQL one-table check by @pmoust in #3622
  • fix(sql): refuse lock clauses and SELECT INTO in the read-only gate by @pmoust in #3623
  • feat(mcp): return an unfinished SQL draft and its cursor pins by @pmoust in #3627
  • feat(sql): add BucketedFinalAggregateExec behind a config field by @pmoust in #3628
  • feat(logseg): serve LIKE-deferred columns when the scan fits the cache by @pmoust in #3630
  • feat(metricsbench): add a real-S3 lane to run.sh by @pmoust in #3632
  • feat(image): compile the MCP server into the published image by @pmoust in #3631
  • test(sql): stop using the TopK as its own oracle over both zero signs by @pmoust in #3634
  • fix(server,sql): bill a cancelled exemplars or SQL call for every attempt by @pmoust in #3636
  • feat(mcp): serve ravel_explain_query and ravel_find_labels by @pmoust in #3635
  • fix(server): hand /metrics the audit counters, not the pipeline by @pmoust in #3637
  • feat(sql): cut each bucket to its exact top k in the bucketed final by @pmoust in #3640
  • fix(tools): refuse a fleet review with no structured result by @pmoust in #3647
  • perf(otlp): encode log stream attributes once per scope by @pmoust in #3650
  • feat(operator): serve MCP on query pods from RavelCluster by @pmoust in #3649
  • perf(logseg): stop regrowing per-row column vectors on the flush by @pmoust in #3656
  • feat(mcp): serve ravel_describe_data with keyset cursor paging by @pmoust in #3651
  • fix(mcp): pin a digest of the declared column set in cursors by @pmoust in #3661
  • fix(deps): bump pulldown-cmark to 0.13.6 for RUSTSEC-2026-0433 by @pmoust in #3667
  • docs: add ADR-3565 bucketed final aggregate for fixed-width keys by @pmoust in #3606
  • fix(ingest): back off provisioning reads after a failure by @pmoust in #3665
  • build(metricsbench): run s3s-fs ravel.3 on the portable stack by @pmoust in #3668
  • fix(metricsbench): give every engine 8 GiB and lift VM's series cap by @pmoust in #3669
  • fix(sql): memoise the fast-path decoded-set estimate per segment by @pmoust in #3660
  • feat(ingest): default to eight flush slots per shard by @pmoust in #3673
  • fix(server): fail ravel_describe_data on a failed metadata read by @pmoust in #3672
  • feat(ingest): make the flush object backstop multiple configurable by @pmoust in #3677
  • fix(mcp): report describe_data's committed row count as an upper bound by @pmoust in #3678
  • fix(sql): split the SQL request clock into resolve and caller instants by @pmoust in #3680
  • ci: build coverage without debug info and let a PR opt in by @pmoust in #3675
  • fix(sql): page exactly across a float zero and encode resume values by @pmoust in #3684
  • feat(metricsbench): count Ravel's objects by kind after ingest by @pmoust in #3686
  • feat(bench): add --readback-only to metricsbench_ingest by @pmoust in #3688
  • feat(mcp): serve ravel_query_sql with keyset and grouped paging by @pmoust in #3691
  • feat(metricsbench): read Ravel back on a folded catalog by @pmoust in #3697
  • chore(release): prepare 0.29.0 by @pmoust in #3696
  • docs: add ADR-3692 selective investigations across logs and traces by @pmoust in #3698
  • fix(metricsbench): run the folded read-back after a memory failure by @pmoust in #3716
  • feat(mcp): serve ravel_query_promql by @pmoust in #3715
  • docs: add ADR-3718 refuse ingest before the node's cores saturate by @pmoust in #3719
  • fix(metricsbench): start ravel-server alone after the fold by @pmoust in #3723
  • feat(cli): load batches into a range of synthetic ingest hours by @pmoust in #3726
  • feat(mcp): serve ravel_analyze_timeseries by @pmoust in #3724
  • feat(mcp): serve ravel_search_logs by @pmoust in #3728
  • feat(ingest): refuse ingest before the node's cores saturate by @pmoust in #3729
  • feat(mcp): report the span tree in ravel_get_trace by @pmoust in #3732
  • test(server): pin MCP protocol, escape routes and audit rows by @pmoust in #3735
  • fix(sql): check resume tuple types and pin page planner claims by @pmoust in #3736
  • fix(sql): stop the fast-path fit check once the estimate is over the cap by @pmoust in #3742
  • fix(metricsbench): raise Mimir's gRPC message ceilings to 512 MiB by @pmoust in #3743
  • docs(mcp): render the tool reference from the catalog by @pmoust in #3746
  • test(mcp): add the adversarial suite for the MCP tools by @pmoust in #3738
  • feat(bench): add the MCP agent evaluation corpus and runner by @pmoust in #3748
  • fix(mcp): escape every caller string in the compact text block by @pmoust in #3757
  • fix(sql): give aggregate and window order terms no cursor by @pmoust in #3759
  • fix(metricsbench): run Ravel at fetch concurrency 32 by @pmoust in #3758
  • perf(sql): keep a zero-copy values array on each cached string column by @pmoust in #3761
  • fix(mcp): report a partial PromQL answer as inexact by @pmoust in #3762
  • fix(query): fold PromQL sample runs as each object completes by @pmoust in #3763
  • fix(mcp): measure the row-cap fold inside the byte fit by @pmoust in #3765
  • feat(ingest): hold at the head while the flush backlog is over a bound by @pmoust in #3760
  • fix(catalog): refuse declared keys with the reserved _ravel prefix by @pmoust in #3766

New Contributors

Full Changelog: v0.26.0...v0.30.0

Downloads

The attached binaries were extracted unmodified from the signed container
images this release published, so each one is byte-identical to the file inside
the image you can pull. They are glibc-dynamic, built against Debian 12
(the runtime image is gcr.io/distroless/cc-debian12), so they need glibc 2.36
or newer. On an older distribution, run the container image instead:

docker pull ghcr.io/nofireai/ravel-server:0.30.0

One binary per platform, named <binary>-<os>-<arch>:

  • ravel-server-linux-amd64, ravel-server-linux-arm64
  • ravel-cli-linux-amd64, ravel-cli-linux-arm64
  • ravel-operator-linux-amd64, ravel-operator-linux-arm64
  • ravel-ingest-router-linux-amd64, ravel-ingest-router-linux-arm64

Verifying the downloads

SHA256SUMS covers every attached binary and symbol archive. Check the files
you downloaded, then verify SHA256SUMS itself against the signature and
certificate beside it. The signature is keyless, bound to this release
workflow's identity, so there is no key to obtain:

sha256sum --check --ignore-missing SHA256SUMS

cosign verify-blob \
  --certificate SHA256SUMS.pem \
  --signature SHA256SUMS.sig \
  --certificate-identity 'https://github.com/NOFireAI/ravel/.github/workflows/publish-images.yml@refs/tags/v0.30.0' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  SHA256SUMS

Debug symbols

The shipped binaries are stripped of debug info but keep their symbol table,
so a backtrace resolves function names with nothing extra installed. Line
numbers come from the matching symbols archive. Each binary records the
basename of its symbol file, so unpacking the archive next to the binary is
enough and no symbol-file step is needed:

tar -xzf ravel-debug-symbols-linux-amd64.tar.gz
gdb ./ravel-server-linux-amd64

gdb looks for ravel-server.debug in the same directory (and in
.debug/), which is the name inside the archive. Do not rename the extracted
.debug files.