Engineering guide

AI app builder iteration past the first generation, mapped to source

Every comparison post you read about AI app builders judges them on the first prompt. The first prompt is the easy part. The actual engineering problem starts on prompt two, and three, and ten. This guide walks through how iteration works inside mk0r, with the file paths and the exact line numbers, so you can decide whether the mechanism is one you trust.

M
Matthew Diakonov
9 min
Direct answer (verified 2026-04-29)

In mk0r, every user turn becomes a real git commit inside the sandbox. Undo, redo, and revert are git checkout operations that write new commits, not regenerations. The conversation lives on a single long-running ACP session, so the agent edits the existing project instead of rolling a fresh one every prompt. The mechanism lives in src/core/e2b.ts, function names commitTurn, undoTurn, redoTurn, jumpToSha.

The shape of the problem

Imagine you build a habit tracker on the first prompt, get something almost right, and then ask for a streak counter. There are three honest ways to handle that second prompt, and they look almost identical from the outside.

  1. Re-roll the project. Take both prompts as a list, throw away whatever was on disk, and ask the model to produce the whole app again. Cheap to implement. Bad to use, because the model never returns the exact same code twice and your third turn is landing on a different app than your second turn started with.
  2. In-memory edits only. Keep the project alive in a browser tab, ask the model to mutate it. Faster than re-rolling. Fragile, because the project state lives nowhere durable; a refresh, a tab pause, or a stale closure can desync you from what the model thinks exists.
  3. Real filesystem with version control. Keep the project on a real disk inside an isolated sandbox, write each turn as a commit, and treat undo as a checkout. More moving parts to build. The only one that gives iteration the same shape it has when a human edits a project.

mk0r picks the third option. The rest of this guide is what that choice looks like in code.

One persistent session, not a stateless re-prompt

Before any commit logic matters, the agent has to be the same agent across turns. In mk0r the session is created once during prewarm (you can read the prewarm flow in src/app/(landing)/page.tsx around line 64) and lives for the lifetime of the sandbox. Every subsequent prompt POSTs to session.acpUrl + /session/prompt with the same sessionId. The agent's conversation buffer, model state, and working directory all carry forward.

Turn 2 of an iteration

You/api/chatACP in sandboxSandbox /appPOST prompt + sessionKey/session/prompt (same sessionId)edit files (write_file, apply_patch)diff lands on diskstream tokens + tool callsgit add -A; git commit -m '<prompt>'version event { sha, message }

The point of the diagram: the agent and the disk are both stateful across turns. The route does not feed the model a transcript on each call, the ACP process holds it. The route does not write the file itself, the agent writes through the sandbox. The only thing the route adds at the end is the commit.

The commit happens at the end of every turn

When the chat stream finishes, commitTurn runs. It is in src/core/e2b.ts starting at line 1635. The body is small enough that the moving pieces are easy to see: build a shell script that runs git add -A then a guard for empty diffs then git commit -q -m with the prompt as the message, ship it through execInVm on a 45-second timeout, and parse the resulting SHA from stdout.

commitTurn shell script (paraphrased from src/core/e2b.ts:1643)

One important detail: when nothing changes on disk (a chat-only turn, or a question the agent answers without writing files), the script prints NOCHANGE and no commit is created. The history stack only grows on real diffs, which keeps the version sidebar honest.

After the commit, the same function appends the SHA to session.historyStack, bumps session.activeIndex to the new tail, and persists both fields to Firestore via persistSession. Those two fields are the entire iteration model. Everything else is computed from them.

Undo is not a re-roll

The button labelled "Undo" in the version sidebar does not ask the model anything. It calls POST /api/chat/undo, which calls undoTurn (src/core/e2b.ts line 1731). That function picks the SHA at historyStack[activeIndex - 1] and runs revertToSha, which is itself a tiny script: git checkout <sha> -- ., then git add -A, then a new --allow-empty commit named "Undo to <short-sha>". The previous commits stay in git log untouched.

What happens when you click 'Undo'

Tool drops the latest user prompt from the conversation history, re-feeds the older transcript, and asks the model to produce the project again. Output drifts because the model is non-deterministic. Files appear and disappear. The 'undo' is approximate, never exact.

  • Result depends on model sampling that turn
  • File structure can shift
  • No way to point at the exact bytes that existed before

The fork-on-undo rule (one line of code)

The detail nobody else writes about: what happens when you undo a few times, then submit a new prompt. The future you abandoned has to go somewhere. mk0r's answer is in commitTurn at line 1676 of src/core/e2b.ts:

if (
  typeof session.activeIndex === "number" &&
  session.activeIndex >= 0 &&
  session.activeIndex < session.historyStack.length - 1
) {
  session.historyStack = session.historyStack.slice(
    0,
    session.activeIndex + 1
  );
}
session.historyStack.push(sha);
session.activeIndex = session.historyStack.length - 1;

In English: if the active pointer is anywhere except the tail when a new commit lands, drop everything past it, then push. This is how most editors handle undo. It is not how most AI app builders handle it. A lot of them either keep linear history (undo is permanent until you redo) or keep both branches alive with no clear story for which is current. The slice is small but it is the difference between "I can try a different idea and not feel weird about it" and "I am scared to branch."

Five-turn session, with one fork

Jump to any version, not just adjacent ones

The sidebar in src/components/version-history.tsx fetches every commit from GET /api/chat/history?sessionKey=..., which under the hood is just git log --format='%H|||%s|||%an|||%aI' inside the sandbox. Click the Revert button on any non-active row and the client POSTs to /api/chat/revert with the target SHA. That route calls jumpToSha, which is the same revertToSha primitive plus a stack-pointer update. You get a new HEAD whose contents match an arbitrary historical SHA, with the prompt that produced that SHA still attached as the commit message.

Three endpoints, one primitive. Undo is revertToSha(stack[idx - 1]). Redo is revertToSha(stack[idx + 1]). Jump is revertToSha(arbitraryTargetSha). They are the same thing, parameterised differently.

What this gets wrong, honestly

Three real limitations worth naming, because no iteration model handles every case.

  • Anonymous sessions are single-browser. The session key lives in localStorage. If you clear it, the history is unreachable from the UI even though the underlying repo may still exist in the sandbox until the sandbox times out. Signing in attaches the key to your account so it survives device changes.
  • Undo does not roll back the agent's memory. The conversation transcript inside the ACP session keeps growing across undo and redo. So if you undo and then ask "fix it again", the agent may try a similar fix to the one you just rejected. A fresh prompt that explicitly redirects the approach works better.
  • A turn that fails to run cannot be partially committed. If the agent crashes mid-turn or the route timeout fires, no commit is created. You get whatever disk state was left at the moment of the failure, but the head SHA does not move. That is usually what you want, but it means sandboxes occasionally have uncommitted work that you can flush with one more prompt.

None of these break the model. They are the kind of tradeoff worth knowing about before you bet a weekend on a tool.

Want to look at iteration mechanics in your own AI builder?

Happy to walk through how mk0r's git history works against whatever stack you are evaluating. 30 minutes, no pitch.

Frequently asked questions

What does 'iteration past the first generation' actually mean?

It means everything that happens after you have a working app on screen and want to keep changing it. The first generation is the easy part. Most AI app builders look great at the first prompt because the system has a clean slate: empty repo, fresh context, one chance to be impressive. The hard part is the second prompt, and the third, and the tenth. Each one has to land on top of state the agent did not write in this turn, hold the rest of the app stable, and leave a trail you can walk back if it makes things worse. That second-prompt-onward behavior is what 'iteration' refers to.

How does iteration past the first generation work in mk0r?

Every user turn writes a real git commit inside the sandbox. The function is `commitTurn` in src/core/e2b.ts at line 1635, called from src/app/api/chat/route.ts at line 946 after the agent finishes streaming. The commit message is the first 120 characters of your prompt. The agent does not regenerate the project on each turn; it edits files in /app inside the sandbox, and the diff between turn N and turn N+1 is a normal `git diff`. Undo, redo, and revert are all `git checkout` operations that create new commits on top of the existing chain, not regenerations. Conversation context stays attached to one persistent ACP session for the lifetime of the sandbox, so the agent can reason about what it built earlier instead of starting fresh every prompt.

Why are commits used instead of just regenerating from a saved prompt list?

Because regeneration is lossy. If you saved 'a habit tracker, then add streaks, then make the streak page red' as a prompt list and re-ran it, the model would not produce the same code twice. Tiny phrasing differences cascade into different file structures, and the third turn would land on a sibling app, not the one you actually had. A git commit captures the bytes that exist on disk after each turn. Reverting walks the bytes backward, not the prompts. That is the difference between editing your project and rolling a new one each time.

What happens to history when I undo and then submit a new prompt?

The history forks. Line 1676 of src/core/e2b.ts says `session.historyStack = session.historyStack.slice(0, session.activeIndex + 1)`. In English: when a new commit lands while the active pointer is somewhere in the middle of the stack, everything past the pointer is dropped. The future you abandoned is gone, and the new turn becomes the head. This matches how editors handle undo, but a lot of AI builders do not implement it; they either keep linear history (so undo is permanent until you redo) or they keep both branches with no clear story for which is current. Forking is the small detail that lets you actually try a different idea after backing up.

Does the agent re-read the whole codebase on every turn?

No. The ACP session that the agent runs inside is long-lived. The session is created once on `session/new` during prewarm, and every prompt for the rest of the sandbox's life goes to the same session at /session/prompt with the same `sessionId`. The agent's conversation buffer, plus the working directory it has been editing, is what state looks like across turns. Re-reading the codebase happens only when the agent itself decides to grep or open a file. That decision is in the model's hands, not the framework's.

How is this different from how Lovable, Bolt, or v0 iterate?

From the outside they all look similar: you type a follow-up, the app changes. The differences show up in failure modes. Bolt's WebContainer keeps state inside the browser tab, so a refresh in the wrong moment can desync the project. v0 streams React to a renderer with no real filesystem, which is fine until you ask for something that needs more than one file. Lovable does keep a real version history with branching, which is the closest cousin to what mk0r does. The thing mk0r names that the others tend to keep vague is the mechanism itself: per-turn git commits, an explicit active-index pointer, fork-on-undo via a one-line slice, and a single long-lived ACP session, all written down by file path in this guide.

Can I jump back to any previous version, not just step-by-step undo?

Yes. The /api/chat/revert endpoint accepts a target SHA. It calls `jumpToSha`, which is just `revertToSha` plus a stack-pointer update. Internally `revertToSha` does `git checkout <sha> -- .`, then `git add -A`, then `git commit --allow-empty -m 'Revert to <sha>'`. Result: a new commit at the head of the stack whose contents match the target SHA. The original commits stay in `git log`. The sidebar UI in src/components/version-history.tsx lists every commit with its short SHA and the prompt that produced it, and clicking a non-active row revert-jumps you there.

What guarantees do I have if the sandbox dies mid-iteration?

A few. The history stack and active index are persisted to Firestore on every commit (see `persistSession` calls in commitTurn, undoTurn, redoTurn). The git repo lives inside the sandbox's filesystem and is restored when the sandbox resumes from pause. If the sandbox is killed and a new one is provisioned, the session migrates: the new sandbox is brought up to the head SHA of the saved stack. The honest limitation is that anonymous sessions are tied to a localStorage key on one browser; if you clear that key, the work is unreachable. Signing in attaches the same key to your account so it survives across devices.

How fast is a single iteration in practice?

Mostly model-bound. The persistent ACP session removes the boot cost on turns 2 onward, so the wall clock is dominated by how long Claude takes to write tokens. The git commit at the end runs `git add -A` and `git commit` in /app inside the sandbox, which on a small project finishes in a few hundred milliseconds. The whole turn closure (tool calls, commit, version event sent to the client) sits inside the route's 800-second budget that you can read in src/app/api/chat/route.ts at line 18.

mk0r.AI app builder
© 2026 mk0r. All rights reserved.