Repository navigation
Omnigraph 0.15 has five main highlights. Loading a large dataset into a new graph takes less than half the time it took in 0.14. Reads no longer wait for writes to commit. Most branch diffs and merges cost what changed, not how big the tables are or how long the history is. MCP agents get every graph operation, and your Cedar policy alone decides what each agent may run. S3 clusters close three ways in which two servers could write at once.
This release changes what MCP agents may call, how @card minimums and @unique are checked, managed organization commands, saved plans and a Rust error type. Read the upgrade actions before you upgrade. If you serve MCP, agents gain every tool that their policy allows, even if you never set an MCP flag. Graph storage does not change, and existing graphs open without a rebuild.
This page says what each change means for you. The complete changelog keeps the change notes as they were published.
Highlights
CSV bulk load: a new graph in under half the 0.14 time
An initial import, or a rebuild from an export, now finishes in less than half the time. In local runs of the LDBC SNB SF30 dataset (34 GB of CSV, 31 load statements into a new graph, release builds on an Apple silicon workstation with local disk), the load took 261 seconds on 0.14.0 and 87 to 110 seconds on 0.15. Loads from S3 were not measured. An overwrite load csv now works as a pipeline: each parse thread reads its own 8 MiB pieces of the source, checks keys and edge endpoints as rows arrive, and writes data files while the next rows parse. Checks that a new graph cannot fail are skipped, and a refused load still leaves nothing behind. append and merge loads still read the whole source before they parse it. #1141, #1147, #1158, #1178
Reads no longer wait for writes
Within one server process, a read that arrived while a write was committing used to wait for the commit, because the commit held a lock across a storage round trip; on S3 that is a network request. In 0.15 a read never waits for a write in the same process. A write that touches several tables, such as a node type and its edges, also commits them in parallel: two round trips at commit, where it took two for each table, so the next write on the branch starts sooner. The first read after another process commits sends 11 or 12 storage requests instead of 18 to 20. How it works: the graph's current state is an immutable value behind an atomic pointer, and a write swaps the pointer in one step. #1228, #1270, #1230
Merges after 1,024 commits: 54 storage requests instead of 4,168
Reviewing or merging a branch used to cost more the larger the table and the longer its history: a diff read every row of each changed type, and a merge read every data file of each table. Now both read only what changed. Diffing ten updated rows in a type with 100,000 rows took 277 requests and 6.9 MB; it now takes 14 requests and 27 KB, the same as with 4,000 rows. After 1,024 one-row commits, a fast-forward merge sends 54 requests instead of 4,168, and a three-way merge 135 instead of 3,216. These counts come from Omnigraph's simulated in-memory store; on S3 each request is a network round trip. Lance keeps a stable ID for each row and the version that last wrote it, so Omnigraph reads only the rows whose version differs. A diff between two branches that have both moved, and a merge of a table with Blob properties, still compare everything. #1201, #1229,
#1301
MCP: every graph operation, decided by your policy
An agent connected over MCP can now review a branch diff, find a merge base, publish or drop a proposed commit, and run a stored mutation; in 0.14 it needed HTTP for all of these. Only byte streams (Blob payloads and exports) stay HTTP and CLI only. Each tool runs its HTTP route's code under the same Cedar action, so an agent gets the same answers and refusals as an HTTP client with the same credential. The server flags that hid MCP tools are gone, so policy is the one place that decides what an agent may run. Ranked reads now also say how to read their scores, in a ranking object. #1139, #1207, #1130
S3 clusters: no second writer after a pause
An S3 cluster lets one server write at a time, under a lease it renews every 10 seconds (30-second lease by default). Before 0.15, a server paused past its lease could resume and write beside its successor; a restarting server could mistake a slow storage answer for silence and take the lock from a live owner; and the server that lost the lock kept serving. Schema apply, branch creation and deletion, and cleanup rely on one writer. Now every storage request checks the lease deadline first, the lock changes hands only when storage shows that the owner stopped renewing, and a server that loses the lock stops writing and exits with code 2, so that your orchestrator restarts it. A conditional write whose answer was lost is sent again and read back, where it used to fail.
#1221, #1176
Upgrade actions
Each change below closes a gap between what Omnigraph declared and what it did. MCP flags hid tools that the same credential could call over HTTP. A @card minimum held or failed depending on how a write reached the graph. A @unique on a list or vector property could never be satisfied. Organization roles came from provider defaults and local grants. A U64 parameter above i64::MAX was refused, and an in-doubt publication named no commit to check.
- If you serve MCP, check what each agent's credential allows before you upgrade.
/mcpnow lists every tool, and an agent can call every tool its credential's policy allows, writes included. In 0.14, write tools were hidden unless the server ran with--mcp-allow-writes, so this applies even if you never set a flag. A static token on a graph with no policy can still only read. To keep an agent read-only, narrow its policy; if it shares a credential with a person or a service, give it its own credential first. An agent that signs in through OIDC acts as the person who signed in. Remove--mcp-allow-adhocand--mcp-allow-writes, since a server started with either refuses to start;OMNIGRAPH_MCP_ALLOW_*variables are ignored with no warning. Replace calls to thereadtool withquery, which takes GQ source or a stored query'sname. See MCP. (#1207) - If your schema has a
@cardminimum, load each new node with its edges. A minimum such asWorksAt: Person -> Company @card(1..)is now checked on every write that inserts or updates aPerson. 0.14 let you insert a person with no employer, so a graph could hold nodes that break its own schema. A new node must arrive with its edges, in one mutation query or one JSONL load;load csvcannot add such nodes, because a node CSV has no edges, so an import that loadsperson.csvbeforeworks_at.csvis refused. A node that already breaks the minimum is refused at its next update, even a property-only one. Before you upgrade, find such nodes with a query onnot { $p worksAt $c }and add their edges. Schema apply also refuses a new edge type with a minimum on an existing node type (OG-MF-107). See edge cardinality. (#1212) - If your schema has
@uniqueon a list or vector property, remove it. 0.14 accepted it, but every write that set the property failed, so the property holds no values. 0.15 refuses it: the graph still opens and reads, but every schema apply that keeps the constraint is refused, and in a cluster so is everycluster applyof that deployment. Apply one change that drops the properties it names, which loses nothing, then another that adds them back without@unique. A constraint that also names another property cannot be removed in place: export the graph and load it into a new one, which starts a new commit history. See constraints. (#1306) - If you script managed organization members, name their roles. Service permissions now come from each member's roles in your identity provider, and graph and deployment access from the cluster's Cedar policy; the local grants that
organization members applyedited are gone. Useorganization members set <email> --role <role>, repeating--rolefor every role the member keeps, add--roletoinvitations send, and offboard by email or user ID without--expected-version. See team administration. (#1200, #1179) - If you save bound plans in Rust, regenerate them. Plans saved with
Executed::replay_envelopenow use envelope version 11, which can carry aU64parameter abovei64::MAX. 0.15 refuses older plans, and older binaries refuse version 11. The CLI and the HTTP API do not save plans. See saved plans. (#1102) - If you build or read publication-in-doubt errors in Rust, use the commit ID. When Omnigraph cannot tell whether a commit was published, the error now names that commit, so you can check whether it landed. Pass the commit ID to
OmniError::manifest_publish_in_doubt, and read it withpublication_id();ManifestError.publication_in_doubtchanged frombooltoOption<Box<String>>. See the error API. (#1124)
Features
- An agent connected over MCP can review diffs (
diff,branches_diff), find a merge base, publish or drop a proposed commit (commits_publish,commits_drop) and run stored mutations throughmutate. In 0.14 it needed HTTP for these. See MCP tools. (#1139) - A read ordered by
nearest,bm25orrrf()returns arankingobject that names the search, says whether a higher or a lower score is better, lists the score columns, and says whether the search was exhaustive. A client no longer has to know each search function to read its scores, andexhaustive: falsewarns that a nearer row may exist. See the search guide. (#1130) schema applycan make a nullable property required, and add node@uniqueto a type that already has rows. It checks the stored rows first and changes nothing on a null or a duplicate. Before, the only way was to export and rebuild the graph, which loses its commit history (#721). (#1271)omnigraph index build --branch <branch>builds the indexes that a branch's tables lack and rebuilds lagging BTREE and vector indexes. Before, onlyoptimizeonmainbuilt them, so reads on a branch scanned until the branch was merged. See build indexes. (#1150)branch list --detailed,GET /branches?detailed=trueand MCPbranches_listwithdetailedreport each branch's head, where it started, and when it was created and last moved. Before, showing heads took one history read per branch. See branches. (#1012)queryandaliasaccept--format toonanddefaults.output: toon. TOON carries the same document as JSON but names each field once when the rows share their fields, so results take less room in an LLM prompt. See query output. (#1198)- Unattended jobs can sign in to a managed cluster with OAuth client credentials: the CLI exchanges and renews the token itself, without a pre-issued token or a helper script. See workload profiles. (#1154)
- The server records which OAuth client acted for a person:
GET /graphs/{id}/accessreturns it asclient_id, and the policy decision log records it. Before, every client acting for one person looked the same. See sign-in. (#1196) - The server can export OpenTelemetry traces and write JSON logs that carry trace IDs, so a collector can follow a request through the server. Warnings keep their trace IDs at every log level, and neither output contains payloads or credentials. See logs and traces. (#1001)
- Cluster deployment and recovery commands, and maintenance with
--graph, accept ans3://cluster root on the command line. The 0.14 CLI could not reach a stopped S3 cluster, so it could not unlock it, optimize or clean it up, or finish a deployment with its writers stopped. See inline S3 roots. (#1189) omnigraph studio --target NAMEopens Omnigraph Studio on a configured server target. It sends no request and passes no credential; Studio gets its credentials through the CLI. See Open Studio. (#1137)omnigraphandomnigraph helptell a first-time agent to read the Omnigraph skill before it writes a schema or a query, so it starts from the current rules. See the matching skill. (#1198)nix build github:ModernRelay/omnigraphbuildsomnigraph,omnigraph-serverandomnigraph-azure-admissionwith the release toolchain on Linux x86_64 and arm64 and on macOS arm64. See the install guide. (#1153)- macOS release binaries are signed with a Developer ID and notarized by Apple. The CLI keeps one identity across upgrades, so Keychain recognizes it; moving from an older build may ask for one authorization. (#1293)
omnigraph-gqt --measurereports how long each session of a concurrent block waited at each engine gate (wait.<gate>), so a test can show where concurrent work queued. See the GQT runner guide. (#1237)omnigraph-gqt --storeruns test cases on a prepared graph of any size without changing it: reads come from the graph, and writes stay in memory. See the guide. (#1287)omnigraph-benchcan benchmark served writes without moving any branch: a workload writesas commit, reads the result, and drops it, so no reset is needed between runs. (#1238)
Behavior changes
- A diff or branch review across several commits of one history reads only the rows that changed, so its cost no longer grows with the table. A diff between two branches that have both moved, or across a merge commit, still compares everything. The
change_lineagesetting turns the pairing off, or checks it against a full comparison. (#1201) - Traversals do less work per row, with 45 to 85% fewer memory allocations in the measured queries. Results and storage requests are unchanged. (#1241)
- A mutation
wherein which more than four properties are each tested twice or more with=,!=orinis refused before it changes anything. This comes with the fix for filters that matched null rows, under Fixes. See the mutations guide. (#1286) - An overwrite
load csvreserves its share ofquery_memory_bytesbefore it reads, so a load that does not fit is refused at once, and the refusal names the smallest limit that would admit it. A keyed overwrite close to the limit may need a higher one. See settings. (#1187) - For Rust embedders,
ensure_indicesandensure_indices_onalso rebuild lagging BTREE and vector indexes, report lagging full-text coverage as pending, and need an actor withChangewhen a policy is installed: useensure_indices_on_as. (#1150) cleanup --keep Nwithout--older-thanno longer stops when it cannot read the catalog of a commit it drops;--older-thanstill stops. See cleanup. (#1188)- When a publication's outcome is unknown, the server log names its graph commit in a
publicationfield, so you can check whether it landed. The error message itself stays out of the log. See storage failures. (#1124) - Human sign-ins renew for 30 days instead of 8 hours, and a brief failure while renewing no longer forces a new login. Graph operations are never replayed. Older binaries refuse the new login cache, so log out with 0.15 before you go back to an older CLI. (#1199)
- Release pages open with an intro and highlights, explain upgrade actions, and link each change to its pull request. (#880)
Fixes
- A filter or mutation
wherethat tested one nullable property twice with=,!=orin, such asnot (s in ["x"]) or not (s in ["y"]), also matched rows where the property was null, so anupdateordeletewith such a filter changed those rows too. If you ran one, check the rows where that property is null. See the query guide. (#1286) - An S3 server paused past its lease deadline no longer sends a storage write after that deadline, so it cannot write beside the server that took over. See restart after a failure. (#1221)
- A restarting server no longer takes the S3 cluster lock from a server that is still renewing it, and a server that lost the lock stops at its next renewal instead of serving on. See restart after a failure. (#1221)
- When the answer to a conditional write on S3 or Azure (the cluster lock, a lease renewal, a deployment record) is lost, Omnigraph sends the same request again and reads the object back, instead of failing the operation. See storage failures. (#1176)
- Steady writes to one branch no longer keep up to 64 old versions of a busy table in memory. In a 1,500-commit test, the live heap fell from 203 MB to 136 MB. (#1276)
- A
U64query parameter abovei64::MAXnow binds, so a stored value in that range can be matched. Send it as a decimal string so that JSON clients keep every digit. See query parameters. (#1102) - After a branch's parent branch was merged and deleted, diffs,
commit changes,query --snapshotand the change feed could not read the merge base and answerednot_found. They now read it while a live branch still depends on it. (#1240) nearest()always ranks by L2 distance. Over a vector index that another tool built, it could rank by another distance or call approximate results exhaustive; it now ignores such an index and searches every row. Graphs indexed only by Omnigraph were not affected. See the search guide. (#1299)- Whether a one-row
updatefit underwrite_max_bytesdepended on which other rows shared its data file. It is now charged only for the rows it matches. (#1304) - A keyed load of wide vectors was refused at about half of
write_max_bytes. At the default limit, one load now fits about 2,630 rows ofVector(3072)instead of about 1,365. See load limits. (#1308) - OIDC clients no longer loop on sign-in. An identity the server has not admitted gets 403
not_admittedinstead of a 401, which made clients sign in again, and an expired admission snapshot gets 503 withRetry-After. See MCP. (#1185) - Full-text refusals and
optimize's pending full-text reason suggestomnigraph index buildandomnigraph index rebuild --kind full-textwith--target; the commands they suggested before did not parse. (#1150) - Managed
cluster planandcluster applywait for their submission until--timeout(300 seconds by default) instead of a fixed 10 seconds, after which a slow preview used to fail. See managed configuration. (#1208) - Managed plans poll the original preview until it is ready. A timeout keeps its ID, revision and key, so a repeated
planresumes it without submitting again, and--no-waitreturns on acceptance without cancelling the work. (#1209) - Managed
access,queries listandmcpwork with managed targets, using scoped credentials. The MCP bridge renews an expired credential before it sends, never replays a request, and removes credentials from upstream errors. (#1179) - The exported agent skill now covers
load csv, Blob writes, diffs, ranked-readranking, 0.13 graphs and inline S3 roots, and drops advice for 0.12 and the removed MCP flags that sent agents the wrong way. See the skill. (#1215) - The guides, agent skill and policy help describe the released 0.14 CLI, HTTP and MCP contracts, and a release overview collects the final 0.14 upgrade actions. (#1142)
- The 0.13 to 0.14 upgrade guide is an ordered checklist with operator YAML and CLI commands, and says that released 0.13 graphs need no export and rebuild. (#1144)
- Arrow crates move from 58.3.0 to 58.4.1 for RUSTSEC-2026-0442 (RunEndEncoded array validation). The unmaintained
pin-utilsadvisory (RUSTSEC-2026-0441) is ignored with its path, aws-smithy-http through lance-io, because it has no replacement. (#1270) crypto-bigint0.5 is removed for RUSTSEC-2026-0387. The server used it only to verify tokens, and it accepts the same tokens as before. (#1151)omnigraph-gqt --measure: a concurrent block'swait.<gate>ticks and duration now replay the same for a seed, within one tick, where they were wall time. See the runner guide. (#1246)- Tokio 1.53.3 or newer is required for RUSTSEC-2026-0386, an I/O safety issue in its Unix file-descriptor API. Omnigraph's own code does not call that API. (#1145)
Performance
- A branch merge reads only the data files that each side changed, so its storage requests stay flat as one-row commits pile up: 54 instead of 4,168 for a fast-forward merge after 1,024 commits. (#1229)
- An overwrite
load csvreads its source by partition in 8 MiB pieces instead of reading the whole object first, so parsing starts at once. LDBC SF30 loads 1.4× faster. A first record larger than 8 MiB is refused. (#1158) - An overwrite
load csvwrites its data files while rows are still parsing, instead of after validation. LDBC SF30 loads 1.2× faster, and a refused load still leaves no file behind. (#1178) - An overwrite
load csvinto an empty node table or an edge table no longer scans its existing IDs, which was the largest cost of loading a new graph (88 of 261 seconds in the SF30 run). See explain load. (#1141) - An edge
load csvbetween integer-keyed node types checks its endpoints in parallel as rows arrive, not on one thread after parsing. See explain load. (#1147) - A read no longer waits for a commit, refresh or schema change on the same open graph; on S3 that wait was at least one network round trip. (#1228)
- A write that touches several tables commits them side by side instead of one after another, so the branch is free for the next write sooner. What is written does not change. (#1270)
- The first read after another process commits reads the new catalog version once instead of twice: 11 or 12 storage requests instead of 18 to 20. See the freshness probe. (#1230)
- Reading one branch's ref takes one storage request instead of three, so a write, merge or delete on a named branch sends 6 to 10 fewer requests. (#1229)
- A delete reads each table once and finds a deleted
@keythrough its index: a cold delete by key on a table with 256 data files sends 49 requests instead of 327. (#1232) - Appends, merges and mutations on a large table whose key index does not cover every row no longer run out of memory. Their endpoint and
@uniquechecks read the key column once, where they built a filter of every key for each data file; one report saw 7 to 11 GB per load and a process killed in a 24 GB container. (#1231) - A bound edge traversal (
$a $e:knows $b) that returns list or vector edge properties sizes each pair by its own values, so results no longer leave in chunks of a few rows, which took 3 to 4 times as long. Results are unchanged. (#1268) cleanupno longer sends a request before each file it deletes; it takes the size from the listing it already made. (#1266)cleanuplists catalog versions instead of reading every one, so on a long history--keep Nalone reads only the versions it keeps and the merge bases, and--older-thanreads the others concurrently. See cleanup. (#1188)- An index that Lance keeps in several segments counts as covering the rows its segments cover together, so a traversal over such an edge table keeps using the index. Results are unchanged. (#1300)
- A branch merge that deletes rows removes them by the row IDs it already read, so it no longer scans the
idcolumn of data files that theidindex does not cover. (#1301) - An
updatethat sets only plain properties on a type with Blob or vector properties writes just those columns, where it rewrote whole rows with their Blobs: updating 4 rows with 4 MiB Blobs read 16.9 KB instead of 16.8 MB. See the Blob guide. (#1272)
Deprecations
omnigraph rebuild-full-text-indexesis nowomnigraph index rebuild --kind full-text. The old name still works in this release and prints a warning; update runbooks before it is removed. (#1150)
Contributors
Thanks to @aaltshuler, @azimafroozeh, @blarghmatey, @c4lliope, @lab1207, @pronskiy and @ragnorc, who contributed to this release.
Full changelog: v0.14.0...v0.15.0 · Upgrade guide