Hashline Edit Subsystem
What
The hashline edit subsystem provides cryptographically-validated file editing for agent operations. It implements a read-side annotation + write-side validation pattern that prevents stale edits and ensures edits are always applied to the correct line content.
Each line read by the agent is tagged with a short MD5 content hash (2 hex characters = 256 buckets). When the agent requests an edit, the hash is validated against the cached file state. If the hash doesn't match, the edit is rejected with a clear error message instructing the model to re-read the file.
Key Features
- Content-hash validation: Every line is validated before editing
- Three edit operations:
replace,replace_range,insert_after - Side-by-side diff visualization: NeXTSTEP-style panel with before/after view
- Automatic cache updates: Cache stays synchronized after every edit
- Unified diff output: Standard diff format for review and verification
Workflow Flow Diagram
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ │ │ │ │ │ │ │
│ read_file │────▶│ hashline.py │────▶│ line_cache │────▶│ hashline_edit│
│ │ │ (tag lines) │ │ (store) │ │ (validate) │
└─────────────┘ └──────────────┘ └─────────────┘ └──────┬───────┘
│
┌──────────────┐ │
│ │◄──────────────────────────────┘
│ File │ (write changes)
│ │
└──────────────┘
│
▼
┌──────────────┐
│ Diff │────▶ UI Renderer
│ Output │ (side-by-side)
└──────────────┘
Key Files
Core Components
| File | Purpose |
|---|---|
src/tunacode/tools/hashline.py |
Content-hash line tagging and validation primitives |
src/tunacode/tools/line_cache.py |
In-memory cache for hashed lines keyed by file path |
src/tunacode/tools/hashline_edit.py |
Main edit tool with three operations (replace, replace_range, insert_after) |
src/tunacode/tools/read_file.py |
File reading that populates cache with hash-tagged output |
src/tunacode/ui/renderers/tools/hashline_edit.py |
Side-by-side diff display with NeXTSTEP panel styling |
File Relationships
read_file.py
│
├──▶ hashline.py
│ ├── tag_lines()
│ ├── content_hash()
│ └── format_hashline()
│
└──▶ line_cache.py (store)
│
│ ▲
│ │ (validate & update)
▼ │
hashline_edit.py
│
├── _validate_ref() ──▶ line_cache.py (get_line, validate_ref)
├── _apply_replace()
├── _apply_replace_range()
├── _apply_insert_after()
│
└──▶ UI renderer ────▶ hashline_edit_renderer.py
How
1. Content Hashing (hashline.py)
Each line is hashed using MD5 (2 hex characters = 256 buckets), giving enough entropy to detect stale references without bloating the context window.
HASH_LENGTH = 2
LINE_HASH_SEPARATOR = ":"
HASH_SEPARATOR = "|"
# Example output format
# 1:a3|function hello() {
# 2:f1| console.log("world");
# 3:0e|}
Key Functions:
| Function | Purpose |
|---|---|
content_hash(line) |
Compute MD5 hash of line content (2 hex chars) |
tag_lines(content, offset) |
Split content and tag each line with hash |
format_hashline(HashedLine) |
Format as `line:hash |
format_hashlines(content) |
Tag all lines in content for display |
parse_line_ref(ref) |
Parse line:hash reference, e.g., "2:f1" |
2. Line Cache (line_cache.py)
Stores {path: {line_number: HashedLine}} as a module-level singleton. The cache is replaced (not merged) on each read_file call.
Cache Operations:
| Function | Purpose |
|---|---|
store(filepath, lines) |
Cache hashed lines for a file |
get(filepath) |
Return read-only view of cached lines |
get_line(filepath, line_number) |
Return single cached line or None |
validate_ref(filepath, line, expected_hash) |
Check if line hash matches cached state |
update_lines(filepath, updates) |
Update cached lines after an edit |
replace_range(filepath, start, end, new_lines) |
Replace a range and re-number subsequent lines |
invalidate(filepath) |
Remove cached state for a file |
clear() |
Clear entire cache (testing) |
3. Edit Tool (hashline_edit.py)
Three edit operations validate against the cache before applying changes.
Validation Flow:
┌─────────────────┐
│ hashline_edit │
│ (operation) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ _validate_ref │──── Parse "line:hash" reference
│ │──── Check file is cached
│ │──── Verify line exists in cache
│ │──── Compare expected vs actual hash
└────────┬────────┘
│
┌────┴────┐
│ │
▼ ▼
┌──────┐ ┌──────┐
│ Pass │ │ Fail │
└──┬───┘ └──┬───┘
│ │
▼ ▼
┌──────┐ ┌─────────────┐
│ Apply│ │ ToolRetryError│
│ Edit │ │ "hash mismatch│
└──┬───┘ │ Re-read..." │
│ └─────────────┘
▼
┌──────────┐
│ Update │
│ Cache │
└──────────┘
Three Operations:
| Operation | Parameters | Description |
|---|---|---|
replace |
line, new |
Replace a single line identified by hash |
replace_range |
start, end, new |
Replace contiguous range of lines |
insert_after |
after, new |
Insert new lines after a referenced line |
Error Messages:
STALE_REF_MESSAGE = (
"File has changed since last read — line {line} hash mismatch "
"(expected '{expected}', cached '{actual}'). Re-read the file first."
)
UNCACHED_FILE_MESSAGE = "File '{filepath}' has no cached state. Read the file first with read_file."
LINE_NOT_CACHED_MESSAGE = (
"Line {line} is not in the cached state for '{filepath}'. "
"The file may have been read with an offset that skipped this line. "
"Re-read the file to include this line."
)
4. Read Integration (read_file.py)
read_file populates the line cache with every read, enabling hashline_edit to validate references.
# Format: line:hash|content
# Example output:
1:a3|def main():
2:f1| pass
3:0e|
Key Points:
- Each
read_filecall replaces the cache for that file - Paginated reads do not merge prior cache windows
hashline_editcan only edit lines present in the current cache- Missing lines cause
ToolRetryErrorwith offset hints
5. UI Renderer (hashline_edit_renderer.py)
NeXTSTEP-style panel with four zones:
┌─────────────────────────────────────────────┐
│ filename.js +3 -2 ← Zone 1: Header│
│ │
│ /path/to/filename.js ← Zone 2: Path│
│ ───────────────────────────── │
│ 1│function old() { │ 1│function new() { ← Zone 3: Side-by-side
│ - 2│ return old; │+2│ return new; │ diff viewport
│ 3│} │ 3│} │
│ ───────────────────────────── │
│ │
│ 1 hunk [45ms] ← Zone 4: Status │
└─────────────────────────────────────────────┘
Rendering Pipeline:
hashline_edit result (unified diff text)
│
▼
parse_result()
│
├──▶ Extract filepath
├──▶ Count additions (+)
├──▶ Count deletions (-)
└──▶ Count hunks (@@)
│
▼
_parse_side_by_side_rows()
│
├──▶ Parse hunk headers (@@ -1,3 +1,3 @@)
├──▶ Categorize lines: context, insert, delete, meta
└──▶ Build DiffSideBySideLine objects
│
▼
_build_side_by_side_viewport()
│
├──▶ Calculate column widths
├──▶ Apply syntax highlighting styles
└──▶ Build Rich Table with gutters
Why
Prevents Stale Edits
Without hash validation, an agent might reference a line that was:
- Deleted by another process
- Modified by a previous edit
- Never actually existed at that line number
The hash acts as a content-addressed pointer. If the content changes, the hash changes, and the edit is rejected before touching the filesystem.
Provides Clear Edit Visualization
Side-by-side diff makes changes explicit:
Before │ After
──────────────────────────┼──────────────────────────
1│old_function() { │ 1│new_function() {
- 2│ return old_value; │+ 2│ return new_value;
3│} │ 3│}
Enables Safe Concurrent Editing
The cache is session-local and updated immediately after each edit. This allows:
- Multiple edits to the same file in sequence
- Cache stays synchronized with filesystem
- Next edit validates against updated hashes
Minimal Overhead
- 2 hex characters = 256 buckets (0.4% collision probability)
- Cache is in-memory only (no disk I/O)
- Unified diff output is standard and reviewable
Usage Example
# 1. Read file (populates cache)
result = await read_file("/path/to/file.py")
# Output: 1:a3|def main():
# 2:f1| pass
# 2. Edit a line using hash reference
result = await hashline_edit(
filepath="/path/to/file.py",
operation="replace",
line="2:f1", # Line 2, hash f1
new=" print('hello')"
)
# 3. If hash mismatches (file changed)
# Raises: ToolRetryError("File has changed since last read...")
Related Documentation
docs/modules/tools/read_file.md- File reading and cache populationdocs/modules/tools/file_editing.md- General file editing patternsdocs/modules/core/ui_api.md- Panel rendering and UI constants