History-Carrying Replica Merge · HCRM

What happens when people edit separate copies of the same record?

Keep independent changes, make disagreements visible, and recognize when a repeated change has already been counted.

These are recorded outputs from a small reference demo. Download the Python version to execute the same eight examples locally.

Two independent editsExample 02
Shared starting status: draft
Alice’s edit · a1status = ready
Bob’s edit · b1status = review
Displayed statusAlternatives retained
review
Both recorded: ready review
A consistent visible value, with the disagreement kept available for inspection.
Operation
One recorded change, such as setting a status or adding to a counter.
Operation ID
The identifier for that change. The same ID must always refer to the same contents.
History
A collection of operations and their dependencies. Invalid histories are inputs rejected in examples 5–8.

Four successful merges

Different edits.
Understandable outcomes.

Each result below comes from the verified offline examples. Expand a case to inspect its exact input and recorded output.

01

Merged as expected

Disjoint concurrent edits survive

Separate fields, both changes kept.

Alice changes the owner; Bob changes the status. The merged record keeps both edits.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "a1",
        "actor": "Alice",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "owner",
        "value": "Alice"
      }
    ],
    [
      {
        "op_id": "b1",
        "actor": "Bob",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "status",
        "value": "review"
      }
    ]
  ],
  "recorded_result": {
    "state": {
      "values": {
        "owner": "Alice",
        "status": "review"
      },
      "counters": {},
      "conflicts": {},
      "op_ids": [
        "a1",
        "b1"
      ]
    }
  }
}
Recorded outcome

owner = Alice
status = review

02

Merged as expected

Same-field alternatives remain visible as a conflict

One visible status. Both alternatives retained.

Alice sets status to ready while Bob sets it to review. The demo displays review and keeps both alternatives, so the disagreement can still be inspected. The visible choice follows this prototype’s ID ordering; it does not settle which edit is right.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "base",
        "actor": "Seed",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "status",
        "value": "draft"
      },
      {
        "op_id": "a1",
        "actor": "Alice",
        "seq": 1,
        "deps": [
          "base"
        ],
        "kind": "SET",
        "key": "status",
        "value": "ready"
      }
    ],
    [
      {
        "op_id": "base",
        "actor": "Seed",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "status",
        "value": "draft"
      },
      {
        "op_id": "b1",
        "actor": "Bob",
        "seq": 1,
        "deps": [
          "base"
        ],
        "kind": "SET",
        "key": "status",
        "value": "review"
      }
    ]
  ],
  "recorded_result": {
    "state": {
      "values": {
        "status": "review"
      },
      "counters": {},
      "conflicts": {
        "status": [
          {
            "op_id": "a1",
            "value": "ready"
          },
          {
            "op_id": "b1",
            "value": "review"
          }
        ]
      },
      "op_ids": [
        "a1",
        "b1",
        "base"
      ]
    }
  }
}
Recorded outcome

Displayed: review
Retained: ready + review

03

Merged as expected

Duplicate delivery does not double-count increments

Same operation again? Count it once.

The unique increments are 5, 2 and 3. Receiving an identical operation again does not add it again, because its operation ID is already present.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "base",
        "actor": "Seed",
        "seq": 1,
        "deps": [],
        "kind": "INC",
        "key": "visits",
        "value": 5
      }
    ],
    [
      {
        "op_id": "a1",
        "actor": "Alice",
        "seq": 1,
        "deps": [
          "base"
        ],
        "kind": "INC",
        "key": "visits",
        "value": 2
      },
      {
        "op_id": "base",
        "actor": "Seed",
        "seq": 1,
        "deps": [],
        "kind": "INC",
        "key": "visits",
        "value": 5
      }
    ],
    [
      {
        "op_id": "b1",
        "actor": "Bob",
        "seq": 1,
        "deps": [
          "base"
        ],
        "kind": "INC",
        "key": "visits",
        "value": 3
      },
      {
        "op_id": "base",
        "actor": "Seed",
        "seq": 1,
        "deps": [],
        "kind": "INC",
        "key": "visits",
        "value": 5
      }
    ]
  ],
  "recorded_result": {
    "state": {
      "values": {},
      "counters": {
        "visits": 10
      },
      "conflicts": {},
      "op_ids": [
        "a1",
        "b1",
        "base"
      ]
    }
  }
}
Recorded outcome

visits = 10
5 + 2 + 3; repeats add nothing

04

Merged as expected

Causal deletion removes an earlier value

Delete the value, keep the operation record.

The deletion refers to the earlier operation that set status. The merged record has no status value. Its operation IDs remain recorded, including the deletion.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "base",
        "actor": "Seed",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "status",
        "value": "draft"
      }
    ],
    [
      {
        "op_id": "a1",
        "actor": "Alice",
        "seq": 1,
        "deps": [
          "base"
        ],
        "kind": "DELETE",
        "key": "status",
        "value": null
      },
      {
        "op_id": "base",
        "actor": "Seed",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "status",
        "value": "draft"
      }
    ]
  ],
  "recorded_result": {
    "state": {
      "values": {},
      "counters": {},
      "conflicts": {},
      "op_ids": [
        "a1",
        "base"
      ]
    }
  }
}
Recorded outcome

status: absent
Operation IDs retained

The distinction that matters

Same ID. Check the contents.

Identical repeat → count oncea1: visits +2
a1: visits +2

One change, received again.

The operation has already contributed. It is kept once, so the demo’s total stays 10.

Different contents → rejectsame: Alice sets ready
same: Bob sets review

Two changes, one identity.

These contents cannot both identify the same operation. The input is rejected.

Four rejected inputs

Some inputs should
stop the merge.

Here, rejection is the expected result. “Invalid history” refers to operations that violate one of these input rules.

05

Rejected as expected

Conflicting operation IDs are rejected

One ID cannot name two different operations.

Alice and Bob each use the ID same, but attach different contents. These are conflicting identities, so the input is rejected. An identical repeat under that ID would be harmless.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "same",
        "actor": "Alice",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "status",
        "value": "ready"
      }
    ],
    [
      {
        "op_id": "same",
        "actor": "Bob",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "status",
        "value": "review"
      }
    ]
  ],
  "recorded_result": {
    "rejected": "op_id collision with different content: same"
  }
}
Recorded outcome

Rejected
Same ID, different contents

06

Rejected as expected

Missing causal dependencies are rejected

An earlier change is missing.

This operation says it depends on another operation that is absent. The demo rejects the incomplete input instead of producing a merged state.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "a1",
        "actor": "Alice",
        "seq": 1,
        "deps": [
          "missing"
        ],
        "kind": "SET",
        "key": "status",
        "value": "ready"
      }
    ]
  ],
  "recorded_result": {
    "rejected": "unknown dependencies for a1: ['missing']"
  }
}
Recorded outcome

Rejected
Missing dependency

07

Rejected as expected

Scalar and counter mixing is rejected

A field needs one meaning.

One operation sets visits to an ordinary value; another treats visits as an additive counter. This prototype does not support mixing those uses for one field.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "a1",
        "actor": "Alice",
        "seq": 1,
        "deps": [],
        "kind": "SET",
        "key": "visits",
        "value": 1
      }
    ],
    [
      {
        "op_id": "b1",
        "actor": "Bob",
        "seq": 1,
        "deps": [],
        "kind": "INC",
        "key": "visits",
        "value": 2
      }
    ]
  ],
  "recorded_result": {
    "rejected": "counter/scalar operation mixing on key visits"
  }
}
Recorded outcome

Rejected
Value and counter mixed

08

Rejected as expected

Dependency cycles are rejected

A dependency cannot loop back on itself.

Operation a1 depends on b1, and b1 depends on a1. That loop cannot establish a consistent before-and-after relationship.

See the input and recorded output
{
  "input_histories": [
    [
      {
        "op_id": "a1",
        "actor": "Alice",
        "seq": 1,
        "deps": [
          "b1"
        ],
        "kind": "SET",
        "key": "status",
        "value": "ready"
      },
      {
        "op_id": "b1",
        "actor": "Bob",
        "seq": 1,
        "deps": [
          "a1"
        ],
        "kind": "SET",
        "key": "status",
        "value": "review"
      }
    ]
  ],
  "recorded_result": {
    "rejected": "dependency cycle"
  }
}
Recorded outcome

Rejected
Dependency cycle

Run the reference examples

Take the demo
with you.

Run the same eight cases on your own computer. The package includes the explained README, inputs, expected results and unchanged reference files.

Download the demo ZIP

11,225 bytes · Python 3.9+ · No extra packages

Save the README separately
Verify the ZIP’s SHA-256

e60cca94df63460d35f68c522a0ec61e0b8b0baf2f0fa6a5bbd74d22916918a4

  1. Download and extract the ZIP.
  2. Open Terminal in the extracted hcrm-demo-candidate folder.
  3. Run this command:
python3 -I -S run_demo.py

Expected: LOCAL_DEMO_PASS, eight cases passed, seven reference files verified.

No account, purchase, API key or network access is needed to run it. Python must already be available. The webpage itself shows recorded examples; it does not run Python or merge your own inputs.

Read the result within its scope

A small prototype.
A specific claim.

This evaluation prototype covers map values, deletion and additive counters. It demonstrates how the supplied operation histories merge; successful examples do not prove that a complete collaborative app works.

  • List, text, rich-text, storage and synchronization are outside this demonstration.
  • It is not an official Automerge implementation, a wire-compatible client or an endorsed Automerge product.
  • The visible conflict winner uses this prototype’s own deterministic ID ordering.

The runnable reference comes from the Automerge Merge Semantics Machine Edition evaluation public companion. Its evaluation status and non-claims are retained. No commercial catalog authorization is asserted.

Read the reference license
Copyright (c) 2019-2025 the Automerge contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Reference package identity

Source companion: automerge-merge-semantics-me-candidate-v0.1-public-evaluation.zip

SHA-256: 9ae5d9320609b4fd2e4a89755e1333f5ed481d1757ced6e7d18a527d40fc5f09

Seven reference files are unchanged. Their identities are verified by the downloaded demo before it runs. Original evaluation and capability metadata remain inside the package.