Supervising Long-Running Antigravity Agents — Watchdog and Tiered Recovery
Eight weeks of running AdMob revenue optimization on Antigravity background agents revealed three quiet failure modes. Here is the watchdog plus tiered recovery design I landed on.
I am Masaki Hirokawa (@dolice), an indie developer since 2014, running a portfolio of wallpaper and mindfulness apps with around 50 million cumulative downloads. For the past eight weeks I handed off AdMob eCPM tuning and Crashlytics triage to Antigravity background agents. After the first three weeks I learned an uncomfortable thing: agents keep running while completely broken. The process is alive, the terminal stays quiet, but nothing inside is actually moving.
Below are the three stall modes I observed across that period, and the watchdog plus heartbeat plus tiered recovery design I eventually settled on. Notes for fellow indie developers running agents overnight.
Three observed stall modes
When a long-running agent goes quiet, the failure mode almost always falls into one of three buckets. Across eight weeks I logged 142 abnormal terminations with the following breakdown.
Unresponsive (41%) — An LLM call goes out and never returns, but the SDK never raises a timeout or retry either. Process is alive, CPU sits at 0%, everything looks fine.
Session-dead (34%) — The Antigravity workspace session has expired internally. New tool calls fail silently. The agent keeps planning the "next step" as if nothing was wrong.
Context-bloat (25%) — Input context exceeds limits, the agent enters elision mode, can no longer recall its earlier decisions, and loops over the same step.
None of these is detectable from outside. The process is alive, memory and CPU look calm. You have to look at internal logical state. This is the biggest trap I fell into as someone who used to rely on process-level monitoring.
The agent itself only writes its own progress to heartbeat.json. All judgment lives in the supervisor. The premise is that an agent in trouble cannot be relied on to rescue itself.
Why an external supervisor instead of self-monitoring
I tried the pattern of "if you find yourself stuck, please recover" inside the agent itself first. It failed on two of the three stall modes. The unresponsive mode never reaches the recovery code path, and the context-bloat mode forgets the very fact that it is stuck. An independent supervisor process is non-negotiable.
✦
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
✦Three stall modes observed across 8 weeks (unresponsive / session-dead / context-bloat) and how to detect each
✦Full TypeScript implementation of watchdog and heartbeat protocol in about 90 lines
✦Tiered recovery (soft retry / context reset / agent rotation) with the measured 87% auto-recovery ratio across 6 parallel sites
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.
A simple "I was alive N seconds ago" heartbeat turned out to be insufficient. The unresponsive mode freezes right before an LLM call, so the timestamp at that exact moment still gets updated. You need both stage and progress (a monotonically increasing counter) to tell them apart.
Two points to highlight. First, step_idx is a monotonically increasing counter, and that is the field you check for progress. The agent can sit in the same stage for 60 seconds and still be healthy as long as step_idx is climbing. If it sits still, you start suspecting the unresponsive mode. Second, the write must be atomic via tmp then rename. I once had the supervisor read a half-written JSON and trigger a recovery on a phantom failure.
Watchdog timer and verdict logic
The supervisor reads the heartbeat every 5 seconds and classifies the state.
// supervisor/watchdog.tsimport { readFileSync } from "fs";interface Verdict { state: "healthy" | "stalled" | "session_dead" | "context_bloat"; reason: string;}const STALL_MS = 90_000; // 90s without progress -> stallconst TOKEN_LIMIT_RATIO = 0.92; // 92% of token budgetlet lastIdx = -1;let lastIdxAt = Date.now();export function evaluate(hbPath: string, tokenLimit: number): Verdict { const hb = JSON.parse(readFileSync(hbPath, "utf8")); const now = Date.now(); // (1) Stale file -> session is dead if (now - hb.ts > 30_000) { return { state: "session_dead", reason: `hb age ${now - hb.ts}ms` }; } // (2) step_idx not advancing -> stalled if (hb.step_idx !== lastIdx) { lastIdx = hb.step_idx; lastIdxAt = now; } else if (now - lastIdxAt > STALL_MS) { return { state: "stalled", reason: `step ${hb.step_idx} for ${now - lastIdxAt}ms` }; } // (3) Context near limit -> bloat risk if (hb.context_tokens / tokenLimit > TOKEN_LIMIT_RATIO) { return { state: "context_bloat", reason: `tokens ${hb.context_tokens}/${tokenLimit}`, }; } return { state: "healthy", reason: "ok" };}
The 90-second stall threshold deserves a note. I started at 30 seconds and hit 10+ false positives per day. The AdMob reporting API had a median latency of 22 seconds and a P95 of 71 seconds, so a value with some headroom above P95 was the practical floor. If you accept that revenue-related external APIs are slow and assign per-stage thresholds, accuracy improves further.
Tiered recovery (escalation tiers)
Judgment is useless without action. The key is to avoid jumping straight to heavy interventions. I recommend three tiers.
Tier 1: soft retry (lightweight, idempotent)
When stall is detected, re-inject the current tool call while making the agent aware that the previous attempt produced zero progress.
async function softRetry(agentBus: AgentBus): Promise<boolean> { await agentBus.send({ type: "supervisor_nudge", msg: "Previous tool call produced no progress for 90s. Retry the same operation or pick a different approach.", }); return waitForProgress(15_000); // success if step_idx moves within 15s}
This succeeds 58% of the time across the eight-week dataset. Most are transient LLM-side hiccups, and Tier 1 alone clears more than half of all incidents.
Tier 2: context reset (medium weight, state preserving)
If soft retry does not work, suspect either context_bloat or session_dead and reset. Specifically, replace long historical tool outputs with summaries, then restart the agent. A trick: do the summarization in a small separate LLM call. Asking the main agent to summarize itself led to recursive bloat several times in my testing.
Tier 2 succeeds 29% of the time. Together with Tier 1 you cover 87% via automation.
Tier 3: agent rotation (heaviest, last resort)
If context reset still fails, spawn a new workspace session, hand over the heartbeat and a state snapshot, and resume. Tier 3 is the point where a human should be notified. In my setup Tier 3 triggers a Pushover alert. Tier 3 fires on 13% of incidents, and almost all of those turned out to be external API outages (AdMob or Crashlytics) rather than agent-side faults.
Recovery tier results (8 weeks, 142 incidents)
Tier
Attempts
Success
Cumulative coverage
Tier 1 soft retry
142
58%
58%
Tier 2 context reset
60
49%
79%
Tier 3 agent rotation
31
39%
87%
The remaining 13% is genuinely external (API outages, network partitions) and requires human intervention. Chasing 100% auto-recovery makes the watchdog logic balloon and ironically introduces its own bugs. I recommend drawing the line at 87% and letting alerts handle the rest.
Three things that bit me
A few traps I want to spare you.
(1) Forgetting atomic heartbeat writes
I started with a plain writeFileSync and the supervisor read a partially written JSON, declared session_dead, and triggered a phantom recovery. The tmp-then-rename pattern is mandatory. Worth noting: on WSL2, rename occasionally returns EBUSY, so I keep up to three retries on the supervisor side.
(2) Letting the main agent summarize its own context
If you ask a bloated agent to summarize its own context, it takes on the summarization task while still bloated and the loop deepens. Push the summarization out to a small dedicated LLM (I use Gemini Flash) outside the main agent.
(3) Pushover alert reentrancy
If Tier 3 fires multiple times within a minute, the human gets carpet-bombed with notifications. Always rate-limit alerts at the supervisor (I do "same kind of alert at most once per 10 minutes"). I forgot this on day one and received 47 notifications in two hours.
When this design is worth the cost
I recommend the full watchdog plus tiered recovery stack if any of the following applies:
A single run takes 30+ minutes and you let it run overnight
The agent depends on external APIs (AdMob, App Store Connect, Crashlytics) that occasionally lag
One agent spans multiple apps or sites
You want fully automated supervision as a solo indie developer
If your task finishes within five minutes, the cost of building the supervisor outweighs the benefit. Just retry blindly. I do not put a watchdog on Antigravity Lab article generation (5 to 8 minutes per run). The full design is only on the 30+ minute AdMob optimization job and the Claude in Chrome plus Crashlytics auto-triage (averaging 90 minutes).
Next step
Drop just heartbeat.ts into an existing agent first. Writing is side-effect free if you do nothing with the data yet. Collect one week of stall traces and look at which mode dominates your workload, then build the supervisor around what you actually see. That avoids a lot of speculative code.
Hope this helps you ship something that survives the night. 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.