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_id that 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_unmet and the list of what is missing.
  • Retrying is safe. If the same agent repeats the request with the same execution_id while 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_lock before 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

  1. Connect your agent over MCP with its own token.
  2. Install the MarQ skill: it teaches the agent to lease before writing and to always release, even on failure.
  3. 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.

Blog