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:
- PUT response on 409 returns structured JSON:
{
"error": "mtime_conflict",
"current_mtime_ns": "1714400000000000000",
"current_content": "<full body>",
"your_mtime_ns": "1714399000000000000"
}
- 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
Motivation
The Context Gateway editor uses optimistic concurrency:
GETreturnsthe canonical content +
mtime_ns;PUTechoes backmtime_nsandthe server returns 409 if the file changed underneath the editor.
Today the front-end's only response to 409 is:
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:
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
PUT /context/{type}/{name}returns 409 with body{"detail": "..."}and the currentmtime_nsin thedetail. (Verify — currently the body is plain text "File was
modified by another process. Reload and retry.")
no force-save, no merge.
runtime), but it's not wired to the conflict path.
Proposed change
Introduce a conflict resolution dialog triggered on 409:
Key UX rules:
are not destroyed without explicit user choice.
force: truefieldon the PUT body that bypasses the mtime_ns check. Server logs the
override at WARN with both mtime values.
the Diff tab, and renders user-buffer-vs-on-disk. The user copies
the chunks they want to keep, then clicks Save.
Backend changes:
{ "error": "mtime_conflict", "current_mtime_ns": "1714400000000000000", "current_content": "<full body>", "your_mtime_ns": "1714399000000000000" }?force=truequery param (orforce: truebody field)skips the mtime check when set.
Alternatives considered
optimistic-concurrency contract.
user work.
files that are hand-edited prose, not real-time collaboration.
pitfalls (stale locks, multi-tab dead ends) and changes the
filesystem contract. The optimistic + diff path is the
better-bounded change.
Open questions
localStorageper
(type, name)survives a tab close;sessionStorageiscleaner but loses the buffer if the user navigates away. Probably
sessionStorage+ a "draft restored" toast on next mount.user buffer) or 2-way (on-disk, user buffer)? 3-way needs us to
remember the GET-time canonical, which means more state.
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