pi-better-edit
Hash-anchored read/edit/undo tools for pi-coding-agent. Every line gets a unique 4-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.
Package details
Install pi-better-edit from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-better-edit- Package
pi-better-edit- Version
2.12.0- Published
- Oct 13, 2026
- Downloads
- 3,505/mo · 1,299/wk
- Author
- rianico
- License
- MIT
- Types
- extension
- Size
- 1.4 MB
- Dependencies
- 4 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./dist/index.js"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
What is
pi-better-edit? A high-precision file editing extension forpi-coding-agentthat replaces volatile line numbers and token-wasting code echoes with immutable 4-character line hashes addressed by (file, content) (szJx│code).Core Philosophy: Local compute is free; the model's context window is the most precious resource. By shifting verification, snapshotting, and alignment to the host,
pi-better-editslashes output tokens — line-anchored feedback cut repair tokens by 22–58% in published paired experiments (Lamberti 2026, arXiv:2607.12713) — auto-rebases external file drift (e.g., Prettier, Git), and eliminates silent miswrites without forcing full-file re-reads.
Why You Need It
The 3 Fatal Editing Traps of Autonomous Coding Agents
File editing is the #1 point of failure for autonomous agents. Traditional tools break down in three distinct ways:
| Fatal Trap in Traditional Tools | Why It Breaks Agents | How pi-better-edit Solves It |
|---|---|---|
str_replace Token Bleed |
Must re-type 30+ lines of unchanged code just to change 1 line ($O(S+R)$), burning expensive output tokens (billed ~5–6× input). | $O(R)$ Payloads: Sends only two 4-char hashes (anchor_from, anchor_to) + replacement. Cuts output tokens — 22–58% repair-token cuts in published paired experiments (Lamberti 2026). |
| Line-Number Coordinate Rot | Inserting 1 line shifts all line numbers below it. Agents suffer off-by-one errors or must repeatedly re-read the file. | Position-Independent Anchors: Line hashes follow content, not line coordinates. Exterior shifts auto-rebase cleanly. |
| Silent Miswrites & Drift | Duplicate lines match the wrong function; external formatters (Prettier) or git updates cause blind overwrites or fatal errors. | Line-Identity MVCC: Unique anchors via coprime probing; format-tolerant whitespace hashing; fail-closed reject-and-serve. |
[!NOTE] Empirical Grounding Published findings corroborate the failure modes above:
- Token Bleed: Line-anchored feedback cut repair tokens by 22–58% while improving patch correctness in paired experiments (Lamberti 2026, arXiv:2607.12713).
- Silent Drift: Subword tokenizers drift across model versions, so tools must select anchors from tokenizer-stable regions (TokDrift, arXiv:2510.14972).
Core Pillars
1. 🪙 Token Economics
Published paired experiments report 22–58% repair-token cuts for line-anchored feedback (Lamberti 2026, arXiv:2607.12713).
- $O(R)$ Edit Payloads: The model emits only
{ "anchor_from": "a1bX", "anchor_to": "c3dX", "text": "..." }, never regurgitating existing code. - Self-Serving Diffs: Every applied edit returns fresh anchors in the post-edit diff — zero re-read roundtrips to chain edits.
- Disjoint Multi-Window Reads: Query up to 16 disjoint slices (
windows: [{offset, limit}, ...]) in one turn instead of dumping 2,000 lines into context. - Zero-Token Auto-Rebase: Non-conflicting shifts resolve locally via $O(m \log m)$ Patience LIS alignment — 0 tokens, 0 retries.
- Atomic Multi-Item Batches: Apply up to 32 same-file edits in one tool call; overlapping spans abort atomically before touching disk.
2. 🛡️ Resistance to External Writes (Drift & Concurrency)
- Auto-Formatter Immunity: Strips a frozen 28-code-point whitespace class (ADR-0029) before hashing, so Prettier, Black, gofmt, and rustfmt format-on-save passes do not rotate anchors. Not total immunity: measured formatter churn includes rewriting U+200B ZWSP to a space, which rotates the affected anchor and fails closed.
- Exterior Shift Auto-Rebase: External edits, git checkouts, or background processes outside the edit span rebase seamlessly without agent intervention.
- Fail-Closed Reject-and-Serve: Contested interior spans fail closed without disk corruption and immediately return fresh on-disk rows in the error (
[E_STALE_RANGE],[E_UNVERIFIED_RANGE]) — recovering in exactly 1 turn. - Session-Keyed Leases: Leases are isolated per session (
served_leases), preventing cross-agent race conditions or state pollution.
3. 🎯 Zero Silent Miswrites (Formal MVCC)
- Decoupled Line Identity: Every line is tracked by an immutable, monotonic
line_idin CAS snapshot storage, not ephemeral coordinates. - Collision-Free Anchors: Coprime bitset probing ensures duplicate lines in a file receive distinct, unambiguous 4-character hashes.
- No Heuristic Guessing (ADR-0016): Retires fuzzy matching. If an anchor cannot be unambiguously resolved via lease lineage, it fails closed safely.
- Persisted Undo:
undo_last_editrestores exact file content, BOM, line endings, and original anchors, persisting across session restarts.
Quick Start
Installation
# From npm
pi install npm:pi-better-edit
# From GitHub
pi install git:github.com/Rianico/pi-better-edit
# From local directory
pi install /path/to/pi-better-edit
Zero configuration required. pi automatically activates the extension on start.
| Runtime Requirement | Supported Version |
|---|---|
| Node.js | ≥ 24.0.0 |
pi-coding-agent |
≥ 0.75.0 (peer dependency) |
How It Works
1. Read the file
read returns each line prefixed by a stable 4-character hash anchor:
ve7x│function hello() {
szJx│ console.log("world");
kQmx│}
2. Apply an edit
edit targets inclusive anchor bounds using the canonical named-object payload:
{
"file": "src/main.ts",
"edits": [
{
"anchor_from": "szJx",
"anchor_to": "szJx",
"text": " console.log('hi');\n"
}
]
}
3. Receive the diff with fresh anchors
The tool applies the edit and returns a unified diff showing fresh anchors for subsequent edits—eliminating the need for follow-up read calls:
- szJx│ console.log("world");
+ a3mX│ console.log('hi');
kQmx│ }
4. Batch multiple edits atomically
Batch up to 32 edits to the same file in a single transaction. If any edit fails or overlaps, none write:
{
"file": "src/main.ts",
"edits": [
{ "anchor_from": "a1bX", "anchor_to": "a1bX", "text": "// Header comment\n" },
{ "anchor_from": "c3dX", "anchor_to": "c3dX", "text": " return true;\n" }
]
}
Systematic Architecture
pi-better-edit v2 replaces ad-hoc string matching and heuristic healing with a formal Multi-Version Concurrency Control (MVCC) architecture.
┌──────────────────────────────────────────────────────────────────────────────────┐
│ STORAGE TIER │
│ src/hash-store.ts & src/snapshot-store/ │
│ - file_snapshots: CAS snapshots (snapshot_id, path, snapshot_hash, line_count) │
│ - line_lineage: Coordinate authority (snapshot_id, line_number) -> (line_id) │
│ - line_id_counters: Monotonic integer block allocator per path │
│ - served_leases: Session-keyed immutable leases (session_id, file_path, anchor) │
│ - file_undo: Snapshot-pinned undo history surviving restarts │
└────────────────────────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────────────────────────▼─────────────────────────────────────────┐
│ SESSION TIER │
│ src/served-session/session.ts │
│ - Leases: Granted on every serve path: read, diff, write auto-read, truncated │
│ serves, fresh-read rejections, undo (see §1) │
│ - Immutability: Leases are strictly READ-ONLY during edit resolution │
│ - Re-Serve Upsert: Atomic upsert updates leases when presentation changes │
└────────────────────────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────────────────────────▼─────────────────────────────────────────┐
│ RESOLUTION & REBASE TIER │
│ src/hashline/lease-resolve.ts & src/hashline/served-verification.ts │
│ - On-Demand CAS Materialization: Materializes current disk state │
│ - Patience LIS Pin Backbone: O(m log m) non-crossing line alignment │
│ - Minimal Displacement Tie-Breaking: Deterministic unique pairing │
│ - Span Contiguity Gate: Asserts interior span is not torn │
└────────────────────────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────────────────────────▼─────────────────────────────────────────┐
│ MUTATION TIER │
│ src/mutation-engine/pipeline.ts & src/hashline/apply.ts │
│ - Working Buffer: Preceding delta rebase for multi-item batches │
│ - WAL Lineage Commit: Atomically commits final snapshot and updates leases │
│ - Fail-Closed Intercepts: Rejections emit fresh read ranges │
└──────────────────────────────────────────────────────────────────────────────────┘
1. Immutable Line Identity & Leases
- Every line has an immutable surrogate key (
line_id) allocated from a monotonic counter (line_id_counters). - When lines are delivered to an agent on any serve path — a default (
served)read, diffs, thewriteauto-read hook, truncated serves, fresh-read rejections, orundo_last_edit— a session-scoped lease (served_leases) binds(session_id, file_path, anchor) -> line_id. - During an
edit, lease lookups are strictly read-only. An edit cannot re-stamp or guess a lease.
2. Multi-Version Snapshot Lineage
- Content-addressed CAS snapshots (
file_snapshots) track each materialized file version. line_lineagemaps each line coordinate to its immutableline_id, 32-bit canon hash, and verbatim presentation anchor.- When disk content shifts externally, the tool pairs the latest snapshot ($S_{latest}$) with disk using Patience LIS alignment, preserving identities for surviving lines and allocating fresh IDs only for novel lines.
3. Patience LIS Pin Backbone ($O(m \log m)$)
- Uniquely matching anchor pins form candidate pairs.
- The engine computes the Longest Increasing Subsequence (LIS) via patience sorting in $O(m \log m)$ time.
- Multiple maximal LIS candidates are disambiguated by minimal total displacement ($\sum |p_i - c_i|$).
- Contested symmetric swaps (e.g. equal-length function swaps) or ambiguous duplicate blocks fail closed, marking affected lines as retired rather than guessing.
4. Working Buffer with Preceding Deltas
- Multi-item batches (
edits: [e_0, e_1, ...]) resolve their baseline coordinates $s'_k$ in the current snapshot. - Active in-memory buffer positions are computed by accounting strictly for preceding edits: $$\Delta_k = \sum_{j < k, s'{end, j} < s'{start, k}} \left( |R_j| - (s'{end, j} - s'{start, j} + 1) \right)$$
- If any two edit items overlap or nest, the batch aborts atomically (
[E_BATCH_ABORT]) before modifying disk.
5. Unified Span Verification (ADR-0023)
resolveLeasedEditverifies the entire span againstline_lineagebefore touching disk.- Canon evidence is file-scoped and verified via 32-bit digests (
canon_hash), eliminating duplicate plaintext storage.
Tools
| Tool | Parameters | Description |
|---|---|---|
read |
file, offset (1-based), limit, windows (optional), mode (optional) |
Returns file content formatted as HASH│content by default. mode: "verbatim" returns plain text with no hash prefixes and records no leases; the default "served" leases every shown line. Lines >50KB are replaced with a marker hint. windows: [{offset, limit}, …] reads up to 16 disjoint ranges in one turn: each renders under === Lines A-B of N === and, in the default "served" mode, every shown line is leased, so anchors from all of them work in one edit. |
edit |
file, edits, mode (optional) |
Applies single or batched edits atomically. Each item bounds an inclusive anchor_from/anchor_to range, places its payload with optional at, and carries exactly one payload — text or text_ref. mode: "literal" declares verbatim text. |
undo_last_edit |
path |
Restores the previous file state, BOM, line endings, and original anchors. Persists across restarts. |
Payload Contract
{
"file": "src/example.ts",
"edits": [
{
"anchor_from": "a1bX",
"anchor_to": "c3dX",
"text": "const status = 'ready';\n"
}
],
"mode": "general"
}
An edit item is flat, with no discriminator field: it bounds its range with the inclusive
anchor pair, places its payload with the optional at, and carries exactly one payload —
text or text_ref. Both or neither is refused with the offending keys named. No operation tag
exists because the payload's presence is the intent, and placement is an independent axis.
| Item key | Values | Meaning |
|---|---|---|
anchor_from / anchor_to |
bare 4-char hash anchors | Inclusive target range — both boundary lines are touched. |
at (optional) |
"in-place" | "before" | "after" |
Placement relative to the resolved range. Omitted means "in-place". "before"/"after" insert at the range boundary and require a single-line resolved target; the underscore spelling "in_place" is refused — the canonical spelling is "in-place". |
text |
string | By-value payload — bare file content (\n joins lines). "" deletes the range when placed in-place (no at). With "before"/"after" an empty text does NOT delete: it writes nothing and the call narrates [W_NOOP_INSERT] as a no-op. |
text_ref |
{ anchor_from, anchor_to, file?, mode } |
By-reference payload — the bytes of a served span bounded by two anchors of the file it names. mode is required (never inferred): "copy" re-inserts the span and keeps the source; "cut" additionally retires it. file may name another served file — a foreign-source copy (never called "cross-file" here; that word named a dropped multi-file batching idea). Both modes apply to a foreign file, and a foreign cut commits insert and retirement as one correlated transaction. |
Example items — every shipped shape, one each:
{ "anchor_from": "a1bX", "anchor_to": "c3dX", "text": "const status = 'ready';\n" }
{ "anchor_from": "a1bX", "anchor_to": "c3dX", "text": "" }
{ "anchor_from": "kQmX", "anchor_to": "kQmX", "at": "after", "text": "// Footer comment\n" }
{ "anchor_from": "p9rX", "anchor_to": "p9rX", "at": "before", "text": " return true;\n" }
{ "anchor_from": "m2zX", "anchor_to": "m2zX", "text_ref": { "anchor_from": "d2xX", "anchor_to": "d2xX", "mode": "copy" } }
{ "anchor_from": "m2zX", "anchor_to": "m2zX", "text_ref": { "anchor_from": "d2xX", "anchor_to": "e5vX", "mode": "cut" } }
{ "anchor_from": "m2zX", "anchor_to": "m2zX", "text_ref": { "anchor_from": "g7tX", "anchor_to": "h3sX", "file": "src/helper.ts", "mode": "cut" } }
(in order: in-place replace; in-place delete; after-insert; before-insert; by-reference copy;
by-reference cut in this file; foreign-source cut.) The legacy fused verb for retiring a
referenced span is retired — the current word is cut.
file: Path to the target text file (must be a file, never a directory).edits: Array of 1 to 32 edit items;textandtext_refare mutually exclusive per item.mode:"general"(default) refuses text containing served anchor prefixes;"literal"allows verbatim insertion of lines beginning withHASH│.
Interoperability with pi-lens
pi-better-edit composes with pi-lens diagnostics, formatting, and its read-before-edit guard:
- Read expansion: pi-lens widens a partial read (
limit <= 100) to the enclosing symbol or markdown heading section (cap 300 lines, 200 ms budget, disabled by--no-lsp); a read withoutlimitis never widened. Anchors always describe the rows actually served, so failure triage counts served rows rather than the requested window. To keep the requested window, passlimit > 100, read the whole file, or start pi with--no-lsp. - Format and autofix: pi-lens' deferred
agent_endformat and autofix passes rewrite files outside the model's turn. A whitespace-only rewrite is absorbed by the whitespace-insensitive canon (ADR-0005), so anchors survive; a real fix rotates the affected anchors, and the next edit fails closed with[E_STALE_RANGE]or[E_TARGET_LOST]and serves the current rows. - Guard compatibility: every served row and every landed mutation (reads, multi-window reads, bash views, post-edit diffs,
reject-and-servepayloads, edits and writes) is reported to pi-lens through its unified v2 I/O bridge (pi-lens:io-bridge). Reads whose producer holds the file's bytes travel as caller evidence — one single-range slice per span; diffs andreject-and-servepayloads, which hold no raw text, travel as one disk-evidence call. Mutations lead the rows they produced, so the guard sees the post-edit state; retries and chained edits satisfy the read-before-edit guard without manual re-reads. - Overlapping reports are merged by pi-lens and are expected, not a bug.
- Opt-in:
/pi-better-edit lens(autoby default when pi-lens is detected, else off; override viaPI_BETTER_EDIT_LENS_BRIDGE=auto|on|off). Core editing never depends on pi-lens — with the bridge off or absent, behavior is unchanged. - Removal recipe: delete
src/integrations/pi-lens/, drop its two imports plus theregisterLensCommand(pi)andattachIOBridgeAdapter()calls fromindex.ts, and drop thecontentargument from the seam producers (src/read.ts, and the write and bash auto-read paths insrc/lifecycle-hooks/index.ts). The seams stay domain-neutral and need no other change;test/arch/lens-bridge-boundary.test.tsfails until the directory and the wiring are gone.
Bash View
Pure single-file views (cat, head, tail, sed -n prints, and the admitted grep/rg search shapes) are intercepted: the raw stdout is swapped for the same lines as HASH│content rows under a frozen header, and the rows carry working leases, so the next edit anchors with no re-read round trip (ADR-0033). Anything else — pipelines with transforms, compounds, multi-file shapes, errors, truncation — passes through byte-identical.
- Option:
bash.viewin.pi/agents/pi-better-edit.json(onby default;offpasses every view and search through raw). Resolves per key through envPI_BETTER_EDIT_BASH_VIEWover project file over global file over the default, with the same rung-source reporting as the other settings. - Slash command:
/pi-better-edit bash-view [on|off|status] [project|global](alias/pi-better-edit bash ...). Bare invocation offers the five-option picker (On/Offper scope,Status); headless hosts get the combined status text instead. - First notice: the first interception of each session warns once — naming the round trip saved and the off-ramp — and later interceptions stay silent until the next session.
Settings overlay
/pi-better-edit settings (bare /pi-better-edit opens the same overlay) reviews the lens bridge, bash view and store janitor rows with effective-source diagnostics (ADR-0038). The target scope switch steers later saves without persisting anything itself; headless hosts get the combined status text instead.
Error and Warning Contract
pi-better-edit enforces a strict, machine-actionable diagnostic contract (ADR-0021):
[E_*]marks a refusal from the call that raised it:read,edit, orundo_last_edit. Nothing was written to disk.[W_*]indicates an applied mutation with an informational warning.- Range-family rejections carry structured
details.causevalues (retirement,never-served,served-range staleness) —never-servedon a leased span means a boundary row, because an unread interior between two leased boundaries is accepted (ADR-0024).
Domain Rejections ([E_*])
| Error Code | Description | Remedy / Agent Action |
|---|---|---|
[E_BAD_PAYLOAD] |
Payload fails schema validation (missing fields, wrong types). | Fix the payload fields and retry. |
[E_MALFORMED_ANCHOR] |
Anchor is not valid — not a bare 4-char anchor (e.g. carries │, a diff prefix, or a row suffix). |
Pass the bare 4-char anchor (e.g. "szJx") and retry. |
[E_STALE_ANCHOR] |
Anchor no longer resolves to its leased identity in the file. | The latest known status is in this rejection. Retry as it directs. |
[E_UNKNOWN_ANCHOR] |
The path has not served the anchor in this session. | Re-read the file to establish fresh anchor leases. |
[E_FOREIGN_ANCHOR] |
Anchor was served for a different file, not for the targeted one. | Ensure anchors match the target file path. |
[E_STALE_RANGE] |
A line in the edit range changed on disk, or a boundary line was never served (an unread interior between leased boundaries applies, ADR-0024). | Current range served as a fresh read; decide next edit from fresh rows. |
[E_UNVERIFIED_RANGE] |
One boundary lease retired while surviving bound is live and unshifted. | Named window served as fresh read; decide next edit from fresh rows. |
[E_TARGET_LOST] |
Target line identity deleted or reordered without a stable anchor bound. | Read the file and re-target. |
[E_SUSPICIOUS_TEXT] |
Replacement line starts with a served HASH│ anchor. |
Omit the copied anchors or declare mode: "literal". |
[E_BATCH_ABORT] |
Two or more items in the batch target overlapping spans. | Merge the overlapping ranges into a single edit, or split them into separate calls. |
[E_NOOP_LOOP] |
Identical edit submitted 3 consecutive times without change; the range already has this text. | Inspect the current range; the range already contains the target content. |
[E_EMPTY_RANGE] |
Edit would result in an empty non-empty file. | Use write to clear the file. |
[E_NOT_FOUND] |
Target file does not exist on disk. | Check the parent directory and retry with the corrected file. |
[E_ACCESS] |
Target file is unreadable, unwritable, or in a symlink loop. | Fix the path or permissions and retry. |
[E_UNSUPPORTED_FILE] |
Target path is a directory, binary file, image, or UTF-16/32 text. | Choose a text file and retry. |
[E_LOSSY_TEXT] |
File bytes do not round-trip a UTF-8 decode; a line-addressed rewrite would destroy the original bytes. | Re-encode the file as valid UTF-8 with a byte-level tool, then retry. |
[E_UNDO_STALE] |
Cannot undo on the target path: the file was modified or deleted after the last edit. | Undo refused to prevent data loss; re-read the file. |
[E_UNDO_UNAVAILABLE] |
Cannot persist undo history to the hash store. The edit was not applied. The file is unchanged. | Retry the edit. If the store cannot be recovered, use write. |
[E_UNDO_REVERT_FAILED] |
A correlated cut-undo revert was interrupted mid-transaction and could not be completed; no undo history was cleared. | Fix the file access failure. The next run repairs the interrupted revert. |
[E_LARGE_FILE] |
A served read or edit load exceeds the served admission budget (200,000 lines, SERVED_MAX_LINES) or the 14,766,336-line ceiling of allocatable 4-char anchors; mode: "verbatim" reads are not capped. |
Split the file or use a non-line-based approach for very large files. |
[E_INTERNAL_INVARIANT] |
Internal tool invariant failure (the message names the failed invariant). | No remedy; check the error message details. |
[E_UNKNOWN] |
Unknown filesystem or invariant failure (first message line reported). | Check the error message details. |
Applied Warnings ([W_*])
| Warning Code | Audience | Description |
|---|---|---|
[W_NEVER_SERVED_SHAPE] |
[MODEL] |
Replacement line starts with an anchor-shaped token never served for this session and file. Applied verbatim. |
[W_SERVED_PREFIX_MISMATCH] |
[MODEL] |
Replacement line starts with a served anchor but the content differs. Applied verbatim. |
[W_REVERSED_ANCHORS] |
[USER] |
The anchors were reversed (anchor_from after anchor_to). Applied with the range swapped. |
[W_UNICODE_LITERAL] |
[USER] |
Replacement line contains a literal \uDDDD sequence. Applied verbatim. |
[W_LITERAL_BYPASS] |
[USER] |
Served-echo check bypassed by literal declaration. |
[W_NOOP] |
[USER] |
Identical edit did not change the range; the range already has this text. A resend rejects. |
[W_NOOP_INSERT] |
[MODEL] |
An empty insertion with "before"/"after" and text: "" writes nothing. Provide text or drop the item. |
Comparison
Capability Comparison
| Feature | pi-better-edit v2 | @oh-my-pi/hashline | Traditional str_replace |
|---|---|---|---|
| Addressing Model | 4-char (file, content)-addressed anchors | File tag + line numbers | Verbatim code strings |
| Line Identity | Immutable MVCC line_id |
Coordinate line numbers | None (text matching) |
| Exterior Shift Tolerance | Auto-rebases (0 tokens, 0 retries) | Model must recalculate line numbers | Fails if surrounding context shifts |
| Duplicate Line Safety | Collision-resolved unique anchors | Ambiguous position-based indexing | Prone to matching wrong instance |
| Concurrent Disk Drift | Fail-closed reject-and-serve | Tag mismatch / best-effort 3-way merge | Silent overwrite or blind failure |
| Batch Support | Atomic up to 32 items with delta shifts | Multi-section patch preflight | Sequential individual calls |
| Undo Persistence | Survives restarts (CAS snapshot pinned) | None | None |
| Session Isolation | Session-keyed leases (served_leases) |
None | N/A |
| Deterministic Battery | 27 deterministic scenarios run by pnpm test (test/e2e/tool-battery.test.ts) |
N/A | N/A |
Edge Case Behavior
| Edge Case Scenario | pi-better-edit v2 | @oh-my-pi/hashline |
|---|---|---|
| Wrong Coordinate / Off-by-one | Impossible: Anchors bind to line_id; verified against lineage before writing. |
Possible: Wrong line number against a valid tag silently mutates the wrong code. |
| Lines Inserted Above Target | Auto-rebases cleanly: Identity is decoupled from coordinates. | Every edit renumbers: Agent must track offsets. |
| Deleted Function Guard Target (Probe E) | Fail-closed intercept: Rejects edit; zero code corruption. | Tag mismatch / merge hazard. |
| Equal-Length Symmetric Function Swap (Probe K) | Fail-closed intercept: Contested reorder retires safely. | Applies to wrong block or requires manual recovery. |
| Batch Items Overlap | Atomic abort ([E_BATCH_ABORT]); nothing written. |
Preflight validation failure. |
Independent Benchmark
Explicit Edit Benchmark is an independent, community-run dataset that scores harnesses and Pi editing extensions on the same 226 byte-exact edit tasks (replacements, insertions, deletions, moves, copies, unicode, large files). It is maintained by alexshpunt, not by this project, and every observation ships with its configuration.
Published pi-better-edit arm |
Value |
|---|---|
| Quality score | 95.9% |
| First-attempt exact | 94.7% |
| Exact after recovery | 99.6% |
| Coverage | 226 tasks · Pi 0.85.1 · gpt-5.6-luna, low reasoning |
Scope. That arm is pinned to
pi-better-edit@1.7.0— the retired 1.x heuristic era — so the score describes the predecessor architecture, not the MVCC v2 line. It updates here when the benchmark pin moves to 2.x.
- Explorer, filtered to
pi-better-edit - Raw observations
- Contribute a run:
npm run benchmark:extension:submit -- --extension pi-better-edit --auth-file ~/.pi/agent/auth.json
How Anchors Work
- Whitespace Canonicalization: Each line is stripped of a frozen 28-code-point whitespace class (ASCII plus Unicode spaces, NBSP, BOM, and directional marks — ADR-0029) before hashing. External formatting passes (
prettier,black,gofmt,rustfmt) do not alter line hashes. Token-level edits (quotes, semicolons, variable names) rotate the hash, and so do the significant zero-width characters the class deliberately excludes (U+200B ZWSP, U+200C/D ZWNJ/ZWJ) —oxfmtnormalizes ZWSP to a space, which rotates the anchor and fails closed rather than passing silently. - xxHash32 & Base62 Space: Canonical lines are hashed using xxHash32 seeded with the file's canonical path (
(canonical path, canonical line)→fileBaseIndex) and mapped to 4-character base62 strings (A-Za-z0-9), providing $62^4 - 10^4 = 14,766,336$ allocatable anchors — the 10,000 all-digit spellings are reserved and never served, since a served one would be indistinguishable from a line number. Base62 strings occupy tokenizer-stable token regions across model families (TokDrift, arXiv:2510.14972). The live width record is ADR-0030. - Collision-Free Coprime Probing: When duplicate lines occur in a file, collision resolution probes using a stride coprime to the hash space ($62^2 + 62 + 1 = 3,907$). Every line in a file receives a unique anchor.
- SQLite WAL CAS Storage: Line hashes and snapshots are persisted in
~/.config/pi-better-edit/hash-store.sqlite(honoringXDG_CONFIG_HOME;PI_BETTER_EDIT_CONFIG_DIRoverrides both it andXDG_CONFIG_HOME, and names the app config dir itself — nopi-better-editsuffix is appended, and it is the ONLY thing that can move the store: no config file key can). Snapshot retention is governed by proportional LRU vacuuming under a 50MB budget. Maintenance is split by WHAT IT COSTS: an open reads the database header and takes no write lock unless the store's provenance stamp has moved, while the store janitor (src/store-janitor.ts) runs the generation sweep and SQLite's fullPRAGMA quick_checkonsession_startandsession_shutdown, throttled through onemetarow to about once a day and never on a tool call. That periodic deep check is the half of issue #100 that closes "not detected at open" and "not healed on reopen" — a detected failure goes to the open path's own quarantine-and-rebuild — and it deliberately leaves the use-time clause, "not quarantined at use", to #100. The journal has one owner too:PRAGMA wal_checkpoint(TRUNCATE)runs onexit,SIGINTandSIGTERM, so the janitor adds no checkpoint of its own and its writes ride the WAL every other writer's do./pi-better-edit lens statusreports the resolved store path, the store settings with the scope each value came from, and that override by name.
Upgrading from 1.x
Version 2.0 represents a major architectural upgrade from heuristic healing to formal MVCC:
- Heuristic Healing Deleted (ADR-0016): Heuristic guessing of relocated anchors (
tryHealOrphanedSpan) is completely removed to eliminate silent miswrites on duplicate code. - Boundary Rule Replaces E_UNSERVED_RANGE (ADR-0020): The old
E_UNSERVED_RANGEcode is retired. If one boundary lease is retired while the other survives unshifted, the tool emits[E_UNVERIFIED_RANGE]with a fresh read. If both bounds are lost, it emits[E_TARGET_LOST]. - Unified Diagnostic Contract (ADR-0021): Rejections use
[E_*]; successful mutations with caveats use[W_*]. Structured diagnoses live indetails.cause. - Additive Store Migration: The SQLite schema migrates additively from version 6 to 7. Existing project files are untouched.
Development
# Install dependencies
pnpm install
# Run unit and integration tests
pnpm test
# Run quality checks
pnpm run lint
pnpm run format
pnpm run typecheck
License
Acknowledgments
- Can Bölük for seminal insights on The Harness Problem.
- @oh-my-pi/hashline by can1357 for pioneering the hashline patch concept.
- pi-hashline-edit by RimuruW and pi-hashline-edit-pro by YuGiMob for foundational agent extension designs.