Skip to content

read_state: flag write overwriting a file the agent never read - #241

Open
bobzhang wants to merge 1 commit into
mainfrom
write-read-state
Open

read_state: flag write overwriting a file the agent never read#241
bobzhang wants to merge 1 commit into
mainfrom
write-read-state

Conversation

@bobzhang

Copy link
Copy Markdown
Collaborator

What

Adds a session-scoped ReadState (agent_tool/read_state) — a seen-set the file tools share for one agent run — and uses it to flag a write that overwrites a file the agent never read this session:

ok: wrote N chars to <path> (overwrote existing file you had not read this session)

read records a path only on a complete read (whole file shown, untruncated); write records after writing (it authored the content) and checks before overwriting. The agent loop creates one ReadState and hands it to read/write. Standalone tool use (no ReadState) keeps prior behavior.

ReadState::ReadState() is the constructor, as requested.

Why a soft flag, and these exact rules (data-driven)

I analyzed 19 recorded toml-parser agent runs before building this. Across 395 writes: only 71 (18%) followed a read; 175 were blind re-writes of self-authored files; 149 were first-touch creates. In a from-scratch task (empty workspace) that's almost all benign — there's nothing pre-existing to clobber. So:

  • Soft annotation, not a hard "read first" error. A hard guard would loop on those 175 self-rewrites. write records the path after writing, so re-writing a file the agent itself created is never flagged.
  • Seen-set only, no mtime/staleness. The "changed since you read it" check guards against concurrent editors/linters — which don't exist in a single batch CLI session.
  • Net effect: dormant in from-scratch tasks (would have fired ~0 of those 395 writes); the value shows up on pre-existing codebases.

(The live e2e against DeepSeek is firewalled in CI, so those recorded runs are the evidence.)

Subtleties (caught by codex review, now fixed)

  • A partial read (start_line, small max_lines, or a max_output_chars truncation) must not mark the file fully seen — otherwise a later overwrite of the unseen remainder wouldn't warn. Recording now requires shown_lines == total_lines && !truncated (a high max_lines that still covers the file counts).
  • edit deliberately does not record. openseek doesn't require read-before-edit, so an edit only shows the agent a fragment; promoting an edit-only path to "fully seen" would wrongly suppress a later write's warning.

Validation

  • moon check + moon fmt clean; 75 tests pass across the touched packages (incl. partial-vs-full read and unread-overwrite cases).
  • Public API: read/write definition() gain an optional read_state?; new agent_tool/read_state package. edit unchanged.
  • Reviewed by codex over three rounds (it found the partial-read gap and the edit-promotion issue; both fixed): final "No actionable correctness issues were found in the diff."

🤖 Generated with Claude Code

…ession

Adds a session-scoped ReadState (agent_tool/read_state) — a seen-set the file
tools share for one agent run. `read` and `edit` record the paths they touch;
`write` consults it and, when it overwrites an existing file that was never read
or written this session, appends " (overwrote existing file you had not read
this session)" so a wholesale replacement of unseen content is visible in the
transcript. The agent loop (tool_definitions) creates one ReadState and hands it
to read/edit/write; standalone tool use (no ReadState) keeps the prior behavior.

Two deliberate, data-driven choices (from analyzing 19 recorded toml-parser
runs, where 175/395 writes were blind re-writes of self-authored files):

- It is a soft annotation, not a hard "read first" error. A hard guard would
  loop on those self-rewrites; `write`/`edit` record into ReadState after a
  successful write so a re-write of a file the agent itself created is not
  flagged.
- It tracks only "seen vs not seen", not modification times. The "changed since
  you read it" half guards against concurrent editors/linters, which do not
  exist in a single batch CLI session.

This stays dormant in from-scratch tasks (nothing pre-exists to clobber) and
earns its keep on pre-existing codebases. The type is a public subpackage
because the agent loop (outside agent_tool) must construct it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant