The Problem: Stale Edits
Consider this scenario:- Agent A reads
auth.rsline 42:let token = request.header("Authorization"); - Agent B (running in parallel) modifies
auth.rsline 42 to:let token = request.bearer_token(); - Agent A tries to edit line 42 based on its stale view of the file
Hashline Format
When Enki tags a file with hashlines, each line gets a prefix:line_number: 1-based line number, right-padded to width of total line countxxh3_hash: 2-digit hex hash (low byte of XXH3-64)content: Original line content
Original file
Tagged with hashlines
| delimiters align:
File with 100+
Hash Computation
Hashes are computed using XXH3-64, a fast non-cryptographic hash. Enki uses the low byte (2 hex digits) for compact representation. Implementation:crates/core/src/hashline.rs:4-17
Line content is trimmed before hashing. Leading/trailing whitespace changes don’t invalidate the hash. This makes hashlines robust to indentation normalization.
Tag and Strip
Tagging Content
Thetag_content function adds hashline prefixes:
crates/core/src/hashline.rs:23-48
Stripping Hashlines
Thestrip_hashlines function removes prefixes:
crates/core/src/hashline.rs:93-118
Lines without valid hashline prefixes are passed through unchanged.
Anchor Verification
When an agent edits a file, it provides anchors (hashline-prefixed lines) mixed with new content:Edit content
- Referenced line numbers are in range
- Hashes match the current file content at those line numbers
crates/core/src/hashline.rs:123-161 (verify_hashlines), crates/core/src/hashline.rs:171-260 (apply_edit)
Verification Flow
Edit Semantics
Edits are specified by mixing anchors (existing lines referenced by hashline) and new lines (no prefix):Replace Lines
Anchor the lines before and after the region to replace, put new content between:Insert After Line
Anchor a line, follow with new content:Insert Before Line
New content first, then anchor:Delete Lines
Anchor the lines before and after the region to delete, with no content between:Rules
- At least one anchor required — edits without anchors are rejected
- Anchors must be in ascending order —
3:...cannot come before2:... - Hashes must match current file — stale edits are rejected
- Region between first and last anchor is replaced — everything else is preserved
MCP Tool: enki_edit_file
Workers call theenki_edit_file MCP tool to apply edits:
MCP tool call
crates/cli/src/commands/mcp/handlers.rs:544-557
Why Hashlines Prevent Stale Edits
Consider the parallel agent scenario: Timeline:- T=0: File content is
let x = 42;at line 2 (hash3c) - T=1: Agent A reads file, sees
2:3c| let x = 42; - T=2: Agent B modifies line 2 to
let x = 99;(new hash7f) - T=3: Agent A tries to edit:
2:3c| let x = 42;+ new content - T=3: Enki computes current hash for line 2:
7f - T=3: Hash mismatch: expected
3c, got7f→ edit rejected
let x = 99;), and provide a fresh edit with the correct hash (7f).
Hash Collision Risk
Enki uses 1 byte (256 possible values) for line hashes. What’s the collision probability? Birthday paradox: With ~16 lines, there’s a ~5% chance of collision. With ~50 lines, collision is likely. Why this is acceptable:- Collisions are safe, not catastrophic: A collision means two different lines have the same hash. Enki will incorrectly accept an edit if the agent references the wrong line. This is rare and detectable (the edit produces nonsensical code, tests fail, human reviews the diff).
- Edits are localized: Agents typically edit small regions (5-10 lines). Collision within a small region is unlikely.
- Context from line numbers: Line numbers + hashes together make collisions even rarer. For a collision to cause a bad edit, two lines with the same hash must be in the same region and the agent must reference the wrong one.
If collision becomes a problem, Enki could switch to 2-byte hashes (65k values) or full 8-byte XXH3 hashes. The current 1-byte design prioritizes compactness and readability.
Implementation Details
Hash Trimming
Line content is trimmed before hashing:- Indentation changes (tabs ↔ spaces)
- Trailing whitespace normalization
- Editor auto-formatting
Anchor Parsing
Anchors are parsed withtry_parse_anchor:
- Line must contain
| - Prefix before
|must be{digits}:{2hex} - Leading whitespace is ignored (allows indented anchors)
Apply Edit Algorithm
Implementation:crates/core/src/hashline.rs:171-260
- Parse edit content into anchors and new lines
- Verify all anchors against current file (hash + range check)
- Extract edit region: From first anchor to last anchor (inclusive)
- Build result:
- Lines before edit region (unchanged)
- Replacement content (anchors + new lines from edit)
- Lines after edit region (unchanged)
- Preserve trailing newline behavior of original file
- Single anchor: inserts/appends relative to that anchor
- Anchors at start/end of file: preserves leading/trailing content
- Empty edit region (two consecutive anchors): deletes lines between them
Debugging Hashlines
Check if Content is Tagged
{digits}:{2hex}| pattern in first non-empty line.
Compute Hashlines Manually
Verify Anchors
Best Practices
- Always re-read before editing: If an edit fails with “stale hash”, the agent should re-read the file and compute a fresh edit.
- Edit small regions: Large edits (50+ lines) increase collision risk. Break into smaller edits if possible.
- Use anchors strategically: Anchor the lines immediately before and after the edit region for precise targeting.
- Test with conflicts: Simulate parallel edits in your test suite to ensure hashline verification catches stale edits.
Limitations
- No multi-file transactions: Hashlines verify per-file. If an agent edits two files based on a consistent view, and another agent changes one file in between, the edits may be inconsistent. Enki relies on merge conflict detection to catch this.
- No content-addressable storage: Hashlines verify anchors, but don’t prevent agents from seeing inconsistent views of multiple files. Enki’s CoW copy manager isolates workers, but changes merge asynchronously.
- 1-byte hash collisions: Rare but possible. If collision causes a bad edit, tests/review should catch it.
