Field guide

Vibe coding state limits, expressed as one number and one stack

When people on X say vibe coding has a state problem, they usually mean three different things at once: tab refresh blanks the project, the sandbox times out and the work is gone, undo gives you something close to but not equal to the previous version. On mk0r the whole thing collapses to one constant in source and one ordered array of git SHAs in Firestore. This page traces what that actually looks like, in real file paths, across the moments where state usually breaks for everyone else.

M
Matthew Diakonov
7 min
Direct answer (verified 2026-05-01)

The state limit on mk0r is one number: E2B_TIMEOUT_MS = 3_600_000 (1 hour idle), declared on line 33 of src/core/e2b.ts. When the sandbox is idle longer than that it auto-pauses on the E2B side. Below that line, state is effectively unbounded: every successful turn pushes a new git SHA onto a historyStack: string[] persisted to a Firestore collection called app_sessions, and on wake the disk is restored byte-exact through git checkout sha -- .. There is no turn cap, no transcript-replay step, no probabilistic recreation. Source for every claim on this page is linked below.

The number, and what it covers

The honest move when somebody asks about a tool’s state limit is to give them the literal constant. Most pages on this topic punt and say something like “state can be tricky in AI builders.” Here is the constant and what it actually buys you.

0E2B_TIMEOUT_MS (ms)
0Idle ceiling (minutes)
0Constants you have to know
0Turn cap on history

Some context for those numbers. The 1-hour ceiling is set on the sandbox itself: when the SDK calls sandbox.setTimeout(E2B_TIMEOUT_MS) (line 337 of src/core/e2b.ts), E2B starts a clock; if no work happens for an hour, the sandbox pauses. The pause is recoverable. The clock is reset on every activity, including a reconnect after wake.

The other three numbers are the absence of additional limits. There is no second constant for “maximum turns to keep,” no LRU policy on historyStack, no “summarize older turns” step. You can read the type at line 100 to confirm: it is just an array of strings.

What lives where

To say where state breaks, you need a clear map of where state lives. On mk0r there are exactly three places that hold pieces of your project, and each one has a different lifetime.

The three places state lives, ranked by durability

  • On the E2B sandbox disk at /app: every file the agent has written, plus a real .git directory. Lifetime: until the sandbox is reaped (after pause + extended idle). Survives one tab close. Restored byte-exact on reconnect.
  • In Firestore, collection app_sessions, doc keyed by sessionKey: vmId, sandboxId, historyStack, activeIndex, plus provisioned-service metadata. Lifetime: forever, or until you delete it. Survives sandbox death and Cloud Run restart.
  • In the browser localStorage as mk0r_session_key: just the key. Lifetime: until you clear storage. Sign in to bind the key to a Firebase uid and the project moves across devices.

The interesting consequence of that map: even when the sandbox is gone, the historyStack is not. If E2B reaps your VM (rare, only after extended pause), the next session can boot fresh and the history of what you built is still readable in Firestore. The disk is gone. The list of SHAs is not. Practical implication: if you were going to ship something, hit publish before you walk away for the night.

One turn, end to end, with the actual shell script

The whole “state on each turn” mechanism is one bash script and one Firestore write. Reading the actual commands that run inside the VM at the end of every turn is the fastest way to see why state is durable here. The script below is a copy of what commitTurn (src/core/e2b.ts:1635) executes:

commitTurn() inside the VM at /app

Two things matter in that sequence. First, git add -A captures the entire working tree at once, so multi-file edits land atomically. There is no per-file checkpoint that can drift out of sync with the rest. Second, the SHA returned by git rev-parse HEAD becomes the only thing the server needs to remember about that turn. The transcript is not what holds state. The disk is.

Undo across many turns

If turns are commits, undo is checkout. The function at line 1731 of src/core/e2b.ts reads historyStack[activeIndex - 1], passes that SHA into revertToSha, and runs the same five-line script that puts you back to that turn:

undoTurn() inside the VM at /app

Two reasons this matters more than it sounds. The first is that the model is not consulted. Most builders implement undo as a re-prompt: drop the last user message from the transcript, replay the older prompts, regenerate. The model is non-deterministic so the “undone” project is similar but not equal. mk0r’s undo never asks the model anything; it asks git. Zero token cost, zero variance.

The second is that the new SHA written by undo is appended to the stack, not substituted. So you can also redo. And when you change your mind and start typing in a different direction, line 1676 in the same file slices everything past the active pointer off the stack before pushing the new turn. That single line is the difference between a trustworthy iteration loop and one that starts feeling weird around turn fifteen because abandoned futures keep coming back.

Idle, pause, wake: what a one-hour gap actually does

You leave the tab open while you go to lunch. The sandbox sees no activity for 60 minutes and pauses. You come back, hit refresh, type a new prompt. Here is the trace.

  1. Refresh fires. The client reads mk0r_session_key from localStorage and posts it as part of the next request.
  2. ensureSessionLoaded(sessionKey) runs (line 1451). The in-memory map has been wiped (Cloud Run cycles workers), so the function falls through to a Firestore read of app_sessions/<sessionKey>.
  3. reconnectSandbox(sandboxId) calls Sandbox.connect, which resumes the paused VM. The disk that existed before the pause is the disk now. The dev server may need a kick; startup.sh respawns the things that died on sleep (line 387).
  4. sandbox.setTimeout(E2B_TIMEOUT_MS) resets the idle clock (line 337). You have another full hour.
  5. historyStack rehydrates. The Firestore doc carries the same array of SHAs and the same activeIndex. The next prompt lands on the same persistent ACP session and the next commit pushes onto the existing stack.

What does the user see? A short pause on first prompt after lunch while the sandbox wakes up, then nothing else. Files are where they were. Undo still walks back through everything you did this morning. The 1-hour ceiling is real, but it is not where state dies; it is where state pauses and resumes.

Where state actually does have a hard edge

Honesty matters more than marketing here. There are two real edges on this state model and you should know both before you build anything you cannot afford to lose.

The two real state edges

  • External services are not in the historyStack. Postgres rows in Neon, emails sent through Resend, commits to the per-app GitHub repo: those are real side effects on third-party systems. git checkout cannot roll them back. If you undo across a turn that wrote rows, the rows stay; if your schema diverges, you may need a fresh provision or a manual cleanup.
  • If the E2B sandbox is reaped (long pause, rare on active sessions), the disk is gone. The historyStack of SHAs is still in Firestore, but the actual git objects lived on the VM. Practical move: hit publish whenever you reach a state worth keeping; that flow archives the project off the sandbox.

The mk0r sandbox state is byte-exact undoable. External-service state is not. For a prototype this is almost always the right tradeoff: the parts of the project that you want to mash on (layout, components, copy, logic) are reversible, and the parts that touch the world (rows, emails, repo commits) you usually want to keep anyway. For a production app where you do need rollback of side effects, that is the moment to bring in a developer or write code yourself; no vibe coding tool replaces a real migration story.

Frequently asked questions

What is the actual numerical state limit on mk0r?

One constant. E2B_TIMEOUT_MS = 3,600,000 milliseconds, declared on line 33 of src/core/e2b.ts. That is the idle ceiling on the sandbox where your project lives. After one hour of no activity the sandbox pauses on the E2B side. Below that ceiling, no turn cap, no history cap, no transcript truncation. The historyStack is just a string array of git SHAs in Firestore; it grows for the lifetime of the session.

What happens to my work when the one-hour idle timer fires?

Nothing visible to you, until you come back. The sandbox pauses on the E2B side, but the project itself is already a real filesystem with real git commits, and the latest state is mirrored into Firestore by persistSession (src/core/e2b.ts:234). When you reload, ensureSessionLoaded at line 1451 reads the doc from app_sessions, calls Sandbox.connect, resets the idle timer with setTimeout(E2B_TIMEOUT_MS) (line 337), and the disk is the same disk. The historyStack is the same array. Your work is intact.

Is there a turn cap or a token cap that bites before the timeout does?

No turn cap. The historyStack is just an array (line 100 in src/core/e2b.ts). Every successful turn calls commitTurn (line 1635), which appends a SHA. Look at the code: if the active pointer is at the tail, push; if not, slice the future off and push. There is no max-length check, no LRU eviction, no compression of older turns. Token caps are a model-side concern handled inside the persistent ACP session, not the project state.

What does undo actually do across many turns?

It runs git checkout sha -- . inside the VM. The undoTurn function at src/core/e2b.ts:1731 reads historyStack[activeIndex - 1], passes that SHA into revertToSha (line 1691), and restores every file in /app to that snapshot byte-exact. A new commit is then written with --allow-empty so the history stays linear. The model is not consulted, the transcript is not replayed, and the disk is identical to the disk that existed at that turn. You can do this an arbitrary number of times within a session.

If I undo five turns and try a different idea, does the abandoned future haunt me?

No. Line 1676 in src/core/e2b.ts has a one-line slice. Before pushing a new SHA, if the active pointer is anywhere except the tail, drop everything past it. This is exactly how a text editor handles undo: take a different path, the abandoned future is gone. Most AI builders keep both branches alive with no clear current pointer, which is why iteration starts feeling weird around turn fifteen. mk0r forks honestly.

What about state that lives outside the project files: Postgres rows, sent emails, GitHub commits to the user repo?

Those services are provisioned per session in service-provisioning.ts and live on real third-party infrastructure (Neon for Postgres, Resend for email, GitHub for the user-facing repo). They are not rolled back when undoTurn fires, because git checkout only touches /app. If you undo across a turn that wrote rows, the rows stay, and the schema your app expects might not match again. That is the honest seam: sandbox state is byte-exact undoable, external service state is not. For a prototype this is almost always the right tradeoff.

What survives a refresh, a closed tab, a closed laptop, and a phone backgrounding?

Everything. The session key lives in localStorage as mk0r_session_key. On reload, the client posts that key to the server, the server calls ensureSessionLoaded, which either returns the in-memory ActiveSession or reads the Firestore doc and reconnects the paused sandbox. If the sandbox is genuinely gone (E2B reaped it), the doc is deleted and the next prompt provisions a fresh sandbox. The historyStack is still in Firestore though; what gets lost in that case is the running dev server state, not the committed code.

Can I move state to a new device or a new browser?

Yes, after you sign in. Anonymous Firebase uids are bound to a single browser. When you sign in with Google on another device, the migrate path (POST /api/auth/migrate) calls migrateSessionOwnership, which opens a Firestore batch and rewrites every app_sessions and projects doc from the old uid to the new one. The historyStack moves with the doc. Sign in once, your prototype is on every device.

What is the limit that does not move, even on mk0r?

The envelope, not the state. mk0r generates working HTML, CSS, and JS or a Vite plus React plus Tailwind project. The state mechanism is solid inside that envelope. The honest exit point is when the requirement outgrows the envelope: native iOS or Android, multi-user real-time state with strong consistency, payments with serious data invariants, complex backend logic that generated TypeScript cannot cover. None of that is a state limit; it is a product limit. When that is what is breaking, the move is to bring in a developer or write code yourself.

Want to see the historyStack in your own session?

20 minutes, screen sharing on. We will open the Firestore doc together and walk through commitTurn and undoTurn live.

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