How to stop two AI agents from editing the same task
MarQ · 2026-11-08
If two AI agents edit the same task at the same time, whoever writes last wins and the other's work disappears without warning. The classic answer in distributed systems is a lease: before touching something, the agent asks for it; while it holds it, nobody else can change it; and if the agent hangs, the lease expires on its own. A fencing token adds the guarantee that was missing: even if a slow agent shows up late, its old writes are recognized as stale and rejected.
The problem: two agents, one task
Picture two coding agents, one in Claude Code and one in Cursor, working on the same board. Both read the task "Build the REST API" in to do. Both start, both write notes and change the status. The outcome depends on the order the requests arrive in, and nobody finds out they stepped on each other.
With people you solve this by talking. With agents there is no conversation: there are concurrent requests. You need a mechanical rule, enforced by the server rather than by each agent's good faith.
The lease: ask before you write
In MarQ an agent leases the task with the MCP tool pm_acquire_task before modifying it. If it is free, the agent receives three things:
- a
lease_idthat identifies its lease, - a
fencing_token, a number that goes up every time a new lease is granted on that task, - an expiry time (
expires_at).
If another agent already holds it, the answer is not a generic error but useful information: who holds it, until when, and how many seconds to wait before retrying (retry_after_seconds). A well-instructed agent moves on to another task instead of insisting.
Two important details:
- Dependencies count. If the task depends on others that are not done, the lease is refused with the reason
dependencies_unmetand the list of what is missing. - Retrying is safe. If the same agent repeats the request with the same
execution_idwhile its lease is still valid, it gets the same lease with the deadline extended, instead of a conflict with itself.
Leases that expire: nobody stays blocked
An agent can crash, lose its connection or run out of context. If a lease lasted forever, the task would be blocked forever. So every lease has a deadline: 15 minutes by default and 2 hours at most (configurable on the server with LOCK_DEFAULT_TTL_SECONDS and LOCK_MAX_TTL_SECONDS).
- An agent that is still working renews it with
pm_renew_task_lockbefore it runs out. - When it finishes, it releases it with
pm_release_task. - A lease that has already expired cannot be renewed retroactively: another agent may have taken it in the meantime. The agent has to lease again.
When a lease expires without being released, MarQ detects it once, records it in the history and notifies the people on the project, so it is known that an agent abandoned work halfway.
The fencing token: the slow agent that comes back late
There is a case that expiry alone does not cover. Agent A leases the task, gets paused for longer than the deadline, the lease expires, agent B takes it and makes progress. When A wakes up, it believes it is still the owner and tries to write.
This is where the fencing token comes in. Every new lease increments that task's counter; A's was generation 4 and B's is generation 5. If A presents its old lease, the server recognizes it as stale (stale_lock) and rejects the write. The idea comes from distributed locking: the resource checks the generation, not the trust.
Three modes: from measuring to enforcing
Not everyone wants to start by rejecting writes. MarQ has three modes, set with the LOCK_ENFORCEMENT variable:
| Mode | What it does with a write that has no valid lease |
|---|---|
off |
Checks nothing. |
warn (default) |
Allows it, but records a warning in the history so you can see who skipped the rule. |
enforce |
Rejects it with a clear error and records it. |
There are four rejection codes: lock_required (there was no lease), lock_expired (the lease ran out), locked_by_other (someone else holds it) and stale_lock (old generation). An agent that understands them knows what to do in each case: lease, lease again, wait or switch tasks.
The practical recommendation: start in warn, review the history for a few days looking for writes without a lease, and move to enforce once the list is clean.
And people
Leases mostly coordinate agents. A person editing from the panel only meets the rule when someone else holds the task, and an administrator can override it with their permission; that change is audited as an exception. They can also force-release a stuck lease by giving a reason, and that goes in the history too.
How to try it
- Connect your agent over MCP with its own token.
- Install the MarQ skill: it teaches the agent to lease before writing and to always release, even on failure.
- See what a leased task looks like in the read-only demo: the "Build the REST API" task carries an agent's lease.
Leases do not replace other layers, such as giving each agent its own git branch: they protect the state of the board, that is, who does what and what state each task is in, which is exactly what gets corrupted when several agents write at once.