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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions