Three Languages, Two FFIs, and One Rope: The Hardest Problems in Building Rope Notes

Building a local-first, agentic IDE with P2P sync across six platforms means wrestling with CRDT corruption, cross-compiled Rust for ARM64, and an AI pipeline that runs entirely on-device. Here are the hardest problems we've solved — and the ones we haven't.

Building a local-first, agentic IDE with P2P sync across six platforms means wrestling with CRDT corruption, cross-compiled Rust for ARM64, and an AI pipeline that runs entirely on-device. Here are the hardest problems we've solved—and the ones we haven't.

Rope Notes is a local-first editor with a real AI agent, Dart analysis, Git, a terminal, and optional P2P sync—all running natively across Linux, macOS, Windows, Android, iOS, and the web. It couples a native Rust rope buffer with a Flutter UI, embeds the Dart analyzer inside a background isolate on mobile platforms, and streams LLM tokens through a hand-written orchestration engine.

Getting that stack to work reliably has been—to put it charitably—a journey. Here are the hardest engineering roadblocks we've faced, and the architectural decisions that got us through them.


1. The CRDT Corruption Bug That Took Weeks to Find

The P2P sync subsystem uses Loro (a CRDT library) sitting directly on top of Iroh (a P2P networking library), bridged into Dart through a hand-written C ABI. It is easily the most architecturally complex piece of the entire application.

Early in development, we hit a destructive bug where file bodies would concatenate $N$ times. If two peers seeded the exact same file contents at index zero, a race condition occurred between attach and load, causing the file to suddenly contain a dozen duplicate copies of itself.

// The Point of Failure
Loro Doc Panic: entity_index > len in richtext_state

When this panic triggered, Loro would poison the mutex it held. Because a poisoned Loro document remains completely unusable even after catching the raw panic, we had to build a comprehensive recovery loop:

  • Catch Unwind: Intercept the panic before it takes down the runtime.
  • Detect Inflation: Calculate byte size variances dynamically.
  • Reset Workspace: Flush memory and forcibly restore the workspace state cleanly from disk.

The Lesson: CRDT synchronization is deceptively hard. Off-the-shelf libraries handle the Happy Path beautifully. The real edge cases—races during initial seed, partial state imports, and concurrent edits during network flakiness—are where you spend your engineering capital. The entire RopeSyncCoordinator._reconcileOnAttach() architecture exists solely to mitigate this single bug.


2. Two FFIs, Two Worlds

Rope Notes operates two separate Rust libraries utilizing completely disconnected Foreign Function Interface (FFI) mechanisms. This doubles the application's surface area for subtle memory bugs.

SubsystemUnderlying EngineFFI Mechanism & Characteristics
rope_editorText Buffer, Find/Replace, Agent OrchestrationUses flutter_rust_bridge (FRB). Features auto-generated bindings, high-level structural types, and streaming callbacks.
sync_coreP2P Networking, CRDT Merging, mDNS DiscoveryUses a hand-written C ABI. Features raw pointers, manual memory allocation management, and dedicated OS threads running separate Tokio runtimes.

To enforce memory safety, these two systems have strict architectural boundaries:

  1. Strict Routing: Editor text mutations driven by network sync are routed exclusively through RopeSyncCoordinator—they are never permitted to write directly into the rope buffer.
  2. Error Filtering: Because the FRB port can close mid-message during a Flutter hot reload, the agent orchestrator must gracefully catch and filter PanicException, ParseIntError, and UnexpectedEof anomalies without crashing the active agent session.

3. Mobile Dart Analysis: Bundling an SDK Into an App

On desktop platforms, handling static analysis is straightforward: we spawn dart language-server --protocol=lsp as a background subprocess. On mobile, there is no pre-installed Dart binary. So we bundled one.

The analyzer runtime embeds the core Dart analyzer package into a dedicated worker isolate, with a minimized Flutter SDK packaged as assets/flutter_framework.tar.gz.

[Startup Sequence]
Extract tarball ──> .dart-cache/flutter_sdk ──> Initialize Isolate Worker

To keep the binary size reasonable, we wrote a custom build script that walks the native Flutter tree and extracts only the essential .dart, .json, and .yaml files required for compilation matching.

The Build Matrix Overhead

  • SDK Upgrades: Every major Flutter SDK update requires re-generating and re-versioning the core asset tarballs.
  • Android compilation: A 121-line Gradle script drives the NDK toolchains to cross-compile sync_core simultaneously for arm64-v8a and x86_64.
  • iOS Lifecycles: Dedicated background completion tokens are required to safely flush the sync buffer before the OS suspends the thread.

4. The Agent Pipeline: Streaming, Editing, and File Safety

The AI agent is where all of our architectural complexity converges. The Rust orchestrator streams raw LLM tokens through the FRB layer, where Dart-side parsers decode streaming deltas into explicit text edits. Those actions pass into a volatile buffer, render visually as ghost preview overlays, and wait for explicit user approval before mutating the underlying rope.

Building an edit parser that doesn't break files means designing for massive variance:

  • Format Flexibility: The parsing layer handles unified diffs, search-and-replace blocks, and custom sentinel-tagged file edits simultaneously.
  • Parsing Roadblocks: We frequently combat sentinel collisions (e.g., the model generating a closing tag that accidentally appears in the actual source code block), whitespace sensitivity, and placeholder heuristics matching the wrong code blocks.
  • Token Optimization: Across fourteen distinct LLM backend providers, token counting relies on a rough heuristic ($\text{text.length} \sim 4$) with a hard compaction limit firing at 8,192 tokens. However, routing to 1M-token context windows like Kimi K3 completely upends these legacy tracking assumptions.

5. What We Haven't Solved Yet

While the core editor is stable, several engineering challenges remain open in our issue tracker:

  • NAT Traversal for P2P: While local LAN synchronization is rock solid via mDNS, Iroh relay support remains unreliable across complex, real-world residential firewalls.
  • Persistent Cognitive Memory: On-device semantic searching using EmbeddingGemma 300M runs smoothly, but vectors are currently stored strictly in-memory and drop upon application exit.

The Philosophy That Carries Us Through

Every single one of these technical challenges traces back to our core architectural constraint: local-first, private, and fully offline-capable.

If we compromised and uploaded user documentation to a centralized cloud platform, CRDT conflict resolution would be handled by a remote server. If we abandoned mobile optimization, we wouldn't have to compress and bundle runtime SDK components. If we built a browser-based Electron client, we could skip the native Rust orchestrator entirely.

But that's not the tool we want to use. Rope Notes is built for the engineer who demands that their agents run on their silicon, their data syncs over their physical network, and their intellectual property never touches a third-party corporate server.

That constraint makes every subsystem significantly harder to build—and that is exactly why we are building it.