Skip to content

Feature: conditional-write (CAS) precondition on mutate (--if-manifest-version / --if-commit) for atomic claims #365

Description

@blarghmatey

Type: enhancement / feature request

Summary

Add a conditional-write (compare-and-swap) precondition to mutate so a client can say "apply this write only if the graph/branch/table has not advanced past version N (or commit <id>), otherwise reject it". Today the OCC token needed for CAS is readable but not enforceable on write, so a single-round-trip store-level compare-and-swap is impossible from the client.

Current surface (verified against the 0.8.x binary)

  • An optimistic-concurrency token exists and is exposed:
    • omnigraph commit list --json → per-commit graph_commit_id (ULID) + monotonic manifest_version.
    • omnigraph snapshot --json → branch manifest_version and per-table table_version.
    • Each write advances these.
  • But there is no conditional-write primitive. omnigraph mutate accepts no --if-version / --expected-commit / precondition flag, and the query engine cannot express a compound ... WHERE status = 'open' guard inside the mutation itself.
  • Lance OCC conflicts do surface on a genuinely-raced manifest version (stale view / manifest table version), but that only fires when two writes collide on the exact same base version — it is a race detector, not a value/precondition guard, and it cannot express "reject unless HEAD is still at the version I read."

Net: a client cannot ask the store to "set this field only if the row/branch is still in the state I read," and have the store atomically reject a lost race.

Requested behavior

A precondition on mutate (and ideally load) that the engine checks at commit time and rejects if unmet. Any one of these shapes would unblock us; listed roughly in order of preference:

  1. Version/commit precondition (coarse, simplest):

    omnigraph mutate --server <url> --graph <id> \
      --if-manifest-version <N>     # reject if branch manifest_version != N
      # or: --if-commit <graph_commit_id>
    

    Reject with a distinct, machine-detectable error (distinct exit code or a stable error token in the JSON body — not just a message substring) when the branch has advanced past <N> / <id>.

  2. Per-table version precondition: same idea keyed on the table_version from snapshot (--if-table-version <table>=<v>), so an unrelated write to a different table doesn't spuriously fail the CAS.

  3. Conditional mutation guard (finest, most work): let the mutation itself carry a WHERE-style predicate evaluated atomically at commit (SET status='in_progress' ... IF status='open'), so the precondition is on the row's value rather than a global version. This is the strongest form and removes the read-then-CAS round trip entirely.

The coarse version-based form (1) is enough for our use case.

Use case / motivation

We use omnigraph as a shared, multi-writer work-coordination graph (an MCP "task tracker" backing parallel AI agents and multiple humans). task_claim is the mutex: read a task, and if it is unclaimed, write status=in_progress, assignee, claimed_at.

On a local .omni store an advisory flock serializes writers, so this is safe. But in the deployed topology the store is a shared omnigraph-server over http(s)/S3, where the local lock cannot coordinate across pods. Two agents that both read a task as open both write their own claim; without a store-enforced precondition, last-write-wins silently clobbers the first claimant.

We shipped a best-effort CAS to work around this (conflict-surfacing writes that don't blind-retry + a conflict-aware claim + post-write ownership verification + a lease backstop). It collapses the double-claim race to near-zero and never silently clobbers, but it is explicitly not atomic: a residual window remains where two callers each run their post-write verification after both writes settle. A version/commit precondition on mutate would let us drop the post-write verification and make task_claim a real single-round-trip mutex.

Acceptance (from our side)

  • mutate accepts a manifest-version or commit-id precondition and atomically rejects the write if HEAD advanced.
  • The rejection is distinguishable from other errors without string-matching a human message (distinct exit code or a stable error token in --format json output), so a client can branch on "lost the CAS" vs. a real failure.

Happy to test against a pre-release build. Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions