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.16.0- Published
- Nov 1, 2026
- Downloads
- 3,844/mo · 959/wk
- Author
- rianico
- License
- MIT
- Types
- extension
- Size
- 1.9 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
Core Philosophy
The Most Precious Resource is Context Space
In autonomous coding agent design, the model's context window and attention budget are the single most scarce and expensive resource. Conversely, host runtime compute (hashing, diffing, snapshotting, and alignment) is effectively free.
Traditional agent editing tools violate this fundamental economic reality:
str_replaceburns context: Forcing the model to echo 30–50 lines of unchanged code ($O(S+R)$) burns expensive generation tokens and crowds out subsequent task reasoning.- Context bloat degrades attention: Long contexts trigger attention dispersion ("Lost in the Middle", Liu et al. 2023). Regurgitating boilerplate code pollutes the context with repetitive tokens that distract the model from subtle invariants.
- Line-number coordinates rot immediately: Inserting one line shifts all subsequent lines down. Agents hallucinate coordinates, suffer off-by-one errors, or waste context on defensive full-file re-reads.
pi-better-edit shifts the entire burden of verification, coordinate tracking, snapshotting, and alignment to the host runtime:
- $O(R)$ Edit Payloads: The model emits only two 4-character hashes (
anchor_from,anchor_to) plus the replacement text. It never regurgitates existing code. - Zero-Token Auto-Rebase: Harmless exterior file shifts (format-on-save passes like Prettier or Black, git checkouts, concurrent edits) rebase seamlessly locally using $O(m \log m)$ Patience LIS alignment with a bounded DP fallback — 0 tokens burned, 0 retries.
- Fail-Closed Reject-and-Serve: For same-file edits, contested spans fail closed without file corruption and immediately return current on-disk rows with fresh, valid leases in the error payload (
[E_STALE_RANGE],[E_UNVERIFIED_RANGE]), recovering in exactly 1 turn with no extrareadcall (test/e2e/tool-battery.test.ts).
Empirical Evidence: Token Savings & Attention
The design of pi-better-edit is grounded directly in published empirical research on LLM context dynamics and tokenization:
1. Token Economics: 22%–58% Generated Token Savings
In published paired experiments on AI code editing, line-anchored feedback demonstrated a 22% (Claude Opus) and 58% (Claude Sonnet) reduction in generated tokens, reaching 24%–80% savings on files of 100 lines or more while improving overall patch correctness (Lamberti 2026 preprint, arXiv:2607.12713).
- Traditional search-and-replace tools require outputting $O(S + R)$ tokens.
pi-better-editrequires only $O(R)$ output tokens.- Self-serving diffs return updated anchors in the edit result, eliminating follow-up
readcalls when chaining edits.
2. Attention Preservation & Cognitive Headroom
Repeating massive blocks of existing code dilutes attention weights across the prompt. Research on transformer attention mechanisms demonstrates that:
- Extraneous context tokens increase needle-retrieval failure rates and degrade multi-step reasoning accuracy ("Lost in the Middle", Liu et al. 2023).
- Anchored editing keeps model output focused strictly on the intended change, preserving context space for reasoning traces, test outputs, and project architecture.
3. Tokenizer Drift Immunity
Subword tokenizers (BPE, WordPiece, Unigram) drift across model versions and architectures. As demonstrated by TokDrift (Li, Deng, Nie 2025, arXiv:2510.14972), semantically identical code snippets tokenize differently depending on superficial factors like whitespace, line coordinates, and formatting, causing significant behavioral shifts across LLMs.
- Traditional coordinate lines (
12:34|,@@ -1,5 +1,5 @@) fragment into volatile multi-token sequences that trigger off-by-one errors. pi-better-editaddresses this by projecting line digests into 4-character base62 strings ([A-Za-z0-9]) separated by│(U+2502) (ADR-0002). These anchors tokenize as a small fixed count (~3 tokens in ≥99% of measured samples) where width-3 anchors fragment unpredictably, ensuring stable copy-target references across model families.
Quick Start
Installation
Install as an extension into pi:
# From npm (recommended)
pi install npm:pi-better-edit
# From GitHub
pi install git:github.com/Rianico/pi-better-edit
# From local directory (for development)
pi install ./local-package
Zero configuration required. pi automatically detects and activates pi-better-edit on startup.
| Requirement | Supported Version |
|---|---|
| Node.js | ≥ 24.0.0 (.nvmrc 26) |
| pi-coding-agent | ≥ 0.75.0 (peer dependency) |
30-Second Workflow
1. Read the file
read returns lines prefixed by stable 4-character hash anchors:
ve7x│function calculateTotal(items: CartItem[]): number {
szJx│ let total = 0;
kQmx│ for (const item of items) {
m2zX│ total += item.price;
d2xX│ }
p9rX│ return total;
a1bX│}
2. Apply an edit
edit targets inclusive anchor bounds using the flat named-object payload:
{
"file": "src/cart.ts",
"edits": [
{
"anchor_from": "szJx",
"anchor_to": "m2zX",
"text": " let total = items.reduce((sum, item) => sum + item.price, 0);\n"
}
]
}
3. Receive self-serving diff with fresh anchors
The response returns a unified diff with newly allocated anchors for modified lines, allowing immediate follow-up edits without a re-read:
- szJx│ let total = 0;
- kQmx│ for (const item of items) {
- m2zX│ total += item.price;
+ w4kQ│ let total = items.reduce((sum, item) => sum + item.price, 0);
d2xX│ }
Configuration & Settings
Settings are partitioned into two distinct roots (ADR-0005):
- Settings Root (human configuration):
${PI_CODING_AGENT_DIR:-~/.pi/agent}/extensions/pi-better-edit/settings.json(global) or.pi/agents/pi-better-edit/settings.json(project-local). - State Root (internal MVCC CAS database):
${XDG_STATE_HOME:-~/.local/state}/pi-better-edit/(relative$XDG_STATE_HOMEcounts as unset per ADR-0005; storeshash-store.sqlite, legacyhash-store.json,diagnostics.log, andread-expansion.log).
Override the layout globally via PI_BETTER_EDIT_CONFIG_DIR=/path/to/dir (places settings at <dir>/settings.json and state at <dir>/state/ without suffix). Environment variables (PI_BETTER_EDIT_BASH_VIEW, PI_BETTER_EDIT_READ_EXPANSION, PI_BETTER_EDIT_LENS_BRIDGE, PI_BETTER_EDIT_STORE_JANITOR*) override file settings.
Interactive Settings & Slash Commands
Run the interactive settings overlay inside pi:
/pi-better-edit settings
/pi-better-edit(alias/pi-better-edit settings): Interactive overlay inspecting active effective settings across env, project, and global rungs. Headless hosts receive structured status text./pi-better-edit bash-view [on|off|status] [project|global](alias/pi-better-edit bash ...): Configures Bash View interception. Subcommandmulti-file [status | max-files N | max-rows N]tunes multi-file limits./pi-better-edit read-expansion [on|off|status] [project|global](alias/pi-better-edit read ...): Configures Read Expansion./pi-better-edit lens [auto|on|off|status] [project|global]: Configures pi-lens bridge./pi-better-edit lens statusreports resolved store and bridge states.
Tools
pi-better-edit registers three core tools that replace pi's default file manipulators:
1. read
Returns file content formatted as HASH│content with active session leases.
read({
file: "src/index.ts",
windows?: [ // Disjoint window slices (up to 16 ranges)
{ offset: 1, limit: 50 }, // Line offset slice (default limit: 50)
{ around_anchor: "kQmx", radius: 5 } // Anchor-centered neighborhood slice
],
mode?: "served" // "served" (default, grants leases) | "verbatim" (raw text, no hashes)
})
- Whole-File vs Windowed Reads: Omitting
windowsreads the entire file from line 1. Supplyingwindowsscopes the output to up to 16 disjoint slices (either{ offset, limit }or{ around_anchor, radius }). - Disjoint Multi-Window Leases: In the default
servedmode, every row across all returned windows receives an active lease, enabling batched edits across distant parts of the file in the subsequent turn. - Large-Line Protection: Individual lines exceeding pi's
DEFAULT_MAX_BYTES(50 KB) are replaced with a concise unservable placeholder marker to prevent context explosion.
2. edit
Applies atomic single or batched modifications.
edit({
file: "src/index.ts",
edits: [
{
anchor_from: "szJx",
anchor_to: "szJx",
text: " const maxRetries = 5;\n",
at?: "in-place" // "in-place" (default) | "before" | "after"
}
],
mode?: "general" // "general" (default) | "literal" (declares served-row bytes as intended content)
})
Edit Payload Contract
An edit item carries flat, discriminator-free semantics: exactly one payload per item (supply text or text_ref, both or neither refused naming the offending keys).
| 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
- In-place replacement:
{ anchor_from: "a1bX", anchor_to: "c3dX", text: "new code\n" } - In-place deletion:
{ anchor_from: "a1bX", anchor_to: "c3dX", text: "" } - Before / After insertion:
{ anchor_from: "kQmx", anchor_to: "kQmx", at: "after", text: "// note\n" } - By-reference copy:
{ anchor_from: "m2zX", anchor_to: "m2zX", text_ref: { anchor_from: "d2xX", anchor_to: "d2xX", mode: "copy" } } - By-reference cut:
{ anchor_from: "m2zX", anchor_to: "m2zX", text_ref: { anchor_from: "d2xX", anchor_to: "e5vX", mode: "cut" } } - Foreign-source cut:
{ anchor_from: "m2zX", anchor_to: "m2zX", text_ref: { anchor_from: "g7tX", anchor_to: "h3sX", file: "src/helper.ts", mode: "cut" } }
3. undo_last_edit
Restores the exact previous file content, BOM, line endings, and original anchors.
undo_last_edit({ path: "src/index.ts" })
- Backed by snapshot-pinned undo records in the CAS store; survives agent restarts.
- Refuses with
[E_UNDO_STALE]if the file was modified externally after the last edit.
Features
Read Expansion
read automatically widens small requested windows to their enclosing structural block (ADR-0008):
- Trigger & Bounds: A requested window of 24 lines or fewer widens to its enclosing block (function, method, class, module, Markdown section, or blank-line paragraph). Overlapping or adjacent requested windows fold into one span before this test. Caps are 80 lines of code, 80 lines of Markdown, and 40 lines of plain text.
- Chrome & Notes: A widened read begins with a chrome line
[widened <requested> -> <served>, <label>]; more than three requested windows collapse to[widened N windows: <ranges>]. Construct labels use LSPSymbolKindnames plusarrow,impl,trait,typeAlias,section, andparagraph. - Refusals & Markdown clamping: Code and plain-text blocks above their caps refuse and serve the requested window with no note. Markdown sections above the cap clamp to a cap-sized window and report
(section <extent>). - Mode Independence: Expansion applies in
servedmode (anchored and leased) and inmode: "verbatim"(raw text without anchors). - Configuration & Audit: Toggle via
read.expansion("on"|"off", default"on") in settings files orPI_BETTER_EDIT_READ_EXPANSIONenvironment variable. Expansions and refusals append toread-expansion.login the extension state root.
Interoperability with pi-lens
pi-better-edit deeply integrates with pi-lens language intelligence:
- Formatter & Autofix Immunity: Whitespace rewrites by formatters during deferred passes are absorbed by the 28-code-point whitespace class (ADR-0002). Note that zero-width characters (U+200B ZWSP) are deliberately excluded; formatters rewriting ZWSP to a space rotate the anchor and fail closed safely.
- v2 I/O Bridge Compatibility: Every served line and mutation (reads, bash views, diffs, reject-and-serve rows,
writetool calls) is reported to thepi-lens:io-bridge, satisfying the read-before-edit guard with zero redundantreadcalls.
Bash View Interception
Pure views and searches (cat, cat -n, nl -ba, head, tail, sed -n, grep, rg, rg --files) are intercepted: raw stdout is swapped for the same lines as HASH│content rows under a [Bash view: <file> (<N> lines total | lines A-B of <N> total)] header, and the rows carry working leases, so the next edit needs no re-read (ADR-0006).
- Display transforms & context: Also served are display tails (
cat -A,cat -vet,od -c,xxd,hexdump -C), context windows (grep -A/-B/-C,rg -C), and verified compound chains with an inert count tail (ADR-0006). - Standalone multi-file views: Multi-file views (
cat FILE…,sed -n RANGE FILE…) are served, capped at 10 files and 2,000 rows (ADR-0006). - Passed through byte-identical: Pipelines with transforms, unverified compounds, errors, truncation, and any command writing to stderr pass through unmodified.
Multi-Session Support (MVCC)
pi-better-edit v2 introduces first-class multi-session support for single workspaces through MVCC:
How MVCC Isolates Sessions
- Session-Keyed Leases: Leases are keyed to
(session_id, file_path, anchor)inserved_leases. Session identity is retrieved fromctx.sessionManager.getSessionId(). - Zero Cross-Session Authority: Anchors served to Session A authorize nothing in Session B. Session B calling an edit with A's anchors is rejected with
[E_UNKNOWN_ANCHOR](or[E_FOREIGN_ANCHOR]if served for another file). - Concurrent Edits as External Changes: If Session A mutates lines 1–5 while Session B holds leases for lines 20–30, Session B's edit auto-rebases cleanly without error.
- Fail-Closed Contention: If Session A modifies lines that Session B targets, Session B's edit fails closed with
[E_STALE_RANGE]and returns fresh rows. - Persistent Continuation: In the pi agent runtime, resuming a session preserves the session ID (e.g. via
pi -c), ensuring previously served anchors remain valid across restarts.
[!WARNING] Concurrency Recommendation While MVCC makes multi-session operation inside a single workspace safe against data corruption, running concurrent coding agents against the exact same workspace files is not recommended. Agents can race on intermediate states or suffer merge churn.
Recommended Pattern: Use isolated git worktrees (
git worktree) per agent role (e.g. Task Manager, Implementer, Reviewer), as practiced in multi-agent environments using isolated worktrees.
System Architecture
┌──────────────────────────────────────────────────────────────────────────────────┐
│ STORAGE TIER │
│ src/hash-store.ts & src/snapshot-store/ (Principal Tables) │
│ - 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 leases (session_id, file_path, anchor) │
│ - file_undo: Snapshot-pinned undo record per path, surviving restarts │
│ - cut_intent: Transaction recovery for correlated cross-file cuts │
└────────────────────────────────────────┬─────────────────────────────────────────┘
│
┌────────────────────────────────────────▼─────────────────────────────────────────┐
│ SESSION TIER │
│ src/served-session/session.ts │
│ - Leases: Granted on all serve paths (read, diff, write auto-read, bash view) │
│ - 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/served-session/serve-render.ts │
│ - On-Demand CAS Materialization: Materializes current disk state │
│ - Patience LIS Pin Backbone: O(m log m) alignment with bounded DP fallback │
│ - 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. Anchor Generation Pipeline
- Frozen Whitespace Canonicalization: Strips a frozen 28-code-point class (ADR-0002) before hashing. Formatter passes (
prettier,black,gofmt) leave anchors intact. - xxHash32 & Base62 Space: Canonical lines are hashed using xxHash32 seeded with the file's canonical path and projected into 4-character base62 strings (
A-Za-z0-9), yielding $62^4 - 10^4 = 14,766,336$ allocatable anchors per file (all-digit spellings reserved to prevent confusion with line numbers). - Coprime Collision Probing: Duplicate lines probe with a stride coprime to the hash space ($62^2 + 62 + 1 = 3,907$). Every line in a file receives a guaranteed unique anchor.
2. Multi-Version Snapshot Lineage
Every file modification commits a new CAS snapshot to file_snapshots. The line_lineage table tracks the monotonic line_id for every line coordinate, maintaining coordinate-independent identity across arbitrary external shifts.
Error and Warning Contract
pi-better-edit enforces a strict diagnostic contract (ADR-0004). Rejections report machine-actionable details.cause values (retirement, never-served, served-range staleness). Detailed cause classifications and refusal-scoped options are documented in Error and Warning Contract Reference.
Domain Rejections ([E_*])
| Error Code | Description | Served Options |
|---|---|---|
[E_BAD_PAYLOAD] |
Payload fails schema validation (missing fields, wrong types). | — |
[E_MALFORMED_ANCHOR] |
Anchor is not valid — not a bare 4-char base62 token. | — |
[E_STALE_ANCHOR] |
Anchor no longer resolves to its leased identity in the file snapshot. | — (the served rows are the state) |
[E_UNKNOWN_ANCHOR] |
The path has not served the anchor in this session. | — |
[E_FOREIGN_ANCHOR] |
Anchor was served for a different file, not the targeted one. | — |
[E_STALE_RANGE] |
A line in the edit range changed on disk, or a boundary line was never served (ADR-0004). | — (the fresh read is the state) |
[E_UNVERIFIED_RANGE] |
One boundary lease retired while surviving bound is live and unshifted (ADR-0004). | — (the fresh read is the state) |
[E_TARGET_LOST] |
Target line identity deleted or reordered without stable anchor bounds. | — |
[E_SUSPICIOUS_TEXT] |
Replacement line starts with a served HASH│ anchor prefix. |
Omit anchor prefixes or set mode: "literal". |
[E_BATCH_ABORT] |
Two or more items in the batch target overlapping spans. | Merge overlapping spans or split calls. |
[E_NOOP_LOOP] |
Identical edit submitted 3 consecutive times without change; range already has this text. | — |
[E_EMPTY_RANGE] |
Edit would result in an invalid empty state on a non-empty file target. | — |
[E_NOT_FOUND] |
Target file does not exist on disk. | — |
[E_ACCESS] |
Target file is unreadable, unwritable, or in a circular symlink loop. | — |
[E_UNSUPPORTED_FILE] |
Target path is a directory, binary file, image, or UTF-16/32 text. | — |
[E_LOSSY_TEXT] |
File bytes do not round-trip clean UTF-8 decode without data loss. | — |
[E_UNDO_STALE] |
Cannot undo: file was modified or deleted externally after the last edit. | — |
[E_UNDO_UNAVAILABLE] |
Cannot persist undo snapshot to CAS store. Disk left unchanged. | Retry edit or use write. |
[E_UNDO_REVERT_FAILED] |
Correlated cross-file cut transaction was interrupted mid-revert. | — |
[E_LARGE_FILE] |
Exceeds admission budget (200,000 lines SERVED_MAX_LINES) or anchor address space. |
Fact-only (per ADR-0004). |
[E_INTERNAL_INVARIANT] |
Internal MVCC or CAS store invariant failed. | — |
[E_UNKNOWN] |
Unexpected filesystem or runtime exception. | — |
Applied Warnings ([W_*])
| Warning Code | Audience | Description |
|---|---|---|
[W_NEVER_SERVED_SHAPE] |
[MODEL] |
Replacement line starts with an anchor-shaped token never served in this session. Applied verbatim. |
[W_SERVED_PREFIX_MISMATCH] |
[MODEL] |
Replacement line starts with a served anchor but content differs. Applied verbatim. |
[W_REVERSED_ANCHORS] |
[USER] |
Anchors were reversed (anchor_from after anchor_to). Swapped and applied cleanly. |
[W_UNICODE_LITERAL] |
[USER] |
Replacement text contains literal \uDDDD sequence. Applied verbatim. |
[W_LITERAL_BYPASS] |
[USER] |
Served-echo check bypassed by literal declaration. |
[W_NOOP] |
[USER] |
Identical edit submitted a 2nd consecutive time without changing range text. Resend triggers [E_NOOP_LOOP]. |
[W_NOOP_INSERT] |
[MODEL] |
Empty insertion with before/after writes zero bytes. |
Comparison
Capability Matrix
| 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 refusal | Silent overwrite or text-mismatch error |
| Batch Support | Atomic up to 32 items with delta shifts | Patch preflight | Single replacement per call |
| Undo Persistence | Survives restarts (CAS snapshot pinned) | In-memory / per-process | In-memory / per-process |
| Session Isolation | Session-keyed leases (served_leases) |
Process-global / unkeyed | Process-global / unkeyed |
| Deterministic Battery | 27 deterministic scenarios in CI (test/e2e/tool-battery.test.ts) |
— | — |
Detailed edge-case test matrices, recovery behaviors, and benchmark datasets are documented in Benchmarks & Comparison Reference.
Read-expansion acceptance measurement is documented in Read Expansion Replay: pnpm run replay:read-expansion --baseline <dir> --candidate <dir> --json reports per-language same-file re-read share and served-line waste, then applies the 20% / 200-read gate.
License
Acknowledgments
- Can Bölük for seminal insights on The Harness Problem.
- @oh-my-pi by can1357 for pioneering the hashline patch concept.
- pi-hashline-edit-pro by YuGiMob for foundational agent extension designs.