Skip to content

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 02:10
· 117 commits to main since this release
a9aa285

OmniGraph v0.12.0

Released 2026-10-05.

Upgrade actions

  • --allow-data-loss is removed from omnigraph schema plan and
    omnigraph schema apply. In v0.11 a hard drop deleted the dropped table's
    older data during apply; since table writes publish detached versions, no
    drop reclaims anything at apply, and the flag only labelled drop steps as
    hard. Older commits keep reading dropped data until omnigraph cleanup
    stops retaining them, and then it cannot be recovered. The allow_data_loss
    field of POST /graphs/{id}/schema/apply, the mode of drop steps in plan
    and apply output, and the Rust SchemaApplyOptions and DropMode types go
    too.

    Scripts that pass the flag now fail with an unknown-argument error; remove
    it. The server ignores an allow_data_loss field still sent in a request
    body. Rust callers use plan_schema, preview_schema_apply,
    apply_schema_as(source, actor) and
    apply_schema_as_with_catalog_check(source, actor, check) in place of the
    *_with_options forms. To reclaim the space a drop frees, run
    omnigraph cleanup with a retention that excludes the commits before the
    drop, for example --keep 1. See the
    schema guide and
    cleanup.

  • Clients that send DateTime strings with more than three fractional digits
    must truncate them to milliseconds, unless the extra digits are zeros: such
    a string used to load and match at its millisecond and now fails the
    request. Python's datetime.isoformat() writes microseconds by default; use
    isoformat(timespec="milliseconds"). A stored or cluster-declared query
    whose source holds such a datetime(...) literal now fails type checking,
    so its graph fails startup until the literal is trimmed. A Rust caller that
    binds a DateTime parameter through ParamMap must bind a
    Literal::DateTime (a Literal::List of them for [DateTime], or
    Literal::Null on a nullable parameter); a Literal::String or any other
    literal is refused, as for Date. A parameter named __nanograph_now is
    refused: the name is reserved for the clock now() reads, and a value bound
    under it used to replace that clock without any check.

  • External Blob bases may no longer overlap OmniGraph storage. A base inside
    or above a graph root or the cluster storage root let any authorized writer
    copy another graph's tables, its own manifest, or the cluster ledger into a
    Blob value readable through GET /blob. cluster validate, plan and
    apply now refuse such a base, in either scope, with
    external_blob_base_overlaps_storage_root. A server whose applied state
    already carries an overlapping server_safe base quarantines that graph and
    serves its healthy siblings; startup fails with --require-all-graphs, and
    with cluster_no_healthy_graphs when every applied graph is quarantined.
    Omnigraph::with_external_blob_policy refuses a policy whose base overlaps
    the handle's own graph root. A base is compared only with a storage root of
    its own kind (s3:// with s3://, file:// with a local root); a
    same-kind root spelled with an empty path component or a percent sign
    cannot be compared and is refused with
    external_blob_storage_root_uncomparable instead. To upgrade, move the base to a sibling prefix
    outside the storage root, run cluster apply, and restart. The new checks
    do not rewrite values written earlier under an overlapping base.
    Incremental writes copied those bytes into managed storage;
    load --mode overwrite kept the URI as an external reference, which still
    reads through the process's storage credentials. If the base covered graph
    or cluster data, audit both the copied managed values and the retained
    external references that name a URI under it.

  • Update HTTP integrations with the server: graph inventory now has one graphs list with availability, and readiness uses ready/blocked counts instead of the removed quarantined fields; served_graph_count counts the whole registry. Graphs that fail startup remain visible to authorized callers with a sanitized failure category, and authorized requests to blocked graphs return 503 instead of 404. See deployment status; a 503 does not authorize replaying a write.

  • A branch's __manifest now keeps a byte-bounded buffer of its latest
    commits and releases older ones into immutable Lance files under
    __history, so a publish no longer rewrites the branch's whole history.
    Graph commit ids take the form hb1.<block>.<slot>.<nonce>. This is
    storage format 14: a standalone format 8, 9 or 13 graph is converted by the
    offline omnigraph upgrade, and a graph at any other format is rebuilt by export,
    init and load --mode overwrite, as described in the
    upgrade guide. A session may lower the release
    budget with set history_release_bytes = <bytes>; (1024..=262144, default
    262144); the default is production's and cannot be raised.
  • F64 values written from JSON before this release keep the value the older
    parser gave them, which can differ in the last digits from the value the
    same decimal names now. A JSON parameter with the same digits no longer
    matches such a value in a filter, update or delete, while a GQ literal
    with those digits now does. To correct stored values, reload the affected
    types from the original source with load --mode overwrite, or rewrite them
    with update. A merge load adds a second entity instead of updating the
    old one when the type's @key includes an F64 property, or when the type
    has no @key and the source rows carry no id, and reloading an export
    taken before the upgrade writes the old value again. Upgrade the CLI
    together with the server: the CLI parses --params itself, so an older CLI
    still sends the old value.

  • Rust callers: OmniError has a new variant, StoredExternalBlobDenied,
    for an update that must carry a stored external Blob reference the graph's
    policy refuses. An exhaustive match on OmniError must add an arm for it.

  • Schema apply and system-column upgrades now publish the schema source,
    compiled schema and identity atomically with their table references in the
    catalog. Ordinary open serves storage format 14 only and refuses every other
    format. Convert a standalone format 8, 9 or 13 graph with the offline
    omnigraph upgrade; rebuild a graph at any other format by export, init and
    load --mode overwrite, as described in the
    upgrade guide. Fresh graphs no
    longer create separate root schema files.

  • Cluster execution now uses ledger v2 only. Explicit cluster upgrade-ledger --writers-stopped preserves existing data; legacy import, refresh, approve and apply fallback are removed. Fresh apply creates graphs directly, and cluster apply --server changes schemas and stored queries or adds graphs without restarting the server. See cluster operations.
  • omnigraph upgrade now converts a standalone graph to storage format 14
    in place, keeping its branches, commit ids and commit history; it no longer
    only reports. It accepts two sources: formats 8 and 9, written by release
    0.11.x, and format 13, written by main development builds from 2026-10-01
    to 2026-10-04. The upgrade is offline: stop every process using the graph,
    retain a verified backup of the whole root, run
    omnigraph upgrade <graph> --check, then omnigraph upgrade <graph>. An
    interrupted run leaves the graph refused by normal open until the same
    command is run again. Cluster-managed graphs and every other format are
    still rebuilt by export, init and load --mode overwrite. Pre-upgrade
    commits are kept under __history/legacy/: a 14 build older than this
    change reports pre-upgrade commits as not found by id and refuses
    full-history reads on an upgraded root, so upgrade every binary before the
    graph. For a 0.11.x graph the schema is read from the three schema objects
    at the graph root, which the upgrade leaves in place and which go stale at
    the next schema apply; the objects must match the tables, so a stale copy
    restored from an older backup is refused before any write; every
    pre-upgrade commit is recorded under the one schema live at the upgrade; a
    .staging schema object or a recovery file left by 0.11.x is refused until
    a 0.11.x read-write open (omnigraph snapshot) resolves it, while a
    __schema_apply_lock__ branch left alone by a killed 0.11.x schema apply is
    handled by the upgrade and is not a live branch afterwards; and on a root
    where 0.11.x cleanup or schema apply --allow-data-loss ran, the first
    cleanup after the upgrade is cleanup --keep 1 --confirm alone. The JSON
    report gains a work object, names its route per source
    (history-lance-files-v8-to-v14, history-lance-files-v9-to-v14,
    history-lance-files-v13-to-v14), names history-lance-files-to-v14 as
    recovery.failed_handler when it stops before any route is known (an
    unreadable pending marker, or recovery files on a format no route
    converts), and now reports check_passed and completed. See the
    upgrade guide.

Features

  • Isolate read and write admission, retain request tracing across disconnect,
    and keep proven pre-effect storage refusals nonfatal. Uncertain writes close
    admission across the process and exit nonzero after other logical owners drain.
    Remote discovery reports credential-safe transport causes; dedicated merge
    commands canonicalize branch names before dispatch and receipt validation.
    Malformed branch-create names return HTTP 400 and leave admission open.

  • One v0.12 HTTP contract. Protected graph and registry requests require
    Omnigraph-Http-Api: 0.12; missing or unsupported headers refuse before graph
    access with api_contract_mismatch. Upgrade CLI, server and HTTP integrations
    together. The CLI probes public health before each request and validates
    response headers before consuming JSON or streamed bodies, with redirects and
    automatic retries disabled. A failed preflight sends no data request; an
    incompatible response after dispatch leaves effects unknown. MCP and OAuth
    retain their separate standard protocols. Server URLs must identify the service
    root; replace graph-qualified URLs with the root and explicit graph selection. See
    HTTP server guidance.

  • Exact merge receipts. Branch merge HTTP responses, GQ merge statements and
    CLI JSON now return the commit published by that merge, even when another
    writer advances the target before delivery. Fast-forward returns its own
    target commit; an already-up-to-date merge returns commit: null. Optional
    source deletion failure preserves the successful receipt and exit 0, with
    branch_deleted: false and structured branch_delete_error_details replacing
    the old string field. Embedded callers receive MergeResult instead of only
    MergeOutcome. See merge outcomes.

  • Typed edge selection (GQ 2.1). (knows | likes) selects compatible named
    edge types and * selects compatible types from the captured schema. Every
    omitted bound means exactly {1,1}; explicit finite ranges combine shortest
    distances across types. Bound edges retain concrete type and identity through
    $e.@type and $e.@id. Common properties must have compatible types on every
    member. A shared finite traversal_work_limit fails explicitly on exhaustion.
    Historical wildcard targets and internal forced CSR selection are refused. Explain 4
    records complete member lists, directions, version maps and typed sort ties;
    serialized physical plans use a versioned envelope. Regenerate older saved
    plans, including those without traversal nodes. Scan admission charges full
    selected-table sizes, even for small neighborhoods. See
    traversal and match patterns.

  • Admitted writes outlive their HTTP callers. The server owns mutations,
    loads and branch operations through completion, including optional merge-source
    deletion. Disconnect no longer frees their admission capacity. Aggregate input
    and operation limits bound retained server work; shutdown closes admission and
    waits for owned writes, read bodies and server producers against one deadline.
    An owner panic or indeterminate completion closes admission and stops the
    process. This does not qualify every native storage-I/O lifetime. See
    deployment.

  • Whole-command write outcomes. CLI data-write errors include
    command_outcome with execution/effect evidence and a supported action,
    preserving structured errors and Retry-After. Exit 75 permits a bounded caller
    retry only for a verified typed admission refusal before the whole command's
    effects. Conflicts and uncertain delivery remain exit 1; verified HTTP
    conditional refusals retain exit 4. Embedded mismatches remain exit 1 because
    writable open may complete earlier work. The CLI never automatically repeats a write. See
    failure outcomes.

  • cleanup is a tracing collector over table storage, and --keep
    counts graph commits.
    --keep N retains the newest N graph commits on
    every live branch and the table versions they pin; --older-than also
    retains commits newer than its cutoff. Branch creation bases, selected
    merge bases and exact snapshots protected by native tags are retained too.
    Other published versions are collected; unpublished staging is collected only
    after its recorded publication authority is provably gone. Missing or
    malformed ownership evidence is retained. Native tree candidates are frozen
    before graph capture; complete branch, head and tag views are revalidated
    after all table inventories. A change refuses the plan before deletion.
    Retirement archives exact branch-ref contents before unlinking active refs;
    cleanup reclaims unneeded trees and archives after checking dependencies.
    Branch creation does not scan accumulated retirement history. Accepted
    merge inputs remain protected through cancellation or an in-doubt
    publication until their target publication authority is provably gone.
    Ordinary writes and merges may overlap cleanup; branch creation/deletion
    and cleanup keep the existing single-writer-process control boundary. Dropped table lifetimes,
    base and overlay data files, borrowed origins and Blob sidecars participate
    in retention. Orphan objects and temporary manifests require their own
    seven-day age. Nothing defers a table: the deferred result field is removed,
    and each --json row carries manifests_removed, unpublished_manifests,
    unpublished_bytes and foreign_versions (old_versions_removed stays
    for one release with the value of manifests_removed). Exit 0 means every
    table was visited and no retained table version or file was removed; a table
    whose trace did not finish is left whole and its row says why.

  • Lost acknowledgements resolve against the attempted graph commit even when
    another writer advances HEAD before readback. A publish that failed before
    any object-store request, with the attempted __manifest version absent and
    the branch head below it, returns the original error. A storage error stays
    in doubt, because its request may still land. The in-doubt error names the
    graph_commit_id.

  • omnigraph repair reports foreign drift instead of healing drift. A
    graph table's Lance linear history ends at its creation version, recorded
    on the registration as omnigraph.last_linear_version. Linear commits
    above it came from outside OmniGraph and are resolved by no read or write;
    repair classifies such a table foreign_drift, prints the last linear
    version and the HEAD, takes no action and exits 0. --confirm and
    --force --confirm publish nothing on a v14 graph, and cleanup never
    deletes a foreign version.

  • Inserting into a node type with a camelCase @unique property no longer
    fails with No field named <lowercased name>: the committed-uniqueness probe
    now reads the column by its exact name, for mutate … insert and for
    append/merge loads. Fixes #765.

  • A property named like a SQL prefix keyword (interval, exists, trim)
    can now be the column of a delete/update … where predicate and of an
    indexed Vector: the predicate and the index-training filter reach the
    storage layer with the column quoted.

  • Correlated blocks in match: exists { … }, count { … } > 2,
    sum($d.size) { … } > 100 (also min, max, avg).
    A block holds a
    pattern matched once per outer row and keeps the row by a comparison on the
    aggregate of its matches; not { … } is the count = 0 case of the same
    operator. The rows are narrowed before order and limit, so a paged
    listing filtered by a relationship count is exact. A binding declared inside
    a block (not { $p worksAt $c; $c: Company { name: "Acme" } }) is now
    reached through the block's traversal from the outer row instead of scanned
    as a whole table and cross-joined with the outer rows. A single-hop,
    filter-free count { … } over a directed, unbound
    edge is answered from the graph index's degree without reading the edge's
    rows. The bare count($d) > 2 in match stays refused (T7). Fixes #764
    and the correlated-product half of #763.

  • Writes no longer use recovery sidecars, and the recovery machinery is
    gone (RFC 0067).
    Every writer stages its table effects as detached Lance
    commits that nothing references until the one manifest commit publishes
    them, so an interrupted write leaves the graph unchanged and there is
    nothing to recover. The __recovery sidecars, the reopen-time recovery
    sweep with its roll-forward and rollback, the write-entry recovery barrier
    and the recovery audit table no longer exist. The failure classes they
    caused go with them: a stale or mis-named sidecar can no longer make every
    read-write open fail, a direct optimize beside a live server no longer
    leaves a barrier that refuses writes until a reopen, and a sidecar read can
    no longer race its own cleanup into a transient error. Fixes #602; removes
    the cause of #554, #601 and #330.

  • Optional OIDC identity and MCP access. omnigraph-server --oidc-identity-trust FILE verifies resource-bound human identities locally
    against public keys and explicit principal admissions. Applied Cedar retains
    all graph permissions. Public admission updates have bounded expiry and do
    not restart serving. The option exposes standard protected-resource metadata
    and MCP tools for minimal discovery and stored reads, using the maintained
    protocol SDK and the existing authorization handlers. Direct deployments
    without the option retain their existing routes. See
    OIDC resource identities and MCP.

  • Managed login uses public AuthKit authentication and cached credentials. Repeated login --api
    calls return fresh identity metadata without another browser flow while the
    session remains usable. The WorkOS SDK obtains and rotates credentials directly,
    without an embedded client secret or an API device/refresh broker, for
    up to eight hours from sign-in; each access credential remains short-lived.
    Metadata validation tolerates a bounded clock difference without extending
    either expiry timestamp.
    Credentials stay in the OS keychain, uncertain exchanges are never replayed,
    and logout clears local custody while separately reporting provider revocation.
    This profile requires an API supporting direct AuthKit access; old opaque
    sessions must sign in again. That service issues version-2 identity credentials
    only; retained --actions syntax remains an explicit restricted request for
    older compatible issuers and is never silently widened. Named-server login
    is unchanged. See the
    managed CLI reference.

  • Managed bulk loading uses the native CLI. Implicit load --graph
    reuses the selected folder's data credential; applied policy supplies
    change and branch-creation permissions. Loads use the existing NDJSON
    endpoint with 32 MiB input, 8 MiB response and a 300-second deadline.
    Redirects and automatic retries are disabled; a lost response requires
    branch reconciliation. Explicit targets, positional storage URIs, profiles
    and --direct retain ordinary loading. See
    managed bulk loading.

  • Managed commit reads inspect native lineage. commit list and
    commit show use the selected folder's identity and applied read
    permission to inspect graph history after a lost write response. They share
    the 30-second request deadline and never replay the uncertain write.

  • Managed queries and mutations allow 30 seconds to finish. The total
    deadline includes connecting and reading the complete response, with at most
    10 seconds to connect. Redirects and automatic retries remain disabled, and
    responses remain limited to 8 MiB. A timed-out mutation may have committed;
    inspect the graph before resubmitting it. See
    managed data access.

  • CLI graph errors retain their structured JSON. With --json or a read
    command's --format json, server refusals preserve their error code and
    typed details on stdout. Policy denials and resource limits exit 1;
    qualified HTTP conditional mutation mismatches retain exit 4. Data-write
    diagnostics additionally report the whole-command outcome described above.

  • Identity credentials leave permissions in applied policy. Graph commands
    acquire and cache a cluster-bound identity credential with no graph or
    action grants. Applied Cedar policy decides protected graph and schema
    operations. Minimal graph discovery shows every effective graph ID/name
    without exposing schema, roots or other operational metadata. Explicit
    server addressing selects it with graphs list --discovery. The older
    restricted credential remains available only through explicit --actions
    requests. See managed data access.

  • Session settings. A .gq file may open with set <name> = <value>;
    and reset <name>; lines, and show <name>; or show all; is a read
    statement returning name, value, default, source and scope. A
    value applies to that invocation or request and is never persisted. Seven
    settings: engine, rrf_plan, merge_lineage, ann_nprobes,
    stage_write_concurrency, traversal_work_limit and history_release_bytes.
    history_release_bytes is the byte budget of a branch's buffer of unreleased
    commits: a mutate, load or branch merge of the session that reaches it closes
    a history block, and the next publish writes the block under __history;
    production keeps the default, and a caller may only lower it. The CLI verbs query, mutate, branch merge,
    commit changes, changes poll, load and ingest take a repeatable
    --set NAME=VALUE;
    POST /query, POST /mutate, POST /mutate/if-graph-commit and
    POST /branches/merge take an optional settings object; GET /changes
    and GET /commits/{id}/changes take a repeatable set=<name>=<value>
    parameter. omnigraph lint reports a set line's error with its position.
    See Session settings.

  • Every read query runs from a plan (engine v2). Engine v2 runs the plan as
    one DataFusion physical plan under omnigraph's own memory pool:
    DataFusion's operators for filter, join, projection, sort, limit and
    aggregate, omnigraph's own for the scan, the traversal, the negation and
    the rank fusion. Lance scans and graph hydration execute under the same
    query context, and shared Arrow buffers are charged once across custom
    operators. DataFusion operators that support spilling can use bounded
    scratch space; custom graph breakers return a typed resource-limit error
    when their memory reservation is refused. These query budgets do not bound
    shared caches or total process memory. Ordinary v2 reads build only the
    executable plan; explain JSON and structural hashes are generated only by
    explicit explain requests. explain describes the v2 plan the query runs
    and lists its lowered tree as datafusion
    rows when it can be built without running the query. The
    planner answers an unfiltered bare-variable count from the pinned dataset's
    live-row metadata through aggregate_pushdown, without scanning IDs or
    properties. Counts with filters, grouping, traversal or search retain their
    scans. The columns each remaining scan reads are decided before execution:
    the identity and key for a bare-variable count, the projected node
    object's members for return { $var }, and each property an expression
    names; a Vector or Blob column is read only when an expression names
    it, so a plain count($s) over a type with a Vector column no longer
    materializes every embedding (#704: 5.27 GiB peak RSS over 213,109 rows of
    Vector(3072) on the executor). Engine v2 is the engine for every read at
    every door: the embedded Session, the CLI, the server, stored queries
    and DST. The session setting engine keeps the one value v2, its
    default, so set engine = v2; is accepted and changes nothing. Engine v1,
    the executor, is retired from production: it is kept, frozen and
    hash-pinned, as the test reference omnigraph-reference-engine, a crate
    that is never published and that only the GQ logic tests depend on, where
    a query step's --- expect same as v1 compares v2's rows with v1's.
    Traversal destination
    hydration also uses the planned projection before copying rows per edge,
    avoiding unused text and vector payloads in grouped counts (#703). GQT
    expect plan sections assert destination columns on the dependent scan.

  • Engine v2 decides the traversal mode in the planner. The cost model
    that picks per-hop BTREE scans or the in-memory CSR for an unbound
    traversal moves from the executor into omnigraph-planner, decided at plan
    time from a row-count estimate (the pinned snapshot's entity counts,
    key-equality selectivity and the edge type's average fanout) and recorded on the
    physical Expand as mode and frontier_estimate; explain prints both
    and lists the pass expand_mode, and a GQ logic test asserts the mode with
    expand $s Knows $d: mode indexed_scan. The engine keeps two runtime
    corrections and can reconsider a CSR choice for a smaller observed first
    frontier. A degraded BTREE (or a CSR an earlier operator already built)
    re-runs the model before an indexed start, and the mid-flight switch on the
    observed frontier stays.

  • Engine v2 reads a traversal's destination table once when the frontier
    is dense.
    The planner chooses an access path for every traversal
    destination: a HashJoin node reads the destination table once, with its
    pushed filters and projection, as the build side the traversal probes in
    its own order; a dependent scan (access id_lookup) keeps the per-batch
    id IN (...) read in slices of at most 256 input rows. The rule checks the
    table's row count against eight times the frontier estimate and requires
    its estimated projected bytes to fit one quarter of the query pool;
    explain prints the HashJoin node with its fallback and lists the pass
    access_path, and a GQ logic test asserts it with hash join $d.

  • ann_nprobes is a request setting. A query sets it with
    set ann_nprobes = N; or the settings field of POST /query, and show
    prints its scope as request.

  • The profile row carries attempts[].drained. It is true or false
    for an omnigraph operator (its stream reached its end or not) and null
    for a DataFusion operator, whose end no counter observes. An attempt's
    actual_rows is complete only when drained is true.

  • Engine v2 streams a single-hop traversal. An unbound one-hop Expand
    no longer retains its frontier or its pairs: per input batch it walks the
    CSR adjacency into dense arrays and emits slices of at most 256 pairs,
    copying only those destination ids from the CSR dictionary; a
    bound-edge Expand runs its pair producer and sort per input batch.
    Rows, multiplicity and order are unchanged; explain prints
    streaming=true on the operator. The HashJoin node's operator charges
    its build and its output batches to the query pool (hash join output).
    Multi-hop traversal keeps the BFS breaker.

  • Engine v2 emits a multi-hop traversal's pairs a chunk at a time. An
    unbound knows{1,2}-style Expand still drains its frontier before it
    emits, but its pairs now leave in batches of at most 256, each charged to
    the query pool on its own and released once hydrated, so the walk no longer
    retains every pair and a trailing limit ends it once the limit has its
    rows; a second multi-hop Expand between the two still drains the first
    one whole before it walks. A 1,024 → hub → 1,024 fixture (1,050,624 one-or-two-hop pairs) under
    limit 1 refused with resource limit exceeded for query_memory_bytes and
    now answers. Rows, multiplicity and order are unchanged; explain still
    prints streaming=false on the operator, and its expand_pairs counter
    reports the pairs the walk handed on. Fixes
    issue 782.

  • Engine v2 bounds bound-edge traversal before grouped aggregation.
    Matched edges pass through bounded producer queues, native spillable ordering
    and incremental destination hydration. Counts and sums can consume expanded
    rows without retaining the complete expansion. Source-row and edge-identity
    ordering, parallel edges, destination filters and null amounts are preserved.
    The source frontier is still retained and charged to the query pool;
    unbound traversal keeps its existing execution path. Fixes the bound-edge
    materialization in issue 723.

  • One expression language for filters, mutation where, return, order
    and assignments (RFC 2026-09-24, shared expression model).
    Conditions
    compose with and, or, not, parentheses and is null / is not null,
    with three-valued null rules: a comparison with a null operand is null, a
    filter or a mutation where keeps the rows whose expression is true. A
    mutation where takes the same expression over the target's properties,
    @id, @src, @dst, parameters and now(), so delete Knows where @src = "a" and @dst = "b" removes exactly the intersected edges (issue 660) and
    $p.name = "Alice" or $p.name = "Bob" selects either (issue 651). A
    comparison or Boolean expression in return is a Bool column under its
    alias. An order key that is not a property or a system field binds to a
    return alias or to an expression written in return, so order { count($d) desc } sorts by the returned count (issue 566 part 3); a key that matches
    nothing is refused with T42. Assignment values and inline binding matches
    are constants over literals, parameters and now(), evaluated once per
    invocation. GQ_LANGUAGE_VERSION is 2.1. LOGICAL_PLAN_VERSION remains 2;
    logical Filter stores conjuncts. Explain version 3 added
    physical ContainsJoin and CrossJoin filters; EXPLAIN_VERSION is now 4.
    Physical Expand records all selected members and versions, and Sort and
    RankFuse record typed ties. Adding or removing a node kind bumps the explain
    version.

  • Synthetic benchmark cases can prepare deterministic reversible updates before branching, adding bounded real write history while preserving current graph contents. The versioned YAML recipe retains exact history assertions, fixture verification, and identical-state reset; see the benchmark guide.
  • Blob delivery failures log their error class. A GET or HEAD /blob 500
    logs error_kind="blob_pre_header_internal" with a stage: target is a
    failure resolving a snapshot target for a policy-gated request, cell is
    any failure of the engine's Blob read, including the branch or snapshot
    resolution it does itself, and transport is the server's own refusal
    before headers. The target and cell stages log error_variant (for
    example Storage or BlobIntegrity), storage_kind and manifest_kind;
    transport logs error_variant="unclassified". A managed Blob body that
    fails mid-stream logs the byte range with one of three kinds:
    blob_payload_read with the same error class, blob_payload_short_read
    with the returned and expected byte counts, or blob_payload_permit_closed.
    The error's message stays out of both the response and the log, because it
    can hold object URIs or credentials; the response is unchanged. See
    HTTP errors.
  • Existing clusters can explicitly convert their ledger under stopped writers to durable offline schema and stored-query deployments without resetting graph data or history. Deployments capture immutable input, retain an original identity for root-only status and reconciliation, and allow corrective successors after a terminal partial result. Converted clusters share exclusive admission across servers and supported CLI operations, retaining their lock even on success until operator-established quiescence and exact-ID unlock; see the deployment workflow and limits.
  • GQ has a list-membership predicate: $x.number in $numbers, or
    $x.number in ["A-1", "A-2"] with a literal list. On a matched binding's
    property it filters that binding's scan. in is a reserved word as a
    result. See the query guide.
  • Servers retain per-graph request ownership while pausing and resuming the same serving bindings, including disconnected operations and streamed responses. Authorized graph inventory reports transitioning and wait_for_transition; schema and stored-query deployments still require restart. See deployment status.
  • Embedded callers can prepare schema changes before execution, receive the
    operation's exact commit receipt, and reconcile retained publication evidence
    without applying again. An unknown reconciliation outcome does not authorize
    retry; see the graph write protocol.
  • Server health, readiness and authorized graph inventory are available while graphs load. Ready graphs can serve immediately; --require-all-graphs keeps graph admission closed until the entire startup succeeds. Readiness adds loading_graph_count, and inventory adds loading with action wait_for_startup; startup attempts remain owned through shutdown. See deployment.

Behavior changes

  • Named traversal correctness. Disconnected traversal patterns without an
    executable endpoint now fail type checking instead of disappearing from the
    query. Named reverse traversals in not, exists and block aggregates now
    use their checked direction and lexical expression scope. Later outer
    declarations no longer capture earlier block-local variables. Missing edge
    datasets on historical targets fail consistently when named, bound or
    explicit alternative traversals open them.

  • Writers stop serializing on the process-local schema gate (RFC
    2026-09-18, shared schema gate).
    Mutations, loads, merges, index
    maintenance, optimize, cleanup, repair, branch delete, the system-column
    upgrade's --check and read-view captures take a shared permit on the
    schema gate; schema apply, the system-column upgrade, open, refresh,
    reload, sync_branch and branch create take it exclusive. A
    writer on one branch no longer waits for a writer on another, and a read
    no longer waits behind another handle's publish hold (a read and a write
    on the same handle and branch still wait for each other while it
    publishes); same-branch writers still serialize on the branch gate, and
    a schema apply still excludes every writer for its whole pass. No flag;
    per-operation object-store request counts on the success path are
    unchanged.

  • A branch merge finds what changed without requiring either side to
    descend from the merge base's table version.
    With merge_lineage at
    on (the release default), candidate discovery compares the table
    versions the merge base, the source and the target pin by file identity.
    A merge whose side does not descend from the base's, such as merging
    main back into a branch after a three-way merge, now discovers
    candidates from version metadata where it fell back to the full
    three-way scan. A merge that succeeds produces the same result in every
    mode.

  • A write on a branch other than the handle's bound one reads that
    branch's __manifest once less.
    Before its table effects the write
    compares the branch's latest version and identifier with the ones it
    captured; when both are unchanged it keeps the captured state and skips
    the reopen and scan. A branch that moved, or was deleted and recreated,
    is read again and the write is prepared again or refused as before. No
    flag; writes on the bound branch are unchanged.

  • Every table write is a detached Lance commit, published once and never
    promoted (RFC 0067 and RFC "Detached-only tables").
    Each table effect
    is a detached commit of the pinned base, published as the pin
    (published_dataset_version, staged_version, transaction_uuid) in the one
    __manifest CAS, and that detached commit is the table's version for its
    whole life: readers open staged_version, nothing replays it onto the
    table's linear history, and a graph table's linear HEAD stays at its v1
    create. published_dataset_version keeps its number, base + 1, as the
    table's logical version and names no Lance version. Handle and topology
    cache identities include the detached pin, including on backends without
    e-tags. Persisted topology format v3 carries that pin; older artifacts are
    cache misses and rebuild. A mutation or load
    that fails before publication leaves the graph unchanged with nothing to
    recover, and one that publishes is complete at the CAS; a
    RecoveryRequired from a mutation or load, the __recovery sidecar it
    used to write, and the reopen-time recovery of that sidecar are gone. If
    the manifest CAS lands durably but its
    acknowledgement is lost, the write is already graph-visible, so the publisher
    reads the commit back and returns success rather than an opaque failure a
    non-idempotent retry could double-apply; only an outcome it genuinely cannot
    confirm surfaces as an in-doubt error. A failed operation never wedges its
    own live handle: once the fault source stops, the same handle's next write
    succeeds without reopening. Writes to registered tables do not read their
    linear HEAD, so a foreign linear commit does not block those writes;
    omnigraph repair reports it as foreign_drift. Schema apply checks an
    unregistered added-type path before reuse and requires its original empty
    version 1. ensure_indices and the explicit full-text rebuild
    follow: each index batch is a detached commit published as a pin, a
    rebuild interrupted after publication keeps serving search through the
    staged batch, and no EnsureIndices sidecar is written. Branch merge
    follows: a merge into a named branch chains every chunk of a merged table
    as detached commits and publishes one pin per table, a merge onto main is
    a pointer switch in which main's registration takes the source's pin with
    no row copy, and a merge that fails before publication leaves the target
    untouched; no BranchMerge sidecar or RecoveryRequired outcome remains.
    No per-table fork is created for a graph branch: a branch write stages
    detached on the dataset its inherited registration names, two branches
    writing one table produce two detached chains in one dataset, and forks
    created by earlier builds stay valid and are reclaimed by cleanup once
    unreferenced. Schema apply follows: existing-table rewrites are detached
    commits published as pins. An added type's original empty v1 Create can
    be reused after a failed attempt; a different qualified original schema
    uses a detached replacement, preserving the path and its versions. The
    table delta and replacement schema-contract row publish together. No
    SchemaApply sidecar or root-contract installation remains. A failure
    before publication leaves the previous graph visible; a post-publication
    error identifies a complete published outcome, including its contract.
    Optimize follows: each table's compaction is one detached rewrite
    of its pin, a scalar or vector index whose coverage lags is rebuilt whole
    as a detached commit chained on it, the batch publishes once with an exact
    check on every pin it planned from, and no Optimize sidecar is written;
    a run that loses a table to a concurrent write fails with a read-set
    conflict and the next run re-plans. Optimize no longer strips a stale
    lance.auto_cleanup configuration from data tables: every engine commit
    skips Lance's auto-cleanup, so the key is inert. Every detached commit
    records the branch incarnation and graph head it was staged against in its
    transaction properties (omnigraph.staged_against_branch_incarnation,
    omnigraph.staged_against_graph_head), and a delete records the ids it
    removed (omnigraph.deleted_ids, or omnigraph.deleted_ids_path above
    64 KiB); the collector and the change feed read them. The optimization
    admits at most 16 MiB of encoded JSON; larger records take exact discovery
    without downloading an oversized spill. An external Lance
    reader pointed at a table directory opens the table's last linear version
    and sees stale rows; omnigraph export is the supported external route.
    Test seams mutation.post_arm_pre_effect, mutation.post_sidecar_pre_fork,
    mutation.sidecar_confirm_put, mutation.sidecar_post_publish_delete,
    ensure_indices.post_sidecar_pre_fork,
    ensure_indices.post_effects_pre_confirm, branch_merge.post_sidecar_pre_fork,
    branch_merge.post_effects_pre_confirm, branch_merge.pre_error_recovery
    and optimize.post_compact_pre_reindex are gone, and so are the promotion
    and fork windows that never shipped in a release
    (mutation.post_publish_pre_promotion,
    ensure_indices.post_publish_pre_promotion,
    branch_merge.post_publish_pre_promotion,
    schema_apply.post_publish_pre_promotion,
    optimize.post_publish_pre_promotion, promotion.pre_replay,
    fork.post_create_pre_open, cleanup.pre_reap, cleanup.reap_delete);
    mutation.post_table_commit, publish.pre_merge,
    branch_merge.post_table_effect, optimize.post_table_effect,
    cleanup.collector_post_snapshot, cleanup.sweep_pre_manifest_delete,
    cleanup.sweep_between_manifests and cleanup.sweep_pre_file_delete name
    the windows that exist, and the GQT cases that armed the retired seams are
    retired or repointed with them.

  • Graphs that still carry a recovery sidecar. No 0.12 build reads or
    writes one. A read-write open refuses a graph
    whose __recovery/ directory holds a sidecar, naming it, until the 0.11.x
    binary that wrote it has opened the graph read-write and finished its own
    recovery; a read-only open is unaffected. Refresh validates the published
    schema-contract row and updates the in-memory view; it does not install
    root schema files. A schema or system-column operation can still report
    RecoveryRequired after a proven publication, but the named commit already
    contains its tables and contract.
    Test seams recovery.*, optimize.post_recovery_check_pre_main_gate,
    cleanup.post_recovery_check_pre_gates and
    system_column_upgrade.after_lock_reclaim are gone, and
    branch_control.post_recovery_barrier is now branch_control.pre_gates.
    The GQ logic test format loses its --- known_failure section, which
    existed only to pin known recovery-sidecar defects.

  • Storage format v14; a standalone v8, v9 or v13 graph is converted in
    place, any other format is rebuilt.
    Normal open serves v14 only and never
    migrates. Fresh graphs of either system-column vintage start at v14. A
    standalone graph at v8 or v9 (release 0.11.x) or at v13 is converted in
    place by the offline omnigraph upgrade, which keeps its branches, commit
    ids, commit history and table data. A graph at any other format, and every
    cluster-managed graph below v14, is refused, and the path is
    omnigraph export with the binary of the release that wrote the graph,
    then omnigraph init and omnigraph load --mode overwrite. That rebuild
    preserves data, vectors and blobs; commit history and branches are not
    kept.

    omnigraph upgrade <graph> converts a standalone v8, v9 or v13 graph to
    v14 offline: stop every process using the graph, retain a verified backup
    of the whole root, run omnigraph upgrade <graph> --check, which writes
    nothing, then omnigraph upgrade <graph>. It reports check_passed or
    completed for a convertible graph, already_current for a v14 graph, the
    finding unsupported_source for any other format below 14,
    newer_than_binary above 14, and unsupported_target for a --to-format
    other than 14. A graph carrying a pending conversion marker reports
    recovery_required and stays refused by ordinary open; run the same
    command again with the executable that started the conversion.
    Cluster-managed roots are refused. See the
    upgrade guide.

    omnigraph schema upgrade-system-columns separately respells a served
    graph without changing v14. Its detached table renames and replacement
    contract publish together. An interruption before publication leaves the
    graph unchanged; afterward both contract and table references are complete.
    Test seam schema_apply.post_sidecar_pre_effect is now
    schema_apply.post_lock_pre_effect, and
    system_column_upgrade.after_stamp_advance is gone. The
    V5 ↔ V9 Format Fence CI job is now V5 ↔ V10 Format Fence.
    Historical queries retain current-contract semantics; older contract rows
    do not add a schema history API. Export with this binary followed by init
    and load with an older one is a separate logical rebuild; it loses history
    and branches.

  • A published commit id is hb1.<block>.<slot>.<nonce>. Every commit
    after genesis is published under an id of that shape, so every commit,
    snapshot and changes output and every /commits/{id} path carries it;
    the genesis commit keeps its ULID, and on a graph converted by
    omnigraph upgrade the commits from before the conversion keep the ids
    they had. A prepared schema apply's commit is addressed by its published
    id: the intent id no longer resolves through
    GET /commits/{id}, and RecoveryRequired.operation_id names the
    published id.

  • Engine v1 is retired from production; engine v2 answers every read.
    The session setting engine has the one value v2, its default:
    set engine = v1;, --set engine=v1, {"settings": {"engine": "v1"}} and
    OMNIGRAPH_ENGINE=v1 are refused as an unknown value
    (unknown value `v1` for setting `engine`; expected one of v2), and the
    process default refuses startup like any other invalid setting value.
    Engine v1 survives only as the frozen test reference omnigraph-reference-engine, which no production
    door reaches. Three answers differ from v0.11.0, whose reads all ran on
    engine v1:

    • A bm25() or nearest() order on a traversal destination, a shape
      engine v1 answered, is refused with a bad-request user error that names
      the function and the binding, says engine v2 does not support it, and
      suggests ordering on the traversal's source binding or matching the
      destination with search().
    • BM25 ties order by identity under a limit. Full-text candidates stay
      uncapped until the final score and identity ordering, so an equal-score
      entity outside an initial search window can still win, where engine v1
      cut the candidates first.
    • Several text predicates (search, fuzzy, match_text) on one scan
      are conjoined, where engine v1 applied only the last one.
  • Engine v2 no longer materializes the product of two unconnected bindings
    under a contains filter (issue 775).
    A $p.text contains $m.number
    between bindings no traversal connects runs on their join, below any
    traversal, and, when the text binding's scan sits on the join's right
    (written second, or moved there when both row counts are known and the
    needles are no more than the texts), as a
    ContainsJoin that sieves the text scan through one Aho-Corasick automaton
    over the needles and pairs each text only with the needles it holds. Rows
    are unchanged; a query that refused under the memory cap now answers.
    explain shows the ContainsJoin node, the conjuncts a CrossJoin tests
    (CrossJoin.filters) and the scan's runtime_filter.

  • Search predicates: search, match_text, and fuzzy filters accept
    a standalone call or = true. Other comparisons now fail type checking
    (T38) instead of being silently interpreted as a positive search. A
    text filter on a BM25-ranked binding is preserved by applying its
    membership set without changing the BM25 scoring query.

  • A refused query carries its diagnostic. Every parse or type refusal
    now reports a stable code (Q…, T…), where the failure is (line and
    column, or the compiler stage), what was expected, and one fix. The HTTP
    400 body gains an additive diagnostic object; the CLI prints the four
    fields on stderr for human formats and the API's error body for --json
    and --format jsonl, with no colour codes and no backtrace footer.
    queries validate --json and cluster plan --json carry the same detail
    beside their messages. A declaration without its parameter list, query name {, is refused at the name's end with the fix query name() where it
    was reported at the file's first position; the one-line error text of a
    parse refusal no longer embeds the parser's multi-line rendering. See
    Diagnostics.

  • Setting environment variables are process defaults, and an invalid value
    refuses startup.
    OMNIGRAPH_RRF_PLAN,
    OMNIGRAPH_MERGE_LINEAGE, OMNIGRAPH_ANN_NPROBES and
    OMNIGRAPH_LOAD_CONCURRENCY keep their names and meaning as the default
    of the setting each one seeds; they are read once, by the server at startup
    and by the CLI per run, no longer by the engine. A value outside the
    setting's row refuses the server start or the CLI invocation with the
    setting's message instead of warning and running a default:
    OMNIGRAPH_LOAD_CONCURRENCY must be in 1..=64 (0 and 128 are
    refused), OMNIGRAPH_MERGE_LINEAGE must be off, on or verify.
    OMNIGRAPH_TRAVERSAL_MODE is no longer read: the expand path is no session
    setting, so nothing seeds it from the environment. A process setting
    (rrf_plan, stage_write_concurrency) is refused in a request's
    settings field, set parameter or text; a request may set
    merge_lineage, ann_nprobes, traversal_work_limit and
    history_release_bytes. traversal_work_limit defaults
    from OMNIGRAPH_TRAVERSAL_WORK_LIMIT. The GQ logic-test runner refuses to run
    while a settings-default variable or retired OMNIGRAPH_TRAVERSAL_MODE is set.

  • Malformed request JSON answers the API's own error body. A body that
    does not parse on POST /query, POST /mutate,
    POST /mutate/if-graph-commit, POST /branches/merge or POST /change is
    refused with the JSON ErrorOutput body under the code bad_request,
    where earlier releases answered axum's bare extractor rejection.

  • Policy embedder source compatibility. Cluster configuration authorization
    adds PolicyAction::ConfigManage, PolicyResourceKind::Cluster, and
    PolicyEngineKind::Cluster. Exhaustive matches on these public enums need
    the new variants. Existing static credentials, direct storage-holder APIs
    and version 1 token structs keep their behavior and construction contracts.
    An existing cluster needs an explicitly applied management policy before
    activating identity-authorized configuration execution; proposed policies
    cannot grant permission to install themselves.

  • GQ: a new file-level statement, explain before a read declaration
    (explain query q() { … }), describes the v2 plan the query runs,
    instead of returning query results. Its table
    has one row per plan node (tree, depth, node, detail): the logical
    and physical trees, then the available DataFusion tree, each in pre-order,
    followed by plan rows for fired passes and document fields (route,
    logical_hash, …). Served by omnigraph query and
    POST /query under the query's own target, parameters and read policy
    decision; refused at mutate, at the deprecated routes, and over a mutation
    declaration. One statement per file, like the branch statements of RFC
    0055.

  • GQ logic tests: a query step may carry an --- expect plan section
    after its --- expect shape, asserting facts of the query's v2 plan.
    Forms include scan <Type>[ as $var]: columns [..],
    scan <Type>[ as $var]: not columns [..], scan <Type>[ as $var]: filter reads [..], scan <Type>[ as $var]: no filter, filter reads [..],
    pass <name>, not pass <name>. The facts are matched against the
    engine's explain document, never against rendered text. See
    crates/omnigraph-gqt/README.md.

  • GQ logic tests: a query step may end with --- expect same as v1,
    after its shape or plan section. The runner runs the query again on the
    frozen engine v1 (omnigraph-reference-engine) and fails the step on a v1
    error or a row difference; the step's own rows expect still applies. The
    section is refused on mutate, error-expect, show and branch list
    steps and skipped under DST. The engine-v2 CI mode is gone, and
    OMNIGRAPH_GQ_ENGINE accepts only v2 or empty.

  • GQ logic tests: a --- concurrent section runs two to four labeled
    GQ statements at the same time on the case's one handle, under the DST
    runner, in the order its order: line names over store requests
    (<label> [park] <verb> <key-suffix>), session starts and completions;
    a park entry holds a session inside whatever the engine holds at that
    request. The block is one step, its bare --- expect names each
    session's outcome, a starved script fails with the entry it stopped at,
    and under --measure each session is its own cost row (RFC 0045
    §Concurrent block).

  • GQ logic tests: several --- seam blocks may precede one mutate step
    when they name distinct seams; each is armed and recorded on its own, and
    the same seam twice before one step is refused.

  • GQ logic tests: a fault injected at the object store is a --- seam,
    not a --- fault (RFC 0066, RFC 0045): at may name a store place from
    STORE_PLACES in omnigraph-dst (storage.put first) with a subject
    glob over the object's root-relative name, or a decision seam that declares
    a store effect; the site passes and the storage decoration acts on the next
    put. The DST target installs that decoration on every run. The delivery
    record carries the store's own hit (method, requested, stored). At most
    one store action precedes one step.

  • GQ logic tests: the --- fault block that named a code hook is replaced
    by --- seam. Rename the section, keep at, occurrence and scope, and
    write action: fail (was return_error) or action: skip; a seam precedes
    a mutate step and must name a seam in the engine catalog with a matching
    effect. An old --- fault block is refused with that instruction. Delivery
    is proved by the runner's own decision (seam_delivered, seam_unobserved)
    instead of the injected error text. See crates/omnigraph-gqt/README.md.

  • Rust consumers of the engine's test seams: the feature-gated
    omnigraph::failpoints module (registry, set_registry,
    ScopedFailPoint, names) and the dst_clock/dst_ids/dst_gate
    install and uninstall functions are removed. Seams are statics in
    omnigraph::seams::catalog armed through the omnigraph-seams crate
    (SEAM.fire_always(), .fire_once_at(n), .panic_at(), .hold(),
    .observe(f); CLOCK.install(..) and friends for the thread-local seams),
    each returning a guard that uninstalls on drop. fail-parallel is no longer
    a dependency.

  • Rust consumers of the engine workspace: two new crates hold code moved
    out of the omnigraph-engine package with no behavior change:
    omnigraph-core (OmniError, Lance dataset access, request
    instrumentation, Lance native ref control (branch_control), dataset
    addressing) and omnigraph-catalog (the __manifest catalog: graph branch
    registrations and lineage, the publish). Both are internal API with no
    compatibility promise. The documented engine paths are unchanged:
    omnigraph::db::{Snapshot, SnapshotDataset, SnapshotScanner} (engine-owned
    types), omnigraph::db::manifest::*, omnigraph::db::commit_graph::*,
    omnigraph::error::*, omnigraph::storage::* and
    omnigraph::instrumentation::*. Code that was private or crate-private in
    the engine and is now called across the crate boundary is pub in
    omnigraph-core and omnigraph-catalog (about 440 items, among them the
    ManifestCoordinator commit and branch methods, CommitGraph in-memory
    methods, OmniError constructors and DatasetEntry/DatasetUpdate
    fields). The engine does not re-export ManifestCoordinator or the other
    catalog writers, so their methods are reachable only by depending on the
    internal crates directly. Items on types the engine does re-export are now
    callable through omnigraph: the OmniError constructors, the CommitGraph
    in-memory methods, QueryMemoryProbes::record_refusal and the
    DatasetEntry/DatasetUpdate fields. None of them writes to the object
    store.

  • GQ reserves and, or, not, is, null and in; GQ language 2.0.
    Before upgrading, an operator checks every schema and every stored query for
    three spellings that no longer parse, since the server quarantines a graph
    whose stored query fails to parse at boot: a property named one of the six
    words written bare as an operand of a mutation where ($p.and after a dot
    in a read, and and: 1 in an assignment or binding match, stay legal), a
    return alias spelled as one of them (as and), and an edge type named one of
    them traversed bare ($a and $b is now a Boolean filter; not was already
    excluded). A property named true or false cannot be named bare in a
    mutation where either: the parser reads the literal, and the type checker
    refuses the ambiguous spelling with T46 instead of selecting rows by a
    constant. The fix for all four is a schema rename with @rename_from, which
    keeps the data, plus a rewrite of the stored queries that name the old
    spelling. Two grammar rules, match_value and text_search_clause, are
    deprecated in this release and removed in the next
    (crates/omnigraph-compiler/compat/gq_language.deprecated.txt).

  • An update no longer reads the Blob cells it assigns, and a stored external
    reference the policy refuses has its own error. Replacing or clearing a
    Blob never takes, probes or reads its old value, so it succeeds when that
    value is an external reference whose source is gone or outside the graph's
    external Blob policy. An update that must carry such a reference in a Blob
    it does not assign fails with the new typed
    OmniError::StoredExternalBlobDenied (HTTP 400), naming the type, id and
    property, where it earlier failed with the misleading ExternalBlobPolicy
    message about new URI ingress.

  • Export refuses a ranged external Blob descriptor. Export writes an external
    Blob as a bare URI, which reloads as the whole object, so a descriptor
    naming a byte range, which only a writer outside OmniGraph can create, is
    refused instead of silently widened. Change-feed images, the change-feed
    baseline and entity reads by id instead describe it exactly as
    {"uri", "offset", "length"} without reading the object, so the feed still
    passes the commit that holds it and a baseline still succeeds; such a
    baseline does not reload with load.

  • A CLI data write refused because an allowed external Blob source could not
    be read (HTTP 424 with external_blob_source) now reports
    command_outcome.action refresh instead of reconcile. The source is
    read during admission, before the write has any effect, so restore the
    source or correct the reference and run the command again. See
    failed data-write commands.

  • Query success and errors wait for registered graph producers and blocking
    workers; cancellation retains their resources until they stop. Multi-table
    keyed writes, keyed parsed-payload estimates and removed-ID collections now
    have separate 32 MiB operation-wide limits, with typed refusal before data
    staging. Large deletes and overwrite loads that previously succeeded can now
    be refused: the removed-ID limit holds 671,088 generated IDs, no setting
    changes it, and an overwrite cannot be split. These limits do not bound total
    memory or qualify native I/O settlement and online deployment. See
    mutation limits.
  • Release notes are collected from individual changes and grouped by their
    effect on users. Future release documents list upgrade actions first;
    the existing v0.12.0 notes retain their original sections.

  • An update evaluates its assignments before it opens or scans its table,
    so a null assigned to a non-nullable property of any type, Blob included,
    is refused even when the predicate matches no row. Such an update returned
    0 affected rows before.

Fixes

  • A DateTime string with a non-zero digit past the third fractional digit,
    such as "2024-01-01T00:00:00.123456Z", is now refused with
    invalid DateTime literal as a load value, a query or mutation parameter,
    or a datetime(...) literal. Before, the extra digits were dropped without
    an error, so an equality filter on such a value matched the row stored at
    its millisecond. Trailing zeros, as in .123000, are still accepted, and
    stored values do not change; see
    JSON result spelling.
  • Export and change-baseline streams retain transport capacity through the last
    owner of each yielded buffer. Consumers holding all available chunks receive
    a bounded stream failure without a usable baseline cursor; releasing chunks
    lets streaming continue. See server resource limits.
  • A JSON number for an F64 now parses to the nearest F64 value, the one a
    GQ literal with the same digits names, in load files and in query and
    mutation parameters from the CLI or over HTTP, so equality filters find such
    values and an exported value reloads unchanged. A graph whose schema has a
    float @range bound that parsed inexactly failed to reopen with a
    schema_ir_hash mismatch; it opens again. Before, decimals with 16 or more
    significant digits, or a magnitude above about 1e22 or below about 1e-22,
    could land one or two units in the last place away, and CLI jsonl, csv,
    kv and table output printed those digits; see
    JSON result spelling.
  • Loads now explicitly report embedding_generation: "unsupported" when a
    loaded node type declares @embed, and null otherwise. CLI human output
    explains how to supply vectors. This applies to HTTP JSON and NDJSON loads and
    deprecated ingest paths; supplied vectors, nullable omissions, and
    required-field validation retain their existing behavior.

  • A malformed external Blob descriptor with an offset but no length fails as
    a Blob integrity error instead of reading past the end of its object.

  • A merge now reports fast_forward whenever the target's schema and every
    table version still equal the merge base's. Merging a branch again after an
    earlier fast-forward from it, with no target write in between, reported
    merged before. The merged data is the same. Such a merge also skips
    constraint validation when it only inserts nodes of types with no @unique,
    @range, @check or enum property, as a fast-forward onto an unmoved target
    already did, so a repeat merge over the 32 MiB validation budget now
    succeeds instead of being refused. Exception: when a fast-forward into
    main carried a table whose changes cancel out on the branch, or a table on
    a fork left by a branch created before storage format v11, main ends on its
    own version of that table, and a later merge from that branch can still
    report merged.

  • A null assigned to a nullable Blob clears it. update … set { content: $content } with $content: Blob? bound to null now writes a null cell.
    Earlier updates kept the old value and still published a commit.

  • Read-only CLI graph commands no longer acquire the exclusive cluster writer lock. Completed preflight refusals for new deployments release admission, and recovery diagnostics preserve bounded engine causes. Every deployment checks the achieved schema identity; deliberately accepting drift requires an exact schema-correction acknowledgement. See cluster deployment.

  • omnigraph optimize bounds the Blob payload of each compaction batch.
    Compaction reads every managed Blob value of a batch into memory, and a
    batch could span a whole fragment. Optimize now sizes each compaction
    task's batches from that task's largest row, summed over the row's managed
    Blob values (external references are never read and count nothing), so one
    batch holds at most 32 MiB of managed Blob payload. A task holding a row
    over 16 MiB compacts one row per batch, and a row over 32 MiB is
    materialized whole. This bounds the payload of a batch, not total memory.
    Every task of a Blob table gets the derived size explicitly, 1 to 8192 rows,
    so it takes precedence over LANCE_DEFAULT_BATCH_SIZE there, including when
    the variable is smaller. When a fragment being compacted holds a Blob
    descriptor the engine's decoder rejects, sizing can refuse the compaction
    with a Blob integrity error before that table is rewritten, and nothing is
    committed; a table with nothing to compact is not scanned. See
    Optimize.
  • Commit changes, the change feed, its baseline and export no longer fail with
    internal error while reading changes or an ordered_scan_input_batch_bytes
    resource error on tables holding a very wide row or a large fragment of
    multi-kilobyte rows. These walks now sort only row keys and read complete rows
    in bounded chunks, as branch merge already did.

  • Disconnected HTTP reads and cancelled or timed-out MCP reads retain their input and concurrency reservations until execution finishes. Shutdown waits for these reads, and query execution panics wait for registered graph workers before propagating. See server admission and shutdown.

  • Schema apply now persists comment and formatting changes even when no table
    migration is needed. Cluster apply no longer reports convergence while the graph
    still exposes the previous schema source. See schema changes.
  • Export and change-baseline streams resume after slow clients release backpressure instead of failing after 250 ms. Startup readiness stays unavailable until all graph opening attempts finish, while healthy graphs may serve direct requests. Expired or abandoned serving transitions keep the affected graph closed without blocking other graphs’ transitions. See deployment.

Performance

  • omnigraph snapshot and GET /graphs/{graph_id}/snapshot read the storage
    format version from the snapshot they return instead of resolving the branch
    again and opening its newest manifest version, so the reported version always
    describes the same graph version as the tables beside it. Reading the version
    itself now costs no storage request: a served snapshot of main with no new
    commit since the previous read does one version probe and opens no manifest
    version. How many storage requests one probe makes depends on the object
    store. A named branch also reads its branch reference, a handle that sees a
    new commit refreshes its state first, and the CLI opens the graph on every
    embedded command.