You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
api: /snapshot does not name the graph commit it reflects #1167
GET /snapshot (SnapshotOutput in crates/omnigraph-api-types/src/lib.rs) reports graph_branch, graph_manifest_version, internal_schema_version and datasets, but not the graph commit at that version. A client that shows what a snapshot holds (per-type row counts, for example) and wants to say which commit they are for has to:
read /snapshot;
read the branch's newest commit;
match the commit's graph_manifest_version to the snapshot's.
That is a second request. It also races: a commit that lands between the two reads leaves the versions apart, and the client has to report the counts as being for no known commit. OmniGraph Studio does exactly this today.
Proposed change
Add graph_commit_id to SnapshotOutput: the id of the commit that graph_manifest_version holds, or null when that manifest version holds no commit. The answer then names its commit consistently with its datasets, with no second read. I have not checked what naming the commit costs the server.
This is a narrow step beside #1054, which asks for answers to report their read set and schema identity. Naming the commit does not replace that, but a client gets a consistent (commit, datasets) pair in one request.
GET /snapshot returns the branch, the manifest version, the storage-format version, and each table's entity count. It does not return the graph commit those counts reflect. A client that wants the commit reads the branch's newest commit and compares graph_manifest_version. A commit that lands between the two reads makes the versions differ, and the client cannot name the commit for the counts it already has.
The same gap is on omnigraph snapshot and on the MCP snapshot tool. Both use the same SnapshotOutput.
POST /query already returns graph_commit_id for the snapshot it read. The snapshot route drops that id.
Reproduction
Reproduced on main da1c46368c33876d61615add0ceb6778f9ce2b58.
Environment: Linux, rustc 1.97.1, the debug omnigraph and omnigraph-server binaries in that checkout. Local file cluster, graph knowledge, schema node Person { name: String @key, age: I32? }. Server: omnigraph-server --cluster file:///tmp/og1167/data --bind 127.0.0.1:18080 --unauthenticated. Every graph request sent Omnigraph-Http-Api: 0.14.
GET /graphs/knowledge/snapshot returned only graph_branch, graph_manifest_version, internal_schema_version, and datasets. There is no graph_commit_id. The first answer was manifest version 1, Person count 0. GET /commits?branch=main listed commit 01M5VHCY0Y81YFT9T0XC9S940T at manifest version 1. POST /query for the same branch returned that commit id.
After inserting Ada, the snapshot was manifest version 2, Person count 1, still with no commit id. The newest commit was hb1.01M5VHDVX5FX7PH39Z4T7X2X76.1.01M5VHDVX5FX7PH39Z4T7X2X76 at version 2. The query answer used that same id. With no commit between the two reads, the version match succeeds. The snapshot body still does not name the commit.
Race, in order:
GET /snapshot held manifest version 2 and Person count 1.
An insert of Grace published hb1.01M5VHDVX5FX7PH39Z4T7X2X76.2.01M5VHDW066847D66MR0PHF7N2 at manifest version 3.
GET /commits?branch=main&limit=2 then showed that version-3 commit as the newest.
Version 2 does not equal version 3. The newest-commit match fails. The held counts are Ada's world. The page still contains the version-2 commit, so a client that walks the whole history can still find it. The workaround in the report reads the newest commit only.
branch create feature published no new commit (commit is null). The receipt's from_commit_id is the Grace commit. GET /snapshot?branch=feature was manifest version 3, Person count 2, and again had no commit id. The newest feature commit is also version 3, because branch create clones that manifest version. POST /query on feature returned the Grace commit id. The snapshot did not.
CLI, embedded on file:///tmp/og1167/data/graphs/knowledge.omni:
omnigraph snapshot prints graph_branch, graph_manifest_version, internal_schema_version, and the dataset lines. It prints no commit id.
omnigraph snapshot --json and omnigraph snapshot --branch feature --json use the same four fields.
This matches the report. It is a missing field, not a crash. The report's null case (a manifest version that holds no commit) did not appear. The empty graph's version 1 is the schema commit, and the fresh branch still has the inherited head.
Cause
SnapshotOutput has four fields: graph_branch, graph_manifest_version, internal_schema_version, and datasets (crates/omnigraph-api-types/src/lib.rs, SnapshotOutput). snapshot_payload copies the manifest version and the datasets. It does not copy a commit id.
GET /snapshot (server_snapshot in crates/omnigraph-server/src/handlers.rs) loads one snapshot with snapshot_of, reads the storage stamp from that snapshot, and calls snapshot_payload. snapshot_of (crates/omnigraph/src/db/omnigraph.rs) calls resolved_target and keeps only resolved.snapshot. resolved_target already sets graph_commit_id from effective_graph_head on that same coordinator. That call reads the head record of the pinned __manifest version. It does not list history.
The catalog carries graph_heads on the snapshot for this reason: a later read of the commit graph can pair these datasets with another version's head (crates/omnigraph-catalog/src/lib.rs, Snapshot).
ReadOutput.graph_commit_id is that effective head. Its comment says the id and the rows come from one pinned version, and that a fresh branch returns the inherited source head so the id can be used as Omnigraph-If-Graph-Commit. query_with_head returns resolved.graph_commit_id.
Snapshot::graph_head(branch) is the wrong source for this field. It is the materialized graph_head:<branch> row. A fresh branch has no such row. fresh_fork_publishes_on_top_of_the_head_it_inherits expects exact_graph_head() to be None and snapshot().graph_heads to be empty, while the manifest head record is the inherited commit. writes.rs expects query_with_head on that fresh branch to return the inherited id. Copying graph_head(branch) onto the snapshot would omit the id that query returns, on the branch whose datasets are that inherited commit.
The embedded CLI builds the same SnapshotOutput (GraphClient::snapshot) and the human printer writes the four fields (write_snapshot_human). The MCP snapshot tool calls server_snapshot and returns that body (crates/omnigraph-server/src/mcp.rs). Its description names the manifest version, the storage version, and the entity counts (crates/omnigraph-mcp/src/catalog.rs). The operation table binds one snapshot operation to GET /graphs/{graph_id}/snapshot, the MCP tool, and the CLI command (crates/omnigraph-api-types/src/operations.rs).
OpenAPI SnapshotOutput requires those four fields and has no commit property (openapi.json).
A production publish that moves table rows records a graph commit in the same overwrite. The catalog comment on commit_changes_with_lineage says a publish without lineage leaves the head commit in place while the table rows move, and that no production writer does that. On current main, branch create clones the parent manifest version (create_branch). It does not publish a later version. That is why the fresh branch's manifest version equals the fork commit's graph_manifest_version.
Fix
Put optional graph_commit_id on SnapshotOutput. Set it from the graph_commit_id that resolved_target already computed for that call. Thread it through snapshot_payload so the HTTP handler and the embedded CLI share one builder. Print it in the human snapshot. Say it in the MCP tool description. Regenerate openapi.json.
Use the same meaning as ReadOutput.graph_commit_id: the effective head of the pinned manifest. On a fresh branch that is the inherited commit. Omit the property when the head is absent, as query does (skip_serializing_if). Leave it out of the OpenAPI required list.
Keep HTTP contract 0.14. The HTTP conventions RFC treats an added response field as additive. Old clients that ignore unknown properties keep working. Clients with a fixed struct need a regenerated schema. MCP clients refresh tools/list.
Extend the existing owners:
snapshot_route_returns_graph_and_published_dataset_versions in crates/omnigraph-server/tests/integration/data_routes.rs
snapshot_json_returns_graph_version_and_datasets and snapshot_human_output_uses_graph_and_dataset_vocabulary in crates/omnigraph-cli/tests/integration/cli_data.rs
snapshot_output_schema_has_expected_fields in crates/omnigraph-server/tests/integration/openapi.rs
The assertion is: the snapshot id equals the query id for that branch, including a fresh branch, and it equals the head commit. A later commit does not change the body already returned. Update the CLI reference in the same change, and add a changelog fragment. The user-visible sentence is that snapshot names the commit the counts reflect.
This costs no extra storage read. The id is already on the resolved target.
Other designs
Scan the commit list for a commit whose graph_manifest_version equals the snapshot. That is the workaround. It is a second read, it races, and it reads lineage to recover a fact the pinned head already holds. RFC 0063 describes a later head-only publish on branch create. create_branch does not do that today. If it lands, the manifest version can move past every commit record while the datasets still reflect the fork commit. A version scan returns no commit. The effective head still names the fork commit.
Copy Snapshot::graph_head(branch). That row is absent on a fresh branch, so the snapshot would disagree with query.
Embed a full CommitOutput. The client then has parents and time in one response. The commit shape then lives on two responses, and the snapshot payload grows. The id is the value query already uses for a conditional write. Parents and time stay on GET /commits/{commit_id}, addressed by an id taken from the same body as the counts.
Wait for #1012. That pull request adds each branch's head to GET /branches?detailed=true, from a read of that branch's latest manifest. A branch menu can use it. The counts still come from /snapshot. The two reads can still observe two versions. #1012 does not name the commit inside the snapshot body.
Liability
The observable contract is: one snapshot response names the commit whose world the counts reflect. The physical source is the head record of the __manifest version that supplied the datasets. Five more read answers should take that same resolved id. A per-route lookup of the commit list becomes a second authority and can disagree with the counts. Invariant 3 (one coherent snapshot) and invariant 12 (one source of truth) both point at the pinned head.
What this choice spends: the snapshot body gains one optional string, and a client that wants the full commit record still fetches it. That is the right spend. The race goes away. The payload stays the id query already defined.
Unreleased lines checked: v14.0.0-rc.2 (2026-10-22) and v15.0.0-beta.1 (2026-10-22). Their notes are encoding, scalar indexes, MemWAL, cleanup concurrency, and RaBitQ. A search of open Lance issues for a graph-commit field does not show a change that would add this id.
Lance owns each dataset version. The snapshot already reports that as published_dataset_version. The graph commit is the head record in OmniGraph's __manifest. A Lance release does not add graph_commit_id to SnapshotOutput. The Lance fences in docs/dev/lance.md (torn branch-ref reads, manifest version CAS) change how a version is read. They do not close this gap.
Related work
#1054 is open. It asks read answers and /snapshot to report the table versions they read and the schema they used. Naming the commit does not replace that. The same rule applies: the facts already exist on the pinned read, and the wire type drops them. No pull request adds those fields. The comment on api: read answers report the table versions they read and the schema they were answered under #1054 is the reporter's summary. It is not a fix.
#1012 is open. It adds branch head, start, and times to the branch listing. It does not change SnapshotOutput.
#1130 is open. It adds retrieval and score descriptors to query answers. It does not change /snapshot.
#1149 is open. It narrows the graph-access RFC to client contracts. That direction supports one client answer for what a read reflects. The RFC text does not add this field.
RFC 0063 is why a fresh branch's exact graph_head:<branch> row can be absent while the effective head is the inherited commit.
The HTTP conventions RFC says an additive response field keeps contract 0.14.
No open pull request implements SnapshotOutput.graph_commit_id. This issue is not a duplicate. It was not closed.
Limitations
The race was three requests in order from one client. It shows the newest-commit match fail. It does not show two clients racing inside the server. A walk of the whole commit list would still find the version-2 commit after this insert.
A live MCP tools/call for snapshot did not return a body. The debug server aborted with thread 'tokio-rt-worker' has overflowed its stack on POST /mcp. The MCP conclusion is from the handler, which returns server_snapshot's SnapshotOutput.
No failing test was left in the tree. The proof is the HTTP and CLI transcript above. The existing snapshot tests assert the fields that exist today, so they stay green.
Activity
ragnorc commentedon Oct 26, 2026
Summary
GET /snapshotreturns the branch, the manifest version, the storage-format version, and each table's entity count. It does not return the graph commit those counts reflect. A client that wants the commit reads the branch's newest commit and comparesgraph_manifest_version. A commit that lands between the two reads makes the versions differ, and the client cannot name the commit for the counts it already has.The same gap is on
omnigraph snapshotand on the MCPsnapshottool. Both use the sameSnapshotOutput.POST /queryalready returnsgraph_commit_idfor the snapshot it read. The snapshot route drops that id.Reproduction
Reproduced on main
da1c46368c33876d61615add0ceb6778f9ce2b58.Environment: Linux, rustc 1.97.1, the debug
omnigraphandomnigraph-serverbinaries in that checkout. Local file cluster, graphknowledge, schemanode Person { name: String @key, age: I32? }. Server:omnigraph-server --cluster file:///tmp/og1167/data --bind 127.0.0.1:18080 --unauthenticated. Every graph request sentOmnigraph-Http-Api: 0.14.GET /graphs/knowledge/snapshotreturned onlygraph_branch,graph_manifest_version,internal_schema_version, anddatasets. There is nograph_commit_id. The first answer was manifest version 1,Personcount 0.GET /commits?branch=mainlisted commit01M5VHCY0Y81YFT9T0XC9S940Tat manifest version 1.POST /queryfor the same branch returned that commit id.After inserting Ada, the snapshot was manifest version 2,
Personcount 1, still with no commit id. The newest commit washb1.01M5VHDVX5FX7PH39Z4T7X2X76.1.01M5VHDVX5FX7PH39Z4T7X2X76at version 2. The query answer used that same id. With no commit between the two reads, the version match succeeds. The snapshot body still does not name the commit.Race, in order:
GET /snapshotheld manifest version 2 andPersoncount 1.hb1.01M5VHDVX5FX7PH39Z4T7X2X76.2.01M5VHDW066847D66MR0PHF7N2at manifest version 3.GET /commits?branch=main&limit=2then showed that version-3 commit as the newest.Version 2 does not equal version 3. The newest-commit match fails. The held counts are Ada's world. The page still contains the version-2 commit, so a client that walks the whole history can still find it. The workaround in the report reads the newest commit only.
branch create featurepublished no new commit (commitis null). The receipt'sfrom_commit_idis the Grace commit.GET /snapshot?branch=featurewas manifest version 3,Personcount 2, and again had no commit id. The newest feature commit is also version 3, because branch create clones that manifest version.POST /queryonfeaturereturned the Grace commit id. The snapshot did not.CLI, embedded on
file:///tmp/og1167/data/graphs/knowledge.omni:omnigraph snapshotprintsgraph_branch,graph_manifest_version,internal_schema_version, and the dataset lines. It prints no commit id.omnigraph snapshot --jsonandomnigraph snapshot --branch feature --jsonuse the same four fields.omnigraph query --jsonreturnsgraph_commit_idhb1.01M5VHDVX5FX7PH39Z4T7X2X76.2.01M5VHDW066847D66MR0PHF7N2.This matches the report. It is a missing field, not a crash. The report's null case (a manifest version that holds no commit) did not appear. The empty graph's version 1 is the schema commit, and the fresh branch still has the inherited head.
Cause
SnapshotOutputhas four fields:graph_branch,graph_manifest_version,internal_schema_version, anddatasets(crates/omnigraph-api-types/src/lib.rs,SnapshotOutput).snapshot_payloadcopies the manifest version and the datasets. It does not copy a commit id.GET /snapshot(server_snapshotincrates/omnigraph-server/src/handlers.rs) loads one snapshot withsnapshot_of, reads the storage stamp from that snapshot, and callssnapshot_payload.snapshot_of(crates/omnigraph/src/db/omnigraph.rs) callsresolved_targetand keeps onlyresolved.snapshot.resolved_targetalready setsgraph_commit_idfromeffective_graph_headon that same coordinator. That call reads the head record of the pinned__manifestversion. It does not list history.The catalog carries
graph_headson the snapshot for this reason: a later read of the commit graph can pair these datasets with another version's head (crates/omnigraph-catalog/src/lib.rs,Snapshot).ReadOutput.graph_commit_idis that effective head. Its comment says the id and the rows come from one pinned version, and that a fresh branch returns the inherited source head so the id can be used asOmnigraph-If-Graph-Commit.query_with_headreturnsresolved.graph_commit_id.Snapshot::graph_head(branch)is the wrong source for this field. It is the materializedgraph_head:<branch>row. A fresh branch has no such row.fresh_fork_publishes_on_top_of_the_head_it_inheritsexpectsexact_graph_head()to beNoneandsnapshot().graph_headsto be empty, while the manifest head record is the inherited commit.writes.rsexpectsquery_with_headon that fresh branch to return the inherited id. Copyinggraph_head(branch)onto the snapshot would omit the id that query returns, on the branch whose datasets are that inherited commit.The embedded CLI builds the same
SnapshotOutput(GraphClient::snapshot) and the human printer writes the four fields (write_snapshot_human). The MCPsnapshottool callsserver_snapshotand returns that body (crates/omnigraph-server/src/mcp.rs). Its description names the manifest version, the storage version, and the entity counts (crates/omnigraph-mcp/src/catalog.rs). The operation table binds onesnapshotoperation toGET /graphs/{graph_id}/snapshot, the MCP tool, and the CLI command (crates/omnigraph-api-types/src/operations.rs).OpenAPI
SnapshotOutputrequires those four fields and has no commit property (openapi.json).A production publish that moves table rows records a graph commit in the same overwrite. The catalog comment on
commit_changes_with_lineagesays a publish without lineage leaves the head commit in place while the table rows move, and that no production writer does that. On current main, branch create clones the parent manifest version (create_branch). It does not publish a later version. That is why the fresh branch's manifest version equals the fork commit'sgraph_manifest_version.Fix
Put optional
graph_commit_idonSnapshotOutput. Set it from thegraph_commit_idthatresolved_targetalready computed for that call. Thread it throughsnapshot_payloadso the HTTP handler and the embedded CLI share one builder. Print it in the human snapshot. Say it in the MCP tool description. Regenerateopenapi.json.Use the same meaning as
ReadOutput.graph_commit_id: the effective head of the pinned manifest. On a fresh branch that is the inherited commit. Omit the property when the head is absent, as query does (skip_serializing_if). Leave it out of the OpenAPIrequiredlist.Keep HTTP contract
0.14. The HTTP conventions RFC treats an added response field as additive. Old clients that ignore unknown properties keep working. Clients with a fixed struct need a regenerated schema. MCP clients refreshtools/list.Extend the existing owners:
snapshot_route_returns_graph_and_published_dataset_versionsincrates/omnigraph-server/tests/integration/data_routes.rssnapshot_json_returns_graph_version_and_datasetsandsnapshot_human_output_uses_graph_and_dataset_vocabularyincrates/omnigraph-cli/tests/integration/cli_data.rssnapshot_output_schema_has_expected_fieldsincrates/omnigraph-server/tests/integration/openapi.rsThe assertion is: the snapshot id equals the query id for that branch, including a fresh branch, and it equals the head commit. A later commit does not change the body already returned. Update the CLI reference in the same change, and add a changelog fragment. The user-visible sentence is that
snapshotnames the commit the counts reflect.This costs no extra storage read. The id is already on the resolved target.
Other designs
Scan the commit list for a commit whose
graph_manifest_versionequals the snapshot. That is the workaround. It is a second read, it races, and it reads lineage to recover a fact the pinned head already holds. RFC 0063 describes a later head-only publish on branch create.create_branchdoes not do that today. If it lands, the manifest version can move past every commit record while the datasets still reflect the fork commit. A version scan returns no commit. The effective head still names the fork commit.Copy
Snapshot::graph_head(branch). That row is absent on a fresh branch, so the snapshot would disagree with query.Embed a full
CommitOutput. The client then has parents and time in one response. The commit shape then lives on two responses, and the snapshot payload grows. The id is the value query already uses for a conditional write. Parents and time stay onGET /commits/{commit_id}, addressed by an id taken from the same body as the counts.Wait for #1012. That pull request adds each branch's head to
GET /branches?detailed=true, from a read of that branch's latest manifest. A branch menu can use it. The counts still come from/snapshot. The two reads can still observe two versions. #1012 does not name the commit inside the snapshot body.Liability
The observable contract is: one snapshot response names the commit whose world the counts reflect. The physical source is the head record of the
__manifestversion that supplied the datasets. Five more read answers should take that same resolved id. A per-route lookup of the commit list becomes a second authority and can disagree with the counts. Invariant 3 (one coherent snapshot) and invariant 12 (one source of truth) both point at the pinned head.What this choice spends: the snapshot body gains one optional string, and a client that wants the full commit record still fetches it. That is the right spend. The race goes away. The payload stays the id query already defined.
Lance
Pinned dependency: unmodified Lance 13.0.0 (
Cargo.lock, release v13.0.0, 2026-10-07).Unreleased lines checked: v14.0.0-rc.2 (2026-10-22) and v15.0.0-beta.1 (2026-10-22). Their notes are encoding, scalar indexes, MemWAL, cleanup concurrency, and RaBitQ. A search of open Lance issues for a graph-commit field does not show a change that would add this id.
Lance owns each dataset version. The snapshot already reports that as
published_dataset_version. The graph commit is the head record in OmniGraph's__manifest. A Lance release does not addgraph_commit_idtoSnapshotOutput. The Lance fences indocs/dev/lance.md(torn branch-ref reads, manifest version CAS) change how a version is read. They do not close this gap.Related work
/snapshotto report the table versions they read and the schema they used. Naming the commit does not replace that. The same rule applies: the facts already exist on the pinned read, and the wire type drops them. No pull request adds those fields. The comment on api: read answers report the table versions they read and the schema they were answered under #1054 is the reporter's summary. It is not a fix.SnapshotOutput./snapshot.graph_head:<branch>row can be absent while the effective head is the inherited commit.0.14.No open pull request implements
SnapshotOutput.graph_commit_id. This issue is not a duplicate. It was not closed.Limitations
The race was three requests in order from one client. It shows the newest-commit match fail. It does not show two clients racing inside the server. A walk of the whole commit list would still find the version-2 commit after this insert.
A live MCP
tools/callforsnapshotdid not return a body. The debug server aborted withthread 'tokio-rt-worker' has overflowed its stackonPOST /mcp. The MCP conclusion is from the handler, which returnsserver_snapshot'sSnapshotOutput.No failing test was left in the tree. The proof is the HTTP and CLI transcript above. The existing snapshot tests assert the fields that exist today, so they stay green.