feat(edit): enforce read-before-edit at the dispatch gate#1563
Merged
Conversation
Soft prompt hint ("read_file first so SEARCH matches byte-for-byte")
was easy for the model to skip — the failure mode is a tool round
spent on "search text not found", a guess at the bytes, and a retry.
Each round is a cache-miss tail and an obviously-not-grounded edit
that erodes trust.
New ReadTracker lives on CacheFirstLoop and threads through
ToolCallContext. read_file marks the absolute path on any successful
load (full, range, head, tail, outline); write_file marks too — the
model knows what it just wrote. edit_file / multi_edit consult the
tracker before applying and refuse unread targets with a message
that names the path and tells the model to call read_file first.
multi_edit refuses the whole batch if any target is unread, so a
partial-knowledge cross-file refactor can't half-apply.
ContextManager fires a new onLogRewrite callback from fold and
mechanicalTruncate; the loop wires it to ReadTracker.reset() because
once the byte-level history is folded away the model's "I saw it"
claim no longer holds. The tracker is in-memory only, so session
resume also starts empty — the model must re-read before editing
after a restart.
tools.ts's repeat-rejection detector recognizes the "read_file first"
gate so a 2nd identical unread edit gets the sharper "do not retry
identical args — call read_file on the target path first" hint
instead of the same refusal echoed back.
Path normalization lower-cases on win32 so case variants of the same
file resolve to one tracker entry.
11 new tests in tests/read-before-edit-gate.test.ts cover: refusal,
post-read success, write_file-counts-as-read, partial-read accepted,
multi_edit all-or-nothing, repeat-rejection sharpening, reset
semantics, and backwards compat when no tracker is injected.
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ReadTrackeronCacheFirstLooprecords files the model has seen this session (viaread_fileorwrite_file).edit_file/multi_editnow consult it viaToolCallContextand refuse unread targets up front with"<path> was not read this session — read_file first".multi_editis all-or-nothing across the batch.ContextManager.fold/mechanicalTruncate(newonLogRewritecallback) — the model's byte-level view of folded history is gone, so re-reads are required before mutating again. In-memory only, so session resume also starts empty.tools.tsrecognizes the gate, so a 2nd identical unread edit gets the sharper "do not retry identical args — call read_file on the target path first" instead of echoing the same refusal.src/code/prompt.tsupgrades the soft- read_file first so SEARCH matches byte-for-byte.bullet to an enforced rule that names the gate behavior.Why
Soft prompt hints don't bind. A model that skips
read_fileand guesses its SEARCH text wastes a tool round on"search text not found", retries with another guess, and burns cache-miss tail tokens — and the user watches it flail. Failing fast at dispatch with a clear "call read_file first" message turns a multi-round failure into a single corrective hop, and forces evidence into the prefix before any mutation.Claude Code enforces the same gate on its
Edittool; aligning the contract on our side closes the most visible "agent is dumb" complaint about edits.Test plan
tests/read-before-edit-gate.test.ts— 11 new cases: refusal, post-read success,write_file-counts-as-read, partial-read (range/head) accepted,multi_editall-or-nothing, repeat-rejection sharpening, reset semantics, backwards compat when no tracker is injected, Windows path normalization.3560 passed | 16 skipped— no regressions intests/preflight.test.ts,tests/loop.test.ts,tests/filesystem-tools.test.ts,tests/edit-blocks.test.ts.tsc --noEmitclean.biome checkclean on touched files.tests/comment-policy.test.tsclean (gate enforced on every PR).