pi-mono-multi-edit

Pi extension that replaces edit with batch edits and Codex-style patches

Package details

extension

Install pi-mono-multi-edit from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-mono-multi-edit
Package
pi-mono-multi-edit
Version
1.7.2
Published
Apr 23, 2026
Downloads
1,591/mo · 214/wk
Author
emanuelcasco
License
unknown
Types
extension
Size
126.5 KB
Dependencies
1 dependency · 4 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

Multi-Edit — Enhanced Edit Tool

A pi extension that replaces the built-in edit tool with a more powerful version that supports batch edits across multiple files and Codex-style patch payloads — all validated against a virtual filesystem before any real changes are written.

Origins

Initially derived from mitsuhiko/agent-stuff's pi-extensions/multi-edit.ts. The substrate has since been substantially rewritten — the patch engine is now a recursive-descent parser over a line cursor with indexOf-based hunk anchoring, the diff renderer is a two-pass design, and the classic edit path gained atomic multi-file rollback, eager write-permission preflight, curly-quote fallback matching, a read-cache backed workspace, and context-guard:file-modified event integration. Shape-level divergences (notably the Hunk { oldBlock, newBlock } shape vs upstream's UpdateChunk { oldLines[], newLines[] }) and dropped compatibility corners are documented below.

Overview

The standard edit tool handles one oldText → newText replacement at a time. Multi-Edit extends it with three modes so an agent can make many targeted changes in a single tool call, dramatically reducing round-trips and the risk of partial edits leaving the codebase in an inconsistent state.

All modes run a preflight pass on a virtual (in-memory) copy of the filesystem first. If any replacement fails, no real files are touched.

Modes

1. Single (classic)

Identical to the built-in edit tool. Provide path, oldText, and newText.

{
  "path": "src/index.ts",
  "oldText": "const foo = 1;",
  "newText": "const foo = 2;",
}

2. Multi (batch array)

Pass a multi array of edit objects. Each item has path, oldText, and newText. A top-level path can be set as a default that individual items inherit when they omit their own path.

{
  "path": "src/utils.ts", // inherited by items that omit path
  "multi": [
    {
      "oldText": "import foo from 'foo';",
      "newText": "import foo from '@scope/foo';",
    },
    {
      "path": "src/other.ts", // overrides the top-level path
      "oldText": "const bar = 0;",
      "newText": "const bar = 42;",
    },
  ],
}

You can also mix a top-level single edit with multi — the top-level edit is prepended as the first item in the batch:

{
  "path": "src/index.ts",
  "oldText": "version: 1",
  "newText": "version: 2",
  "multi": [{ "oldText": "// old comment", "newText": "// new comment" }],
}

3. Patch (Codex-style)

Pass a patch string delimited by *** Begin Patch / *** End Patch. This format supports adding, deleting, and updating files with hunk-based diffs — similar to the patch format used by OpenAI Codex.

*** Begin Patch
*** Add File: src/new-file.ts
+export const greeting = "hello";
*** Delete File: src/deprecated.ts
*** Update File: src/existing.ts
@@ function oldName() {
-function oldName() {
+function newName() {
*** End Patch

Supported operations inside a patch:

Header Effect
*** Add File: <path> Creates (or overwrites) the file with +-prefixed lines as content
*** Delete File: <path> Removes the file (errors if it doesn't exist)
*** Update File: <path> Applies one or more @@-delimited hunks to the file

Note: *** Move to: (rename) operations are not supported and will throw an error.

Codex apply_patch compatibility

The patch engine implements a pragmatic subset of the Codex apply_patch format. The following edge cases are intentionally not supported and raise a parse error instead of degrading silently:

Feature Status Notes
@@ hunk header Required Every hunk inside an *** Update File: block must start with @@
Trailing-whitespace tolerance Supported Hunks fall back to per-line trimEnd matching when exact indexOf misses
Full trim / unicode-normalized Dropped Only trimEnd is supported — normalize curly quotes or dashes in the patch before sending
*** End of File sentinel hunks Dropped Use a normal hunk anchored on the last real line
*** Move to: rename Rejected Emit an Add + Delete pair instead

These restrictions keep the parser simpler and more predictable than a full 4-pass fuzzy matcher while still catching the most common class of whitespace mismatch.

Key Features

Preflight Validation

Before writing a single byte to disk, every edit is applied to a virtual (in-memory) snapshot of the affected files. If any replacement fails — wrong oldText, file not found, missing context — the entire operation is aborted and no real files are modified. The preflight also checks write permissions against the real filesystem, so read-only targets fail fast before any virtual apply is attempted.

Atomic Multi-File Rollback

When a classic batch spans multiple files, the applier snapshots each file's pre-edit content before writing it. If a later file in the batch fails mid-write, every file already written is restored from its snapshot on a best-effort basis — the original failure is still surfaced, but the filesystem ends up in its pre-batch state.

Positional Ordering for Same-File Edits

When multiple edits target the same file, they are automatically sorted by their position in the original file content (top-to-bottom). This ensures the forward-search cursor works correctly regardless of the order the model listed the edits.

Quote-Normalized Matching for Classic Edits

Classic oldText lookups escalate through an ordered list of normalizer passes applied to both oldText and file content. The first pass that locates the transformed string wins:

  1. Exact — character-for-character match
  2. Curly → straight quotes' / ' / " / " in the model's oldText are rewritten to ASCII before the second search
  3. Trailing whitespace tolerance — per-line trimEnd on both sides catches the most frequent class of mismatch (model generates trailing spaces the file doesn't have, or vice versa)

Extending the chain is a matter of appending another normalizer to the MATCH_PASSES array in classic.ts.

Patch @@ hunks also support a trimEnd fallback — when the exact indexOf misses, the applier retries with per-line trailing-whitespace stripping on both the hunk and the file content.

Redundant Edit Detection

If the same oldText → newText pair appears more than once in a multi batch for the same file (e.g. the model over-counted occurrences), subsequent duplicates are skipped gracefully with a success status rather than raising an error.

Diff Generation

Every successful edit returns a unified diff attached to the tool result so the agent and user can inspect exactly what changed. For multi-file operations, per-file diffs are concatenated. The first changed line number is also surfaced for UI scrolling.

Path Inheritance

In multi mode, items that omit path automatically inherit the top-level path. This is convenient when most edits target a single file with one or two exceptions.

Parameters

Parameter Type Description
path string (optional) Target file path (absolute or relative to cwd). Serves as default for multi items.
oldText string (optional) Exact text to find and replace. Must match including all whitespace.
newText string (optional) Replacement text.
multi EditItem[] (optional) Array of { path?, oldText, newText } objects for batch mode.
patch string (optional) Codex-style patch payload (*** Begin Patch … *** End Patch). Mutually exclusive with all other parameters.

EditItem shape:

{
  path?: string;   // inherits top-level path if omitted
  oldText: string;
  newText: string;
}

Dependencies

Package Role
@mariozechner/pi-coding-agent ExtensionAPI type and tool registration
@sinclair/typebox Runtime JSON Schema / TypeBox parameter definitions
diff Line-level diff generation for result output

Error Handling

Situation Behaviour
patch used together with path/oldText/newText/multi Throws immediately — parameters are mutually exclusive
Incomplete top-level edit (e.g. path + oldText but no newText) Throws listing the missing fields
multi item missing path and no top-level path set Throws identifying which item is affected
oldText not found in file Preflight throws; no files are modified
patch context line not found Preflight throws; no files are modified
File does not exist or is not writable Throws before any mutations
Patch *** Move to: operation Throws — not supported

Performance vs Base Edit

Measured across 38 real pi sessions (81 JSONL files) using npm run bench -- --from-session --all. Sessions are auto-classified: base = only single path/oldText/newText calls; multi-edit = uses multi or patch at least once.

Headline Numbers

Metric Base Multi-Edit Delta
Sessions 22 16
Tool calls 92 137
Logical edits 92 247
Edits / tool call 1.00 1.80 +0.80
Failure rate 6.5% 6.6% +0.0 pp
P50 duration 7 ms 11 ms
P95 duration 31 ms 25 ms
Cost / logical edit $0.29 $0.17 -41.8%
Calls saved vs base 110 44.5% fewer

What the data says

Wins:

  • Cost per edit drops 42%. Batching N edits into one tool call avoids N-1 round-trips of assistant→tool→assistant, each of which carries the full conversation context as input tokens. At $0.17 vs $0.29 per logical edit, multi-edit pays for itself on any batch ≥ 2.
  • 44.5% fewer tool calls. 110 hypothetical round-trips eliminated. This is time the model spends re-reading its own context, waiting for tool dispatch, and generating boilerplate tool-call framing — all wasted.
  • P95 latency is lower (25 ms vs 31 ms). The preflight + read cache avoids wasted disk I/O on doomed edits, and the cache deduplicates reads when multiple edits touch the same file.

Neutral / watch items:

  • P50 latency is slightly higher (11 ms vs 7 ms). Expected: multi-edit does a full preflight pass before the real write. The delta is negligible per-edit (~2 ms extra for the safety guarantee).

Mode Breakdown (pre-v1.5.1)

Mode Calls % Failure rate
single 61 45% 1.6%
patch 41 30% 9.8%
multi 35 25% 11.4%

Root-cause analysis of the 8 multi/patch failures showed: 5 were trailing-whitespace mismatches in oldText, 2 were batch-poisoned (1 bad edit killed 5 good siblings), and 1 was a legitimate ENOENT. Three fixes shipped in v1.5.1 to address this:

  1. trimEnd matching for classic editsfindActualString now tries a per-line trimEnd normalization pass on both oldText and file content when exact and curly-quote passes miss. Catches the dominant failure class (model generates trailing spaces the file doesn't have, or vice versa).
  2. Partial success for multi batches — when one edit in a batch can't be found, the remaining edits are still applied. Failures are reported individually instead of aborting the entire batch. A 6-edit call with 1 bad edit now produces 5 successes + 1 failure instead of 0 + 6.
  3. trimEnd fallback for patch hunks — the patch applier now falls back to per-line trimEnd matching when exact indexOf misses, using the same strategy for both oldBlock and contextPrefix anchors.

Projected impact: multi mode failure rate ~11.4% → ~3%, patch mode ~9.8% → ~2.5%. The batch-poisoning fix alone eliminates the inflated failure count — previously a single bad edit in a 6-edit batch counted as 1 failed tool call; now it counts as 1 failed edit + 5 successes.

Multi-edit sessions still use single mode 45% of the time — room to push batch adoption via prompt guidelines.

Running the Benchmark & Analysis

The benchmark-edits tool provides two modes: a synthetic benchmark that measures engine latency on controlled scenarios, and a session analysis mode that parses historical pi session JSONL logs to compute cost, token, failure, and throughput metrics.

# Synthetic benchmark — built-in scenarios
npm run bench

# Custom scenario file (JSON array — see header comment in benchmark-edits.ts)
npm run bench -- scenarios.json

# Session analysis — all pi sessions
npm run bench -- --from-session --all

# Specific session files or directories
npm run bench -- --from-session ~/.pi/agent/sessions/<project-dir>/
npm run bench -- --from-session session1.jsonl session2.jsonl