# What Is Atomic Task Checkout in Paperclip and How Execution Locks Prevent Double-Work

> Learn about Paperclip's atomic task checkout and execution locks. Discover how this database locking mechanism prevents duplicate work by ensuring only one agent claims and works on an issue at a time.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: deep-dive
- Published: 2026-08-18

---

**Atomic task checkout in Paperclip is a database-level locking mechanism that guarantees only one agent can claim an issue at a time, while execution locks prevent any second agent from starting work on an already-active task.**

Paperclip's control-plane implements a **single-assignee model** for every issue. Before an agent can work on a task, it must obtain an exclusive checkout through an atomic database transaction. This design eliminates race conditions where multiple agents might accidentally process the same issue—commonly known as "double-work."

## How Atomic Checkout Works

The checkout flow lives in [`server/src/services/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/issues.ts). It combines row-level locking with strict ownership validation to ensure mutual exclusion.

### The Database Lock Acquisition

When an agent requests checkout, the server issues a `SELECT … FOR UPDATE` query to lock the row:

```typescript
await sql`SELECT ${heartbeatRuns.id} FROM ${heartbeatRuns}
          WHERE ${heartbeatRuns.id} = ${issue.checkoutRunId}
          FOR UPDATE`.execute();

```

This query at **lines 5312–5317** in [`server/src/services/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/issues.ts) guarantees that no other transaction can modify the issue until the current one completes. The lock applies to both `checkoutRunId` and `executionRunId` columns.

### Same-Run Lock Validation

The helper function `sameRunLock` enforces ownership rules:

```typescript
const sameRunLock = (checkoutRunId, actorRunId) =>
  actorRunId ? checkoutRunId === actorRunId : checkoutRunId == null;

```

Located at **lines 773–775**, this function allows an agent to **reuse its own lock** but rejects any attempt by a different agent to steal the checkout.

### Atomic Update on Success

If validation passes, the checkout columns update in the same transaction:

```typescript
await sql`UPDATE ${issues}
          SET checkoutRunId = ${requestRunId}
          WHERE id = ${issueId}`.execute();

```

This pattern at **lines 8096–8103** ensures the assignment happens atomically—no gap exists where another transaction could sneak in.

## What Happens on Checkout Conflict

When another agent already holds the lock, the server immediately returns a **409 Conflict**:

```http
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "error": "checkout_conflict",
  "ownerAgentId": "agent-1234"
}

```

The client receives the competing agent's ID and can decide whether to retry, select a different issue, or wait.

## How Execution Locks Prevent Double-Work

After checkout, agents transition issues to `in_progress` by acquiring an **execution lock**. This second lock prevents the scenario where one agent checks out a task, stalls, and a second agent starts working anyway.

### Execution Lock Acquisition

```typescript
await sql`UPDATE ${issues}
          SET executionRunId = ${runId},
              executionAgentNameKey = ${agentNameKey}
          WHERE id = ${issueId}
          AND (executionRunId IS NULL OR executionRunId = ${runId})`.execute();

```

The `WHERE` clause enforces idempotency—only unclaimed issues or the same run can proceed. This blocks any interleaved execution attempt cold.

### Two-Layer Protection

| Layer | Purpose | Prevents |
|-------|---------|----------|
| **Checkout lock** | Claims issue for preparation work | Duplicate assignment, agent collisions |
| **Execution lock** | Grants exclusive work authorization | Double-execution, split-brain processing |

Together these implement **pessimistic concurrency control**: the system assumes conflicts will occur and actively prevents them through database-level enforcement rather than optimistic retry loops.

## Stale Lock Recovery

Dead agents could theoretically block progress forever. Paperclip solves this through a **sweeper service** in [`server/src/services/recovery/service.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/recovery/service.ts):

```typescript
// Simplified sweeper logic from lines 5640–5665
async function clearStaleLocks() {
  const deadRuns = await findTerminatedRuns();
  await sql`UPDATE ${issues}
            SET checkoutRunId = NULL,
                executionRunId = NULL
            WHERE checkoutRunId IN (${deadRuns})
               OR executionRunId IN (${deadRuns})`.execute();
}

```

This background process runs continuously, detecting when heartbeat runs terminate or go missing, then releasing their associated locks. The sweeper guarantees that **atomic task checkout** cannot indefinitely stall the work queue.

## Client-Side Checkout Integration

Agents initiate checkout through the API endpoint defined in [`ui/src/api/issues.ts`](https://github.com/paperclipai/paperclip/blob/main/ui/src/api/issues.ts) (**lines 219–220**):

```typescript
await api.post<Issue>(`/issues/${issueId}/checkout`, {
  agentId,
  expectedStatuses: ["open", "assigned"],
  checkoutRunId: run.id,
});

```

The `expectedStatuses` array provides additional validation—checkout only succeeds when the issue is in an appropriate state, adding a third layer of protection against stale data.

## Design Documentation

Paperclip's specification documents the atomic checkout intent explicitly:

- **[`doc/SPEC.md`](https://github.com/paperclipai/paperclip/blob/main/doc/SPEC.md) (line 391)**: Defines the single-assignee requirement
- **[`doc/SPEC-implementation.md`](https://github.com/paperclipai/paperclip/blob/main/doc/SPEC-implementation.md) (line 39)**: Maps the abstract design to concrete `checkoutRunId` and `executionRunId` database columns

This traceability from specification through implementation ensures the **execution lock** behavior remains consistent across code changes.

## Summary

- **Atomic task checkout** uses `SELECT … FOR UPDATE` to serialize all claim attempts on a single database row
- The `sameRunLock` helper enforces that only the owning agent can reuse or release a lock
- **Execution locks** create a second barrier that prevents work from starting on already-active issues
- **409 Conflict responses** surface lock contention immediately so agents can redirect effort
- The **recovery sweeper** in [`server/src/services/recovery/service.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/recovery/service.ts) automatically clears locks from dead runs
- Combined, these mechanisms implement **pessimistic concurrency control** that prevents double-work without requiring distributed coordination protocols

## Frequently Asked Questions

### What happens if two agents request checkout on the same issue simultaneously?

The database's `FOR UPDATE` lock serializes the transactions. Whichever request acquires the lock first performs the validation and update; the second waits for the lock, then fails the `sameRunLock` check and receives a **409 Conflict** with the winning agent's ID. No double-work occurs because the atomic transaction boundary prevents any interleaved state.

### Can an agent lose its checkout to another agent?

No. The `sameRunLock` function at `server/src/services/issues.ts:773-775` explicitly requires `checkoutRunId === actorRunId` for reuse. A different run ID always triggers rejection. The only way a checkout transfers is if the original agent's run terminates and the **recovery sweeper** clears the stale lock.

### How does Paperclip handle agent crashes during execution?

The **recovery sweeper** monitors heartbeat runs continuously. When a run terminates or fails health checks, the sweeper (at `server/src/services/recovery/service.ts:5640-5665`) atomically NULLs out `checkoutRunId` and `executionRunId` for all associated issues. This releases the locks without manual intervention and makes issues available for new checkout attempts.

### What's the difference between checkout and execution locks?

**Checkout** grants permission to prepare for work—read code, fetch dependencies, initialize tools. **Execution** grants permission to actually modify state. This separation prevents an agent from stalling after checkout (with work half-done) while another agent starts fresh. Both locks use the same atomic database patterns but protect distinct lifecycle phases.