mech.app

The mech.app newsletter

Agentic AI, minus the noise.

Get practical field notes on AI agents, automation, developer tools and security delivered to your inbox.

No spam. Unsubscribe anytime.

Dev Tools

Oh-My-Pi's Hash-Anchored Edits: How a Terminal Agent Avoids the \\\"Replace Entire File\\\" Trap

Hash-anchored edit boundaries prevent coding agents from clobbering unrelated code. Here's how Oh-My-Pi's LSP integration and tool harness work.

Source: github.com
Oh-My-Pi's Hash-Anchored Edits: How a Terminal Agent Avoids the \\\"Replace Entire File\\\" Trap

Most coding agents fail at scale because they replace entire files when making small edits. One misaligned line number or stale context window and the agent clobbers unrelated code. Oh-My-Pi solves this with hash-anchored edits: the agent specifies a hash of the exact lines it wants to replace, and the edit only applies if those lines still match. If the file changed underneath, the edit fails cleanly instead of silently corrupting state.

This is a fork of Pi by Mario Zechner, now at 26,497 stars. It ships 60+ provider integrations (Ollama, OpenAI, Anthropic, Gemini, DeepSeek), 31 built-in tools, 14 LSP operations, and 28 DAP operations. The core is ~80k lines of Rust with a Bun runtime wrapping TypeScript orchestration. The architecture is terminal-native: no Electron, no web UI, just a TUI that pipes LSP and DAP state directly into the agent’s tool harness.

Hash-Anchored Edit Mechanics

Traditional agents send a replace_file tool call with new content. If the file changed between the agent’s last read and the write, the agent overwrites everything. Hash-anchored edits work differently:

  1. The agent reads a file and computes a hash of the lines it wants to modify.
  2. It sends an edit request with the hash, the line range, and the replacement text.
  3. The tool harness re-reads the file, hashes the current content at that range, and compares.
  4. If the hashes match, the edit applies. If not, the tool returns an error with the current state.

This turns silent corruption into an explicit failure mode. The agent sees the error, re-reads the file, and retries with updated context. The cost is one extra hash comparison per edit, which is negligible compared to the token budget of re-reading an entire file after a bad write.

The hash anchoring also enables concurrent edits. Multiple agents (or a single agent with subagents) can propose edits to different parts of the same file. The tool harness serializes the writes and rejects any edit whose anchor hash is stale. This avoids the classic race condition where two agents both read version N, both write version N+1, and the second write silently clobbers the first.

LSP Integration as State Boundary

Oh-My-Pi exposes 14 LSP operations as agent tools: go-to-definition, find-references, hover, diagnostics, code actions, rename, format, and more. The agent doesn’t parse code itself. It calls lsp_goto_definition and gets back a file path and line number. It calls lsp_diagnostics and gets structured error messages with severity, range, and suggested fixes.

This creates a clean boundary: the agent reasons about what to do, the LSP server maintains the semantic model of the codebase. The agent never needs to understand TypeScript’s module resolution or Rust’s borrow checker. It just asks the LSP server where a symbol is defined or what errors exist.

The LSP server runs in a separate process. The tool harness communicates over JSON-RPC. If the LSP server crashes, the tool returns an error and the agent can retry or fall back to grep-based search. The agent’s prompt includes LSP tool descriptions, so it learns to prefer lsp_find_references over grep when looking for call sites.

The DAP (Debug Adapter Protocol) integration works the same way: 28 operations for setting breakpoints, stepping, inspecting variables, and evaluating expressions. The agent can start a debugger, set a breakpoint, run to that point, inspect state, and decide what to do next. All without custom debugger logic in the agent itself.

Tool Harness and Subagent Spawning

The tool harness decides when to execute a tool inline versus spawning a subagent. Inline execution is cheaper (no extra prompt, no extra model call) but blocks the main agent. Subagent spawning costs tokens but allows parallelism and isolation.

The decision logic:

  • Inline: file reads, LSP queries, shell commands under 5 seconds, Python REPL expressions.
  • Subagent: browser automation, long-running scripts, tasks that need their own context window (e.g., “refactor this module”).

Subagents inherit the parent’s tool access but get a fresh context window. The parent agent sends a task description, the subagent runs, and the result (success or error) flows back as a tool result. The parent agent sees the subagent’s final output, not the intermediate reasoning steps. This keeps the parent’s context window from filling with subagent chatter.

Subagents can spawn their own subagents. The harness enforces a depth limit (default 3) to prevent runaway recursion. Each subagent gets a token budget. If it exceeds the budget, the harness kills it and returns a truncated result.

Architecture: Rust Core, Bun Runtime, TypeScript Glue

The core is Rust: file I/O, process spawning, LSP/DAP clients, hash computation, edit application. The Rust code compiles to a native binary that the Bun runtime calls via FFI. The TypeScript layer handles:

  • Prompt construction and model API calls.
  • Tool result parsing and error handling.
  • Subagent lifecycle management.
  • TUI rendering (using Ink-like primitives).

This split keeps the hot path (file edits, LSP queries) in Rust while keeping the orchestration logic in TypeScript where it’s easier to iterate. The Bun runtime is single-threaded but uses async I/O for model calls and subagent communication. The Rust core uses Tokio for async file I/O and process management.

The tool harness is a TypeScript class that wraps the Rust FFI. Each tool is a method that validates arguments, calls Rust, and returns a structured result. The agent sees tools as JSON schemas in the prompt. The model outputs a tool call, the harness executes it, and the result goes back into the context window.

Provider Abstraction and Token Budget

Oh-My-Pi supports 60+ providers through a unified interface. Each provider implements:

  • chat(messages, tools, options): send a chat completion request.
  • stream(messages, tools, options): same, but streaming.
  • embeddings(texts): generate embeddings for semantic search.

The provider layer handles retries, rate limits, and token counting. The agent doesn’t know if it’s talking to OpenAI or Ollama. The tool harness tracks token usage per request and per session. If the session exceeds a budget (default 200k tokens), the harness truncates old messages and re-summarizes.

The token budget is critical for long-running tasks. Without it, the agent’s context window fills with old tool results and the model starts hallucinating. The summarization step uses a cheaper model (e.g., GPT-3.5) to condense the last N messages into a single summary message. The summary replaces the original messages, freeing up tokens for new tool calls.

Failure Modes and Observability

Common failure modes:

FailureCauseRecovery
Hash mismatchFile changed between read and writeAgent re-reads file, retries edit
LSP timeoutLanguage server crashed or hungTool returns error, agent falls back to grep
Subagent runawaySubagent exceeds token budgetHarness kills subagent, returns truncated result
Model refusalAgent asks for dangerous operationTool returns error, agent rephrases or skips
Context overflowSession exceeds token budgetHarness summarizes old messages, continues

The tool harness logs every tool call, result, and error to a structured log file (JSON lines). The log includes:

  • Timestamp, tool name, arguments.
  • Execution time, token count, success/failure.
  • Parent agent ID (for subagent calls).
  • Hash values (for edit operations).

This log is the primary observability surface. You can replay a session by feeding the log back into the harness. You can analyze which tools the agent uses most, which ones fail most often, and where token budget goes.

The TUI shows a live view of the agent’s reasoning: the current message, the tool it’s calling, and the result. You can pause the agent, inspect the context window, and manually approve or reject tool calls. This is useful for debugging but not practical for production automation.

Code Example: Hash-Anchored Edit Tool

// Simplified tool harness method for hash-anchored edits
async applyEdit(args: {
  file: string;
  startLine: number;
  endLine: number;
  anchorHash: string;
  newContent: string;
}): Promise<{ success: boolean; error?: string }> {
  // Call Rust FFI to read current file content
  const currentLines = await this.rust.readLines(
    args.file,
    args.startLine,
    args.endLine
  );

  // Compute hash of current content at the specified range
  const currentHash = await this.rust.hashLines(currentLines);

  // Compare hashes
  if (currentHash !== args.anchorHash) {
    return {
      success: false,
      error: `Hash mismatch: file changed. Current hash: ${currentHash}`,
    };
  }

  // Apply edit
  await this.rust.replaceLines(
    args.file,
    args.startLine,
    args.endLine,
    args.newContent
  );

  return { success: true };
}

The Rust side uses blake3 for hashing (fast, collision-resistant). The hash is computed over the exact byte range, including newlines. If the file uses CRLF on Windows and LF on Linux, the hash will differ. The tool harness normalizes line endings before hashing to avoid spurious mismatches.

When to Use Hash-Anchored Edits

Hash-anchored edits make sense when:

  • The agent makes many small, localized changes to large files.
  • Multiple agents (or humans) might edit the same file concurrently.
  • You need deterministic failure instead of silent corruption.
  • The cost of re-reading a file after a failed edit is acceptable.

They don’t make sense when:

  • The agent always replaces entire files (e.g., generating new files from scratch).
  • The codebase is small enough that full-file diffs are cheap.
  • You have a transactional file system (e.g., Git-backed edits with automatic conflict resolution).

For terminal-native coding agents, hash-anchored edits are a forcing function: they make the agent’s assumptions explicit. If the agent thinks line 42 contains function foo() but it actually contains function bar(), the edit fails and the agent sees the mismatch. This feedback loop is faster and cheaper than waiting for a test suite to catch the corruption.

Technical Verdict

Use Oh-My-Pi when you need a terminal-native coding agent with production-grade LSP/DAP integration and hash-anchored edit safety. The 60+ provider support and 31 built-in tools mean you can start automating without writing custom tool wrappers. The Rust core is fast enough for large codebases (tested on repos with 100k+ files).

Avoid it if you need a web UI, collaborative editing, or tight integration with a specific IDE (VSCode, IntelliJ). The terminal-only interface is a feature for automation but a limitation for interactive use. The subagent spawning logic is optimized for task parallelism, not for fine-grained human oversight.

The hash-anchored edit pattern is worth stealing even if you don’t use Oh-My-Pi. It’s a simple, low-overhead way to turn silent corruption into explicit failure. Pair it with structured logging and you get a reliable foundation for agentic file edits.