How OpenMAIC Manages Document Updates with Versioning: Optimistic Concurrency Deep Dive

OpenMAIC prevents lost updates and race conditions by requiring every document write to include an incrementing version number; the system rejects stale writes with a 409 FUTURE_VERSION error, forcing clients to re-fetch the latest state before retrying.

The THU-MAIC/OpenMAIC repository implements a robust versioning strategy for collaborative document editing. By embedding a monotonic version counter within every document's metadata and enforcing optimistic concurrency checks at the persistence layer, OpenMAIC ensures that distributed sessions never overwrite each other's changes accidentally.

How OpenMAIC Implements Versioned Document Storage

OpenMAIC treats every editable entity—whiteboard scenes, notebooks, and user-generated documents—as an immutable record where the version field acts as a logical clock. This design provides three fundamental guarantees for concurrent access.

Optimistic Concurrency via Version Checks

The system stores a version integer within each document's metadata structure. When a client proposes an update via stage.setItem, the underlying persistence layer in lib/store/kv-persist.ts validates that the submitted version equals the latest stored version plus one. If the stored version is greater than or equal to the submitted version, the write is rejected immediately.

This mechanism prevents the "lost update" problem without requiring distributed locks. The check occurs atomically during the write operation, ensuring that only the holder of the most recent version can successfully commit changes.

Atomic Snapshots and Metadata Consistency

Updates are persisted as atomic units containing both state and version. The snapshot module in lib/store/snapshot.ts generates immutable snapshots that embed the current version number, ensuring that readers always retrieve a consistent view of the document without observing partial writes. When persisted via kv.set, the entire payload—including the incremented version—is written in a single transaction.

The Document Update Flow in OpenMAIC

OpenMAIC follows a strict read-modify-write cycle that clients must implement to maintain consistency. This pattern ensures that every mutation builds upon the most recent known state.

Step 1: Reading the Current Version

Before editing, the client fetches the document including its version metadata through the high-level API:

// Fetch current document from lib/store/stage.ts
const doc = await stage.getItem('my-notebook');
// doc contains: { state: {...}, version: number }

Step 2: Local Mutation and Version Increment

The application applies local edits and increments the version field to signal intent to update:

const updated = {
  ...doc,
  state: { ...doc.state, title: 'Updated Title' },
  version: doc.version + 1,  // Critical: bump version
};

Step 3: Conditional Write with Conflict Detection

The client submits the updated payload through stage.setItem, which delegates to the persistence layer:

await stage.setItem('my-notebook', updated);

Internally, lib/store/kv-persist.ts performs the version validation before writing to the key-value store:

// Simplified from lib/store/kv-persist.ts
export async function setItem(
  name: string, 
  payload: { state: any; version: number }
) {
  const existing = await kv.get(name);
  if (existing && existing.version >= payload.version) {
    throw new HttpRuntimeStoreError(
      409, 
      'FUTURE_VERSION', 
      'newer runtime DSL version'
    );
  }
  await kv.set(name, payload);
}

If the check passes, the new document state and version are persisted atomically.

Step 4: Real-Time Synchronization

Upon successful persistence, the new version is broadcast to other active sessions through the workbench's real-time sync channel. This ensures all participants observe the same monotonic version sequence, allowing them to detect stale state immediately.

Key Implementation Files in OpenMAIC

The versioning system spans several modules with distinct responsibilities:

  • lib/store/stage.ts – Provides the public facade for document CRUD operations. This file implements getItem and setItem methods that orchestrate version checks and snapshot creation before delegating to the persistence layer.

  • lib/store/kv-persist.ts – Implements the low-level key-value storage wrapper that enforces version monotonicity. This module contains the critical comparison logic that throws HttpRuntimeStoreError with status 409 and code FUTURE_VERSION when detecting stale writes.

  • lib/store/snapshot.ts – Generates immutable snapshots embedding version metadata. This utility ensures that document state captures are atomic and include the necessary version information for subsequent optimistic concurrency checks.

  • tests/store/stage-document-persistence.test.ts – Integration test suite verifying end-to-end version enforcement, ensuring that stage.setItem correctly propagates version conflicts from the underlying store.

  • tests/store/kv-persist.test.ts – Unit tests for the persistence layer specifically exercising race condition handling and verifying that only strictly increasing version numbers are accepted.

Handling Version Conflicts and Recovery

When a client attempts to write using an outdated version number, the system responds with a specific error signature that enables graceful client-side handling.

The FUTURE_VERSION Error Contract

As implemented in lib/store/kv-persist.ts, version conflicts produce a HttpRuntimeStoreError with HTTP status 409 and error code FUTURE_VERSION. This signals that the server's current version is newer than the version supplied by the client.

// Test case from tests/store/kv-persist.test.ts
await persist.setItem(NAME, { state: { nickname: 'Ada' }, version: 4 });
await expect(
  persist.setItem(NAME, { state: { nickname: 'Bob' }, version: 4 })
).rejects.toMatchObject({ 
  status: 409, 
  code: 'FUTURE_VERSION' 
});

Client Recovery Strategy

Upon receiving a 409 FUTURE_VERSION error, the standard recovery pattern requires the client to re-fetch the current document via stage.getItem, merge or reapply local changes to the new state, increment the updated version number, and retry the write operation. This enforced retry loop guarantees that no update is silently lost due to network reordering or concurrent modifications.

Summary

OpenMAIC manages document updates through a lightweight yet robust optimistic concurrency system:

  • Version Monotonicity: Every document carries an integer version that must strictly increase with each successful write.
  • Conflict Prevention: The persistence layer in lib/store/kv-persist.ts rejects stale writes with a 409 FUTURE_VERSION error before any data corruption occurs.
  • Atomic Operations: Document state and version are written together as immutable snapshots, ensuring readers never observe partial updates.
  • Distributed Consistency: Real-time synchronization broadcasts version updates across sessions, maintaining a consistent view of document history without heavy locking protocols.

Frequently Asked Questions

What happens when two users edit the same document simultaneously in OpenMAIC?

The first write to reach the persistence layer with the correct next version number succeeds and increments the stored version. Subsequent writes from other clients supply the now-stale version number, causing lib/store/kv-persist.ts to reject them with a 409 FUTURE_VERSION error. Those clients must re-fetch the updated document and retry their changes.

How does OpenMAIC recover document state after a crash or reload?

During initialization, the runtime compares the local in-memory version with the persisted version retrieved from the key-value store via stage.getItem. If the persisted version is newer, the system loads the external state; if the local version is newer (indicating unsaved changes before the crash), the system can attempt to reconcile or persist the local changes, depending on the specific recovery policy implemented in the stage and canvas subsystems.

Where exactly is the version number stored within an OpenMAIC document?

The version field resides at the top level of the document object structure alongside the state payload. According to the type signatures in lib/store/kv-persist.ts, persisted documents follow the structure { state: any; version: number }, where version is a monotonically increasing integer managed entirely by the storage layer and client coordination.

Can developers disable version checking for non-critical document updates?

No, the versioning mechanism is intrinsic to the setItem implementation in lib/store/kv-persist.ts and cannot be bypassed through the public API. This design invariant ensures data consistency across all document types, preventing accidental configuration of unsafe write paths that could lead to silent data loss in collaborative environments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →