
# Diff is a bad interface for LLMs — why Bindly removed 4 update modes

Bindly v0 had five ways to update a Binding's content:

```text
mode: "full"             — send the entire new content
mode: "git-unified-diff" — send a git-style unified diff
mode: "merge-field"      — send specific fields to merge
mode: "merge-full"       — send full content, merge with existing
mode: "inherit"          — inherit content from parent binding
```

By v1, only `full` remained. The other four were cut.

This wasn't a simplification decision. It was a reliability decision. The other modes broke in ways that were hard to detect and impossible to recover from.

## The problem with diff modes

Git's unified diff format looks like this:

```diff
@@ -12,7 +12,7 @@
 This is unchanged context.
-This line is being removed.
+This line is the replacement.
 More unchanged context.
```

Applying a diff requires knowing the **exact current state** of the file. The `-` lines must match character-for-character. If they don't, the patch fails — or worse, applies incorrectly.

When a human runs `git diff`, they're working with the file on disk. The diff is generated from reality. When an LLM generates a diff, it's generating from memory — its best reconstruction of what the document currently says based on whatever it loaded into context.

Several things go wrong in practice:

**The LLM's cached content is stale.** If the Binding was updated since the LLM last fetched it, every `-` line it generates is wrong.

**The LLM hallucinates whitespace.** LLMs are inconsistent about trailing spaces, blank lines, and indentation. A diff that looks correct in the LLM's output fails application because the whitespace doesn't match.

**The LLM generates partial context.** The unified diff format requires context lines (unchanged lines around the change). LLMs often generate too few context lines or slightly wrong ones, causing the diff to fail to locate the right position.

**The LLM doesn't know what it doesn't know.** When a diff fails to apply, the failure is a generic error: "patch failed." The LLM can't diagnose why. It tries again from the same stale memory, fails again.

In v0 testing, `git-unified-diff` mode had roughly a 30-40% failure rate across real LLM sessions. The failures were silent on the user side — the LLM would report success, but the content hadn't changed.

## Why full replacement works reliably

`full` mode sends the complete new content. The server replaces the current content. No matching, no patching, no context lines. The operation either succeeds or fails — and failure is always explicit (network error, auth error, server error).

The LLM can always execute full replacement correctly because it doesn't need to know the current state precisely. It constructs the new document from its understanding of what should be there. If the LLM's understanding is wrong, that's a knowledge problem — but it's a visible one. The user can see the incorrect content and correct it.

## The token cost argument

The objection to full replacement is token cost: sending the entire document on every update is wasteful compared to sending a small diff.

Valid concern for codebases. A 50,000-line file — you don't want to transmit it in full on every line change. Git diff makes sense there.

For knowledge documents (which is what Bindly stores), it's rarely a problem. A well-written Binding is 500-3,000 words. Even at 3,000 words (~4,000 tokens), transmitting the full content on update costs less than a typical reasoning chain the LLM produces when deciding what to change. We're not storing source code.

## What merge modes were trying to solve

`merge-field` and `merge-full` were attempts at something reasonable: the LLM wants to update just the summary, not the entire content. Valid use case.

In v1, this is handled differently. Fields are separate parameters:

```typescript
// Only update metadata — no new Version
mcp_update_binding({
  id: "bnd_abc",
  title: "Updated title"
})

// Update content — creates new Version
mcp_update_binding({
  id: "bnd_abc",
  content: "New full content...",
  summary: "Updated summary",
  keyPoints: ["New point 1", "New point 2"]
})
```

When `content` is not provided, no new Version is created — only the Binding's metadata updates. When `content` is provided, a new immutable Version is created. No merging, no patching. The behavior is explicit and predictable.

## The lesson for LLM API design

Interfaces that require the LLM to have precise knowledge of current state are fragile. The LLM's context is a reconstruction, not ground truth. Any operation that compares the LLM's reconstruction against actual current state will fail when they diverge — and they will diverge.

Design operations that don't depend on precise current-state knowledge:

| Operation | Reliability |
| --------- | ----------- |
| "Set this to X" (replace) | Always works |
| "Add Y to the list" (append) | Works if the list exists |
| "Increment by 1" (server-side delta) | Works with atomic operation |
| "Apply this diff" (patch) | Works only if LLM state matches exactly |

For update operations in knowledge management, replacement is almost always right. The cost of transmitting a full document is low. The cost of debugging silent diff failures — in user trust, in support time, in LLM retries burning context — is high.