Edits and change transactions
Every tool in Mercury that changes files uses one vocabulary. A read gives the model an anchor for what it saw, an edit that carries the anchor is refused before it writes if the file has changed since, and each change leaves exactly one receipt of what happened. The Transaction tool shows what changed and what was checked across a coding loop.
Read anchors
fa:<hex12>: Identifies the contents of a whole file (returned by whole-fileRead,Write, andReadwithrangeswhen the whole file fits within the size cap).ra:<hex12>:L<start>+<n>: Identifies the exact returned line range, using one-based line positions.expected_anchor: Supplies the expected content to a mutation, checked against the current bytes before writing. A stale or malformed anchor returns a structured result naming the reason and the smallestReadthat refreshes the anchor, without returning a current anchor value.N#hhhh: Identifies a line by its one-based position and the first hex of its own hash when a Read asks forline_anchors. Identical lines share a hash and never share an address.
File and range digests are SHA-256 over CRLF-normalized text, truncated to 12 hex characters. Unicode codepoints are not normalized. These digests detect stale content and are not a security boundary.
Edit hunks
The Edit tool accepts one or more disjoint, one-based line replacements or insertions in hunks, applied atomically. A batch applies against the snapshot named by expected_anchor, which is required in this mode; a range anchor bounds the editable window. Stale, overlapping, out-of-bounds or unparseable hunks write nothing and name the failing hunk.
Anchor-qualified positions such as 12#ab3f carry endpoint hashes that are checked before anything is written. A batch whose hunks all use these positions can omit expected_anchor; a file changed by something else since the read still refuses the batch. A refusal answers the current neighborhood anchors and a few candidate positions the text may have moved to. Mercury never moves the edit to a new position on its own.
An Edit that touches lines the model has not read, in a file it has read as it stands, lands in one call when those lines fit a Read window: the result carries the edited lines as they now stand, widened by a small margin and each block with its own range anchor, and they count as read. A file the session itself wrote or edited counts as read as it stands, and every applied edit says no re-read is needed. The lines a session's own crewmate writes through Write or Edit in the same process also count as the session's knowledge so the main thread and sibling crewmates can edit them without a Read (while a crewmate's Read stays its own), and a session resumed in a new process or a crewmate resumed with ResumeCrewmate keeps the lines its own earlier process read or wrote (a windowed Read included) while those lines still read the same.
The edit is refused when the file was never read this session, when it changed after the lines the model read, when expected_anchor no longer matches or, for hunks, covers only part of the lines addressed, or when the unread lines exceed a Read window. A refusal names the path and the reason and carries no line of the file and no anchor over it, so it reveals nothing about a file that a Read deny rule or the project boundary protects. A file the session has no current knowledge of gets the same refusal whatever the old string says. With partial knowledge, the refusal names the lines read, the lines the edit touches and the Read(offset, limit) that covers the gap; a non-unique old string names the line of every match and the Read that shows the first.
A Read of a file over the token cap answers with its first window as an ordinary result, and its first line names the file's size in tokens, the lines returned and the Read that continues from the window's end. Individual lines are clipped past 2,000 characters by default; Read accepts lossless: true with offset + limit or ranges (without outline) to return complete lines within the token budget (refusing if a single line exceeds the token budget), while Edit, Write and ChangeSet refuse to replace or overwrite a line that was clipped in Read or omitted as a long line in Grep, naming the exact Read(file_path: "...", offset: <line>, limit: 1, lossless: true) call needed before replacement. Read also accepts offset: -N to read the last N lines, ranges (up to 32 one-based line ranges in one call, each keeping its continuation mark), outline: true to return imports and declaration signatures with multi-line function and method bodies elided through the vendored tree-sitter grammars, and crop: [x1, y1, x2, y2] on an image to cut a pixel box before downscaling, plus a one-line count when a text file carries unresolved merge-conflict blocks.
Read opens .zip, .tar, .tar.gz and .tgz archives (listing entries or reading <archive>:<entry>), read-only SQLite databases (.sqlite, .sqlite3, .db, schema and sample rows or <db>:<table>), and .docx, .xlsx, .pptx, .odt, .ods, .odp, .epub and .rtf documents as text. Write and Edit update .zip archive entries and keyed SQLite rows through Read's <container>:<target> grammar behind the host file's write permission and read-before-overwrite check, check JSON, YAML and TOML syntax, report newly introduced syntax errors and missing syntax nodes in TypeScript, Python, Rust, Go and C files through the bundled tree-sitter grammars when no language server covers the file, and attach only the LSP diagnostics introduced by the edit. An edit or write carrying an omission placeholder such as rest of code unchanged is refused before anything is written, and a hand edit of a generated file, by name or by its leading marker, is refused with the generator named.
The replacement's bytes
The Edit tool writes a replacement as typed. Its search tolerates the file's typographic quotes, so an old string typed with straight quotes still finds a line whose quotes are curly, and the bytes outside the change keep the file's own typography. The search also forgives differences in indentation, trailing whitespace and typographic dashes and spaces, and matches an old_string whose \n, \t or \r the model double-escaped when those unescaped bytes appear once in the file; any other difference in content is a miss. The result says what it matched and how, and two different candidate blocks refuse as ambiguous. Inside the change, only prose takes the file's quote style, and only on a line that already carries it: Markdown, plain text and AsciiDoc, as the diff highlighter's registry names them. Code, configuration, data and every file the registry cannot name, such as a file with no extension or a dotfile, land byte for byte, a comment block inside a code file included.
What a write keeps
Write, Edit and ChangeSet read a file's shape before writing it back, so a change leaves everything it did not touch as it was:
- Every untouched line keeps its own line ending. A CRLF file stays CRLF and a mixed file stays mixed, whatever line endings the model's text arrived with. A line the change adds takes the file's majority ending.
- A file that does not end with a newline keeps ending without one, an append included, and a file that ends with one keeps it.
- A byte-order mark stays, even when the change is on the first line.
A one-word change through any of the three tools is a one-line diff. Write on an existing file takes its line endings from the file and not from the content it is given, so the same lines in another line-ending style count as no change. Changing a file's line endings is a job for a shell command. NotebookEdit edits a Jupyter notebook (.ipynb) cell in place: it changes only that cell's own line(s), preserves a list-shaped source as a list, and keeps the file's layout, indentation, line endings and trailing newline so a diff shows only the edited cell.
Change receipts
Every tool call that can change files or state produces one record of what it intended to do and what actually happened. This record is called a change receipt, and a single shared recorder writes it once. A cancelled or refused call does not create a partial receipt. Repeated calls that make no change are reported by the repetition policy. Mercury warns on the third, fifth and eighth repetition of a call with the same arguments and result, and on a sequence of up to five calls repeated five times. When turns.loopGuard is on, detecting that same sequence a second time ends the turn. On a second consecutive failed call with the same tool and canonical JSON arguments (including permission or hook refusals), Mercury warns the model with the error text to change its call, and on a third consecutive identical failing call crewmates and headless runs end the turn once the round settles with loop_stopped (while interactive foreground sessions receive reminders only); loop-guard reminders and repeated-result pointers name a call by its address fields (file_path, path, notebook_path, cwd, directory, name, tool, skill, task_id, shell_id) and value shapes (<N chars>, <N items>, <object N keys>), never echoing content or secret values. The durable state of change transactions lives under <config-home>/changesets.
ChangeSet operations
previewwithchanges: Produces an immutable, content-addressed plan without writing files.applywithchanges: Plans the set, obtains one aggregate permission decision and applies it.applywithplan_id: Applies a previously previewed plan. A stale plan is refused and names theReadthat refreshes each drifted file's anchor.statuswithplan_id: Inspects a retained plan.discardwithplan_id: Retires a plan.
The JSON form targets existing text files, each with its required expected_anchor and either hunks (the Edit hunk vocabulary) or edits (old_string and new_string pairs with optional replace_all, matched against the anchored snapshot by Edit's exact-string matcher: stripping pasted line-number prefixes, refusing identical strings and overlapping edits, tolerating indentation and quote style, naming matching line numbers on non-unique strings and closest lines on misses); op: "delete" removes the file and op: "move" renames it to new_path after any edits or hunks. It refuses file creation, binary files and notebooks by name. The optional patch dialect, patch beside changes, adds block edits, cross-file moves, whole-file deletion and file movement over the same anchors, committed through the same planner.
Preview a change set
Read the target files to get their anchors, then call ChangeSet with preview and one changes member per file, each with its expected_anchor and either hunks or edits (or op: "delete" / op: "move"). The tool returns a retained plan and leaves the files unchanged.
Apply a previewed change set
Call ChangeSet with apply and the returned plan_id, and approve the aggregate permission request if one appears. The complete set is applied and checked by reread, or the refusal names why nothing was written.
A set needs one aggregate decision from you, and one denied path means no writes. Each executed set produces one effect, one receipt and one aggregate inline change-view card. Editor and language-server synchronization after the write is bounded, and a timeout is classified as indeterminate.
The Transaction record
Mercury records a coding loop on its own. Every change a tool lands (Edit, Write, ChangeSet, AstEdit, NotebookEdit, language-server writes and Structure) and every check that runs (tests, builds, type checks and lints through Bash, and Test, Launch and Journey runs; the masked-exit-status guard stays quiet on a command that prints the exit status, a wait and a read-only diagnostic, and flags a piped check with no captured status) is added to the record as it happens. Where Mercury finds no test or build setup, reading each changed file back in full counts as the check.
The Transaction tool has two operations:
status: Lists what changed, which checks ran since the last change and whethercompletedcan be saved yet.finish: Saves the record with averdictofcompleted,failedorabandoned.completedis refused until a check has run after the last change and the newest such check passed; the refusal lists what is missing and the record stays open.failedandabandonedalways save, andunresolvedkeeps any remaining doubt word for word.
A saved record is readable as mercury://ide/transaction/<id>.
Limits and switches
- Set size: At most 16 files, 32 hunks per file, 128 hunks per set, 4 MB of staged bytes and 240 rendered diff lines, with per-file cuts named.
- Plans: At most 16 retained plans per owner, with a 30-minute lifetime.
MERCURY_CHANGE_RECEIPTS: Enabled by default;0removes anchors on reads, theexpected_anchorfield, their enforcement and the receipt record.MERCURY_EDIT_HUNKS: Enabled by default;0restores the plainexpected_anchorschema ofEdit.MERCURY_LINE_ANCHORS: Enabled by default;0removes theline_anchorsRead parameter, and hash spellings then refuse as unparseable.MERCURY_ANCHOR_PATCH: Opt-in; unset or0, theChangeSetschema carries no patch field.MERCURY_CHANGESET: Enabled by default and requires receipts and hunks;0removes the tool while the shared commit core keeps serving the Structure tool and language-server edits.