Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Snapshot and rollback of a folder

After this lesson: I can put a folder back as it was before an agent acted.

65 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

Putting a folder back as it was

A snapshot is a copy of every file in a folder, taken before an agent acts. A rollback puts the folder back to that copy.

That matters because even a confined agent can ruin the files it is allowed to touch. With a snapshot you can say: put it all back, exactly.

By the end of this lesson

Note

I can put a folder back as it was before an agent acted.

Key words

WordWhat it means
Snapshota copy of every file and its text
Diffthe list of what changed between two states
Restoreto put files back from a snapshot
Rollbacka restore of the whole folder
Added filea file that was not in the snapshot

The cells students fill in are left blank in this preview.

A snapshot holds every file

Before the agent starts, read every file in the folder and keep its path and its text. Keep it somewhere the agent cannot reach.

Compare by path. After the agent acts, a file is added if it is new, removed if it has gone, and changed if its text is different.

Worked example: three edits

Edit the agent madeIn the diffTo put it back
Wrote src/app.js with new textChangedWrite the old text
Deleted notes.txtRemovedWrite the file again
Created new.txtAddedDelete it
Wrote a file back to its old textNothingNothing to do

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think a restore only has to rewrite the files that changed. It must also delete the files the agent added, or the folder is not as it was. Then check: after the restore, the diff must be empty.

Prove the restore worked

A restore that you do not check is a hope. After restoring, take a new snapshot and compare it with the first. If the diff is empty, the folder is exactly as it was.

This compares files only. An empty folder the agent made is not in a list of files, so say what your snapshot does not cover.

Write the model helper, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' '  return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the model helper, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    const data = await res.json();' '    const text = data.choices[0].message.content;' '    return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    return { text: sample, inTokens: null, outTokens: null, recorded: true };' '  }' '}' 'module.exports = { ask };' >> model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the rollback script, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const fs = require(\"fs\");' 'const path = require(\"path\");' 'const root = path.join(process.cwd(), \"snap-lab\");' 'function put(rel, text) {' '  const f = path.join(root, rel);' '  fs.mkdirSync(path.dirname(f), { recursive: true });' '  fs.writeFileSync(f, text);' '}' 'function names(dir) {' '  let out = [];' '  for (const n of fs.readdirSync(dir).sort()) {' '    const f = path.join(dir, n);' '    out = fs.statSync(f).isDirectory() ? out.concat(names(f)) : out.concat([f]);' '  }' '  return out;' '}' > rollback.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the rollback script, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function take() {' '  const s = {};' '  names(root).forEach(function (f) { s[path.relative(root, f)] = fs.readFileSync(f, \"utf8\"); });' '  return s;' '}' 'function diff(a, b) {' '  const d = { added: [], changed: [], removed: [] };' '  Object.keys(b).forEach(function (k) { if (a[k] === undefined) d.added.push(k); else if (a[k] < b[k] || a[k] > b[k]) d.changed.push(k); });' '  Object.keys(a).forEach(function (k) { if (b[k] === undefined) d.removed.push(k); });' '  return d;' '}' 'function restore(snap) {' '  const now = take();' '  Object.keys(now).forEach(function (k) { if (snap[k] === undefined) fs.rmSync(path.join(root, k)); });' '  Object.keys(snap).forEach(function (k) { put(k, snap[k]); });' '}' >> rollback.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the rollback script, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'fs.rmSync(root, { recursive: true, force: true });' 'put(\"src/app.js\", \"let x = 1;\");' 'put(\"src/util.js\", \"export {};\");' 'put(\"notes.txt\", \"todo\");' 'async function main() {' '  const snap = take();' '  console.log(\"snapshot: \" + Object.keys(snap).join(\" \"));' '  put(\"src/app.js\", \"let x = 2;\");' '  fs.rmSync(path.join(root, \"notes.txt\"));' '  put(\"new.txt\", \"hello\");' '  console.log(\"changes: \" + JSON.stringify(diff(snap, take())));' '  restore(snap);' '  console.log(\"same after restore: \" + (JSON.stringify(take()) === JSON.stringify(snap)));' '  const q = \"In one short sentence, why take a snapshot before an agent edits files?\";' '  const sample = \"If the edits go wrong you can put every file back exactly as it was.\";' '  const reply = await ask([{ role: \"user\", content: q }], sample, 60);' '  console.log(\"model said: \" + reply.text);' '}' 'main();' >> rollback.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the rollback

Command
docker exec -w /home/node/project agent node rollback.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

What the Run prints

LineWhat it shows
snapshot: notes.txt src/app.js src/util.jsThree real files in snap-lab
changes: {"added":["new.txt"],"changed":["src/app.js"],"removed":["notes.txt"]}What the stand-in agent did
same after restore: trueThe folder equals the snapshot again
model said: ...A sentence. The words differ each run

The cells students fill in are left blank in this preview.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.