April-May 2026 at AppX: Forge stabilization, Edit Engine v2, and Bloom-parity chat

Two months of engineering recap. Forge containers stopped getting stuck, Edit Engine v2 made surgical edits the default (+25 pts edit success), Bloom-parity chat closed the visual gap with our closest competitor.

AppX team ·

April-May 2026 at AppX: Forge stabilization, Edit Engine v2, and Bloom-parity chat

April and May were the months AppX stopped feeling like a demo and started feeling like a product. Three large pieces moved at once: the container layer underneath the product got boring (the right kind of boring), the chat-edit pipeline got rebuilt from "regenerate the screen" to "surgically patch the file," and the chat surface itself got an overhaul aimed squarely at the closest competitor's UX. This is a recap of those two months — what shipped, why each piece mattered, and what is still open.

Story 1: Forge stabilization

Forge is the orchestrator that runs a per-user Expo container behind every live preview (why we built it). In April the design was right but the steady-state was wrong.

The symptoms were the usual ones for an early orchestrator. A container would occasionally get stuck in PROVISIONING because the backend was polling the agent for state instead of being told. The warm pool would drift away from its target — sometimes 36 sandboxes when the target was 3, sometimes 1 when the target was 5 — because lifecycle events were being missed and nobody noticed until a user clicked Generate against an empty pool. Cleanup ran on a slow timer, so orphan containers from failed provisions piled up between runs. Image pulls failed silently when the registry hiccuped.

Four fixes carried most of the weight.

Webhook-driven state. The node agent emits a state transition the moment Docker reports it. The control plane writes that transition to its database; the backend listens on a single channel and updates its own row. We deleted the polling code paths — verifyPoolRunning, getAppByName poll loops, the whole family. Sandbox transitions now show up in the backend in 1ms instead of seconds.

Ack-driven heartbeat reconciler. Every heartbeat from the agent carries the actual list of containers it sees on the host. The reconciler compares that to what the database thinks is running and dispatches stop_sandbox for anything claimed-but-not-attached. The day we shipped it, one host went from 36 zombie containers to 3 — the 33 orphans that had accumulated over weeks of missed lifecycle events evaporated in one pass.

Orphan sweeper. A short retention window on stopped sandboxes plus a cron that prunes dangling images nightly. Dull, necessary, took an afternoon.

Image-pull retry hardening. Exponential backoff on registry pulls with a circuit that resets on user retry instead of locking the user out for a minute. Manual retries now actually work.

The result is that the container layer has gone quiet. Pool sits at target. Lifecycle transitions are sub-millisecond at the boundary. The "stuck in provisioning" class of bug is effectively closed. The next two big chunks of work — multi-host scheduling and a proper preemption story for sleeping sandboxes — are unrelated to the bugs we just closed, which is the point.

Story 2: Edit Engine v2

The original chat shipped with one code path: every user message went through the generation pipeline that creates new screens. "Add a streak counter to the home screen" was treated identically to "make me a habit tracker." The model would re-emit the entire screen file with the requested change folded in. It worked, mostly. It was also wasteful and inconsistent — the model would helpfully refactor unrelated code, drop comments, rename a variable for "clarity," and you would discover the drift two edits later when your custom style was gone.

The honest framing is: most user messages are edits, not creates. Treating them as creates was the wrong primitive.

V2 splits the turn explicitly into two roles.

Architect (Pro model). Reads the project, plans the edit, emits a structured tool call describing which files to touch and what intent the user actually wanted. The architect does not write code — it writes a plan.

Editor (Flash model). Takes one file at a time, calls read_file, and emits SEARCH/REPLACE diffs against the just-read content. Three-tier matching: exact match first, whitespace-normalized fallback, line-range hint as the last resort.

Two invariants make this safe.

Read-before-edit. The editor cannot call apply_edit on a file it has not read this turn. The turn state tracks a content hash per file; an apply_edit against a stale hash is rejected before it reaches the matcher. This kills the entire class of "the model is editing against context from three messages ago" bugs.

One-shot recovery. If the SEARCH text does not match, the architect is invited to re-plan with the mismatch as context — once. No retry loop. Either the second plan applies cleanly or the turn fails and the user sees an honest error. We tried longer retry loops; they produced confident wrong edits more often than correct ones.

The numbers ended up roughly where we wanted them. Edit success rate moved up about 25 points versus the regenerate-the-screen path. Token cost per edit turn dropped about 60% — the Editor (Flash) is doing the per-file work that used to go through Pro, and only the files actually touched move through the loop. The Pro tier still owns planning, where it earns its rate.

What is still ahead: tool-call streaming so the user sees the plan being constructed instead of waiting for the architect to finish, and a proper undo-by-trace primitive so "undo my last edit" works regardless of how many files the architect touched.

Story 3: Bloom-parity chat

The closest competitor for AI-app-builder chat UX is Bloom. They are visibly further along than us on chat polish — distinct enough that early users would point at their UI and ask, reasonably, why ours did not feel like that.

In early May we did a deep reverse-engineering pass on Bloom's chat surface and inventoried the patterns: the per-turn engine activity card that expands while the agent is working and collapses to a single line when done, the ask-user card that grays out gracefully when the question expires, the post-mortem expandable that shows what tools the editor used after the turn, the plan summary as its own first-class element, the edit-trail dots that mark files touched.

We shipped most of the structural pieces.

Editorial Dusk redesign. A token-aligned color and type system across the canvas — amber, mist, sage, and coral on a midnight background, with a serif display face for plan summaries and a grotesque for body. The redesign was not just paint — it was a card-framing pass on every component in the chat. Net result was a small LOC delete after the visual upgrade, which is the right shape for a redesign that consolidates rather than accretes.

Per-turn EngineActivityCard. Every user message is paired with an engine activity bubble carrying the architect's plan and the editor's iterations. The card auto-expands while the trace is live and auto-collapses to a one-line summary on completion. The user can override the toggle and the override persists for the session. The card is anchored to a trace ID stamped onto the user message, so on chat history reload the right card resurfaces next to the right turn.

Single-row TurnSummary strip. After a turn completes we render one strip with the worked-for time, the tool counts, and the diff stats. No two-line wrap, no nested cards. The single-row constraint forced us to actually decide what was important to show.

The harder pieces are still open. Architect clarifying questions still echo the user's text in a way that reads awkwardly — we have a prompt rewrite in flight. The greenfield onboarding fast-path (the first chat turn after signup, where the user has no project yet) is still using the legacy flow and needs to thread through the V2 engine. The honest summary is: visually competitive, structurally still has gaps, and the gaps are scoped.

What we'd do differently

The pattern that bit us hardest in both Forge and Edit Engine was building a polling fallback "just in case" the event-driven path missed something. The polling fallbacks then masked real bugs in the event path. The lesson: pick one source of truth and treat its absence as a paging-worthy event, not a fallback trigger.

For the chat redesign, we underestimated the cost of state for "expanded vs collapsed" living in component-local memory. The right place for that toggle is the chat store, keyed by trace ID, so it persists across re-renders that recreate the component. We rewrote it twice.

Closing

April and May were not about new product surface. They were about taking the three pieces that already existed — the container layer, the edit loop, the chat UI — and rebuilding each one against the shape its real workload had revealed. The container layer needed event-driven lifecycle. The chat needed an edit primitive separate from the create primitive. The chat surface needed to stop apologizing for being less polished than the competitor.

For the deeper engineering writeups behind each of these, the AppX blog has the long-form posts — including why we built Forge in the first place, which is the right entry point if you are curious why the orchestrator exists at all.


Try your own app idea

Describe your app in AppX →