ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-05-01Advanced

When Parallel Antigravity Agents Fight Over the Same File — Preventing Conflicts and Surviving Real-World Race Conditions

Running Antigravity agents in parallel surfaces five concrete kinds of conflict and three classic race conditions. This guide walks through each one with reproducible failures and the locking, optimistic, and queue-based fixes I actually run in production.

antigravity429agents124concurrency3parallel-execution7conflict-resolution2

The first week I ran agents in parallel, I broke a build at midnight. Three Antigravity agents were each working on separate features. All three modified package.json. The last write erased what the other two had added — git history showed three clean PRs, but the actual package.json only carried one of the three new dependencies.

The real difficulty of running agents in parallel is that you can no longer trace, with the human eye, who touched what at which moment. This article is a structured account of the failures I hit while taking Antigravity parallelism seriously, organized by the kind of conflict, with reproductions and the fixes that hold up in production. If you are at the stage of believing "more agents will be faster," this is the moment to read it.

Five resources where parallel agents collide

After a week of operating in parallel, the conflicts converge onto roughly five hot spots:

  1. Writes to the same filepackage.json, tsconfig.json, lockfiles, shared schemas.
  2. Commits to the same branch — when two or more agents diverge from the same parent, the late one falls into rebase hell.
  3. Shared caches and build artifacts.next/, node_modules/.cache/, dist/ — one agent reads while another writes.
  4. External API rate limits — three agents calling Gemini or Stripe with the same key cascade into 429s.
  5. Working directories and tempfiles/tmp/ collisions, log file appends, SQLite WAL contention.

The first thing to internalize: do not design for "conflicts might happen." Design for conflicts will happen. The probability scales with the number of agents and time. Even a one-percent collision rate, run a hundred times a day, will fail at least once.

A reproducible minimum: the package.json race

Let me show the most common failure shape in a form you can paste into Node and run. Two parallel "read-then-write" operations easily produce a state where the last writer erases the earlier one.

// reproduce/package-race.ts
import { readFile, writeFile } from "node:fs/promises";
 
async function addDep(name: string, version: string) {
  const pkg = JSON.parse(await readFile("package.json", "utf8"));
  pkg.dependencies = pkg.dependencies ?? {};
  pkg.dependencies[name] = version;
  // Pretend the agent is "thinking"
  await new Promise((r) => setTimeout(r, 200));
  await writeFile("package.json", JSON.stringify(pkg, null, 2) + "\n");
}
 
await Promise.all([
  addDep("zod", "^3.23.0"),
  addDep("date-fns", "^3.6.0"),
  addDep("nanoid", "^5.0.0"),
]);

Run it and only the last write survives. The reads overlapped, so each writer overwrote a stale snapshot.

Parallel agent execution is structurally identical to this code. The only difference is that the "thinking" phase is seconds or minutes, not 200 milliseconds. The longer window makes collisions more likely, not less.

Fix 1: file locks — the most practical single-host choice

For agents running on a single machine, the simplest robust fix is an OS-level file lock. In Node, proper-lockfile is the de-facto choice and provides a lockfile-based mutual exclusion.

// utils/with-lock.ts
import lockfile from "proper-lockfile";
 
export async function withLock<T>(
  path: string,
  fn: () => Promise<T>,
): Promise<T> {
  const release = await lockfile.lock(path, {
    retries: { retries: 30, minTimeout: 200, maxTimeout: 2000 },
    stale: 60_000,
  });
  try {
    return await fn();
  } finally {
    await release();
  }
}

Wrap addDep and even at three-way parallelism the writes serialize cleanly:

await withLock("package.json", () => addDep("zod", "^3.23.0"));

One caveat: always set a stale timeout. If an agent crashes mid-edit, the lock would otherwise sit forever and every subsequent run would hang. I keep it at 60 seconds, longer for jobs with known long-running steps.

Fix 2: optimistic locking — the right answer for branch conflicts

For conflicts at the branch level, locks are wrong. Optimistic locking — remember the version you read, retry if it changed by write time — is the practical fit, and Git is already an optimistic locking system. I encode the rules at the agent definition layer:

# antigravity/agents/branch-aware.yaml
name: branch-aware-coder
constraints:
  - branch_per_task: true
  - rebase_before_push: true
  - max_rebase_attempts: 3
  - on_conflict: "abort_and_report"

branch_per_task is dumb and powerful. The agent works on a dedicated branch, attempts git pull --rebase origin main right before pushing, and aborts and reports if conflicts cannot be resolved cleanly.

I deliberately disable "auto-resolve conflicts." There was a release where one of my agents formatted around a conflict marker in package.json and shipped a broken build. Auto-resolution of merge conflicts is a mechanism for code to be merged without a human reading it. I keep it off, by default, on every project.

Fix 3: queues — for shared APIs and global rate limits

External API rate limits cannot be fixed by locks or branches. The right place to absorb them is outside the agents, in a queue. On my team, every Gemini API call goes through a thin Cloudflare Workers Queue.

// services/gemini-queue.ts
import { Hono } from "hono";
 
const app = new Hono<{ Bindings: { GEMINI_QUEUE: Queue<unknown> } }>();
 
app.post("/enqueue", async (c) => {
  const job = await c.req.json();
  await c.env.GEMINI_QUEUE.send(job);
  return c.json({ status: "queued" });
});
 
export default app;
 
export async function queue(env: { GEMINI_QUEUE: Queue<unknown> }, batch: MessageBatch<{ prompt: string }>) {
  for (const msg of batch.messages) {
    await callGemini(msg.body.prompt);
    msg.ack();
  }
}

The agents follow one rule: never call the API directly. The 429 rate becomes a property of the worker's configured concurrency, not the number of agents. The first time I doubled the agent count and saw the 429 rate stay flat, parallel operation finally felt scalable.

The three classic race conditions, in agent form

On top of the five hot spots, there is another layer: race conditions that look almost the same shape every time. The three I see repeatedly:

1. Read-Modify-Write

The most classic and the most common. The package.json race above is exactly this. A reads → B reads → A writes → B writes and B's write erases A's. Fix with a file lock, or with a single coordinator process: agents propose diffs, one process serializes the application of those diffs.

2. Check-Then-Act

A check (does the file exist?) and an action (write it) are separated by enough time for someone else to slip in between. Antigravity agents trying to create a "fresh" branch and colliding with one that already exists are this pattern. Fix by collapsing check and act into a single atomic operation: git rev-parse --verify new-branch && exit 1 || git checkout -b new-branch runs as one line and either succeeds atomically or fails fast.

3. ABA

An agent forks from branch A, the branch is recreated as a different "A," and the agent comes back assuming everything is the same. The fix is to remember the commit hash, not the branch name.

INITIAL_COMMIT=$(git rev-parse HEAD)
# ... agent work ...
git fetch origin main
git merge-base --is-ancestor "$INITIAL_COMMIT" origin/main \
  || { echo "Base branch was rewritten"; exit 1; }

Choosing among three lock styles

To compress the patterns above:

  • Pessimistic locks (file locks, mutexes) — frequent contention, short writes
  • Optimistic locks (Git rebase) — rare contention, long writes
  • Queues (ordering + backpressure) — shared resources, external APIs, global rate limits

My rule of thumb is simple: if the resource is a file, lock it; if it is a branch, rebase it; if it is an API, queue it. Two exceptions: SQLite, even in WAL mode, only allows one writer at a time and deserves a pessimistic lock; long-running migration jobs go to a dedicated job runner with single-worker concurrency.

Observability: noticing that contention happened

A conflict you do not detect becomes a production incident. The four signals my team watches:

  1. File lock acquisition time — warn at 1s, BLOCK at 10s.
  2. Rebase failure count — auto-pause an agent after 3 consecutive rebase failures.
  3. 429 ratio — if it crosses 5 percent, halve queue worker concurrency automatically.
  4. Job de-duplication — if the same task_id is enqueued twice, drop one.

Surface these four on a Grafana or Cloudflare dashboard and you can review a few hours of data right after raising the parallelism level to confirm whether the new degree is safe.

A pre-flight checklist before turning the dial up

Closing with the checklist I run before bumping parallelism. Even a single pass through this list catches surprising risks.

  • [ ] Are agent-written files protected by locks (or queues)?
  • [ ] Does each agent work on its own branch and rebase before pushing?
  • [ ] Are shared caches (.next/, dist/) split per agent?
  • [ ] Is the external API key shared, and if so, is there a per-key rate budget?
  • [ ] Can logs and traces be filtered by agent_id?

Parallel agents do bring speed. But speed only pays off after the scaffolding is in place to catch what falls during a collision — that was the lesson from the night I had to rebuild package.json by hand. If your finger is currently hovering over the button to spawn a third agent, run this checklist once before you press it.

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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Agents & Manager2026-06-19
How to Orchestrate Multiple Agents: Drawing the Line Between Parallel and Serial Work
Antigravity 2.0 brings true parallel execution across multiple agents. But making everything parallel does not make it faster. Which work should fan out in parallel, and which should stay serial? This is an orchestration design that does not fall apart, viewed through dependencies and contention.
Agents & Manager2026-07-08
Measuring the Rework Rate of What You Delegate to Agents: Drawing Delegation Boundaries with Numbers, Not Instinct
How much should you hand to an agent? I drew that line by instinct for a long time. Here is a practical way to compute a per-category rework rate from your git history and redraw the delegation boundary with numbers, with working code.
Agents & Manager2026-07-05
Protecting Your Agent Stack's Known-Good State with a Single Lockfile — Change-Budget Design for an Era of Simultaneously Moving Parts
When the IDE build, CLI, model, and dependencies all move at once, you can no longer tell which one caused a regression. Here is a change-budget design that pins your known-good state to one lockfile, with working code and operational logs.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →