--- status: draft owner: claude-code --- # Plan: Conflict-Aware Merge in MCP Tools Reduce content loss under concurrent writes by giving `mark_publish` a tool-level merge strategy. Disjoint edits succeed automatically via diff3; genuine overlap escalates to the calling agent's LLM with all three bodies in hand. ## Motivation Current `mark_publish` is strict optimistic concurrency: version mismatch → conflict error → caller re-fetches and republishes. Two failure modes at scale: 1. **Lost content** — naive callers re-publish their stale body, clobbering the intervening writer 2. **Wasted retries** — agents that edited disjoint paragraphs still take a full LLM round-trip to merge, even though a mechanical merge would have been clean Eliminating the first 80% of conflicts mechanically (diff3) and giving the agent everything it needs for the last 20% (semantic merge) keeps both content loss and LLM cost low. ## Non-Goals - **No DIFF verb.** Versions are immutable; clients can compute diffs locally from any two FETCHes. Adding it to the wire format pulls processing into the server with no protocol-level benefit. - **No server-side merge.** The server stays content-agnostic. Merge logic lives in the MCP tool (Go client code). - **No automatic semantic merge.** The MCP tool has no LLM. Semantic resolution stays the calling agent's job. - **No backoff or fairness primitives in this plan.** Livelock under sustained contention is real but is a schema problem (hot single-doc writes), not a merge problem. Address separately if it shows up. ## Design ### New parameter: `on_conflict` Add an optional parameter to `mark_publish`: ``` on_conflict: "fail" | "merge" ``` - `"fail"` — current behavior. Default for backward compatibility. - `"merge"` — attempt diff3 mechanical merge on conflict, escalate on overlap. ### Merge Flow When `on_conflict: "merge"` and the initial PUBLISH returns version mismatch: 1. **FETCH** `/path/v{expected_version}` — retrieve the base body (the version the agent built its edit from). Available because versions are immutable. 2. **FETCH** `/path` — retrieve current body. 3. **diff3(base, ours, theirs)** where: - `base` = body from step 1 - `ours` = body the agent passed to `mark_publish` - `theirs` = body from step 2 4. **If clean** (no conflict markers): **PUBLISH** with `expected_version` = the version from step 2's response. Return success with a `merged: true` field so the caller knows a merge happened. 5. **If conflict markers**: do not publish. Return a structured conflict response (see below) for the agent's LLM to resolve. The PUBLISH in step 4 can itself conflict if a *third* writer slipped in. Retry up to N times (default 3) before escalating to the caller as a contention failure. ### Response Shapes **Success after merge** (status: ok): ```yaml status: ok version: 7 merged: true base-version: 5 their-version: 6 ``` **Escalation on overlap** (status: conflict): ```yaml status: conflict your-version: 5 current-version: 6 mergeable: false ``` Body contains the diff3 output with conflict markers, so the agent can either: - Send the marked body directly to its LLM for resolution - Or use the metadata to refetch base/theirs separately **Escalation on contention** (status: conflict, after retries exhausted): ```yaml status: conflict your-version: 5 current-version: 9 mergeable: false reason: contention retries: 3 ``` ### Wire-Level Impact Zero. `on_conflict` is an MCP tool parameter handled in the Go client. Server protocol unchanged. Versioned FETCH (`/path/vN`) already exists. ## Implementation ### Where the code lives - **MCP handler**: `client/internal/mcp/` (or wherever `mark_publish` is dispatched). Add the `on_conflict` parameter to the tool schema and route to a merge helper. - **Merge helper**: new package `client/internal/merge/` with: - `Diff3(base, ours, theirs string) (merged string, hadConflict bool)` — pure function, easy to test - `MergeAndPublish(ctx, client, path, ours, expectedVersion int, maxRetries int) (Result, error)` — orchestrates the FETCH/FETCH/diff3/PUBLISH dance - **Diff3 algorithm**: use a vetted Go library if one exists (search `github.com/...diff3`), otherwise implement the Myers-based three-way merge. Keep the implementation in one file behind the `Diff3` function so it can be swapped. ### Test coverage Table-driven tests in `client/internal/merge/`: - Disjoint paragraph edits → clean merge - Same-line edits → conflict markers - Adjacent line edits → clean merge (verify diff3 doesn't over-conflict) - One side adds, other side deletes → expected diff3 behavior documented - Empty `ours` or `theirs` → handled gracefully - `MergeAndPublish` retry exhaustion → returns contention status - `MergeAndPublish` success on second try → returns merged: true with correct versions Integration test against a real server: two clients both edit the same doc in different sections, both call `on_conflict: "merge"`, both succeed, final doc contains both edits. ### Backward compatibility `on_conflict` defaults to `"fail"`. Existing callers see no behavior change. New callers opt in. ## Open Questions 1. **Conflict marker format.** Use git-style `<<<<<<< / ======= / >>>>>>>` or a JSON-friendly structured form? Git-style is universally readable by LLMs but uglier in markdown; structured form is cleaner but the agent has to parse it. 2. **Should the tool return the merged body to the caller on success, or just metadata?** Returning the body lets the agent verify; not returning it keeps the response small. 3. **Default retry count.** 3 feels right but pick after observing real workloads. Higher under contention pushes cost onto the unlucky agent. 4. **`mark_append`**: should it also gain `on_conflict: "merge"`? Append already auto-resolves `expected_version`, so it rarely conflicts. Probably not worth the complexity until data shows otherwise. ## Out of Scope (Future) - **`mark_update_section`** — heading-scoped writes that further reduce contention surface. Higher leverage than this plan but bigger change. Track separately. - **Server-side hot-path detection / batching window** — backpressure for sustained contention. Only build if contention shows up in practice. - **Soft leases** — agent reserves a path for N seconds. Fairness primitive, not a merge primitive. ## Success Criteria - Two agents editing different paragraphs of the same document with `on_conflict: "merge"` both succeed without LLM involvement. - Two agents editing the same line both receive structured conflict responses with all three bodies retrievable. - No change to existing callers using default `on_conflict: "fail"`. - Diff3 helper has 100% branch coverage in unit tests. - Integration test with three concurrent writers terminates with all writes reflected in final doc (or one mergeable conflict, never silent loss).