Stopping Parallel Agents from Clobbering the Same File
When you run several agents at once in Antigravity 2.0, two of them can write to the same file and one set of changes silently disappears. Here is how to design write arbitration inside a shared workspace.
I ran three agents in parallel in the Antigravity 2.0 desktop app, then opened the morning log and stopped. The change made by the agent that generated a component had been wiped out by another agent that updated a config file. Tests were still green. The git diff showed no contradiction. One agent's work had simply, quietly ceased to exist.
Parallel execution buys speed, but it carries this specific hazard with it: the fight over who writes last. Worktree isolation articles are everywhere, but those are about splitting repositories, and they do nothing for the case where several agents share one workspace and touch the same file. As an indie developer running four sites in parallel, I have stepped on this more than once, so let me lay out how to design the arbitration.
Why worktree isolation is not enough
Worktree isolation gives each agent its own working copy. That works when agents operate on whole repositories in parallel, because git detects the conflicts when you finally merge.
The trouble starts when several agents split a single feature. In the pattern Antigravity 2.0 promotes — agent A writes a component, agent B wires the API route, agent C runs visual regression tests — all three touch the same package.json, the same type definitions, the same routing config at once. Split that into worktrees and the final merge piles up more conflicts than a human can untangle.
So what you actually need is not repository isolation but concurrency control: a way to order writes to shared resources.
The three collision patterns
Three patterns showed up in practice.
Pattern
What happens
Detectability
Lost update
B overwrites A without reading it; A's change vanishes
Hard (no diff contradiction)
Broken consistency
A changes a type, B writes code against the old type
Medium (type errors may catch it)
Interleaved corruption
Two agents partially edit one file; syntax breaks
Low (the build fails)
The worst is the top row. The build and tests pass, and you only notice "where did that change go?" much later. The design's main job is to kill that case.
✦
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
✦Why worktree isolation does not protect you from same-file write collisions inside one workspace
✦A lease-based file ownership coordinator in Python, with TTL so a crashed agent never freezes the file
✦When to reach for ownership partitioning, a serialization point, or a lease, and the trap I hit in production
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.
Strategy 1: Ownership partitioning (try this first)
The sturdiest move is to never let two agents touch the same file at all. When you split the task, partition the files into per-agent ownership.
Concretely, before the orchestrator issues subtasks it hands each agent a set of writable path globs and forbids writes outside them.
from dataclasses import dataclass, fieldfrom fnmatch import fnmatch@dataclassclass AgentScope: agent_id: str owned_globs: list[str] # patterns this agent may write shared_readonly: list[str] = field(default_factory=list) def can_write(self, path: str) -> bool: # writable only if the path matches an owned glob return any(fnmatch(path, g) for g in self.owned_globs)def assign_scopes() -> list[AgentScope]: return [ AgentScope("ui", ["src/components/**", "src/styles/**"]), AgentScope("api", ["src/app/api/**", "src/lib/server/**"]), AgentScope("test", ["tests/**", "e2e/**"]), ]def guard_write(scopes: dict[str, AgentScope], agent_id: str, path: str) -> None: if not scopes[agent_id].can_write(path): raise PermissionError(f"{agent_id} does not own {path}")
Drop guard_write into the agent's file-write hook and any out-of-scope overwrite stops before it runs. If your Antigravity agent config lets you restrict writable directories, use that as the first line of defense and keep this guard as the second.
Ownership partitioning runs out of road on shared files everyone wants to touch, like package.json or type definitions. The next two strategies handle those.
Strategy 2: A serialization point for shared files
Changes to shared files are not written directly. Each agent enqueues a "change request," and a single dedicated agent (or the orchestrator itself) applies them one at a time.
import json, queue, threadingwrite_queue: "queue.Queue[dict]" = queue.Queue()def request_shared_edit(file: str, mutate) -> None: # mutate: a pure function that takes current content (dict) and returns the new content write_queue.put({"file": file, "mutate": mutate})def serializer_loop(stop: threading.Event) -> None: while not stop.is_set(): try: job = write_queue.get(timeout=0.5) except queue.Empty: continue with open(job["file"]) as f: # re-read right before applying = latest state current = json.load(f) updated = job["mutate"](current) # apply each request in order with open(job["file"], "w") as f: json.dump(updated, f, indent=2, ensure_ascii=False) write_queue.task_done()
The key is making mutate a function that takes the current content and returns new content. Each agent sends only its intent ("add one dependency"), and the serializer re-reads the latest state right before applying it, so lost updates cannot happen by construction. Multiple agents appending to package.json settle most cleanly with this approach.
Strategy 3: A lease-based file ownership coordinator
For the middle ground — where you cannot statically partition ownership but a full serialization queue is overkill — a short-lived lease (a time-boxed lock) fits. Acquire before writing, release when done; if you cannot acquire, wait or move to another task.
import time, threadingclass FileLeaseCoordinator: def __init__(self, lease_ttl: float = 30.0): self._lock = threading.Lock() self._leases: dict[str, tuple[str, float]] = {} # path -> (agent, expiry) self._ttl = lease_ttl def acquire(self, path: str, agent_id: str) -> bool: with self._lock: holder = self._leases.get(path) now = time.monotonic() # free, or an expired lease, means we can take it if holder is None or holder[1] < now: self._leases[path] = (agent_id, now + self._ttl) return True return holder[0] == agent_id # re-entrant if we already hold it def release(self, path: str, agent_id: str) -> None: with self._lock: if self._leases.get(path, (None, 0))[0] == agent_id: del self._leases[path]
The TTL exists so that if an agent crashes without releasing, the file is not locked forever. I got burned here once. I started with an unbounded lock, and the moment one agent stalled on a timeout, everyone else froze waiting on that file. In production, always set an expiry and let expired leases be taken over.
Choosing among the three
1. Try ownership partitioning first (never let two agents touch one file)
2. Route truly shared files through a serialization point
3. Handle only dynamic, short-lived contention with leases
A rough decision table:
Situation
Recommended
Why
Files split cleanly per feature
Ownership partitioning
Removes collisions before they occur
package.json / type defs everyone touches
Serialization point
Re-read-before-apply structurally prevents lost updates
Sporadic writes to temp / cache files
Lease
Lightweight, short wait times
In my case I settled on three layers: partition roughly 80% of files statically, funnel the remaining shared files into a serialization point, and limit leases to genuinely dynamic temp files. Make leases the lead actor and waiting time grows until the parallelism stops paying off. Keep them as a helper — that is my personal recommendation.
Verification: post a sentry that detects clobbering
Even with the design in place, whether there is a gap is a separate question. In production I added a sentry that records a file's hash after writing and checks whether the value I wrote matches the value read next.
import hashlibdef fingerprint(path: str) -> str: with open(path, "rb") as f: return hashlib.sha256(f.read()).hexdigest()[:12]def verify_no_clobber(path: str, expected: str) -> None: actual = fingerprint(path) if actual != expected: # someone touched the file after my write = suspected lost update raise RuntimeError(f"{path} changed unexpectedly expected={expected} actual={actual}")
Wrap this right after an agent's write and right before commit, and changes that would have vanished silently surface early as a "broken build" instead. The undetectable lost update is the biggest trap, so I find this sentry worth as much as the arbitration design itself.
Parallel agents are appealing, but if you just add workers without designing arbitration for shared resources, you gain speed and quietly lose work in equal measure. Ownership partitioning as the first defense, serialization and leases as helpers, and a sentry to verify. That ordering is the setup I actually rely on in my own indie development today. I hope it gives you a foothold if you are facing the same wall.
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.