Antigravity Multi-Agent State Tiers — A Three-Layer Design with Ephemeral, Journal, and Canonical
Before your Antigravity Background Agents and Sub-agents start mixing up their memory, split agent state into three lifetimes — ephemeral, journal, canonical — and map each to the right Cloudflare store. Includes a TypeScript approval gate for write-back, and what changed when headless runs stopped silently auto-approving.
One morning, an Antigravity Background Agent forgot the previous day's AdMob review and emailed me the same warning three times in a row. Following the run logs, the memory was technically present, but the restore key was off by a few characters and the agent could not read it back. I build iOS and Android apps solo, and I run most of the surrounding operations through editor-integrated AI agents like Antigravity. After a few rounds of "the agent forgot too much" and "the agent remembered too much," I landed on a single rule: agent state has to be split into three lifetimes, or it will eventually break.
This piece is a practical write-up of those three lifetimes — ephemeral, journal, canonical — together with how I map them onto Cloudflare Workers stores (KV, Durable Objects, R2, D1), and how I gate the write-back boundary with an explicit approval step. If you are running more than one Antigravity agent in production, the boundaries below should give you a frame to sharpen your own design decisions.
The same Antigravity agent holds information with three very different lifespans
When you call everything "agent memory," it eventually breaks. The split I have settled on is:
Tier
Lifetime
Damage if lost
Examples
ephemeral
1 run
near zero (recomputable)
intermediate prompts, raw LLM output, file snapshots
published slugs, purchase records, membership state
The deciding factor is not byte size but blast radius. A 200KB chain-of-thought can be ephemeral. A 60-byte "we already published this slug" record absolutely cannot be ephemeral.
When you internalize the split, agent code only needs to ask three questions:
Can the next run recompute this? If yes — ephemeral.
If not, can a human accept "we have to redo it"? If yes — journal.
If losing it breaks the user-facing product — canonical.
That is the entire rubric. The tiers map to different physical stores, but agent code talks to all three through a single adapter shape.
Ephemeral — working memory that lives and dies inside one agent run
Ephemeral is everything that is born when an agent run starts and dies when it ends: scratch prompts, raw LLM responses, intermediate file listings, retrieval results, in-flight reasoning. Because the next run can rebuild it cheaply, there is almost no value in writing it to a shared store like KV.
In my Antigravity Background Agents this tier lives in plain JavaScript memory — a Map scoped to the run entry function.
// state/ephemeral.tsexport class EphemeralStore { private bucket = new Map<string, unknown>(); get<T = unknown>(key: string): T | undefined { return this.bucket.get(key) as T | undefined; } put<T = unknown>(key: string, value: T): void { this.bucket.set(key, value); } // Always call this at the end of a run. Disposal is explicit. dispose(): void { this.bucket.clear(); }}
The explicit dispose() is intentional. I want ephemeral to be actively thrown away, not silently abandoned. If you let an intermediate LLM response leak into the journal or canonical tier by accident, your storage bill rises without bound. A single Background Agent in my fleet handles roughly 200–800 KB of ephemeral data per run; written naively into KV that adds up to gigabytes per month, far above the ¥800/month budget I keep per site.
One implementation gotcha worth flagging: Cloudflare Workers can reuse the same Isolate across requests, so a Map declared at the module scope will silently persist between runs. I fell into that exact trap once. The fix was to make sure EphemeralStore is always new-ed inside the run entry function. Before pushing this pattern into production, write a test that deliberately fires two runs against the same Isolate to confirm the store is fresh each time.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Split agent state into ephemeral / journal / canonical so a single Background Agent run can fail without taking your product down
✦Concrete Cloudflare mapping — KV / Durable Objects / R2 / D1 — with a small TypeScript adapter you can drop into a Worker today
✦A minimal TypeScript approval gate that holds canonical writes for review, plus where to skip approval entirely
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Journal — days to weeks of decision logs that you could re-derive, but slowly
Journal is for information that you can technically reconstruct, but only at meaningful cost in time or money. Agent decision rationales, A/B intermediate scores, Browser Sub-agent observations of an AdMob dashboard delta, automatic-repair logs — all journal.
The test for journal is one question: "If we lost this, could we live without it but only at a half-day cost?" I have had multiple incidents where missing journal data meant burning a day or two replaying agent decisions. The painful variant is when the output survived but the reasoning did not — you can see what the agent produced, yet you cannot tell why it branched that way, so you walk into the same mistake again. I take that tier seriously now.
Cloudflare KV is a near-perfect physical match — cheap reads, metered writes, eventual consistency, all of which suit "write rarely, read sometimes, freshness is loose."
Two operational notes. First, always prefix journal keys with journal: even if you do not share KV with canonical today — your future self will thank you when the boundary moves. Second, always attach a TTL. Without one, agents start dragging months of context into their next decision and begin to behave erratically. I have made that mistake twice across this fleet and now hard-code 30 days for everything.
At a small-scale operation (~100k writes / ~1M reads per month), the journal tier on KV bills around ¥50 / month, easily inside the ¥800/month per-site envelope.
Canonical — the source of truth your product cannot survive without
Canonical is the information whose loss ends the product. Published article slugs, Stripe purchases, premium membership records, monthly AdMob revenue rollups (technically refetchable, but throttled by API limits) — canonical.
My personal rule is strict: the agent never owns canonical. Canonical lives in the surrounding system, and the agent only reads or writes back to it. The moment an agent thinks it owns canonical, an agent bug becomes a product-wide outage.
For physical mapping I lean on two choices depending on the shape of the data.
Shape
Store
Why
Row-oriented, transactional
Cloudflare D1 (or an upstream API like Stripe)
strong consistency + SQL for audits
Document-oriented, concurrent writes
Cloudflare Durable Objects
strong ordering, one instance per key
Large, read-heavy
Cloudflare R2
monthly snapshot archive
A concrete case: in Antigravity Lab, the canonical list of published slugs is the GitHub repo itself. The agent clones it, reads it, and writes back only via the GitHub API. The agent's KV and DO are journal-tier; once the change lands in Git, they can be thrown away.
Here is a minimal Durable Object adapter for the case of "concurrent purchase grants" — the kind of canonical write where two requests can hit the same key at the same time.
Keeping canonical outside the agent buys you something priceless: the freedom to throw the agent away and rebuild. An agent that owns canonical is one you are scared to redeploy. Working solo, I rebuild agents constantly, so that freedom is a structural asset rather than a nicety.
Mapping three tiers onto Cloudflare stores — KV / DO / R2 / D1
I decide the mapping with three axes:
Lifetime: 1 run / dozens of days / permanent
Concurrent writes: can multiple agents hit the same key at the same time?
Query shape: single-key read, prefix scan, or SQL/joins?
That collapses into a near-mechanical decision tree.
1 run, no concurrency, single-key reads → ephemeral (in-memory Map).
Early on I dumped everything into KV and accidentally crossed agent boundaries five or six times due to missing prefixes. Switching to the three-axis decision rule effectively eliminated state-tier bugs at design time. I recommend forcing yourself through these three questions every time you add a new state key.
Boundary design — where to write back, where to snapshot
Splitting the tiers is not enough; you also have to decide where information crosses between tiers. In my fleet there are three boundaries worth naming.
Step 1. ephemeral → journal (write-down)
Only the decisions worth replaying should land in journal — typically the moments an agent branched on a non-trivial choice. Writing every intermediate value bloats the journal; I keep the rule "log a journal entry only when the agent made a branching decision."
Step 2. journal → canonical (write-back)
This is the boundary you must design most carefully. I strongly recommend a human-in-the-loop checkpoint here, or at minimum an out-of-band notification (Slack, stand.fm, email). I have had an agent decide on its own to flip 12 premium articles back to free in a single batch, and the recovery took half a day. A 5-second confirmation delay or a single approval click would have caught it.
Step 3. canonical → journal (read-back snapshot)
The reverse flow is just as important. Periodically snapshotting canonical into journal keeps the agent from spamming canonical reads (and helps you stay under GitHub's 5,000 req/hour ceiling). An hourly snapshot is usually enough.
Once you spell these three boundaries out in code, the agent reads dramatically more clearly. Leave them implicit, and you will not be able to tell whether a given function is touching ephemeral or canonical without a 10-minute audit.
Implementing the approval gate — the smallest code that stops a write-back
"Put a human in the loop" is easy to say and easy to lose in implementation. The smallest version I use carries the tier in the type signature and drops canonical writes into a pending queue instead of applying them.
// state-gate.ts — one gate, three tiers, different write semanticstype Tier = "ephemeral" | "journal" | "canonical";interface PendingWrite { id: string; key: string; value: unknown; reason: string; // why the agent wanted to write createdAt: number;}interface GateDeps { journal: KVNamespace; canonical: DurableObjectStub; notify: (w: PendingWrite) => Promise<void>;}const JOURNAL_TTL = 60 * 60 * 24 * 30; // 30 daysexport async function write( tier: Tier, key: string, value: unknown, reason: string, deps: GateDeps,): Promise<{ applied: boolean; pendingId?: string }> { if (tier === "ephemeral") { // caller keeps it in a Map; nothing is persisted return { applied: true }; } if (tier === "journal") { await deps.journal.put(key, JSON.stringify({ value, reason }), { expirationTtl: JOURNAL_TTL, }); return { applied: true }; } // canonical never applies inline const pending: PendingWrite = { id: crypto.randomUUID(), key, value, reason, createdAt: Date.now(), }; await deps.journal.put(`pending:${pending.id}`, JSON.stringify(pending), { expirationTtl: 60 * 60 * 24 * 3, // expires in 3 days if nobody approves }); await deps.notify(pending); return { applied: false, pendingId: pending.id };}export async function approve(id: string, deps: GateDeps): Promise<boolean> { const raw = await deps.journal.get(`pending:${id}`); if (!raw) return false; // expired — a deliberate rejection, not a dropped write const p = JSON.parse(raw) as PendingWrite; await deps.canonical.fetch("https://do/put", { method: "POST", body: JSON.stringify({ key: p.key, value: p.value }), }); await deps.journal.delete(`pending:${id}`); return true;}
The return type of write() is the part that matters. Because there is a path that returns applied: false, the calling agent cannot proceed as if the write landed. Make the function return void instead, and the agent has no way to distinguish "pending" from "applied" — it will happily treat a stale value as canonical.
The three-day expiry on pending: is also deliberate. Let approvals pile up indefinitely and you end up approving a change a month later with no memory of what it was for. A forgotten change is safer left unapplied.
Tier
Write
Approval
Expiry
ephemeral
immediate (memory)
none
end of run
journal
immediate (KV)
none
30 days
canonical
held
required
3 days pending
Headless runs used to auto-approve silently — a premise that changed
An approval gate is worthless if the execution layer approves on your behalf. This is a case where the ground actually shifted, so it is worth recording.
Antigravity CLI 1.1.3 (2026-07-16) shipped two fixes around headless execution (-p). The first: tools requiring confirmation would either hang the process or get silently auto-approved. After the fix, they soft-reject and print the allow rule name needed for permission to stderr. The second: in always-proceed mode, file writes outside the workspace were being auto-approved by mistake.
If you run tasks unattended overnight, both fixes change your assumptions. On anything older than 1.1.3, a flow you believed was gated may have been passing straight through.
Here is how I verified mine:
Check the CLI version with antigravity --version
If it is below 1.1.3, update. If you cannot, stop canonical writes from headless runs until you can
After updating, run one operation that should require approval under -p and watch for the allow rule name on stderr
If nothing appears, that tool is already permitted by an allow rule — go audit the rule list
Step three is the one people skip. Without confirming that a soft rejection actually happens, you are running on the assumption that "the fix landed, so we must be fine." That is where I discovered my own allow rules were written far more broadly than I intended.
One related thing worth checking: a token accounting bug that made conversations hit their limit earlier than expected was fixed in the 2026-07-21 update. If you orchestrate long agent runs, re-measure your consumption afterward. Otherwise it is easy to confuse "stopped because of the limit" with "held by the approval gate."
Three operational rules I refuse to break
A few rules I hold to, drawn from keeping agents running day after day.
Always put either a 1-second delay or an approval step in front of canonical writes. Recovering from a runaway canonical write averages half a day, sometimes two. A 5-second confirmation buffer is free. My AdMob revenue rollup agent uses that exact pattern.
When in doubt, give journal a 30-day TTL. Extending later is trivial; capping an untimed journal after the fact is the storage equivalent of memory loss. Every journal scope in my Background Agents is locked to 30 days.
Never silently drop ephemeral. Provide an explicit dispose() and call it. That single rule wiped out my Isolate-reuse and memory-leak issues. After one painful incident where a Browser Sub-agent ballooned in memory for three days and crashed, I refuse to ship an ephemeral store without it.
All three are about pre-committing to where agent failures are allowed to live — and that is the underlying claim of this whole piece. Agents will misbehave; the design job is to keep their failures inside the smallest tier possible.
The next time you stand up an Antigravity Background Agent, write a one-line comment at the top: "this agent touches ephemeral / journal / canonical." Half of your design review is already done. I would love to hear how this maps onto your own fleet. Thanks for reading.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.