Skip to content

RFC: 409 mtime_ns conflict — diff/merge/force-save UX #763

@memtomem

Description

@memtomem

Motivation

The Context Gateway editor uses optimistic concurrency: GET returns
the canonical content + mtime_ns; PUT echoes back mtime_ns and
the server returns 409 if the file changed underneath the editor.

Today the front-end's only response to 409 is:

// packages/memtomem/src/memtomem/web/static/context-gateway.js:569-571
if (r.status === 409) {
  showToast(t('settings.ctx.mtime_conflict'), 'warning');
  loadCtxDetail(type, name);
}

A toast + silent reload — the user's edits are simply replaced by
whatever's on disk. If the conflict is real (another editor / runtime
migration / git pull mid-session), the user has no way to:

  • see what changed,
  • merge their edit on top, or
  • override the on-disk version with their text intact.

This is fine when conflicts are rare. With multiple sessions per
project and the imminent Settings prod expansion (RFC #761),
conflicts will become more common, and silently
discarding the user's text is the wrong default.

Current state

  • Backend PUT /context/{type}/{name} returns 409 with body
    {"detail": "..."} and the current mtime_ns in the
    detail. (Verify — currently the body is plain text "File was
    modified by another process. Reload and retry.")
  • Front-end loses the user's edits on 409. There is no diff display,
    no force-save, no merge.
  • The Diff tab can already render a 3-way-ish view (canonical vs.
    runtime), but it's not wired to the conflict path.

Proposed change

Introduce a conflict resolution dialog triggered on 409:

┌─ Conflict: someone else modified this file ─────────────┐
│                                                          │
│  Your edits           │   Current on-disk                │
│  ─────────────────    │   ─────────────────              │
│  <user buffer>        │   <fresh GET content>            │
│                                                          │
│  [ Reload (discard my edits) ]                           │
│  [ Force save (overwrite their changes) ]                │
│  [ Open diff in editor ]                                 │
└──────────────────────────────────────────────────────────┘

Key UX rules:

  • No silent reload — the toast-and-reload behavior is gone. Edits
    are not destroyed without explicit user choice.
  • "Force save" requires backend support: a force: true field
    on the PUT body that bypasses the mtime_ns check. Server logs the
    override at WARN with both mtime values.
  • "Open diff in editor" stashes the user's buffer, switches to
    the Diff tab, and renders user-buffer-vs-on-disk. The user copies
    the chunks they want to keep, then clicks Save.

Backend changes:

  1. PUT response on 409 returns structured JSON:
    {
      "error": "mtime_conflict",
      "current_mtime_ns": "1714400000000000000",
      "current_content": "<full body>",
      "your_mtime_ns": "1714399000000000000"
    }
  2. Optional ?force=true query param (or force: true body field)
    skips the mtime check when set.

Alternatives considered

  • Always force-save — rejected, defeats the point of the
    optimistic-concurrency contract.
  • Always reload (current behavior) — rejected, silently discards
    user work.
  • Operational-transform / CRDT — way out of scope for canonical
    files that are hand-edited prose, not real-time collaboration.
  • Lock the file on GET — pessimistic locking has its own UX
    pitfalls (stale locks, multi-tab dead ends) and changes the
    filesystem contract. The optimistic + diff path is the
    better-bounded change.

Open questions

  • Where should the user buffer live during the dialog? localStorage
    per (type, name) survives a tab close; sessionStorage is
    cleaner but loses the buffer if the user navigates away. Probably
    sessionStorage + a "draft restored" toast on next mount.
  • Should the diff view be a 3-way (last-known canonical, on-disk,
    user buffer) or 2-way (on-disk, user buffer)? 3-way needs us to
    remember the GET-time canonical, which means more state.
  • What's the audit trail for force-save? Server WARN log is fine
    internally; do we also want a header on the Detail card surfacing
    "force-saved 3 minutes ago" so a teammate sees it?

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions