When two clients edit the same item from the same starting version, the second write fails with aDocumentation Index
Fetch the complete documentation index at: https://docs.myme.so/llms.txt
Use this file to discover all available pages before exploring further.
version_conflict. Resolution is client-side, layered over the existing 409 envelope and item primitives — no server enforcement, no new endpoints, no new edge types.
The shape of resolution is declared on each type via a merge_policy block. Long-text user-authored fields (body, notes; note on core.highlight) keep both copies. Everything else is last-writer-wins. Custom types inherit or override.
Detection
PATCH /items/{id} with a stale version returns 409 version_conflict. The response carries the three-way context the client needs to resolve, plus the type’s resolved merge policy:
current— the server’s state at the time of the conflict.ancestor— the version the client was working from.conflicting_fields— fields where the client’s update conflicts with server-side changes.merge_policy— the type’s resolved policy, with inheritance applied. Always present.
Resolution strategies
Three modes, picked by the SDK caller.auto(default) — apply the type’smerge_policyper conflicting field.last_writer_winsfields take the server’s value;keep_both_copiesfields spawn a sibling item (see Keep-both semantics). Non-conflicting client changes apply normally; the update retries with the server’s version.manual— surface the error to the app with the full three-way payload. The app decides.callback— the SDK invokes a per-call resolver with the three-way context plusconflictingFields; the resolver returns the properties to apply.
auto is the right default for almost every app. Use callback when the merge logic depends on app-specific semantics that aren’t expressible at the type level.
The merge_policy declaration
merge_policy is a top-level block on each type’s JSON, alongside display_hints and version_policy:
fields object mapping field names to strategies, plus a default for unlisted fields. Two strategies are defined — last_writer_wins and keep_both_copies.
Inheritance behaves the same as display_hints and version_policy. A subtype’s merge_policy merges field-by-field over its parent’s: child entries in fields override parent entries for the same field; child default replaces parent default. A subtype that omits merge_policy entirely inherits the parent’s wholesale.
Fields named in merge_policy.fields must exist in the type’s fields map; unknown strategy values are rejected on POST /types.
Keep-both semantics
Whenauto resolution sees a keep_both_copies field in conflicting_fields, the SDK:
- Calls
items.createwith a new item of the same type — populated from the client’s in-flight values for the keep-both fields and the server’scurrentvalues for everything else, taggedconflicted-copyinline on the create. - Lets the original item retain the server’s winning state for the conflicted keep-both fields. Non-keep-both client changes still apply normally via the usual auto-merge path.
- Emits a
conflictAutoMergedevent with{ itemId, mergedItemId, conflictedCopyId?, fields, strategy }so the app can surface the outcome — typically a toast: “Your edit saved as a conflicted copy — the older version is preserved alongside it.”
conflicted-copy tag is the existing tag-API primitive; no new edge type, no new endpoint. Apps surface conflicted copies the same way they surface any other tagged item.
Keep-both doubles the write count on the conflicting
PATCH: one items.create for the sibling plus the original items.update retry. Two writes versus the current one. At expected conflict rates this is negligible — conflicts are rare per item — but it’s worth knowing if you’re modelling write throughput.Defaults
The principle for the core type set:body and notes keep both copies; everything else is last-writer-wins. core.highlight uses note (singular) as the equivalent long-text field.
| Type | Keep-both fields | Default |
|---|---|---|
core.note | body, notes | last_writer_wins |
core.bookmark | body, notes | last_writer_wins |
core.task | body, notes | last_writer_wins |
core.event | notes | last_writer_wins |
core.highlight | note | last_writer_wins |
core.media and subtypes | body, notes | last_writer_wins |
core.entity and subtypes | — | last_writer_wins |
core.file and subtypes | — | last_writer_wins |
merge_policy.fields fall through to default. A type with no merge_policy block at all behaves as last_writer_wins everywhere — the safe fallback for fields the type maintainers haven’t classified.
The principle is narrow on purpose. Long-text fields are where two parallel edits are most likely to encode independently meaningful content — losing one is a real data-loss event. Scalars, enums, timestamps, and identifiers are conventional metadata where last-writer-wins matches user intent and avoids the noise of a sibling per edit.
Cross-references
- Versions — the snapshot history that makes recovery possible even when policy decides one side wins.
- Errors — version_conflict — the 409 response shape in the error catalog.
- Types — Merge policy — declaring policy on a custom type.