Repository navigation
feat(branch): start a branch at a graph commit - #1016
Conversation
…x directory The collector deletes a swept version's files before its manifest, so a pass that stops in between leaves the manifest for the next pass. On a local store that next pass failed for the table: `delete_paths` called `remove_dir_all` again on the `_indices/<uuid>` directory the stopped pass had removed, and Lance's local provider reports an absent directory as `NotFound`. The table kept the error and deleted nothing on every later run. Object stores list the prefix empty and succeed, so S3, Azure and the DST memory store converged already. An absent index directory is now already deleted, as an absent file already was. The files-before-manifest order is unchanged, and the deletion set still holds only paths no retained root marks. The failpoints test stops a pass at `cleanup.sweep_pre_manifest_delete` after it removed an index directory only the pruned pin names, then proves the next pass removes that manifest, reports no error, and leaves every retained root's paths in place.
`branch create` can start a branch at a published graph commit instead of a branch head: `from_commit` on `POST /branches` (exclusive with `from`), `--from-commit` on the CLI, `from commit "<id>"` in the GQ statement (language version 2.3) and `from_commit` on the MCP branch tool. The engine clones the `__manifest` version that published the commit on the writing incarnation's tree, so the new branch's first manifest names the commit's table pins, lineage and schema contract in one native create. Under the branch-create gates it refuses, before any effect, a commit whose pinned table versions cleanup reclaimed or emptied (410 `not_retained`), one written under an earlier schema or before the storage upgrade, an unpublished commit and an unattached snapshot (409), with the reason in the new `branch_start_refusal` error detail. Every pin the source head does not hold is probed object by object, because the collector deletes a version's files before its manifest. Policy checks a commit start as `branch_create` from the branch that wrote the commit; a denial answers the not-found of an unknown id. Every creation receipt now carries `from_commit_id`, and its `from` names the branch the new branch was forked off.
With the interrupted-sweep fix merged in, the failpoint test now also runs the cleanup retry: it removes the half-swept manifest, the start at that commit stays refused as not retained (the version no longer opens), and a start at the retained head still creates its branch.
Review: feat(branch): start a branch at a graph commitHead reviewed: SummaryThe design is sound. A commit start clones the One public outcome is not consistent. When the branch that wrote the commit is gone, the answer changes with the door and with name reuse. The user guide promises a different answer. Fix this in the engine before merge. BlockingB1. One fact gets three different answers, and the guide promises another. Why: the fact is "the branch incarnation that wrote the commit no longer exists". A client must get one answer for one fact, on every door. Now Studio and the CLI cannot handle this case reliably. What I observed on
Where:
How to fix it at the contract owner (the engine):
Impact if not fixed: the embedded CLI and the server disagree, which the parity matrix exists to prevent. A client must parse message text to tell the cases apart. The RFC 0060 rule change (see Future direction) would then need a change to wire semantics, not only a new reason. OptionalO1. The retention probe sends one object request per file, in sequence, under the process-wide exclusive schema permit. Why: Evidence: a temporary counter shows 14 sequential How (now, small): put the objects of all probed pins into one bounded concurrent stream (reuse How (root cause, follow-up): O2. Why: two guards produce this reason: How: add one case to the storage-upgrade owner ( Before merge (housekeeping, not a design finding)#1011 landed on What I verified as correct
Future direction and other open work
Software-liability assessment
ValidationCommit PR tests on the exact head (all pass):
Temporary proofs for B1 and O1. These were local only and never pushed; I restored the tree, and
Limits:
|
…9e279e # Conflicts: # changelog.d/cleanup-interrupted-sweep-converges.fixed.md # crates/omnigraph-cli/src/cli.rs # crates/omnigraph-cli/src/client.rs # crates/omnigraph-cli/src/main.rs # crates/omnigraph-cli/src/output.rs # crates/omnigraph-cli/tests/integration/parity_matrix.rs # crates/omnigraph/src/db/mod.rs # docs/user/branching/index.md # docs/user/operations/maintenance.md
A start at a commit whose branch was deleted answered three ways: the engine's `BranchNotFound` for a branch the caller never named, an HTTP-only rewrite to 404, and a 400 once the name was created again. The engine now captures the commit's branch under the control gates and decides once: a name that resolves to nothing and a name created again since are the same fact, refused before any effect as `BranchDeleted` (409 `branch_deleted`, with the branch in the new `branch` field). The server's rewrite is gone, so every door gives the engine's answer. The retention probe ran one object request at a time under the exclusive schema permit every write waits on. It now opens the probed pins and probes all their objects in one bounded concurrent stream that stops at the first missing object, and a slope gate in the branch control cost owner pins one request per pinned fragment. Every record carries the contract its commit was written under (the storage upgrade gives converted commits one), so the engine's missing-contract arm could never fire: it is now an integrity error, and the catalog's legacy-version check is the `earlier_storage_format` refusal, covered by a test that upgrades a real pre-upgrade graph. The two branch-start GQT cases run one DST seed, as most cases do, to stay well inside the corpus budget.
|
Thanks for the careful review. Addressed in f91513a: B1. The engine now decides "the branch incarnation that wrote the commit is gone" in one place, Tests:
O1. The probe now opens the pins and probes the objects of all of them in one bounded concurrent stream (8, the existing bound), and stops at the first missing object ( O2. Only one of the two guards can fire first. Every record carries the contract its commit was written under: the storage upgrade gives each converted commit one ( Housekeeping. Main is merged twice, the second time through Future direction. Agreed on RFC 0060: with a typed reason, that change becomes one removed refusal. I posted alignment notes on the open PRs this one meets:
|
ReviewHead reviewed: 0 blocking findings. 0 optional findings. Why, what, how, impactA caller starts a branch at a published graph commit. The new branch holds that commit's rows and history. The start is one publication: Lance clones the The previous review found one fact with three answers. The fact is: the branch incarnation that wrote the commit no longer exists. This head answers that fact once.
A denied actor still receives the not-found answer of an unknown commit id. The refusal is Doors
The engine owns the decision. The other doors copy it. Earlier findingsB1 is resolved. A deleted name and a reused name are one typed refusal, decided under the control gates, before O1 is resolved. O2 is resolved. A record with no schema contract is Contract and merge
The user guide table matches: a commit of a deleted branch, even when the name is reused, is 409
LiabilityThis removes a transport rewrite and two engine messages for one fact. It adds one reason and one optional field on a detail that already exists. A later similar refusal is one more enum arm and one more guide row. Draft RFC 0060 (PR #677) makes a fork select a retained root. When that lands, this refusal can go away as one deleted arm. Until then, a start still requires the writing incarnation. The alignment notes on #963, #1012, #1076, #1078, and #1072 name the right seams. #1072 must keep Lance ValidationHead
Before
The new Limitations
|
Merges main at b8e82ab, including ModernRelay#1016 (start a branch at a graph commit), and reshapes the detailed branch listing: - The creating actor is no longer recorded. Its second write of the new ref added a durable key, a crash seam and an uncertain create outcome, and a late landing after a delete could bring the deleted ref back as live. The listing is now purely derived and writes nothing. - The start is reported as `from` and `from_commit_id`, the names the create receipt uses since ModernRelay#1016, instead of a `fork` commit object. A start at a past commit clones that commit's version, so the same read answers it. - Following the draft HTTP API conventions: the query parameter is `detailed`, and absent optional fields are omitted instead of null.
What & why
OmniGraph Studio's History wants to start a branch at a commit in a branch's history, to try a fix or a revert there.
branch createonly forked a branch's current head.branch createnow starts at a published graph commit on every door:from_commitonPOST /branches,--from-commitonomnigraph branch create,from commit "<id>"in the GQbranch createstatement (language version 2.3), andfrom_commiton the MCP branch tool. The new branch holds the commit's rows and history; merging it back uses the commit as the merge base.Why a typed
from_commit, not a commit id infromdocs/dev/versioning.md: no old-client fallback or negotiation), so the field needs no compatibility shim. The new field and the receipt'sfrom_commit_idappear inopenapi.json.from commit "<id>". A barefrom commitstill names a branch calledcommit.from_commit_id(HTTP, statement outcome, CLI--jsonand human output): the exact commit the new branch starts at. Itsfromnames the branch the new branch was forked off; for a commit start, that is the branch that wrote the commit.Engine
__manifestversion that published the commit, on the tree of the branch incarnation that wrote it. Lancecreate_branchat that version goes through the existing recoverable two-phase create. The first manifest therefore names the commit's table pins, lineage and schema contract in one native create (invariants 2 and 4).BranchStartRefused:not_retained, 410);schema_changed, 409);earlier_storage_format, 409);branch_deleted, 409, with the branch inbranch). The capture of the commit's branch decides this once: a name that resolves to nothing and a name created again since are the same fact and get the same answer on every door.unpublished_commit,unattached_snapshot, 409). Neither is branch history; both are detached__manifestversions.branch_createfrom the branch that wrote the commit. A denial answers the 404 of an unknown id, so the id reveals nothing.RFC #677 (draft RFC 0060) covers forking at a retained snapshot under a new format. This is the current-format slice and follows that RFC's rules where they overlap.
Tests
integration/branching.rs: a main commit (pins, history, writes, a three-way merge back), a named-branch commit, and the refusals, including a deleted writer branch and a name created again.db/upgrade/tests.rs: after a real storage upgrade, starts at pre-upgrade commits refuse asearlier_storage_formatbefore any effect, and a start at a post-upgrade commit succeeds.integration/branch_control_cost.rs: a slope gate of one probe request per added pinned fragment.integration/maintenance.rs: cleanup keeps the started commit, and a reclaimed commit is refused naming the type.failpoints.rs: a pre-native failure and a lost acknowledgement for a commit start. A commit an interrupted cleanup emptied is refused; after the cleanup retry (which converges since fix(cleanup): finish a table whose interrupted sweep removed its index directory #1011) it stays refused because its version no longer opens, and the retained head still starts a branch.policy_engine_chassis.rs: allow, and deny as not-found.from commit "<id>", a barecommitbranch, and malformed ids.--from-commit, and the human receipt line.branch_create_at_a_commit.gqt(start, merge base, merge) andbranch_create_at_a_commit_refusals.gqt(proposal, snapshot, deleted and recreated writer, unknown id, on the engine, DST and server environments), withfrom commit "${name}"support in the runner.