Skip to content

fix(workspace): back up before writing; make edit_file/write_file replay-safe - #665

Merged
LinuxDevil merged 1 commit into
mainfrom
fix/checkpoint-crash-replay
Oct 16, 2026
Merged

LinuxDevil merged 1 commit into
mainfrom
fix/checkpoint-crash-replay

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

Problem

  • WorkspaceCheckpoints.track() wrote the file first and recorded the backup after. A process killed between the two lost the backup; on resume the interrupted write_file re-ran and backed up the agent's own content as "before", so rewind() reported restored: [file] but put back the agent's version. For edit_file the re-run failed, so nothing was recorded and rewind silently did nothing.
  • Interrupted tool calls re-run on resume (at-least-once). The built-in edit_file then threw old_string was not found because its edit was already in the file, so the model was told an applied edit failed and looped.
  • Docs: sessions.md said to call resume()/send() before resolving an approval after a restart, but agent.approvals.resolve()/streamResolve() already find the session from the pause record (covered by an existing test).

Fix

  • track() stores the backup before the write. A path's first write in a turn records the backup; any later record in that turn, including a re-run of the interrupted call, keeps that first backup. Rewind treats a file that still holds its backed-up content as nothing to undo.
  • track() also accepts a PreparedWorkspaceWrite ({ prepare, write }). The built-in tools use it, so the backup's afterHash is known before the write, a failed edit still records nothing, and a change that leaves the file as it is records and writes nothing. The plain-function form still works: it stores the backup first, then adds a second record carrying the written content's hash.
  • edit_file: when old_string is absent and a non-empty new_string is present (with or without replace_all), it returns "the edit was already applied" and writes nothing. write_file was already idempotent.
  • Docs: build-a-coding-agent (new "Long turns and restarts": compaction for long turns, replay safety, session.on()/onEvent for progress during resume, approvals after a restart), durable-execution, workspace-tools, sessions (corrected the approval-after-restart text, documented that resume() doesn't stream). CHANGELOG, API reports and llms-full.txt regenerated.

Verification

  • New tests: a crash between storing the backup and writing (via a store whose append throws); a crash after the write for write_file and edit_file followed by a re-run, after which rewind restores the original; the plain-function track() crash path; already-applied edit_file with and without replace_all, plus the cases that must still fail; session.on() and onEvent seeing a resumed turn's events.
  • npx tsc --noEmit, npm run lint, npm run build clean. vitest run src/tools src/session src/execution scripts src/createAgentApprovals.test.ts: all pass (2011 + 102 tests).

🤖 Generated with Claude Code

…lay-safe

WorkspaceCheckpoints.track() wrote the file first and recorded the backup
after, so a process killed in between lost the original: the re-run call on
resume backed up the agent's own content and rewind() put that back. The
backup is now stored before the write, a re-run keeps the turn's first
backup, and rewind treats a file still holding its backed-up content as
nothing to undo. track() also accepts a PreparedWorkspaceWrite ({ prepare,
write }), used by the built-in tools so a failed edit still records nothing.

edit_file re-run after its edit was applied (old_string gone, non-empty
new_string present) now succeeds as "already applied" without writing,
instead of telling the model an applied edit failed.

Docs: replay safety of the file tools, progress for resumed turns via
session.on()/onEvent, compaction for long coding turns, and approvals after a
restart resolve directly (no resume() first).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LinuxDevil
LinuxDevil force-pushed the fix/checkpoint-crash-replay branch from 221d491 to b6b5876 Compare October 16, 2026 20:55
@LinuxDevil
LinuxDevil merged commit 2acc679 into main Oct 16, 2026
2 of 8 checks passed
@LinuxDevil
LinuxDevil deleted the fix/checkpoint-crash-replay branch October 16, 2026 20:57
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