Skip to content

N7: workspace rewind - snapshot files before write_file / edit_file, rewind by turn - #309

Merged
LinuxDevil merged 4 commits into
mainfrom
lou-n7-workspace-rewind
Oct 2, 2026
Merged

LinuxDevil merged 4 commits into
mainfrom
lou-n7-workspace-rewind

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

N7: workspace rewind

createFsTools(fs, { checkpoints }) with a new WorkspaceCheckpoints backs up every file that write_file and edit_file change. The backups are grouped by session and turn. checkpoints.rewind(toTurn, { sessionId, dryRun, force }) puts the files back as they were before that turn.

What changed

  • src/tools/workspace/checkpoints.ts: WorkspaceCheckpoints (list, rewind, clear), WorkspaceFileBackup, WorkspaceCheckpointStore, MemoryWorkspaceCheckpointStore and RewindResult.
  • src/tools/workspace/checkpointFileStore.ts: FileWorkspaceCheckpointStore(dir) writes one JSON file per session. Each write goes to a temp file that is then renamed into place; the rename is retried on Windows EPERM/EBUSY/EACCES. Session ids that are not plain file names are hashed.
  • src/tools/workspace/keyedQueue.ts: serializes work per key. It covers writes to one path (toolConcurrency) and the file store's read-modify-write for one session.
  • fsTools.ts: adds FsToolsOptions.checkpoints. Without it the tools behave exactly as before (no extra reads). edit_file is split into prepareEdit + write, so a failed edit records nothing.
  • FsProvider gets optional getMode / chmod, which NodeWorkspace implements with its usual path confinement. A forced restore of a file that was deleted outside the agent then gets its permission bits back.
  • Turn numbering: the number of user messages in ctx.messages minus one. Session id: ctx.sessionId, with a checkpointed turn's .turn-<n> suffix removed so all turns of a session group together; 'default' when there is none.

Safety

  • Rewind plans every path before it writes anything: it normalizes the stored path, then stats and reads the file through the provider. An invalid stored path (../x), or one that resolves outside the root through a symlink or junction, makes the whole rewind throw, the dry run included, and nothing changes. Restores go through the provider, so NodeWorkspace confinement applies.
  • A dry run only stats and reads.
  • If a restore fails partway, the error names the files already rewound and the backups are kept.
  • File content lives only in the store, never in events or traces.

Retention (documented)

  • Only the newest maxTurns turns per session are kept (default 20). Rewinding to an older turn throws instead of doing a partial restore.
  • Files over maxFileBytes (default 1,000,000) are not copied; rewind reports them as 'too-large' and never deletes them.
  • When a turn writes the same path more than once, only the first write keeps the old content. The later writes are recorded with omitted: 'same-turn' and keep only the hash of what was written.
  • A real rewind drops the backups of the turns it rewound.
  • clear({ sessionId }) forgets a session.

Docs (no headings added, renamed or removed)

  • docs/workspace-tools.md: a checkpoints row in the createFsTools options table, and paragraphs plus a snippet at the end of ## The tools covering turn numbering, list(), rewind() with a dry run, the changed-since rule and force, what is not tracked, and retention. Under ## Writing your own provider, one paragraph on the optional getMode / chmod.
  • docs/build-a-coding-agent.md: one bullet under ## Next steps pointing to rewind.
  • CHANGELOG (Added), llms.txt / llms-full.txt regenerated.

Deviations from the ticket text

  • session.fork() (N3a) does not exist yet. The docs point to agent.fork() (Fork and replay) for branching the conversation.
  • The store interface has two more methods than the ticket sketch: removeBeforeTurn and earliestTurn, which implement retention. Backups have two optional fields: mode, and omitted: 'too-large' | 'same-turn'.
  • The backup is appended after a successful write; the old content is captured before the write. A write that throws records nothing.

Verification (local, Windows 11, Node 26, after merging origin/main)

  • npx tsc --noEmit: exit 0
  • npm run lint: exit 0 (0 warnings)
  • npm run build: exit 0
  • npm run build --workspace=packages/create-lousho-agent: exit 0
  • npm run test:types: no errors
  • npm run docs:verify-snippets -- --skip-build: all 213 snippets type-check, 8 also run cleanly. The new snippet was also run against dist/, and its output matches the comments.
  • npm run docs:llms:check: exit 0
  • npm run test:coverage: 236 files passed, 1 skipped; 3423 tests passed, 7 skipped
  • npm run fallow: dead code: no issues; complexity: 0 above threshold
  • Agent Forge: typecheck exit 0; typecheck:server exit 0; test 119 passed; test:server 130 passed
  • npm run pack-smoke: all checks passed

Not verified locally: the POSIX permission-bits test in checkpoints.node.test.ts is skipped on Windows and runs in Linux CI. Mode restoring is also covered on every OS with a mode-aware MemoryWorkspace in checkpoints.test.ts.

Live test spend: none (the ticket makes no live calls).

New doc pages: none. Edited only: docs/workspace-tools.md, docs/build-a-coding-agent.md.

Closes #219

🤖 Generated with Claude Code

LinuxDevil and others added 3 commits October 2, 2026 20:45
…rewind by turn

createFsTools(fs, { checkpoints }) with a WorkspaceCheckpoints backs up every
file write_file and edit_file change (content, and mode where the provider has
the new optional getMode/chmod), grouped by session and turn. list() shows the
changed paths per turn; rewind(toTurn, { dryRun, force }) restores them,
deleting files the agent created and skipping files changed since. Retention:
newest maxTurns turns per session, content only for a turn's first write of a
path, files over maxFileBytes not copied. Memory and file stores.

Closes #219

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LinuxDevil

Copy link
Copy Markdown
Owner Author

Merged origin/main again (5fe0b2f, CHANGELOG tidy #308 + githubChannel #306; CHANGELOG conflict resolved keeping both sides). Re-ran the full verification list on 0559a61: tsc, lint, build, create-lousho-agent build, test:types, docs:verify-snippets (215 type-check, 8 run), docs:llms:check all exit 0; test:coverage 238 files passed / 1 skipped, 3481 tests passed / 7 skipped; fallow: no dead code, 0 above threshold; Agent Forge typecheck, typecheck:server, test (119 passed), test:server (130 passed) all exit 0; pack-smoke all checks passed.

@LinuxDevil

Copy link
Copy Markdown
Owner Author

Merged origin/main again (3d2da03, N10a route auth #307; CHANGELOG conflict resolved keeping both entries; llms regenerated). Full verification on 270f541: tsc, lint, build, create-lousho-agent build, test:types, docs:verify-snippets (221 type-check, 8 run), docs:llms:check all exit 0. test:coverage: first run had 1 timing failure in src/tools/built-in/http.test.ts ('rejects near the configured timeout', 4328 ms vs < 2000 ms, machine under load; unrelated to this change). The file passes alone (23/23), and the re-run with --coverage.reportOnFailure passed: 243 files passed / 1 skipped, 3541 tests passed / 7 skipped. fallow: no dead code, 0 above threshold. Agent Forge typecheck, typecheck:server, test (119), test:server (130) all exit 0. pack-smoke: all checks passed.

@LinuxDevil
LinuxDevil merged commit a8711e1 into main Oct 2, 2026
6 of 8 checks passed
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.

[N7] Workspace rewind: snapshot files before write_file / edit_file, rewind by turn

1 participant