Skip to content

Revisions, diffs, and undo

An assistant changes a working grocery list. The check reports three issues, so it makes two more edits. The provider then fails before the assistant can explain what happened. On the next turn, the app has changed, the transcript may be compacted, and nobody knows which edit caused the regression.

Revision history fixes this without turning the product into a full source-control host. Save the file set after each meaningful draft change. Expose history, diff, and undo as narrow app operations. Keep enough snapshots for recovery, then prune them.

The file operations in The file tree is an API define where revisions change. History adds memory across turns.

Concept map

text
edit -> snapshot r18 -> diff r17..r18 -> check
                    \-> undo to r17 -> snapshot r19

Undo should create a new revision. It should not erase the record that the bad edit happened.

Terms

Draft revision is a monotonically increasing number for one app's editable state.

Snapshot is the complete normalized file set at one draft revision.

Revision span is the pair of revisions used for a diff.

Previous-turn span starts at the revision visible before the prior assistant turn and ends at its final revision.

Published revision identifies the draft state copied to the live deployment.

Undo target is the snapshot whose files should become the new draft.

Retention window is the bounded number or age of snapshots kept.

Save complete snapshots

For small generated apps, complete snapshots are usually the right trade. They simplify reads, diffs, undo, pruning, and crash recovery. A row might contain:

sql
create table app_revisions (
  app_id text not null,
  revision integer not null,
  files json not null,
  created_at bigint not null,
  primary key (app_id, revision)
);

Delta storage looks efficient, but it complicates every recovery path. Reconstructing revision 31 may require applying a chain of patches, handling a missing base, and validating path operations. Generated mini apps are commonly measured in kilobytes, so a bounded set of full snapshots is cheaper in engineering time.

Insert the snapshot in the same transaction that updates the current draft. The current revision and its snapshot must agree. If they can diverge, every later guard and diff becomes suspect.

Do not snapshot no-op writes. Normalize paths and encodings first, compare bytes, and return the existing revision if nothing changed.

Make the default diff answer the current question

An unrestricted diff API is useful, but the best default is "what did the previous turn change?" That is usually what the next assistant needs.

Conversation turns should record the revision visible at their start. At the next turn, the system can calculate the previous span and add a one-line summary to context:

text
Previous turn changed r24..r27:
- app.js: +18 -7
- styles.css: +4 -1

The agent can call a diff operation for details. It should not need to infer the range from deployment timestamps or chat messages.

A generic response can include a standard unified diff because the output is for reading, not mutation:

diff
--- app.js@24
+++ app.js@27
@@
-items.sort(byCreatedAt);
+items.sort(byCompletedThenCreatedAt);

Cap diff size. For a large rewrite, return per-file counts and selected hunks, then let the agent request a file or range. Dumping thousands of changed lines recreates the whole-file context problem.

History is an inspection tool

History should return revision numbers, times, file counts, byte totals, and any known relationship to publish or check events. It should not return every snapshot body.

Useful questions include:

  • Which revision is live?
  • Did the last turn edit the draft after publishing?
  • Which revision passed the last check?
  • How many revisions did one turn produce?
  • Is the requested undo target still retained?

If the system records a compact reason, use facts rather than model-written prose. "app_edit styles.css" is reliable. "Improved polish and responsiveness" is not.

Revision history also helps measure tool behavior. Ten revisions during a one-label change suggests brittle edits or needless polish. A high ratio of revisions to shipped apps is a practical signal that the workflow needs better context or better tools.

Undo is a copy, not time travel

undo(\{to: 17\}) should load revision 17, write those files as a new current state, and produce revision 23. This preserves audit history and avoids reusing revision numbers.

Undo must pass the same file normalization and reserved-file rules as any other write. Platform-generated files may need regeneration after the copy. If generated output depends on launch settings or design versions, the preparation stage should rebuild it before the next check.

The default target can be the previous revision, but accepting an explicit to value is more useful. "Undo the last turn" may span several edits. The context already knows that turn's starting revision, so the agent can choose it directly.

Do not make undo publish automatically. Restoring a draft and changing the live site are separate user-visible actions. The next ship should check and publish the restored state.

Decisions and rejected designs

Keep full snapshots, with a cap. Patch chains save storage but raise recovery complexity. For small apps, retain perhaps the newest 20 to 50 snapshots and prune older rows.

Use application revisions, not deployment history. Many revisions never publish, and a failed turn may contain the exact state that needs inspection. Hosting providers only know deployed snapshots.

Generate diffs on demand. Storing every pairwise diff wastes space. A dependency-free line diff is enough for small text files.

Undo to any retained revision. A single "restore published" command cannot recover a useful unpublished state. It also makes the live version the only safe checkpoint.

Do not expose database history as model memory. Return bounded, structured results. The model should ask for specific detail.

Prune by app, not globally. One noisy app should not evict the only recovery point for another.

Failure modes

Snapshot and draft update are not atomic. A crash leaves the same revision number with different bytes. Use one transaction.

Undo mutates an old row. History stops being history. Always create a new revision.

Generated files are restored stale. Rebuild them from current configuration before checking.

A huge diff floods context. Cap lines and return a summary with follow-up ranges.

Published revision is inferred from current files. Store it explicitly when publication succeeds.

Retention deletes the only published snapshot. Keep published files separately or exempt the active published revision.

A retried edit creates duplicate revisions. Use operation idempotency and byte equality.

The model repeatedly reads unchanged history. Within one run, return an unchanged marker or point to the earlier result.

Field checklist

  • Does every successful byte change create one new revision?
  • Are snapshots and the current draft updated atomically?
  • Are no-op writes revision-neutral?
  • Can the system identify the previous assistant turn's revision span?
  • Does diff default to that span?
  • Are large diffs summarized and bounded?
  • Does history identify checked and published revisions?
  • Does undo copy an old snapshot into a new revision?
  • Are generated files rebuilt after restore?
  • Is retention per app and large enough for multi-edit turns?
  • Can provider retries return the existing revision?
  • Are revision counts part of workflow metrics?

History makes experimentation cheap because a bad edit is recoverable. The next step is to make the normal success path cheap too. One call to ship explains why one high-level ship operation belongs above, not instead of, the lower-level tools.

Built from field notes on durable software systems.