Undo for AI image edits: keep a version chain of every saved result

Generative edits have no undo button. Download each result, hash it, record its parent and prompt, and walk the chain back. Python, no database.

5 min readSume
All posts

To undo an AI image edit, you need the previous result saved as a file with a record of which prompt produced the next one. A generation API returns a new image each call; it does not keep a stack for you. A small version chain, with each saved file pointing at its parent, gives you undo, branch and compare, and fits in about thirty lines of Python.

Multi-step editing is the headline of FLUX 3 Image per BFL (read 2026-10-03), and multi-step editing is exactly where undo matters: step four goes wrong and you want step three back without re-running one through three.

What to store

Store the image bytes, not the link. Sume's docs describe data[].url as Sume-hosted and signed, so treat it as a delivery URL and keep your own copy. Alongside each file keep the parent version, the prompt and any model parameters. Add metadata on the request, which Sume stores on the job and does not send to the provider, so a job can be matched to a version from the Sume side too.

read 2026-10-03
FieldWhy
Content hash (ten characters)Stable id; the same bytes never duplicate
Parent hashGives undo and branching
Prompt and parametersLets you re-run or diff
File pathLocal copy of the bytes
Sume job metadataLink back to the job

The chain

save_version downloads the result, hashes it, writes it under that hash and appends an index entry with its parent. lineage walks the parents back to the first image and returns the path from root to the current version. To undo, you call it, take the second to last id and use that file as the next request's source.

import hashlib
import json
import pathlib
import requests

ROOT = pathlib.Path("versions")
ROOT.mkdir(exist_ok=True)
INDEX = ROOT / "index.json"

def load():
    return json.loads(INDEX.read_text()) if INDEX.exists() else {}

def save_version(parent_id, prompt, image_url):
    """Download the result now (hosted URLs are signed) and record who it came from."""
    data = requests.get(image_url, timeout=60).content
    vid = hashlib.sha256(data).hexdigest()[:10]
    path = ROOT / f"{vid}.png"
    path.write_bytes(data)
    idx = load()
    idx[vid] = {"parent": parent_id, "prompt": prompt, "file": str(path)}
    INDEX.write_text(json.dumps(idx, indent=2))
    return vid

def lineage(vid):
    idx, chain = load(), []
    while vid:
        chain.append(vid)
        vid = idx[vid]["parent"]
    return chain[::-1]

A typical session

You start with a hero shot, save it as version A and stamp the request metadata with its id. You ask for a warmer background, save the result as B with A as parent, then try a tighter crop to get C. The client prefers A's background with C's crop. You never lose A, because it is still on disk with its prompt, and you can start a branch D from A with the new instruction. Without the chain, that sequence would have been a hunt through a downloads folder.

Limits of this approach

The chain records files, not model state. Re-running a prompt from the same parent will not give the same image, since Sume's image API takes no seed on most rows, so a version is only reproducible as a file, never as a recipe. That is why you store the bytes. Hashing the bytes also means two identical results collapse into one version, which is usually what you want. If you want a record of a rejected result too, save it and mark it in the index rather than deleting it.

Branching and comparing

Because versions point to parents, two edits of the same parent are siblings. That makes A/B comparison natural: run two prompts from one parent and show both. It also catches drift: line up the whole lineage and see where the subject began to change. For that problem, repeat the preserve list in every turn.

Keep an eye on disk use. Ten-character hashes are safe for a single project, but PNG files are large; prune dead branches once the work is signed off. Multi-round editing practice is covered in the multi-round edits post, and a lighter lineage idea built on job metadata is in Midjourney-style edit history. For the job fields, see the jobs and results docs.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume